soroban_cli/commands/tx/new/
create_claimable_balance.rs1use 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 #[arg(long, default_value = "native")]
31 pub asset: builder::Asset,
32
33 #[arg(long)]
35 pub amount: builder::Amount,
36
37 #[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}