eth_twc_sdk/send_transaction/
signature_transfer.rs

1//! 署名付き `transferWithAuthorization` と `cancelAuthorization`。
2//!
3//! 送信前に [`crate::utils::assert_provider_supported_chain`] と、署名バンドルの [`Eip712Domain`](alloy::sol_types::Eip712Domain) がプロバイダのチェーンおよび [`crate::config::transfer_with_commitment_address`] が返すアドレスと一致することを検証します。
4//! ブロードキャスト前に **`.call()` 相当の `eth_call` シミュレーション**を行い、失敗時は送信しません。
5
6use alloy::primitives::{Address, TxHash};
7use alloy::providers::Provider;
8
9use crate::config::{self, assert_transfer_contract_configured};
10use crate::contract::{CommittedTransferDetail as SolCommitted, TransferDetail as SolDetail, TransferWithCommitment};
11use crate::error::SdkError;
12use crate::types::signed_data::{
13    SignedBatchTransferWithCommit, SignedCancelAuthorization, SignedTransferWithCommit,
14    SignedUniCommitTransfers,
15};
16use crate::utils::{assert_provider_supported_chain, assert_signed_domain_matches_provider_and_config};
17
18/// 署名付き送信の共通前提(コントラクト設定・チェーン・署名 `domain` と設定の整合)。
19async fn assert_signature_transfer_context<P: Provider>(
20    provider: &P,
21    signed_domain: &alloy::sol_types::Eip712Domain,
22) -> Result<(), SdkError> {
23    assert_transfer_contract_configured()?;
24    assert_provider_supported_chain(provider).await?;
25    let chain_id = provider.get_chain_id().await?;
26    let domain_chain = signed_domain
27        .chain_id
28        .ok_or_else(|| SdkError::Alloy("signed domain missing chainId".into()))?;
29    let vc = signed_domain
30        .verifying_contract
31        .ok_or_else(|| SdkError::Alloy("signed domain missing verifyingContract".into()))?;
32    assert_signed_domain_matches_provider_and_config(chain_id, domain_chain, vc)?;
33    Ok(())
34}
35
36/// `transferWithAuthorization`(8 引数オーバーロード)— 単一送金の署名実行。
37///
38/// # 引数
39///
40/// * `provider` — チェーン検証・コントラクトの `.call()`(シミュレーション)と `.send()` に使用。
41/// * `executor` — トランザクション送信者(`msg.sender`)。EIP-712 の `executor` と一致させる必要があります。
42/// * `signed` — [`crate::sign::single_transfer`] 等で生成した署名済みデータ。
43///
44/// # 戻り値
45///
46/// トランザクションハッシュ。
47///
48/// # エラー
49///
50/// [`crate::SdkError`] — 前提検証失敗、シミュレーション/送信失敗など。
51pub async fn single_transfer<P: Provider>(
52    provider: &P,
53    executor: Address,
54    signed: SignedTransferWithCommit,
55) -> Result<TxHash, SdkError> {
56    assert_signature_transfer_context(provider, &signed.domain).await?;
57    let c = TransferWithCommitment::new(config::transfer_with_commitment_address(), provider);
58    c.transferWithAuthorization_0(
59        signed.from,
60        signed.to,
61        signed.token,
62        signed.value,
63        signed.valid_after,
64        signed.valid_before,
65        signed.commitment,
66        signed.signature.clone(),
67    )
68    .from(executor)
69    .call()
70    .await?;
71    let pending = c
72        .transferWithAuthorization_0(
73            signed.from,
74            signed.to,
75            signed.token,
76            signed.value,
77            signed.valid_after,
78            signed.valid_before,
79            signed.commitment,
80            signed.signature,
81        )
82        .from(executor)
83        .send()
84        .await?;
85    Ok(*pending.tx_hash())
86}
87
88/// `transferWithAuthorization`(ユニファイド明細オーバーロード)。
89///
90/// # 引数・戻り値・エラー
91///
92/// [`single_transfer`] と同様。`signed` は [`SignedUniCommitTransfers`]。
93pub async fn unified_transfer<P: Provider>(
94    provider: &P,
95    executor: Address,
96    signed: SignedUniCommitTransfers,
97) -> Result<TxHash, SdkError> {
98    assert_signature_transfer_context(provider, &signed.domain).await?;
99    let details: Vec<SolDetail> = signed
100        .details
101        .iter()
102        .map(|d| SolDetail {
103            to: d.to,
104            token: d.token,
105            value: d.value,
106        })
107        .collect();
108    let c = TransferWithCommitment::new(config::transfer_with_commitment_address(), provider);
109    c.transferWithAuthorization_1(
110        signed.from,
111        details.clone(),
112        signed.valid_after,
113        signed.valid_before,
114        signed.commitment,
115        signed.signature.clone(),
116    )
117    .from(executor)
118    .call()
119    .await?;
120    let pending = c
121        .transferWithAuthorization_1(
122            signed.from,
123            details,
124            signed.valid_after,
125            signed.valid_before,
126            signed.commitment,
127            signed.signature,
128        )
129        .from(executor)
130        .send()
131        .await?;
132    Ok(*pending.tx_hash())
133}
134
135/// `transferWithAuthorization`(バッチ明細オーバーロード)。
136///
137/// # 引数・戻り値・エラー
138///
139/// [`single_transfer`] と同様。`signed` は [`SignedBatchTransferWithCommit`]。
140pub async fn batch_transfer<P: Provider>(
141    provider: &P,
142    executor: Address,
143    signed: SignedBatchTransferWithCommit,
144) -> Result<TxHash, SdkError> {
145    assert_signature_transfer_context(provider, &signed.domain).await?;
146    let details: Vec<SolCommitted> = signed
147        .details
148        .iter()
149        .map(|d| SolCommitted {
150            to: d.to,
151            token: d.token,
152            value: d.value,
153            commitment: d.commitment,
154        })
155        .collect();
156    let c = TransferWithCommitment::new(config::transfer_with_commitment_address(), provider);
157    c.transferWithAuthorization_2(
158        signed.from,
159        details.clone(),
160        signed.valid_after,
161        signed.valid_before,
162        signed.batch_commitment,
163        signed.signature.clone(),
164    )
165    .from(executor)
166    .call()
167    .await?;
168    let pending = c
169        .transferWithAuthorization_2(
170            signed.from,
171            details,
172            signed.valid_after,
173            signed.valid_before,
174            signed.batch_commitment,
175            signed.signature,
176        )
177        .from(executor)
178        .send()
179        .await?;
180    Ok(*pending.tx_hash())
181}
182
183/// `cancelAuthorization`。
184///
185/// # 引数
186///
187/// * `provider` — [`single_transfer`] と同じ。
188/// * `executor` — トランザクション送信者(`msg.sender`)。
189/// * `signed` — [`crate::sign::cancel_authorization`] で生成したデータ。
190///
191/// # 戻り値
192///
193/// トランザクションハッシュ。
194pub async fn cancel_authorization<P: Provider>(
195    provider: &P,
196    executor: Address,
197    signed: SignedCancelAuthorization,
198) -> Result<TxHash, SdkError> {
199    assert_signature_transfer_context(provider, &signed.domain).await?;
200    let c = TransferWithCommitment::new(config::transfer_with_commitment_address(), provider);
201    c.cancelAuthorization(
202        signed.authorizer,
203        signed.commitment,
204        signed.signature.clone(),
205    )
206    .from(executor)
207    .call()
208    .await?;
209    let pending = c
210        .cancelAuthorization(signed.authorizer, signed.commitment, signed.signature)
211        .from(executor)
212        .send()
213        .await?;
214    Ok(*pending.tx_hash())
215}
216
217#[cfg(test)]
218mod tests {
219    use alloy::primitives::{address, Bytes, B256, U256};
220    use alloy::providers::ProviderBuilder;
221    use alloy::sol_types::Eip712Domain;
222    use serde_json::json;
223    use wiremock::{matchers::method, Mock, MockServer, ResponseTemplate};
224
225    use crate::config;
226    use crate::types::SignedTransferWithCommit;
227
228    use super::single_transfer;
229
230    async fn mock_rpc_chain_and_code(chain_id_hex: &str, code_hex: &str) -> MockServer {
231        let server = MockServer::start().await;
232        let cid = chain_id_hex.to_string();
233        let code = code_hex.to_string();
234        Mock::given(method("POST"))
235            .respond_with(move |req: &wiremock::Request| {
236                let body: serde_json::Value =
237                    serde_json::from_slice(&req.body).unwrap_or(json!({}));
238                let id = body.get("id").cloned().unwrap_or(json!(1));
239                let m = body.get("method").and_then(|x| x.as_str()).unwrap_or("");
240                let result = match m {
241                    "eth_chainId" => json!(cid.clone()),
242                    "eth_getCode" => json!(code.clone()),
243                    _ => json!(null),
244                };
245                ResponseTemplate::new(200).set_body_json(json!({
246                    "jsonrpc": "2.0",
247                    "id": id,
248                    "result": result
249                }))
250            })
251            .mount(&server)
252            .await;
253        server
254    }
255
256    fn sample_signed(domain: Eip712Domain) -> SignedTransferWithCommit {
257        let addr = address!("0x1111111111111111111111111111111111111111");
258        SignedTransferWithCommit {
259            domain,
260            from: addr,
261            to: address!("0x3333333333333333333333333333333333333333"),
262            token: addr,
263            value: U256::from(100u64),
264            commitment: B256::repeat_byte(0xaa),
265            valid_after: U256::ZERO,
266            valid_before: U256::MAX,
267            signature: Bytes::from(vec![0xcc; 65]),
268        }
269    }
270
271    #[tokio::test]
272    async fn single_transfer_errors_when_domain_chain_id_differs_from_provider() {
273        let server = mock_rpc_chain_and_code("0x1", "0x6000").await;
274        let url = server.uri().parse().unwrap();
275        let provider = ProviderBuilder::new().connect_http(url);
276
277        let domain = crate::sign::domain_for_typed_data_sign(
278            "TransferWithCommitment".to_string(),
279            "1".to_string(),
280            U256::from(137u64),
281            config::transfer_with_commitment_address(),
282            B256::ZERO,
283        );
284        let signed = sample_signed(domain);
285        let exec = address!("0x1111111111111111111111111111111111111111");
286
287        let r = single_transfer(&provider, exec, signed).await;
288        assert!(matches!(
289            r,
290            Err(crate::SdkError::DomainChainIdMismatch { .. })
291        ));
292    }
293
294    #[tokio::test]
295    async fn single_transfer_errors_when_domain_verifying_contract_mismatches_config() {
296        let server = mock_rpc_chain_and_code("0x1", "0x6000").await;
297        let url = server.uri().parse().unwrap();
298        let provider = ProviderBuilder::new().connect_http(url);
299
300        let domain = crate::sign::domain_for_typed_data_sign(
301            "TransferWithCommitment".to_string(),
302            "1".to_string(),
303            U256::from(1u64),
304            address!("0x5555555555555555555555555555555555555555"),
305            B256::ZERO,
306        );
307        let signed = sample_signed(domain);
308        let exec = address!("0x1111111111111111111111111111111111111111");
309
310        let r = single_transfer(&provider, exec, signed).await;
311        assert!(matches!(
312            r,
313            Err(crate::SdkError::DomainVerifyingContractMismatch { .. })
314        ));
315    }
316}
317
318#[cfg(all(test, not(feature = "test-config")))]
319mod tests_contract_not_deployed {
320    use alloy::primitives::{address, Bytes, B256, U256};
321    use alloy::providers::ProviderBuilder;
322    use alloy::sol_types::Eip712Domain;
323    use serde_json::json;
324    use wiremock::{matchers::method, Mock, MockServer, ResponseTemplate};
325
326    use crate::config;
327    use crate::types::SignedTransferWithCommit;
328
329    use super::single_transfer;
330
331    async fn mock_rpc_chain_and_code(chain_id_hex: &str, code_hex: &str) -> MockServer {
332        let server = MockServer::start().await;
333        let cid = chain_id_hex.to_string();
334        let code = code_hex.to_string();
335        Mock::given(method("POST"))
336            .respond_with(move |req: &wiremock::Request| {
337                let body: serde_json::Value =
338                    serde_json::from_slice(&req.body).unwrap_or(json!({}));
339                let id = body.get("id").cloned().unwrap_or(json!(1));
340                let m = body.get("method").and_then(|x| x.as_str()).unwrap_or("");
341                let result = match m {
342                    "eth_chainId" => json!(cid.clone()),
343                    "eth_getCode" => json!(code.clone()),
344                    _ => json!(null),
345                };
346                ResponseTemplate::new(200).set_body_json(json!({
347                    "jsonrpc": "2.0",
348                    "id": id,
349                    "result": result
350                }))
351            })
352            .mount(&server)
353            .await;
354        server
355    }
356
357    #[tokio::test]
358    async fn single_transfer_fails_when_expected_twc_not_deployed_without_test_config() {
359        let server = mock_rpc_chain_and_code("0x1", "0x").await;
360        let url = server.uri().parse().unwrap();
361        let provider = ProviderBuilder::new().connect_http(url);
362
363        let twc = config::transfer_with_commitment_address();
364        let domain = Eip712Domain {
365            name: Some("TransferWithCommitment".into()),
366            version: Some("1".into()),
367            chain_id: Some(U256::from(1u64)),
368            verifying_contract: Some(twc),
369            salt: None,
370        };
371        let addr = address!("0x1111111111111111111111111111111111111111");
372        let signed = SignedTransferWithCommit {
373            domain,
374            from: addr,
375            to: address!("0x3333333333333333333333333333333333333333"),
376            token: addr,
377            value: U256::from(100u64),
378            commitment: B256::repeat_byte(0xaa),
379            valid_after: U256::ZERO,
380            valid_before: U256::MAX,
381            signature: Bytes::from(vec![0xcc; 65]),
382        };
383        let r = single_transfer(&provider, addr, signed).await;
384        assert!(matches!(
385            r,
386            Err(crate::SdkError::ContractNotDeployed { .. })
387        ));
388    }
389}