openzeppelin_relayer/models/transaction/stellar/
asset.rs

1//! Asset types and conversions for Stellar transactions
2
3use crate::models::SignerError;
4use serde::{Deserialize, Serialize};
5use soroban_rs::xdr::{
6    AccountId, AlphaNum12, AlphaNum4, Asset, AssetCode12, AssetCode4, PublicKey as XdrPublicKey,
7    Uint256,
8};
9use std::convert::TryFrom;
10use std::str::FromStr;
11use stellar_strkey::ed25519::PublicKey;
12use utoipa::ToSchema;
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum AssetSpec {
17    Native,
18    Credit4 { code: String, issuer: String },
19    Credit12 { code: String, issuer: String },
20}
21
22impl TryFrom<AssetSpec> for Asset {
23    type Error = SignerError;
24    fn try_from(a: AssetSpec) -> Result<Self, Self::Error> {
25        Ok(match a {
26            AssetSpec::Native => Asset::Native,
27            AssetSpec::Credit4 { code, issuer } => {
28                let b = code.as_bytes();
29                if !(1..=4).contains(&b.len()) {
30                    return Err(SignerError::ConversionError("asset code 1-4 chars".into()));
31                }
32                let mut buf = [0u8; 4];
33                buf[..b.len()].copy_from_slice(b);
34
35                let issuer_pk = PublicKey::from_str(&issuer)
36                    .map_err(|e| SignerError::ConversionError(format!("Invalid issuer: {}", e)))?;
37
38                let uint256 = Uint256(issuer_pk.0);
39                let pk = XdrPublicKey::PublicKeyTypeEd25519(uint256);
40                let account_id = AccountId(pk);
41
42                Asset::CreditAlphanum4(AlphaNum4 {
43                    asset_code: AssetCode4(buf),
44                    issuer: account_id,
45                })
46            }
47            AssetSpec::Credit12 { code, issuer } => {
48                let b = code.as_bytes();
49                if !(5..=12).contains(&b.len()) {
50                    return Err(SignerError::ConversionError("asset code 5-12 chars".into()));
51                }
52                let mut buf = [0u8; 12];
53                buf[..b.len()].copy_from_slice(b);
54
55                let issuer_pk = PublicKey::from_str(&issuer)
56                    .map_err(|e| SignerError::ConversionError(format!("Invalid issuer: {}", e)))?;
57
58                let uint256 = Uint256(issuer_pk.0);
59                let pk = XdrPublicKey::PublicKeyTypeEd25519(uint256);
60                let account_id = AccountId(pk);
61
62                Asset::CreditAlphanum12(AlphaNum12 {
63                    asset_code: AssetCode12(buf),
64                    issuer: account_id,
65                })
66            }
67        })
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
76
77    #[test]
78    fn test_native_asset() {
79        let spec = AssetSpec::Native;
80        let asset = Asset::try_from(spec).unwrap();
81        assert!(matches!(asset, Asset::Native));
82    }
83
84    #[test]
85    fn test_credit4_asset() {
86        let spec = AssetSpec::Credit4 {
87            code: "USDC".to_string(),
88            issuer: TEST_PK.to_string(),
89        };
90        let asset = Asset::try_from(spec).unwrap();
91        assert!(matches!(asset, Asset::CreditAlphanum4(_)));
92    }
93
94    #[test]
95    fn test_invalid_asset_code() {
96        let spec = AssetSpec::Credit4 {
97            code: "TOOLONG".to_string(),
98            issuer: TEST_PK.to_string(),
99        };
100        assert!(Asset::try_from(spec).is_err());
101    }
102
103    #[test]
104    fn test_asset_spec_serde() {
105        let spec = AssetSpec::Credit4 {
106            code: "USDC".to_string(),
107            issuer: TEST_PK.to_string(),
108        };
109        let json = serde_json::to_string(&spec).unwrap();
110        assert!(json.contains("credit4"));
111        assert!(json.contains("USDC"));
112
113        let deserialized: AssetSpec = serde_json::from_str(&json).unwrap();
114        assert_eq!(spec, deserialized);
115    }
116
117    #[test]
118    fn test_asset_spec_json_format() {
119        // Test Native
120        let native = AssetSpec::Native;
121        let native_json = serde_json::to_value(&native).unwrap();
122        assert_eq!(native_json, serde_json::json!({"type": "native"}));
123
124        // Test Credit4
125        let credit4 = AssetSpec::Credit4 {
126            code: "USDC".to_string(),
127            issuer: TEST_PK.to_string(),
128        };
129        let credit4_json = serde_json::to_value(&credit4).unwrap();
130        assert_eq!(
131            credit4_json,
132            serde_json::json!({
133                "type": "credit4",
134                "code": "USDC",
135                "issuer": TEST_PK
136            })
137        );
138
139        // Test Credit12
140        let credit12 = AssetSpec::Credit12 {
141            code: "LONGASSET".to_string(),
142            issuer: TEST_PK.to_string(),
143        };
144        let credit12_json = serde_json::to_value(&credit12).unwrap();
145        assert_eq!(
146            credit12_json,
147            serde_json::json!({
148                "type": "credit12",
149                "code": "LONGASSET",
150                "issuer": TEST_PK
151            })
152        );
153    }
154}