1use alloy::primitives::{Address, Bytes, B256, U256};
7use alloy::providers::Provider;
8use alloy::signers::Signer;
9use alloy::sol_types::{Eip712Domain, SolStruct};
10
11use crate::config::{self, assert_transfer_contract_configured};
12use crate::contract::{
13 BatchTransferWithCommit, CancelAuthorization, TransferDetail as SolTransferDetail,
14 TransferWithCommit, TransferWithCommitment, UniCommitTransfers,
15};
16use crate::error::SdkError;
17use crate::types::args::{
18 BatchTransferWithCommitArgs, CancelAuthorizationArgs, SingleTransferArgs,
19 UniCommitTransfersArgs,
20};
21use crate::types::signed_data::{
22 SignedBatchTransferWithCommit, SignedCancelAuthorization, SignedTransferWithCommit,
23 SignedUniCommitTransfers,
24};
25use crate::utils::{assert_onchain_eip712_domain_matches_config, assert_transfer_contract_deployed};
26
27fn ensure_signer_is_message_from<S: Signer>(signer: &S, from: Address) -> Result<(), SdkError> {
31 let signer_addr = signer.address();
32 if signer_addr != from {
33 return Err(SdkError::FromSignerMismatch {
34 from,
35 signer: signer_addr,
36 });
37 }
38 Ok(())
39}
40
41fn ensure_signer_is_authorizer<S: Signer>(signer: &S, authorizer: Address) -> Result<(), SdkError> {
43 let signer_addr = signer.address();
44 if signer_addr != authorizer {
45 return Err(SdkError::AuthorizerSignerMismatch {
46 authorizer,
47 signer: signer_addr,
48 });
49 }
50 Ok(())
51}
52
53pub fn domain_for_typed_data_sign(
67 name: String,
68 version: String,
69 chain_id: U256,
70 verifying_contract: Address,
71 salt: B256,
72) -> Eip712Domain {
73 let salt = if salt == B256::ZERO { None } else { Some(salt) };
74 Eip712Domain {
75 name: Some(name.into()),
76 version: Some(version.into()),
77 chain_id: Some(chain_id),
78 verifying_contract: Some(verifying_contract),
79 salt,
80 }
81}
82
83async fn fetch_eip712_domain<P: Provider>(provider: &P) -> Result<Eip712Domain, SdkError> {
85 let chain_id = provider.get_chain_id().await?;
86 let c = TransferWithCommitment::new(config::transfer_with_commitment_address(), provider);
87 let r = c.eip712Domain().call().await?;
88 let _ = r.fields;
89 let domain = domain_for_typed_data_sign(
90 r.name,
91 r.version,
92 r.chainId,
93 r.verifyingContract,
94 r.salt,
95 );
96 assert_onchain_eip712_domain_matches_config(&domain, chain_id)?;
97 Ok(domain)
98}
99
100fn unwrap_valid_time_window(
101 valid_after: Option<U256>,
102 valid_before: Option<U256>,
103) -> Result<(U256, U256), SdkError> {
104 let valid_after = valid_after.unwrap_or(U256::ZERO);
105 let valid_before = valid_before.unwrap_or(U256::MAX);
106 if valid_after > valid_before {
107 return Err(SdkError::InvalidValidTimeWindow {
108 valid_after,
109 valid_before,
110 });
111 }
112 Ok((valid_after, valid_before))
113}
114
115pub async fn single_transfer<P, S>(
134 provider: &P,
135 signer: &S,
136 args: SingleTransferArgs,
137) -> Result<SignedTransferWithCommit, SdkError>
138where
139 P: Provider,
140 S: Signer + Send + Sync,
141{
142 let (valid_after, valid_before) =
143 unwrap_valid_time_window(args.valid_after, args.valid_before)?;
144 ensure_signer_is_message_from(signer, args.from)?;
145 assert_transfer_contract_configured()?;
146 assert_transfer_contract_deployed(provider).await?;
147 let domain = fetch_eip712_domain(provider).await?;
148 let msg = TransferWithCommit {
149 from: args.from,
150 to: args.to,
151 token: args.token,
152 executor: args.executor,
153 value: args.value,
154 validAfter: valid_after,
155 validBefore: valid_before,
156 commitment: args.commitment,
157 };
158 let hash = msg.eip712_signing_hash(&domain);
159 let sig = signer.sign_hash(&hash).await?;
160 let signature = Bytes::copy_from_slice(sig.as_bytes().as_slice());
161 Ok(SignedTransferWithCommit {
162 domain,
163 from: args.from,
164 to: args.to,
165 token: args.token,
166 value: args.value,
167 commitment: args.commitment,
168 valid_after,
169 valid_before,
170 signature,
171 })
172}
173
174pub async fn uni_commit_transfers<P, S>(
182 provider: &P,
183 signer: &S,
184 args: UniCommitTransfersArgs,
185) -> Result<SignedUniCommitTransfers, SdkError>
186where
187 P: Provider,
188 S: Signer + Send + Sync,
189{
190 if args.details.is_empty() {
191 return Err(SdkError::EmptyTransferDetails);
192 }
193 let (valid_after, valid_before) =
194 unwrap_valid_time_window(args.valid_after, args.valid_before)?;
195 ensure_signer_is_message_from(signer, args.from)?;
196 assert_transfer_contract_configured()?;
197 assert_transfer_contract_deployed(provider).await?;
198 let domain = fetch_eip712_domain(provider).await?;
199 let details: Vec<SolTransferDetail> = args
200 .details
201 .iter()
202 .map(|d| SolTransferDetail {
203 to: d.to,
204 token: d.token,
205 value: d.value,
206 })
207 .collect();
208 let msg = UniCommitTransfers {
209 from: args.from,
210 executor: args.executor,
211 details,
212 validAfter: valid_after,
213 validBefore: valid_before,
214 commitment: args.commitment,
215 };
216 let hash = msg.eip712_signing_hash(&domain);
217 let sig = signer.sign_hash(&hash).await?;
218 let signature = Bytes::copy_from_slice(sig.as_bytes().as_slice());
219 Ok(SignedUniCommitTransfers {
220 domain,
221 from: args.from,
222 details: args.details,
223 commitment: args.commitment,
224 valid_after,
225 valid_before,
226 signature,
227 })
228}
229
230pub async fn batch_transfer_with_commit<P, S>(
238 provider: &P,
239 signer: &S,
240 args: BatchTransferWithCommitArgs,
241) -> Result<SignedBatchTransferWithCommit, SdkError>
242where
243 P: Provider,
244 S: Signer + Send + Sync,
245{
246 if args.details.is_empty() {
247 return Err(SdkError::EmptyTransferDetails);
248 }
249 let (valid_after, valid_before) =
250 unwrap_valid_time_window(args.valid_after, args.valid_before)?;
251 ensure_signer_is_message_from(signer, args.from)?;
252 assert_transfer_contract_configured()?;
253 assert_transfer_contract_deployed(provider).await?;
254 let domain = fetch_eip712_domain(provider).await?;
255 let details: Vec<crate::contract::CommittedTransferDetail> = args
256 .details
257 .iter()
258 .map(|d| crate::contract::CommittedTransferDetail {
259 to: d.to,
260 token: d.token,
261 value: d.value,
262 commitment: d.commitment,
263 })
264 .collect();
265 let msg = BatchTransferWithCommit {
266 from: args.from,
267 executor: args.executor,
268 details,
269 validAfter: valid_after,
270 validBefore: valid_before,
271 batchCommitment: args.batch_commitment,
272 };
273 let hash = msg.eip712_signing_hash(&domain);
274 let sig = signer.sign_hash(&hash).await?;
275 let signature = Bytes::copy_from_slice(sig.as_bytes().as_slice());
276 Ok(SignedBatchTransferWithCommit {
277 domain,
278 from: args.from,
279 details: args.details,
280 batch_commitment: args.batch_commitment,
281 valid_after,
282 valid_before,
283 signature,
284 })
285}
286
287pub async fn cancel_authorization<P, S>(
303 provider: &P,
304 signer: &S,
305 args: CancelAuthorizationArgs,
306) -> Result<SignedCancelAuthorization, SdkError>
307where
308 P: Provider,
309 S: Signer + Send + Sync,
310{
311 ensure_signer_is_authorizer(signer, args.authorizer)?;
312 assert_transfer_contract_configured()?;
313 assert_transfer_contract_deployed(provider).await?;
314 let domain = fetch_eip712_domain(provider).await?;
315 let msg = CancelAuthorization {
316 authorizer: args.authorizer,
317 commitment: args.commitment,
318 };
319 let hash = msg.eip712_signing_hash(&domain);
320 let sig = signer.sign_hash(&hash).await?;
321 let signature = Bytes::copy_from_slice(sig.as_bytes().as_slice());
322 Ok(SignedCancelAuthorization {
323 domain,
324 authorizer: args.authorizer,
325 commitment: args.commitment,
326 signature,
327 })
328}
329
330#[cfg(test)]
331mod tests {
332 use alloy::primitives::{address, B256, U256};
333
334 use super::domain_for_typed_data_sign;
335
336 #[test]
337 fn domain_for_typed_data_sign_omits_zero_salt() {
338 let d = domain_for_typed_data_sign(
339 "TransferWithCommitment".to_string(),
340 "1".to_string(),
341 U256::from(1u64),
342 address!("0x2222222222222222222222222222222222222222"),
343 B256::ZERO,
344 );
345 assert!(d.salt.is_none());
346 }
347
348 #[test]
349 fn domain_for_typed_data_sign_keeps_nonzero_salt() {
350 let salt = B256::from_slice(&[0xbb; 32]);
351 let d = domain_for_typed_data_sign(
352 "TransferWithCommitment".to_string(),
353 "1".to_string(),
354 U256::from(1u64),
355 address!("0x2222222222222222222222222222222222222222"),
356 salt,
357 );
358 assert_eq!(d.salt, Some(salt));
359 }
360
361 #[tokio::test]
362 #[cfg(not(feature = "test-config"))]
363 async fn single_transfer_fails_when_provider_unreachable() {
364 use alloy::providers::ProviderBuilder;
365 use alloy::primitives::B256;
366 use crate::error::SdkError;
367 use crate::types::SingleTransferArgs;
368
369 let provider = ProviderBuilder::new().connect_http("http://127.0.0.1:65534".parse().unwrap());
370 let signer = alloy::signers::local::PrivateKeySigner::random();
371 let from = signer.address();
372 let args = SingleTransferArgs {
373 from,
374 to: address!("0x3333333333333333333333333333333333333333"),
375 token: address!("0x1111111111111111111111111111111111111111"),
376 executor: address!("0x1111111111111111111111111111111111111111"),
377 value: U256::from(100u64),
378 commitment: B256::repeat_byte(0xaa),
379 valid_after: None,
380 valid_before: None,
381 };
382 let r = super::single_transfer(&provider, &signer, args).await;
383 assert!(matches!(r, Err(SdkError::Alloy(_))));
384 }
385
386 #[tokio::test]
387 async fn single_transfer_errors_when_from_does_not_match_signer() {
388 use alloy::providers::ProviderBuilder;
389 use crate::error::SdkError;
390 use crate::types::SingleTransferArgs;
391
392 let provider = ProviderBuilder::new().connect_http("http://127.0.0.1:65535".parse().unwrap());
393 let signer = alloy::signers::local::PrivateKeySigner::random();
394 let wrong_from = address!("0x1111111111111111111111111111111111111111");
395 assert_ne!(signer.address(), wrong_from);
396 let args = SingleTransferArgs {
397 from: wrong_from,
398 to: address!("0x3333333333333333333333333333333333333333"),
399 token: address!("0x1111111111111111111111111111111111111111"),
400 executor: address!("0x1111111111111111111111111111111111111111"),
401 value: U256::from(100u64),
402 commitment: B256::repeat_byte(0xaa),
403 valid_after: None,
404 valid_before: None,
405 };
406 let r = super::single_transfer(&provider, &signer, args).await;
407 assert!(matches!(
408 r,
409 Err(SdkError::FromSignerMismatch { .. })
410 ));
411 }
412
413 #[tokio::test]
414 async fn single_transfer_fails_when_valid_after_gt_valid_before() {
415 use alloy::providers::ProviderBuilder;
416 use crate::error::SdkError;
417 use crate::types::SingleTransferArgs;
418
419 let provider = ProviderBuilder::new().connect_http("http://127.0.0.1:65534".parse().unwrap());
420 let signer = alloy::signers::local::PrivateKeySigner::random();
421 let from = signer.address();
422 let args = SingleTransferArgs {
423 from,
424 to: address!("0x3333333333333333333333333333333333333333"),
425 token: address!("0x1111111111111111111111111111111111111111"),
426 executor: address!("0x1111111111111111111111111111111111111111"),
427 value: U256::from(100u64),
428 commitment: B256::repeat_byte(0xaa),
429 valid_after: Some(U256::from(10u64)),
430 valid_before: Some(U256::from(5u64)),
431 };
432 let r = super::single_transfer(&provider, &signer, args).await;
433 assert!(matches!(
434 r,
435 Err(SdkError::InvalidValidTimeWindow { .. })
436 ));
437 }
438
439 #[tokio::test]
440 async fn uni_commit_transfers_errors_on_empty_details() {
441 use alloy::providers::ProviderBuilder;
442 use crate::error::SdkError;
443 use crate::types::UniCommitTransfersArgs;
444
445 let provider = ProviderBuilder::new().connect_http("http://127.0.0.1:65534".parse().unwrap());
446 let signer = alloy::signers::local::PrivateKeySigner::random();
447 let a = address!("0x1111111111111111111111111111111111111111");
448 let args = UniCommitTransfersArgs {
449 from: a,
450 executor: a,
451 details: vec![],
452 commitment: B256::ZERO,
453 valid_after: None,
454 valid_before: None,
455 };
456 let r = super::uni_commit_transfers(&provider, &signer, args).await;
457 assert!(matches!(r, Err(SdkError::EmptyTransferDetails)));
458 }
459
460 #[tokio::test]
461 async fn batch_transfer_with_commit_errors_on_empty_details() {
462 use alloy::providers::ProviderBuilder;
463 use crate::error::SdkError;
464 use crate::types::BatchTransferWithCommitArgs;
465
466 let provider = ProviderBuilder::new().connect_http("http://127.0.0.1:65534".parse().unwrap());
467 let signer = alloy::signers::local::PrivateKeySigner::random();
468 let a = address!("0x1111111111111111111111111111111111111111");
469 let args = BatchTransferWithCommitArgs {
470 from: a,
471 executor: a,
472 details: vec![],
473 batch_commitment: B256::ZERO,
474 valid_after: None,
475 valid_before: None,
476 };
477 let r = super::batch_transfer_with_commit(&provider, &signer, args).await;
478 assert!(matches!(r, Err(SdkError::EmptyTransferDetails)));
479 }
480
481 #[tokio::test]
482 async fn uni_commit_transfers_errors_when_from_does_not_match_signer() {
483 use alloy::providers::ProviderBuilder;
484 use crate::error::SdkError;
485 use crate::types::{TransferDetail, UniCommitTransfersArgs};
486
487 let provider = ProviderBuilder::new().connect_http("http://127.0.0.1:65525".parse().unwrap());
488 let signer = alloy::signers::local::PrivateKeySigner::random();
489 let wrong_from = address!("0x1111111111111111111111111111111111111111");
490 assert_ne!(signer.address(), wrong_from);
491 let detail = TransferDetail {
492 to: address!("0x3333333333333333333333333333333333333333"),
493 token: address!("0x4444444444444444444444444444444444444444"),
494 value: U256::from(1u64),
495 };
496 let args = UniCommitTransfersArgs {
497 from: wrong_from,
498 executor: signer.address(),
499 details: vec![detail],
500 commitment: B256::ZERO,
501 valid_after: None,
502 valid_before: None,
503 };
504 let r = super::uni_commit_transfers(&provider, &signer, args).await;
505 assert!(matches!(
506 r,
507 Err(SdkError::FromSignerMismatch { .. })
508 ));
509 }
510
511 #[tokio::test]
512 async fn batch_transfer_with_commit_errors_when_from_does_not_match_signer() {
513 use alloy::providers::ProviderBuilder;
514 use crate::error::SdkError;
515 use crate::types::{BatchTransferWithCommitArgs, CommittedTransferDetail};
516
517 let provider = ProviderBuilder::new().connect_http("http://127.0.0.1:65524".parse().unwrap());
518 let signer = alloy::signers::local::PrivateKeySigner::random();
519 let wrong_from = address!("0x1111111111111111111111111111111111111111");
520 assert_ne!(signer.address(), wrong_from);
521 let row = CommittedTransferDetail {
522 to: address!("0x3333333333333333333333333333333333333333"),
523 token: address!("0x4444444444444444444444444444444444444444"),
524 value: U256::from(1u64),
525 commitment: B256::repeat_byte(0xdd),
526 };
527 let args = BatchTransferWithCommitArgs {
528 from: wrong_from,
529 executor: signer.address(),
530 details: vec![row],
531 batch_commitment: B256::repeat_byte(0xdd),
532 valid_after: None,
533 valid_before: None,
534 };
535 let r = super::batch_transfer_with_commit(&provider, &signer, args).await;
536 assert!(matches!(
537 r,
538 Err(SdkError::FromSignerMismatch { .. })
539 ));
540 }
541
542 #[tokio::test]
543 async fn cancel_authorization_errors_when_authorizer_does_not_match_signer() {
544 use alloy::providers::ProviderBuilder;
545 use crate::error::SdkError;
546 use crate::types::CancelAuthorizationArgs;
547
548 let provider = ProviderBuilder::new().connect_http("http://127.0.0.1:65523".parse().unwrap());
549 let signer = alloy::signers::local::PrivateKeySigner::random();
550 let wrong_authorizer = address!("0x1111111111111111111111111111111111111111");
551 assert_ne!(signer.address(), wrong_authorizer);
552 let args = CancelAuthorizationArgs {
553 authorizer: wrong_authorizer,
554 commitment: B256::repeat_byte(0xee),
555 };
556 let r = super::cancel_authorization(&provider, &signer, args).await;
557 assert!(matches!(
558 r,
559 Err(SdkError::AuthorizerSignerMismatch { .. })
560 ));
561 }
562}