Skip to main content

soroban_cli/commands/tx/new/
clawback_claimable_balance.rs

1use 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    /// Balance ID of the claimable balance to clawback. Accepts multiple formats:
18    /// - API format with type prefix (72 chars): 000000006f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461
19    /// - Direct hash format (64 chars): 6f2179b31311fa8064760b48942c8e166702ba0b8fbe7358c4fd570421840461
20    /// - Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA
21    #[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    // Handle multiple formats:
51    // 1. Address format (base32): BAAMLBZI42AD52HKGIZOU7WFVZM6BPEJCLPL44QU2AT6TY3P57I5QDNYIA
52    // 2. API format with type prefix (72 hex chars): 000000006f2179b3...
53    // 3. Direct hash format (64 hex chars): 6f2179b3...
54
55    if balance_id.starts_with('B') && balance_id.len() > 50 {
56        // Address format - use stellar-strkey crate to decode claimable balance address
57        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        // Test that we correctly strip the prefix
121        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        // Test that direct format passes through unchanged
139        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        // Test that StrKey format can be decoded
173        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}