soroban_cli/commands/tx/new/
clawback_claimable_balance.rs1use clap::Parser;
2
3use crate::{commands::tx, xdr};
4
5#[derive(Parser, Debug, Clone)]
6#[group(skip)]
7pub struct Cmd {
8 #[command(flatten)]
9 pub tx: tx::Args,
10
11 #[clap(flatten)]
12 pub op: Args,
13}
14
15#[derive(Debug, clap::Args, Clone)]
16pub struct Args {
17 #[arg(long)]
22 pub balance_id: String,
23}
24
25impl TryFrom<&Cmd> for xdr::OperationBody {
26 type Error = tx::args::Error;
27 fn try_from(
28 Cmd {
29 tx: _,
30 op: Args { balance_id },
31 }: &Cmd,
32 ) -> Result<Self, Self::Error> {
33 let balance_id_bytes = parse_balance_id(balance_id)?;
34
35 let mut balance_id_array = [0u8; 32];
36 balance_id_array.copy_from_slice(&balance_id_bytes);
37
38 let claimable_balance_id =
39 xdr::ClaimableBalanceId::ClaimableBalanceIdTypeV0(xdr::Hash(balance_id_array));
40
41 Ok(xdr::OperationBody::ClawbackClaimableBalance(
42 xdr::ClawbackClaimableBalanceOp {
43 balance_id: claimable_balance_id,
44 },
45 ))
46 }
47}
48
49pub fn parse_balance_id(balance_id: &str) -> Result<Vec<u8>, tx::args::Error> {
50 if balance_id.starts_with('B') && balance_id.len() > 50 {
56 match stellar_strkey::Strkey::from_string(balance_id) {
58 Ok(stellar_strkey::Strkey::ClaimableBalance(stellar_strkey::ClaimableBalance::V0(
59 bytes,
60 ))) => Ok(bytes.to_vec()),
61 _ => Err(tx::args::Error::InvalidHex {
62 name: "balance-id".to_string(),
63 hex: balance_id.to_string(),
64 }),
65 }
66 } else {
67 let cleaned_balance_id = if balance_id.len() == 72 && balance_id.starts_with("00000000") {
68 &balance_id[8..]
69 } else {
70 balance_id
71 };
72
73 let balance_id_bytes =
74 hex::decode(cleaned_balance_id).map_err(|_| tx::args::Error::InvalidHex {
75 name: "balance-id".to_string(),
76 hex: balance_id.to_string(),
77 })?;
78
79 if balance_id_bytes.len() != 32 {
80 return Err(tx::args::Error::InvalidHex {
81 name: "balance-id".to_string(),
82 hex: balance_id.to_string(),
83 });
84 }
85
86 Ok(balance_id_bytes)
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn test_valid_balance_id_hex_parsing() {
96 let balance_id = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
97 let balance_id_bytes = hex::decode(balance_id).unwrap();
98 assert_eq!(balance_id_bytes.len(), 32);
99
100 let mut balance_id_array = [0u8; 32];
101 balance_id_array.copy_from_slice(&balance_id_bytes);
102
103 let claimable_balance_id =
104 xdr::ClaimableBalanceId::ClaimableBalanceIdTypeV0(xdr::Hash(balance_id_array));
105
106 let op = xdr::ClawbackClaimableBalanceOp {
107 balance_id: claimable_balance_id,
108 };
109
110 let xdr::ClaimableBalanceId::ClaimableBalanceIdTypeV0(hash) = op.balance_id;
111 assert_eq!(hash.0.to_vec(), balance_id_bytes);
112 }
113
114 #[test]
115 fn test_api_format_with_prefix() {
116 let api_format_id =
117 "000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461";
118 let expected_hash = "6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461";
119
120 let cleaned_id = if api_format_id.len() == 72 && api_format_id.starts_with("00000000") {
122 &api_format_id[8..]
123 } else {
124 api_format_id
125 };
126
127 assert_eq!(cleaned_id, expected_hash);
128 assert_eq!(cleaned_id.len(), 64);
129
130 let balance_id_bytes = hex::decode(cleaned_id).unwrap();
131 assert_eq!(balance_id_bytes.len(), 32);
132 }
133
134 #[test]
135 fn test_direct_hash_format() {
136 let direct_format_id = "6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461";
137
138 let cleaned_id = if direct_format_id.len() == 72 && direct_format_id.starts_with("00000000")
140 {
141 &direct_format_id[8..]
142 } else {
143 direct_format_id
144 };
145
146 assert_eq!(cleaned_id, direct_format_id);
147 assert_eq!(cleaned_id.len(), 64);
148
149 let balance_id_bytes = hex::decode(cleaned_id).unwrap();
150 assert_eq!(balance_id_bytes.len(), 32);
151 }
152
153 #[test]
154 fn test_invalid_balance_id_too_short() {
155 let balance_id = "0123456789abcdef";
156 let balance_id_bytes = hex::decode(balance_id).unwrap();
157 assert_ne!(balance_id_bytes.len(), 32);
158 }
159
160 #[test]
161 fn test_invalid_balance_id_too_long() {
162 let balance_id = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef00";
163 let balance_id_bytes = hex::decode(balance_id).unwrap();
164 assert_ne!(balance_id_bytes.len(), 32);
165 }
166
167 #[test]
168 fn test_strkey_format() {
169 let strkey_id = "BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA";
170 let expected_hex = "c58728e6803ee8ea3232ea7ec5ae59e0bc8912debe7214d027e9e36fefd1d80d";
171
172 let result = parse_balance_id(strkey_id);
174 assert!(result.is_ok(), "StrKey format should decode successfully");
175
176 let bytes = result.unwrap();
177 assert_eq!(bytes.len(), 32, "Should decode to 32 bytes");
178 assert_eq!(
179 hex::encode(&bytes),
180 expected_hex,
181 "Should match expected hex"
182 );
183 }
184
185 #[test]
186 fn test_invalid_balance_id_not_hex() {
187 let balance_id = "not_hex_characters_here_not_valid_at_all_exactly_64_chars";
188 let result = hex::decode(balance_id);
189 assert!(result.is_err());
190 }
191}