eth_twc_sdk/
sign.rs

1//! EIP-712 オフチェーン署名。
2//!
3//! - オンチェーン `eip712Domain()` からドメインを取得し、**ゼロ `salt`** は OpenZeppelin の domain separator と整合するよう [`domain_for_typed_data_sign`] から除外します。
4//! - Alloy では **[`Provider`] + [`Signer`]** でドメイン取得と署名の両方に足ります(別クライアントに分ける必要はありません)。
5
6use 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
27/// EIP-712 の `from` が実際に署名するキーの [`Signer::address`] と一致することを保証する。
28///
29/// 不一致のまま署名するとオンチェーンで `Invalid signature` になるため、呼び出し側の取り違えを防ぐ。
30fn 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
41/// `CancelAuthorization` の `authorizer` が署名キーと一致することを保証する。
42fn 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
53/// ERC-5267 の `eip712Domain().salt` がゼロワードのとき、OZ `EIP712` の domain separator と一致させるため **`salt` キーを付けない**形の [`Eip712Domain`] を構築する。
54///
55/// # 引数
56///
57/// * `name` — ドメイン名(コントラクトから取得した文字列)。
58/// * `version` — ドメインバージョン。
59/// * `chain_id` — EIP-155 チェーン ID(`U256`)。
60/// * `verifying_contract` — 検証コントラクトアドレス。
61/// * `salt` — ERC-5267 の `salt`。**ゼロなら**署名用 domain から省略される。
62///
63/// # 戻り値
64///
65/// [`sign_hash`](Signer::sign_hash) / [`eip712_signing_hash`](alloy::sol_types::SolStruct::eip712_signing_hash) に渡す [`Eip712Domain`]。
66pub 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
83/// コントラクトの `eip712Domain()` を読み、[`domain_for_typed_data_sign`] で正規化したドメインを返す。
84async 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
115/// 単一送金向け EIP-712(型 `TransferWithCommit`)に署名し、署名済みバンドルを返す。
116///
117/// `args.valid_after` / `args.valid_before` のデフォルトは `None` のとき `0` と [`U256::MAX`]。
118/// 解決後は `validAfter <= validBefore` が必須です。
119///
120/// # 引数
121///
122/// * `provider` — `eip712Domain()` 読み取りとチェーン検証に使用。
123/// * `signer` — [`sign_hash`](Signer::sign_hash) で EIP-712 ハッシュに署名。
124/// * `args` — メッセージ本体。`from` は [`Signer::address`] と一致させること([`crate::SdkError::FromSignerMismatch`])。その他 `executor` / `valid_after` / `valid_before` 等。
125///
126/// # 戻り値
127///
128/// オフチェーン転送用の [`SignedTransferWithCommit`](正規化済み `domain` と `signature` を含む)。
129///
130/// # エラー
131///
132/// [`crate::SdkError`] — [`crate::SdkError::FromSignerMismatch`]、[`crate::SdkError::InvalidValidTimeWindow`]、未設定アドレス、非対応チェーン、RPC 失敗、署名失敗など。
133pub 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
174/// ユニファイド明細(型 `UniCommitTransfers`)への署名。
175///
176/// # 引数・戻り値・エラー
177///
178/// [`single_transfer`] と同様のパターン。戻り値は [`SignedUniCommitTransfers`]。`args.details` が空のときは [`crate::SdkError::EmptyTransferDetails`]。
179///
180/// `args.from` は [`Signer::address`] と一致している必要があります([`crate::SdkError::FromSignerMismatch`])。
181pub 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
230/// バッチ明細(型 `BatchTransferWithCommit`)への署名。
231///
232/// # 引数・戻り値・エラー
233///
234/// [`single_transfer`] と同様。戻り値は [`SignedBatchTransferWithCommit`]。`args.details` が空のときは [`crate::SdkError::EmptyTransferDetails`]。
235///
236/// `args.from` は [`Signer::address`] と一致している必要があります([`crate::SdkError::FromSignerMismatch`])。
237pub 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
287/// 取消(型 `CancelAuthorization`)への署名。
288///
289/// # 引数
290///
291/// * `provider` — ドメイン取得・チェーン検証。
292/// * `signer` — ハッシュ署名。
293/// * `args` — `authorizer` と `commitment`(`authorizer` は [`Signer::address`] と一致させること)。
294///
295/// # 戻り値
296///
297/// [`SignedCancelAuthorization`]。
298///
299/// # エラー
300///
301/// [`crate::SdkError::AuthorizerSignerMismatch`] を含む [`crate::SdkError`] 系。
302pub 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}