Skip to main content

synth_ai_core/
x402.rs

1//! x402 client-side helpers (HTTP 402 machine payments).
2//!
3//! Synth uses x402 (Stripe-compatible) headers:
4//! - `PAYMENT-REQUIRED` (server -> client)
5//! - `PAYMENT-SIGNATURE` (client -> server)
6//!
7//! This module implements client-side creation of `PAYMENT-SIGNATURE` for the
8//! x402 "exact" scheme on EVM networks (EIP-3009 / TransferWithAuthorization).
9
10use base64::{engine::general_purpose, Engine as _};
11use chrono::Utc;
12use k256::ecdsa::SigningKey;
13#[cfg(test)]
14use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
15use k256::elliptic_curve::rand_core::{OsRng, RngCore as _};
16use serde::{Deserialize, Serialize};
17use sha3::{Digest as _, Keccak256};
18use thiserror::Error;
19
20const X402_VERSION_V2: i64 = 2;
21const DEFAULT_VALIDITY_BUFFER_SECONDS: i64 = 30;
22
23// EIP-712 type strings (must match upstream x402 / eth_account behavior).
24const EIP712_DOMAIN_TYPE: &str =
25    "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
26const TRANSFER_WITH_AUTHORIZATION_TYPE: &str = "TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)";
27
28#[derive(Debug, Error)]
29pub enum X402Error {
30    #[error("missing payment requirements")]
31    MissingRequirements,
32
33    #[error("unsupported payment scheme: {0}")]
34    UnsupportedScheme(String),
35
36    #[error("invalid base64 header: {0}")]
37    Base64(#[from] base64::DecodeError),
38
39    #[error("invalid json: {0}")]
40    Json(#[from] serde_json::Error),
41
42    #[error("invalid private key")]
43    InvalidPrivateKey,
44
45    #[error("invalid address: {0}")]
46    InvalidAddress(String),
47
48    #[error("invalid u256 decimal: {0}")]
49    InvalidU256(String),
50
51    #[error("signing failed")]
52    SignFailure,
53
54    #[error("malformed payment payload: {0}")]
55    MalformedPayload(String),
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ResourceInfo {
60    pub url: String,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub description: Option<String>,
63    #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
64    pub mime_type: Option<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct PaymentRequirements {
69    pub scheme: String,
70    pub network: String,
71    pub asset: String,
72    pub amount: String,
73    #[serde(rename = "payTo")]
74    pub pay_to: String,
75    #[serde(rename = "maxTimeoutSeconds")]
76    pub max_timeout_seconds: i64,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub extra: Option<serde_json::Value>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PaymentRequired {
83    #[serde(rename = "x402Version")]
84    pub x402_version: i64,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub error: Option<String>,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub resource: Option<ResourceInfo>,
89    pub accepts: Vec<PaymentRequirements>,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub extensions: Option<serde_json::Value>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct PaymentPayload {
96    #[serde(rename = "x402Version")]
97    pub x402_version: i64,
98    pub payload: serde_json::Value,
99    pub accepted: PaymentRequirements,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub resource: Option<ResourceInfo>,
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub extensions: Option<serde_json::Value>,
104}
105
106#[derive(Debug, Clone)]
107struct Eip3009Authorization {
108    from: [u8; 20],
109    to: [u8; 20],
110    value: [u8; 32],
111    valid_after: u64,
112    valid_before: u64,
113    nonce: [u8; 32],
114}
115
116#[derive(Clone)]
117pub struct X402Payer {
118    signing_key: SigningKey,
119    address: String,
120}
121
122impl X402Payer {
123    /// Build a payer from environment configuration.
124    ///
125    /// Looks for:
126    /// - `SYNTH_X402_PRIVATE_KEY`
127    /// - `SYNTH_X402_EVM_PRIVATE_KEY`
128    /// - `X402_PRIVATE_KEY`
129    pub fn from_env() -> Option<Self> {
130        let raw = std::env::var("SYNTH_X402_PRIVATE_KEY")
131            .ok()
132            .filter(|v| !v.trim().is_empty())
133            .or_else(|| {
134                std::env::var("SYNTH_X402_EVM_PRIVATE_KEY")
135                    .ok()
136                    .filter(|v| !v.trim().is_empty())
137            })
138            .or_else(|| {
139                std::env::var("X402_PRIVATE_KEY")
140                    .ok()
141                    .filter(|v| !v.trim().is_empty())
142            })?;
143
144        let signing_key = parse_signing_key(&raw).ok()?;
145        let address = checksum_address(address_from_signing_key(&signing_key));
146        Some(Self {
147            signing_key,
148            address,
149        })
150    }
151
152    pub fn address(&self) -> &str {
153        &self.address
154    }
155
156    /// Create a `PAYMENT-SIGNATURE` header value from a `PAYMENT-REQUIRED` header value.
157    pub fn build_payment_signature_header(
158        &self,
159        payment_required_header: &str,
160    ) -> Result<String, X402Error> {
161        let payment_required = decode_payment_required_header(payment_required_header)?;
162        let accepted = select_accepted_requirements(&payment_required)?;
163        let payment_payload = self.create_payment_payload(&payment_required, accepted)?;
164        encode_payment_signature_header(&payment_payload)
165    }
166
167    fn create_payment_payload(
168        &self,
169        payment_required: &PaymentRequired,
170        accepted: &PaymentRequirements,
171    ) -> Result<PaymentPayload, X402Error> {
172        if accepted.scheme != "exact" {
173            return Err(X402Error::UnsupportedScheme(accepted.scheme.clone()));
174        }
175
176        let chain_id = evm_chain_id_from_network(&accepted.network)?;
177        let verifying_contract = parse_address(&accepted.asset)?;
178        let token_name = accepted
179            .extra
180            .as_ref()
181            .and_then(|v| v.get("name"))
182            .and_then(|v| v.as_str())
183            .unwrap_or("USDC");
184        let token_version = accepted
185            .extra
186            .as_ref()
187            .and_then(|v| v.get("version"))
188            .and_then(|v| v.as_str())
189            .unwrap_or("1");
190
191        let mut nonce_bytes = [0u8; 32];
192        OsRng.fill_bytes(&mut nonce_bytes);
193        let nonce_hex = format!("0x{}", hex::encode(nonce_bytes));
194
195        let now = Utc::now().timestamp();
196        let valid_after = (now - DEFAULT_VALIDITY_BUFFER_SECONDS).max(0) as u64;
197        let valid_before = (now + accepted.max_timeout_seconds.max(1)).max(0) as u64;
198
199        let from_addr = address_from_signing_key(&self.signing_key);
200        let to_addr = parse_address(&accepted.pay_to)?;
201        let value_u256 = u256_from_dec_str(&accepted.amount)?;
202
203        let auth = Eip3009Authorization {
204            from: from_addr,
205            to: to_addr,
206            value: value_u256,
207            valid_after,
208            valid_before,
209            nonce: nonce_bytes,
210        };
211
212        let signature_hex = sign_eip3009_authorization(
213            &self.signing_key,
214            chain_id,
215            verifying_contract,
216            token_name,
217            token_version,
218            &auth,
219        )?;
220
221        let payload = serde_json::json!({
222            "authorization": {
223                "from": self.address,
224                "to": accepted.pay_to,
225                "value": accepted.amount,
226                "validAfter": valid_after.to_string(),
227                "validBefore": valid_before.to_string(),
228                "nonce": nonce_hex,
229            },
230            "signature": signature_hex,
231        });
232
233        Ok(PaymentPayload {
234            x402_version: X402_VERSION_V2,
235            payload,
236            accepted: accepted.clone(),
237            resource: payment_required.resource.clone(),
238            extensions: payment_required.extensions.clone(),
239        })
240    }
241}
242
243/// Decode a `PAYMENT-SIGNATURE` header value into a typed payload.
244pub fn decode_payment_signature_header(value: &str) -> Result<PaymentPayload, X402Error> {
245    let decoded = general_purpose::STANDARD.decode(value.trim())?;
246    Ok(serde_json::from_slice(&decoded)?)
247}
248
249pub fn decode_payment_required_header(value: &str) -> Result<PaymentRequired, X402Error> {
250    let decoded = general_purpose::STANDARD.decode(value.trim())?;
251    Ok(serde_json::from_slice(&decoded)?)
252}
253
254pub fn encode_payment_signature_header(payload: &PaymentPayload) -> Result<String, X402Error> {
255    let json = serde_json::to_vec(payload)?;
256    Ok(general_purpose::STANDARD.encode(json))
257}
258
259/// Recover the payer address (EIP-55 checksummed) from an exact-scheme payment payload.
260///
261/// This is primarily used for tests and debugging to validate that we are signing
262/// EIP-3009 (TransferWithAuthorization) correctly.
263#[cfg(test)]
264pub(crate) fn recover_payer_address_from_payment_payload(
265    payload: &PaymentPayload,
266) -> Result<String, X402Error> {
267    if payload.accepted.scheme != "exact" {
268        return Err(X402Error::UnsupportedScheme(
269            payload.accepted.scheme.clone(),
270        ));
271    }
272
273    let chain_id = evm_chain_id_from_network(&payload.accepted.network)?;
274    let verifying_contract = parse_address(&payload.accepted.asset)?;
275    let token_name = payload
276        .accepted
277        .extra
278        .as_ref()
279        .and_then(|v| v.get("name"))
280        .and_then(|v| v.as_str())
281        .unwrap_or("USDC");
282    let token_version = payload
283        .accepted
284        .extra
285        .as_ref()
286        .and_then(|v| v.get("version"))
287        .and_then(|v| v.as_str())
288        .unwrap_or("1");
289
290    let auth_obj = payload
291        .payload
292        .get("authorization")
293        .and_then(|v| v.as_object())
294        .ok_or_else(|| X402Error::MalformedPayload("missing authorization".to_string()))?;
295
296    let from_str = auth_obj
297        .get("from")
298        .and_then(|v| v.as_str())
299        .ok_or_else(|| X402Error::MalformedPayload("missing from".to_string()))?;
300    let to_str = auth_obj
301        .get("to")
302        .and_then(|v| v.as_str())
303        .ok_or_else(|| X402Error::MalformedPayload("missing to".to_string()))?;
304    let value_str = auth_obj
305        .get("value")
306        .and_then(|v| v.as_str())
307        .ok_or_else(|| X402Error::MalformedPayload("missing value".to_string()))?;
308    let valid_after_str = auth_obj
309        .get("validAfter")
310        .and_then(|v| v.as_str())
311        .ok_or_else(|| X402Error::MalformedPayload("missing validAfter".to_string()))?;
312    let valid_before_str = auth_obj
313        .get("validBefore")
314        .and_then(|v| v.as_str())
315        .ok_or_else(|| X402Error::MalformedPayload("missing validBefore".to_string()))?;
316    let nonce_str = auth_obj
317        .get("nonce")
318        .and_then(|v| v.as_str())
319        .ok_or_else(|| X402Error::MalformedPayload("missing nonce".to_string()))?;
320
321    let sig_str = payload
322        .payload
323        .get("signature")
324        .and_then(|v| v.as_str())
325        .ok_or_else(|| X402Error::MalformedPayload("missing signature".to_string()))?;
326
327    let nonce_hex = nonce_str
328        .trim()
329        .strip_prefix("0x")
330        .unwrap_or(nonce_str.trim());
331    let nonce_bytes =
332        hex::decode(nonce_hex).map_err(|_| X402Error::InvalidU256(nonce_str.to_string()))?;
333    if nonce_bytes.len() != 32 {
334        return Err(X402Error::InvalidU256(nonce_str.to_string()));
335    }
336    let mut nonce = [0u8; 32];
337    nonce.copy_from_slice(&nonce_bytes);
338
339    let auth = Eip3009Authorization {
340        from: parse_address(from_str)?,
341        to: parse_address(to_str)?,
342        value: u256_from_dec_str(value_str)?,
343        valid_after: valid_after_str
344            .parse::<u64>()
345            .map_err(|_| X402Error::InvalidU256(valid_after_str.to_string()))?,
346        valid_before: valid_before_str
347            .parse::<u64>()
348            .map_err(|_| X402Error::InvalidU256(valid_before_str.to_string()))?,
349        nonce,
350    };
351
352    let digest = eip712_signing_hash_transfer_with_authorization(
353        chain_id,
354        verifying_contract,
355        token_name,
356        token_version,
357        &auth,
358    );
359
360    let sig_hex = sig_str.trim().strip_prefix("0x").unwrap_or(sig_str.trim());
361    let sig_bytes = hex::decode(sig_hex).map_err(|_| X402Error::SignFailure)?;
362    if sig_bytes.len() != 65 {
363        return Err(X402Error::SignFailure);
364    }
365    let v = sig_bytes[64];
366    if v < 27 {
367        return Err(X402Error::SignFailure);
368    }
369    let recid = RecoveryId::try_from(v - 27).map_err(|_| X402Error::SignFailure)?;
370    let sig = Signature::from_slice(&sig_bytes[..64]).map_err(|_| X402Error::SignFailure)?;
371
372    let recovered = VerifyingKey::recover_from_prehash(&digest, &sig, recid)
373        .map_err(|_| X402Error::SignFailure)?;
374    Ok(checksum_address(address_from_verifying_key(&recovered)))
375}
376
377fn select_accepted_requirements<'a>(
378    payment_required: &'a PaymentRequired,
379) -> Result<&'a PaymentRequirements, X402Error> {
380    if payment_required.accepts.is_empty() {
381        return Err(X402Error::MissingRequirements);
382    }
383
384    // Prefer exact scheme.
385    if let Some(req) = payment_required
386        .accepts
387        .iter()
388        .find(|req| req.scheme == "exact")
389    {
390        return Ok(req);
391    }
392
393    Ok(&payment_required.accepts[0])
394}
395
396fn parse_signing_key(raw: &str) -> Result<SigningKey, X402Error> {
397    let s = raw.trim();
398    let s = s.strip_prefix("0x").unwrap_or(s);
399    let bytes = hex::decode(s).map_err(|_| X402Error::InvalidPrivateKey)?;
400    if bytes.len() != 32 {
401        return Err(X402Error::InvalidPrivateKey);
402    }
403    let mut buf = [0u8; 32];
404    buf.copy_from_slice(&bytes);
405    SigningKey::from_bytes(&buf.into()).map_err(|_| X402Error::InvalidPrivateKey)
406}
407
408fn address_from_signing_key(signing_key: &SigningKey) -> [u8; 20] {
409    // Ethereum address = last 20 bytes of keccak256(uncompressed_pubkey[1..]).
410    let verify_key = signing_key.verifying_key();
411    let encoded = verify_key.to_encoded_point(false);
412    let bytes = encoded.as_bytes();
413    // bytes[0] == 0x04, then 32-byte X, 32-byte Y.
414    let hash = keccak256(&bytes[1..]);
415    let mut addr = [0u8; 20];
416    addr.copy_from_slice(&hash[12..]);
417    addr
418}
419
420#[cfg(test)]
421fn address_from_verifying_key(vk: &VerifyingKey) -> [u8; 20] {
422    let encoded = vk.to_encoded_point(false);
423    let bytes = encoded.as_bytes();
424    let hash = keccak256(&bytes[1..]);
425    let mut addr = [0u8; 20];
426    addr.copy_from_slice(&hash[12..]);
427    addr
428}
429
430fn checksum_address(address: [u8; 20]) -> String {
431    // EIP-55 checksum.
432    let hex_lower = hex::encode(address);
433    let hash = keccak256(hex_lower.as_bytes());
434    let mut out = String::with_capacity(2 + 40);
435    out.push_str("0x");
436
437    for (i, ch) in hex_lower.chars().enumerate() {
438        let nibble = if i % 2 == 0 {
439            (hash[i / 2] >> 4) & 0x0f
440        } else {
441            hash[i / 2] & 0x0f
442        };
443        if ch.is_ascii_hexdigit() && ch.is_ascii_alphabetic() && nibble >= 8 {
444            out.push(ch.to_ascii_uppercase());
445        } else {
446            out.push(ch);
447        }
448    }
449
450    out
451}
452
453fn parse_address(raw: &str) -> Result<[u8; 20], X402Error> {
454    let s = raw.trim();
455    let s = s.strip_prefix("0x").unwrap_or(s);
456    if s.len() != 40 {
457        return Err(X402Error::InvalidAddress(raw.to_string()));
458    }
459    let bytes = hex::decode(s).map_err(|_| X402Error::InvalidAddress(raw.to_string()))?;
460    let mut out = [0u8; 20];
461    out.copy_from_slice(&bytes);
462    Ok(out)
463}
464
465fn evm_chain_id_from_network(network: &str) -> Result<u64, X402Error> {
466    // Only support CAIP-2 "eip155:<chain_id>" for now.
467    let network = network.trim();
468    let Some(rest) = network.strip_prefix("eip155:") else {
469        return Err(X402Error::InvalidU256(network.to_string()));
470    };
471    rest.parse::<u64>()
472        .map_err(|_| X402Error::InvalidU256(network.to_string()))
473}
474
475fn keccak256(data: &[u8]) -> [u8; 32] {
476    let mut hasher = Keccak256::new();
477    hasher.update(data);
478    let digest = hasher.finalize();
479    let mut out = [0u8; 32];
480    out.copy_from_slice(&digest);
481    out
482}
483
484fn encode_address(addr: [u8; 20]) -> [u8; 32] {
485    let mut out = [0u8; 32];
486    out[12..].copy_from_slice(&addr);
487    out
488}
489
490fn encode_u64(value: u64) -> [u8; 32] {
491    let mut out = [0u8; 32];
492    out[24..].copy_from_slice(&value.to_be_bytes());
493    out
494}
495
496fn u256_from_dec_str(raw: &str) -> Result<[u8; 32], X402Error> {
497    let s = raw.trim();
498    if s.is_empty() {
499        return Err(X402Error::InvalidU256(raw.to_string()));
500    }
501    let mut out = [0u8; 32];
502    for ch in s.chars() {
503        if !ch.is_ascii_digit() {
504            return Err(X402Error::InvalidU256(raw.to_string()));
505        }
506        let digit = (ch as u8 - b'0') as u16;
507
508        // out = out * 10 + digit  (big-endian base-256)
509        let mut carry: u16 = digit;
510        for i in (0..32).rev() {
511            let val: u16 = (out[i] as u16) * 10 + carry;
512            out[i] = (val & 0xff) as u8;
513            carry = val >> 8;
514        }
515        if carry != 0 {
516            return Err(X402Error::InvalidU256(raw.to_string()));
517        }
518    }
519    Ok(out)
520}
521
522fn eip712_domain_separator(
523    name: &str,
524    version: &str,
525    chain_id: u64,
526    verifying_contract: [u8; 20],
527) -> [u8; 32] {
528    let type_hash = keccak256(EIP712_DOMAIN_TYPE.as_bytes());
529    let name_hash = keccak256(name.as_bytes());
530    let version_hash = keccak256(version.as_bytes());
531    let chain_id_enc = encode_u64(chain_id);
532    let verifying_contract_enc = encode_address(verifying_contract);
533
534    let mut encoded = Vec::with_capacity(32 * 5);
535    encoded.extend_from_slice(&type_hash);
536    encoded.extend_from_slice(&name_hash);
537    encoded.extend_from_slice(&version_hash);
538    encoded.extend_from_slice(&chain_id_enc);
539    encoded.extend_from_slice(&verifying_contract_enc);
540    keccak256(&encoded)
541}
542
543fn eip712_transfer_with_authorization_struct_hash(auth: &Eip3009Authorization) -> [u8; 32] {
544    let type_hash = keccak256(TRANSFER_WITH_AUTHORIZATION_TYPE.as_bytes());
545
546    let mut encoded = Vec::with_capacity(32 * 7);
547    encoded.extend_from_slice(&type_hash);
548    encoded.extend_from_slice(&encode_address(auth.from));
549    encoded.extend_from_slice(&encode_address(auth.to));
550    encoded.extend_from_slice(&auth.value);
551    encoded.extend_from_slice(&encode_u64(auth.valid_after));
552    encoded.extend_from_slice(&encode_u64(auth.valid_before));
553    encoded.extend_from_slice(&auth.nonce);
554    keccak256(&encoded)
555}
556
557fn eip712_signing_hash_transfer_with_authorization(
558    chain_id: u64,
559    verifying_contract: [u8; 20],
560    token_name: &str,
561    token_version: &str,
562    auth: &Eip3009Authorization,
563) -> [u8; 32] {
564    let domain_sep =
565        eip712_domain_separator(token_name, token_version, chain_id, verifying_contract);
566    let struct_hash = eip712_transfer_with_authorization_struct_hash(auth);
567
568    let mut encoded = Vec::with_capacity(2 + 32 + 32);
569    encoded.extend_from_slice(&[0x19, 0x01]);
570    encoded.extend_from_slice(&domain_sep);
571    encoded.extend_from_slice(&struct_hash);
572    keccak256(&encoded)
573}
574
575fn sign_eip3009_authorization(
576    signing_key: &SigningKey,
577    chain_id: u64,
578    verifying_contract: [u8; 20],
579    token_name: &str,
580    token_version: &str,
581    auth: &Eip3009Authorization,
582) -> Result<String, X402Error> {
583    let digest = eip712_signing_hash_transfer_with_authorization(
584        chain_id,
585        verifying_contract,
586        token_name,
587        token_version,
588        auth,
589    );
590
591    let (signature, recid) = signing_key
592        .sign_prehash_recoverable(&digest)
593        .map_err(|_| X402Error::SignFailure)?;
594
595    let sig64 = signature.to_bytes();
596    let recid_u8: u8 = recid.into();
597    let v = recid_u8.saturating_add(27);
598
599    let mut sig65 = [0u8; 65];
600    sig65[..64].copy_from_slice(sig64.as_slice());
601    sig65[64] = v;
602
603    Ok(format!("0x{}", hex::encode(sig65)))
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    #[test]
611    fn test_eip712_digest_matches_python_x402() {
612        // Golden computed with x402 (python) + eth_account:
613        // - chain_id=84532
614        // - verifying_contract=0x036CbD53842c5426634e7929541eC2318f3dCF7e
615        // - name=USDC version=2
616        // - from=0xFCAd0B19bB29D4674531d6f115237E16AfCE377c
617        // - to=0x1111111111111111111111111111111111111111
618        // - value=250000
619        // - validAfter=1700000000 validBefore=1700000300
620        // - nonce=0x1111... (32 bytes)
621        let signing_key =
622            parse_signing_key("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
623                .unwrap();
624        let from = address_from_signing_key(&signing_key);
625        assert_eq!(
626            checksum_address(from),
627            "0xFCAd0B19bB29D4674531d6f115237E16AfCE377c"
628        );
629
630        let auth = Eip3009Authorization {
631            from,
632            to: parse_address("0x1111111111111111111111111111111111111111").unwrap(),
633            value: u256_from_dec_str("250000").unwrap(),
634            valid_after: 1_700_000_000u64,
635            valid_before: 1_700_000_300u64,
636            nonce: [0x11u8; 32],
637        };
638
639        let digest = eip712_signing_hash_transfer_with_authorization(
640            84532,
641            parse_address("0x036CbD53842c5426634e7929541eC2318f3dCF7e").unwrap(),
642            "USDC",
643            "2",
644            &auth,
645        );
646
647        assert_eq!(
648            format!("0x{}", hex::encode(digest)),
649            "0x798f516cfe5a9cc10934b46623d65b0facb181da516e4dc7cfea11a16cc44a81"
650        );
651
652        let sig = sign_eip3009_authorization(
653            &signing_key,
654            84532,
655            parse_address("0x036CbD53842c5426634e7929541eC2318f3dCF7e").unwrap(),
656            "USDC",
657            "2",
658            &auth,
659        )
660        .unwrap();
661
662        assert_eq!(sig, "0xdbed925a525095c7d6933ab969b8421521c160de32d28e4628fa01913908382745a0b499c5562376e4d275e0626b30355fa03342bb7168dfe3dfae277eab4eb41c");
663    }
664}