openzeppelin_relayer/models/transaction/stellar/
asset.rs1use 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 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 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 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}