eth_twc_sdk/send_transaction/
signature_transfer.rs1use 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
18async 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
36pub 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
88pub 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
135pub 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
183pub 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}