1cfg_if::cfg_if! {
2 if #[cfg(feature = "signer")] {
3 pub mod signer;
4 pub use signer::*;
5 }
6}
7
8pub use crate::solidity_types::Envelope;
9use crate::{
10 ServiceId, ServiceManagerEnvelope, ServiceManagerSignatureData, SignatureData, SignatureKind,
11 SubmitAction, TriggerAction, TriggerData, WasmResponse, WorkflowId,
12};
13use alloy_primitives::{eip191_hash_message, keccak256, FixedBytes, SignatureError};
14use alloy_sol_types::SolValue;
15use async_trait::async_trait;
16use ripemd::Ripemd160;
17use serde::{Deserialize, Serialize};
18use sha2::Digest;
19use thiserror::Error;
20use utoipa::ToSchema;
21
22#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
23#[serde(rename_all = "snake_case")]
24pub struct AggregatorInput {
25 pub trigger_action: TriggerAction,
26 pub operator_response: WasmResponse,
27}
28
29impl AggregatorInput {
30 pub fn event_id(&self) -> Result<EventId, bincode::error::EncodeError> {
31 let salt = match &self.operator_response.event_id_salt {
32 Some(bytes) => EventIdSalt::WasmResponse(bytes),
33 None => EventIdSalt::Trigger(&self.trigger_action.data),
34 };
35
36 EventId::new(
37 &self.trigger_action.config.service_id,
38 &self.trigger_action.config.workflow_id,
39 salt,
40 )
41 }
42}
43#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
44#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
45pub trait WavsSignable {
46 fn encode_data(&self) -> anyhow::Result<Vec<u8>>;
48
49 fn prefix_eip191_hash(&self) -> anyhow::Result<FixedBytes<32>> {
50 let envelope_bytes = self.encode_data()?;
51 Ok(eip191_hash_message(keccak256(&envelope_bytes)))
52 }
53
54 fn unprefixed_hash(&self) -> anyhow::Result<FixedBytes<32>> {
55 let envelope_bytes = self.encode_data()?;
56 Ok(keccak256(&envelope_bytes))
57 }
58}
59
60impl WavsSignable for Envelope {
61 fn encode_data(&self) -> anyhow::Result<Vec<u8>> {
62 Ok(self.abi_encode())
63 }
64}
65
66impl WavsSignable for &[SubmitAction] {
67 fn encode_data(&self) -> anyhow::Result<Vec<u8>> {
68 Ok(bincode::serde::encode_to_vec(
69 self,
70 bincode::config::standard(),
71 )?)
72 }
73}
74
75impl From<Envelope> for ServiceManagerEnvelope {
76 fn from(envelope: Envelope) -> Self {
77 ServiceManagerEnvelope {
78 eventId: envelope.eventId,
79 ordering: envelope.ordering,
80 payload: envelope.payload,
81 }
82 }
83}
84
85impl From<SignatureData> for ServiceManagerSignatureData {
86 fn from(signature_data: SignatureData) -> Self {
87 ServiceManagerSignatureData {
88 signers: signature_data.signers,
89 signatures: signature_data.signatures,
90 referenceBlock: signature_data.referenceBlock,
91 }
92 }
93}
94
95#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, PartialEq, Eq, Hash)]
96#[serde(rename_all = "snake_case")]
97pub struct WavsSignature {
98 pub data: Vec<u8>,
99 pub kind: SignatureKind,
100}
101
102#[derive(
103 Serialize,
104 Deserialize,
105 Clone,
106 Eq,
107 PartialEq,
108 Debug,
109 Hash,
110 bincode::Decode,
111 bincode::Encode,
112 Ord,
113 PartialOrd,
114 Default,
115)]
116#[serde(transparent)]
117pub struct EventId(#[serde(with = "const_hex")] [u8; 20]);
118
119impl EventId {
120 pub fn new(
121 service_id: &ServiceId,
122 workflow_id: &WorkflowId,
123 salt: EventIdSalt<'_>,
124 ) -> Result<Self, bincode::error::EncodeError> {
125 let bincode_config = bincode::config::standard();
126 let mut hasher = Ripemd160::new();
127 hasher.update(service_id.inner());
128 hasher.update(workflow_id.as_bytes());
129 match salt {
130 EventIdSalt::WasmResponse(bytes) => hasher.update(bytes),
131 EventIdSalt::Trigger(trigger_data) => match trigger_data {
132 TriggerData::AtProtoEvent {
141 repo,
142 collection,
143 rkey,
144 action,
145 cid,
146 record,
147 rev,
148 op_index,
149 ..
150 } => {
151 let encoded = bincode::serde::encode_to_vec(
152 (repo, collection, rkey, action, cid, record, rev, op_index),
153 bincode_config,
154 )?;
155 hasher.update(encoded);
156 }
157 _ => hasher.update(bincode::serde::encode_to_vec(trigger_data, bincode_config)?),
158 },
159 }
160 let result = hasher.finalize();
161 Ok(Self(result.into()))
162 }
163
164 pub fn as_bytes(&self) -> &[u8; 20] {
165 &self.0
166 }
167}
168
169pub enum EventIdSalt<'a> {
170 WasmResponse(&'a [u8]),
172 Trigger(&'a TriggerData),
174}
175
176impl From<[u8; 20]> for EventId {
177 fn from(value: [u8; 20]) -> Self {
178 Self(value)
179 }
180}
181
182impl From<FixedBytes<20>> for EventId {
183 fn from(value: FixedBytes<20>) -> Self {
184 Self(value.0)
185 }
186}
187
188impl From<EventId> for FixedBytes<20> {
189 fn from(value: EventId) -> Self {
190 FixedBytes(value.0)
191 }
192}
193
194impl std::fmt::Display for EventId {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 write!(f, "{}", const_hex::encode(self.0))
197 }
198}
199
200impl AsRef<[u8]> for EventId {
201 fn as_ref(&self) -> &[u8] {
202 &self.0
203 }
204}
205
206#[derive(Serialize, Deserialize, Clone)]
207#[serde(transparent)]
208pub struct EventOrder([u8; 12]);
209
210impl EventOrder {
211 pub fn new_u64(value: u64) -> Self {
212 let mut bytes = [0; 12];
213 bytes[0..8].copy_from_slice(&value.to_be_bytes());
214 Self(bytes)
215 }
216}
217
218impl From<FixedBytes<12>> for EventOrder {
219 fn from(value: FixedBytes<12>) -> Self {
220 Self(value.0)
221 }
222}
223
224impl From<EventOrder> for FixedBytes<12> {
225 fn from(value: EventOrder) -> Self {
226 FixedBytes(value.0)
227 }
228}
229
230impl std::fmt::Display for EventOrder {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 write!(f, "{}", const_hex::encode(self.0))
233 }
234}
235
236impl AsRef<[u8]> for EventOrder {
237 fn as_ref(&self) -> &[u8] {
238 &self.0
239 }
240}
241
242#[derive(Debug, Error)]
243pub enum SigningError {
244 #[error("Unable to recover signer address: {0:?}")]
245 RecoverSignerAddress(SignatureError),
246
247 #[error("Unable to get data hash: {0:?}")]
248 DataHash(anyhow::Error),
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::AtProtoAction;
255
256 #[test]
257 fn atproto_event_id_ignores_seq_and_timestamp() {
258 let service_id = ServiceId::hash(b"service");
259 let workflow_id = WorkflowId::new("flow1").unwrap();
260
261 let base = TriggerData::AtProtoEvent {
262 sequence: 1,
263 timestamp: 1000,
264 repo: "did:example:123".to_string(),
265 collection: "app.bsky.feed.post".to_string(),
266 rkey: "abc".to_string(),
267 action: AtProtoAction::Create,
268 cid: Some("cid123".to_string()),
269 record: None,
270 rev: Some("rev1".to_string()),
271 op_index: Some(0),
272 };
273
274 let different = match &base {
275 TriggerData::AtProtoEvent {
276 repo,
277 collection,
278 rkey,
279 action,
280 cid,
281 record,
282 rev,
283 op_index,
284 ..
285 } => TriggerData::AtProtoEvent {
286 sequence: 9999,
287 timestamp: 999999,
288 repo: repo.clone(),
289 collection: collection.clone(),
290 rkey: rkey.clone(),
291 action: action.clone(),
292 cid: cid.clone(),
293 record: record.clone(),
294 rev: rev.clone(),
295 op_index: *op_index,
296 },
297 _ => unreachable!(),
298 };
299
300 let id1 = EventId::new(&service_id, &workflow_id, EventIdSalt::Trigger(&base)).unwrap();
301 let id2 =
302 EventId::new(&service_id, &workflow_id, EventIdSalt::Trigger(&different)).unwrap();
303
304 assert_eq!(id1, id2);
305 }
306}