Skip to main content

soroban_cli/commands/tx/new/
create_claimable_balance.rs

1use clap::Parser;
2use serde_json;
3use std::str::FromStr;
4
5use crate::{commands::tx, config::address, tx::builder, xdr};
6
7fn parse_claimant_string(input: &str) -> Result<(String, Option<xdr::ClaimPredicate>), String> {
8    if let Some((account, predicate_str)) = input.split_once(':') {
9        let predicate: xdr::ClaimPredicate = serde_json::from_str(predicate_str)
10            .map_err(|e| format!("Invalid predicate JSON: {e}"))?;
11        Ok((account.to_string(), Some(predicate)))
12    } else {
13        Ok((input.to_string(), None))
14    }
15}
16
17#[derive(Parser, Debug, Clone)]
18#[group(skip)]
19pub struct Cmd {
20    #[command(flatten)]
21    pub tx: tx::Args,
22
23    #[clap(flatten)]
24    pub op: Args,
25}
26
27#[derive(Debug, clap::Args, Clone)]
28pub struct Args {
29    /// Asset to be held in the ClaimableBalanceEntry
30    #[arg(long, default_value = "native")]
31    pub asset: builder::Asset,
32
33    /// Amount of asset to store in the entry, in stroops. 1 stroop = 0.0000001 of the asset.
34    #[arg(long)]
35    pub amount: builder::Amount,
36
37    /// Claimants of the claimable balance. Format: account_id or account_id:predicate_json
38    /// Can be specified multiple times for multiple claimants.
39    ///
40    /// Examples:
41    ///
42    /// - `--claimant alice (unconditional)`
43    /// - `--claimant 'bob:{"before_absolute_time":"1735689599"}'`
44    /// - `--claimant 'charlie:{"and":[{"before_absolute_time":"1735689599"},{"before_relative_time":"3600"}]}'`
45    #[arg(long = "claimant", action = clap::ArgAction::Append)]
46    pub claimants: Vec<String>,
47}
48
49impl TryFrom<&Cmd> for xdr::OperationBody {
50    type Error = tx::args::Error;
51    fn try_from(
52        Cmd {
53            tx,
54            op:
55                Args {
56                    asset,
57                    amount,
58                    claimants,
59                },
60        }: &Cmd,
61    ) -> Result<Self, Self::Error> {
62        let claimants_vec = claimants
63            .iter()
64            .map(|claimant_str| {
65                let (account_str, predicate) =
66                    parse_claimant_string(claimant_str).map_err(|e| {
67                        tx::args::Error::Address(address::Error::InvalidKeyNameLength(e))
68                    })?;
69
70                let account_address = address::UnresolvedMuxedAccount::from_str(&account_str)
71                    .map_err(tx::args::Error::Address)?;
72                let muxed_account = tx.resolve_muxed_address(&account_address)?;
73
74                let predicate = predicate.unwrap_or(xdr::ClaimPredicate::Unconditional);
75
76                Ok(xdr::Claimant::ClaimantTypeV0(xdr::ClaimantV0 {
77                    destination: muxed_account.account_id(),
78                    predicate,
79                }))
80            })
81            .collect::<Result<Vec<_>, tx::args::Error>>()?;
82
83        Ok(xdr::OperationBody::CreateClaimableBalance(
84            xdr::CreateClaimableBalanceOp {
85                asset: tx.resolve_asset(asset)?,
86                amount: amount.into(),
87                claimants: claimants_vec.try_into().map_err(|_| {
88                    tx::args::Error::Address(address::Error::InvalidKeyNameLength(
89                        "Too many claimants".to_string(),
90                    ))
91                })?,
92            },
93        ))
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_parse_claimant_string_unconditional() {
103        let result =
104            parse_claimant_string("GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S");
105        assert_eq!(
106            result,
107            Ok((
108                "GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S".to_string(),
109                None
110            ))
111        );
112    }
113
114    #[test]
115    fn test_parse_claimant_string_explicit_unconditional() {
116        let input = r#"GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S:"unconditional""#;
117        let result = parse_claimant_string(input);
118        assert_eq!(
119            result,
120            Ok((
121                "GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S".to_string(),
122                Some(xdr::ClaimPredicate::Unconditional)
123            ))
124        );
125    }
126
127    #[test]
128    fn test_parse_claimant_string_before_absolute_time() {
129        let input = r#"GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S:{"before_absolute_time":"1735689599"}"#;
130        let result = parse_claimant_string(input);
131        assert_eq!(
132            result,
133            Ok((
134                "GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S".to_string(),
135                Some(xdr::ClaimPredicate::BeforeAbsoluteTime(1_735_689_599))
136            ))
137        );
138    }
139
140    #[test]
141    fn test_parse_claimant_string_before_relative_time() {
142        let input = r#"GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S:{"before_relative_time":"3600"}"#;
143        let result = parse_claimant_string(input);
144        assert_eq!(
145            result,
146            Ok((
147                "GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S".to_string(),
148                Some(xdr::ClaimPredicate::BeforeRelativeTime(3600))
149            ))
150        );
151    }
152
153    #[test]
154    fn test_parse_claimant_string_not_predicate() {
155        let input = r#"GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S:{"not":{"before_relative_time":"3600"}}"#;
156        let result = parse_claimant_string(input);
157        assert!(result.is_ok());
158        let (_, predicate) = result.unwrap();
159        match predicate {
160            Some(xdr::ClaimPredicate::Not(Some(inner))) => match inner.as_ref() {
161                xdr::ClaimPredicate::BeforeRelativeTime(3600) => {}
162                _ => panic!("Expected BeforeRelativeTime inside Not"),
163            },
164            _ => panic!("Expected Not predicate"),
165        }
166    }
167
168    #[test]
169    fn test_parse_claimant_string_and_predicate() {
170        let input = r#"GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S:{"and":[{"before_absolute_time":"1735689599"},{"before_relative_time":"7200"}]}"#;
171        let result = parse_claimant_string(input);
172        assert!(result.is_ok());
173        let (_, predicate) = result.unwrap();
174        match predicate {
175            Some(xdr::ClaimPredicate::And(predicates)) => {
176                assert_eq!(predicates.len(), 2);
177            }
178            _ => panic!("Expected And predicate"),
179        }
180    }
181
182    #[test]
183    fn test_parse_claimant_string_or_predicate() {
184        let input = r#"GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S:{"or":[{"before_absolute_time":"1735689599"},"unconditional"]}"#;
185        let result = parse_claimant_string(input);
186        assert!(result.is_ok());
187        let (_, predicate) = result.unwrap();
188        match predicate {
189            Some(xdr::ClaimPredicate::Or(predicates)) => {
190                assert_eq!(predicates.len(), 2);
191            }
192            _ => panic!("Expected Or predicate"),
193        }
194    }
195
196    #[test]
197    fn test_parse_claimant_string_invalid_json() {
198        let input = r#"GCNV6VMPZNHQTACVZC4AE75SJAFLHP7USOQWGE2HWMLXDKP6XOLGJR7S:{"invalid": json}"#;
199        let result = parse_claimant_string(input);
200        assert!(result.is_err());
201        assert!(result.unwrap_err().contains("Invalid predicate JSON"));
202    }
203}