xrpl/cli/
mod.rs

1use alloc::string::String;
2use thiserror_no_std::Error;
3
4#[cfg(feature = "std")]
5mod std_cli;
6
7#[cfg(feature = "std")]
8pub use std_cli::run;
9
10/// Default mainnet URL for JSON-RPC requests
11pub const DEFAULT_MAINNET_URL: &str = "https://xrplcluster.com/";
12
13/// Default testnet URL for faucet functionality
14pub const DEFAULT_TESTNET_URL: &str = "https://s.altnet.rippletest.net:51234";
15
16/// Default WebSocket URL
17pub const DEFAULT_WEBSOCKET_URL: &str = "wss://xrplcluster.com/";
18
19/// Default limit for paginated results
20pub const DEFAULT_PAGINATION_LIMIT: u32 = 10;
21
22// CLI commands with subcommand hierarchy
23#[cfg_attr(feature = "std", derive(clap::Parser))]
24#[derive(Debug, Clone)]
25#[cfg_attr(
26    feature = "std",
27    command(name = "xrpl", about = "XRPL command line utility")
28)]
29pub struct Cli {
30    #[cfg_attr(feature = "std", command(subcommand))]
31    pub command: Commands,
32}
33
34#[cfg_attr(feature = "std", derive(clap::Subcommand))]
35#[derive(Debug, Clone)]
36pub enum Commands {
37    /// Wallet operations
38    #[cfg_attr(feature = "std", command(subcommand))]
39    Wallet(WalletCommands),
40
41    /// Account operations
42    #[cfg_attr(feature = "std", command(subcommand))]
43    Account(AccountCommands),
44
45    /// Transaction operations
46    #[cfg_attr(feature = "std", command(subcommand))]
47    Transaction(TransactionCommands),
48
49    /// Server operations
50    #[cfg_attr(feature = "std", command(subcommand))]
51    Server(ServerCommands),
52
53    /// Ledger operations
54    #[cfg_attr(feature = "std", command(subcommand))]
55    Ledger(LedgerCommands),
56}
57
58#[cfg_attr(feature = "std", derive(clap::Subcommand))]
59#[derive(Debug, Clone)]
60pub enum WalletCommands {
61    /// Generate a new wallet
62    Generate {
63        /// Save the wallet to a file
64        #[cfg_attr(feature = "std", arg(long))]
65        save: bool,
66
67        /// Generate a BIP39 mnemonic phrase
68        #[cfg_attr(feature = "std", arg(long))]
69        mnemonic: bool,
70
71        /// Number of words for the mnemonic (12, 15, 18, 21, 24)
72        #[cfg_attr(feature = "std", arg(long, default_value_t = 12))]
73        words: usize,
74    },
75
76    /// Get wallet info from seed
77    FromSeed {
78        /// The seed to use
79        #[cfg_attr(feature = "std", arg(long))]
80        seed: String,
81        /// The sequence number
82        #[cfg_attr(feature = "std", arg(long, default_value = "0"))]
83        sequence: u64,
84    },
85
86    /// Generate a wallet funded by the testnet faucet
87    #[cfg(feature = "std")]
88    Faucet {
89        /// The XRPL node URL
90        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_TESTNET_URL.into()))]
91        url: String,
92    },
93
94    /// Validate an address
95    Validate {
96        /// The address to validate
97        #[cfg_attr(feature = "std", arg(long))]
98        address: String,
99    },
100}
101
102#[cfg_attr(feature = "std", derive(clap::Subcommand))]
103#[derive(Debug, Clone)]
104pub enum AccountCommands {
105    /// Get account info
106    Info {
107        /// The account address
108        #[cfg_attr(feature = "std", arg(long))]
109        address: String,
110        /// The XRPL node URL
111        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
112        url: String,
113    },
114
115    /// Get account transactions
116    Tx {
117        /// The account address
118        #[cfg_attr(feature = "std", arg(long))]
119        address: String,
120        /// The XRPL node URL
121        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
122        url: String,
123        /// Limit the number of transactions returned
124        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_PAGINATION_LIMIT))]
125        limit: u32,
126    },
127
128    /// Get account objects (trust lines, offers, etc.)
129    Objects {
130        /// The account address
131        #[cfg_attr(feature = "std", arg(long))]
132        address: String,
133        /// The XRPL node URL
134        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
135        url: String,
136        /// Type of objects to return (all, offer, state, etc.)
137        #[cfg_attr(feature = "std", arg(long))]
138        type_filter: Option<String>,
139        /// Limit the number of objects returned
140        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_PAGINATION_LIMIT as u16))]
141        limit: u16,
142    },
143
144    /// Get account channels
145    Channels {
146        /// The account address
147        #[cfg_attr(feature = "std", arg(long))]
148        address: String,
149        /// The XRPL node URL
150        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
151        url: String,
152        /// Destination account to filter channels
153        #[cfg_attr(feature = "std", arg(long))]
154        destination_account: Option<String>,
155        /// Limit the number of channels returned
156        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_PAGINATION_LIMIT as u16))]
157        limit: u16,
158    },
159
160    /// Get account currencies
161    Currencies {
162        /// The account address
163        #[cfg_attr(feature = "std", arg(long))]
164        address: String,
165        /// The XRPL node URL
166        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
167        url: String,
168    },
169
170    /// Get account trust lines
171    Lines {
172        /// The account address
173        #[cfg_attr(feature = "std", arg(long))]
174        address: String,
175        /// The XRPL node URL
176        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
177        url: String,
178        /// Peer account to filter trust lines
179        #[cfg_attr(feature = "std", arg(long))]
180        peer: Option<String>,
181        /// Limit the number of trust lines returned
182        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_PAGINATION_LIMIT as u16))]
183        limit: u16,
184    },
185
186    /// Get account NFTs (XLS-20)
187    Nfts {
188        /// The account address
189        #[cfg_attr(feature = "std", arg(long))]
190        address: String,
191        /// The XRPL node URL
192        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
193        url: String,
194    },
195
196    /// Set an account flag
197    SetFlag {
198        /// The seed to use for signing
199        #[cfg_attr(feature = "std", arg(short, long))]
200        seed: String,
201        /// The flag to set (e.g., asfRequireAuth, asfDisableMaster, etc.)
202        #[cfg_attr(feature = "std", arg(short, long))]
203        flag: String,
204        /// The XRPL node URL
205        #[cfg_attr(feature = "std", arg(short, long, default_value_t = DEFAULT_MAINNET_URL.into()))]
206        url: String,
207    },
208
209    /// Clear an account flag
210    ClearFlag {
211        /// The seed to use for signing
212        #[cfg_attr(feature = "std", arg(short, long))]
213        seed: String,
214        /// The flag to clear (e.g., asfRequireAuth, asfDisableMaster, etc.)
215        #[cfg_attr(feature = "std", arg(short, long))]
216        flag: String,
217        /// The XRPL node URL
218        #[cfg_attr(feature = "std", arg(short, long, default_value_t = DEFAULT_MAINNET_URL.into()))]
219        url: String,
220    },
221}
222
223#[cfg_attr(feature = "std", derive(clap::Subcommand))]
224#[derive(Debug, Clone)]
225pub enum TransactionCommands {
226    /// Sign a transaction
227    Sign {
228        /// The seed to use for signing
229        #[cfg_attr(feature = "std", arg(short, long))]
230        seed: String,
231        /// The transaction type (Payment, AccountSet, etc.)
232        #[cfg_attr(feature = "std", arg(short, long))]
233        r#type: String,
234        /// The transaction JSON
235        #[cfg_attr(feature = "std", arg(short, long))]
236        json: String,
237    },
238
239    /// Submit a transaction
240    Submit {
241        /// The signed transaction blob or JSON
242        #[cfg_attr(feature = "std", arg(short, long))]
243        tx_blob: String,
244        /// The XRPL node URL
245        #[cfg_attr(feature = "std", arg(short, long, default_value_t = DEFAULT_MAINNET_URL.into()))]
246        url: String,
247    },
248
249    /// Set or modify a trust line
250    TrustSet {
251        /// The seed to use for signing
252        #[cfg_attr(feature = "std", arg(short, long))]
253        seed: String,
254        /// The issuer account
255        #[cfg_attr(feature = "std", arg(short, long))]
256        issuer: String,
257        /// The currency code (3-letter or 40-char hex)
258        #[cfg_attr(feature = "std", arg(short, long))]
259        currency: String,
260        /// The trust line limit (amount)
261        #[cfg_attr(feature = "std", arg(short, long))]
262        limit: String,
263        /// The XRPL node URL
264        #[cfg_attr(feature = "std", arg(short, long, default_value_t = DEFAULT_MAINNET_URL.into()))]
265        url: String,
266    },
267
268    /// Mint an NFT (XLS-20)
269    NftMint {
270        /// The seed to use for signing
271        #[cfg_attr(feature = "std", arg(short, long))]
272        seed: String,
273        /// URI for the NFT (hex-encoded)
274        #[cfg_attr(feature = "std", arg(long))]
275        uri: String,
276        /// Flags (optional)
277        #[cfg_attr(feature = "std", arg(long))]
278        flags: Option<u32>,
279        /// Transfer fee (optional)
280        #[cfg_attr(feature = "std", arg(long))]
281        transfer_fee: Option<u16>,
282        /// XRPL node URL
283        #[cfg_attr(feature = "std", arg(short, long, default_value_t = DEFAULT_MAINNET_URL.into()))]
284        url: String,
285    },
286
287    /// Burn an NFT (XLS-20)
288    NftBurn {
289        /// The seed to use for signing
290        #[cfg_attr(feature = "std", arg(short, long))]
291        seed: String,
292        /// NFT Token ID to burn
293        #[cfg_attr(feature = "std", arg(short, long))]
294        nftoken_id: String,
295        /// XRPL node URL
296        #[cfg_attr(feature = "std", arg(short, long, default_value_t = DEFAULT_MAINNET_URL.into()))]
297        url: String,
298    },
299}
300
301#[cfg_attr(feature = "std", derive(clap::Subcommand))]
302#[derive(Debug, Clone)]
303pub enum ServerCommands {
304    /// Get current network fee
305    Fee {
306        /// The XRPL node URL
307        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
308        url: String,
309    },
310
311    /// Get server info
312    Info {
313        /// The XRPL node URL
314        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
315        url: String,
316    },
317
318    /// Subscribe to ledger events
319    Subscribe {
320        /// The XRPL node WebSocket URL
321        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_WEBSOCKET_URL.into()))]
322        url: String,
323        /// Stream type to subscribe to (ledger, transactions, validations)
324        #[cfg_attr(feature = "std", arg(long, default_value = "ledger"))]
325        stream: String,
326        /// Number of events to receive before exiting (0 for unlimited)
327        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_PAGINATION_LIMIT))]
328        limit: u32,
329    },
330}
331
332#[cfg_attr(feature = "std", derive(clap::Subcommand))]
333#[derive(Debug, Clone)]
334pub enum LedgerCommands {
335    /// Get ledger data
336    Data {
337        /// The XRPL node URL
338        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_MAINNET_URL.into()))]
339        url: String,
340        /// Ledger index (empty for latest)
341        #[cfg_attr(feature = "std", arg(long))]
342        ledger_index: Option<String>,
343        /// Ledger hash (empty for latest)
344        #[cfg_attr(feature = "std", arg(long))]
345        ledger_hash: Option<String>,
346        /// Limit the number of objects returned
347        #[cfg_attr(feature = "std", arg(long, default_value_t = DEFAULT_PAGINATION_LIMIT as u16))]
348        limit: u16,
349    },
350}
351
352/// Define a custom error type for CLI operations
353#[derive(Debug, Error)]
354pub enum CliError {
355    #[error("Wallet error: {0}")]
356    WalletError(#[from] crate::wallet::exceptions::XRPLWalletException),
357    #[error("Client error: {0}")]
358    ClientError(#[from] crate::asynch::clients::exceptions::XRPLClientException),
359    #[error("URL parse error: {0}")]
360    UrlParseError(#[from] url::ParseError),
361    #[error("JSON error: {0}")]
362    JsonError(#[from] serde_json::Error),
363    #[error("IO error: {0}")]
364    IoError(#[from] std::io::Error),
365    #[error("Helper error: {0}")]
366    HelperError(#[from] crate::asynch::exceptions::XRPLHelperException),
367    #[error("Core error: {0}")]
368    CoreError(#[from] crate::core::exceptions::XRPLCoreException),
369    #[error("Other error: {0}")]
370    Other(String),
371}
372
373/// Helper function to parse a URL string with proper error handling
374#[cfg(feature = "std")]
375fn parse_url(url_str: &str) -> Result<url::Url, CliError> {
376    url_str.parse().map_err(CliError::UrlParseError)
377}
378
379/// Helper function to create a JSON-RPC client with proper error handling
380#[cfg(feature = "std")]
381fn create_json_rpc_client(
382    url_str: &str,
383) -> Result<crate::clients::json_rpc::JsonRpcClient, CliError> {
384    use crate::clients::json_rpc::JsonRpcClient;
385    Ok(JsonRpcClient::connect(parse_url(url_str)?))
386}
387
388/// Helper function to handle response display and error conversion
389#[cfg(feature = "std")]
390fn handle_response<T: core::fmt::Debug>(
391    result: Result<T, crate::asynch::clients::exceptions::XRPLClientException>,
392    response_type: &str,
393) -> Result<(), CliError> {
394    match result {
395        Ok(response) => {
396            alloc::println!("{}: {:#?}", response_type, response);
397            Ok(())
398        }
399        Err(e) => Err(CliError::ClientError(e)),
400    }
401}
402
403/// Helper function to get or create a Tokio runtime
404#[cfg(feature = "std")]
405fn get_or_create_runtime() -> Result<tokio::runtime::Runtime, CliError> {
406    use tokio::runtime::Runtime;
407    Runtime::new().map_err(CliError::IoError)
408}
409
410/// Helper function to handle common transaction encoding and output steps
411#[cfg(feature = "std")]
412fn encode_and_print_tx<T: serde::Serialize>(tx: &T) -> Result<String, CliError> {
413    let tx_blob = crate::core::binarycodec::encode(tx)?;
414    alloc::println!("Signed transaction blob: {}", tx_blob);
415    Ok(tx_blob)
416}
417
418pub fn execute_command(command: &Commands) -> Result<(), CliError> {
419    match command {
420        Commands::Wallet(wallet_cmd) => match wallet_cmd {
421            WalletCommands::Generate {
422                save,
423                mnemonic,
424                words,
425            } => {
426                if *mnemonic {
427                    use bip39::{Language, Mnemonic};
428
429                    let mut rng = rand::thread_rng();
430                    let mnemonic = Mnemonic::generate_in_with(&mut rng, Language::English, *words)
431                        .expect("Invalid word count (must be 12, 15, 18, 21, or 24)");
432                    let seed = mnemonic.to_seed(""); // Returns [u8; 64]
433                    let phrase = mnemonic.words().collect::<Vec<_>>().join(" ");
434
435                    alloc::println!(
436                        "Generated wallet with mnemonic:\nMnemonic: {}\nSeed: {}",
437                        phrase,
438                        hex::encode(seed)
439                    );
440                    // Print derived address, public key, etc.
441                } else {
442                    let wallet = crate::wallet::Wallet::create(None)?;
443                    alloc::println!("Generated wallet: {:#?}", wallet);
444                    if *save {
445                        alloc::println!("Saving wallet functionality not implemented yet");
446                    }
447                }
448                Ok(())
449            }
450            WalletCommands::FromSeed { seed, sequence } => {
451                let wallet = crate::wallet::Wallet::new(seed, *sequence)?;
452                alloc::println!("Wallet from seed: {:#?}", wallet);
453                Ok(())
454            }
455            #[cfg(feature = "std")]
456            WalletCommands::Faucet { url } => {
457                use crate::asynch::clients::AsyncJsonRpcClient;
458                use crate::asynch::wallet::generate_faucet_wallet;
459
460                // Get or create a runtime
461                let rt = get_or_create_runtime()?;
462
463                // Execute within the runtime
464                let result = rt.block_on(async {
465                    let client = AsyncJsonRpcClient::connect(url.parse()?);
466                    generate_faucet_wallet(&client, None, None, None, None).await
467                });
468
469                match result {
470                    Ok(wallet) => {
471                        alloc::println!("Generated faucet wallet: {:#?}", wallet);
472                        Ok(())
473                    }
474                    Err(e) => Err(CliError::Other(format!(
475                        "Failed to generate faucet wallet: {}",
476                        e
477                    ))),
478                }
479            }
480            WalletCommands::Validate { address } => {
481                use crate::core::addresscodec::{is_valid_classic_address, is_valid_xaddress};
482
483                let is_valid_classic = is_valid_classic_address(&address);
484                let is_valid_x = is_valid_xaddress(&address);
485
486                if is_valid_classic {
487                    alloc::println!("Valid classic address: {}", address);
488                    Ok(())
489                } else if is_valid_x {
490                    use crate::core::addresscodec::xaddress_to_classic_address;
491                    let (classic_address, tag, is_test) = xaddress_to_classic_address(&address)?;
492                    alloc::println!("Valid X-address: {}", address);
493                    alloc::println!("  Classic address: {}", classic_address);
494                    alloc::println!("  Destination tag: {:?}", tag);
495                    alloc::println!("  Test network: {}", is_test);
496                    Ok(())
497                } else {
498                    Err(CliError::Other(format!("Invalid address: {}", address)))
499                }
500            }
501        },
502
503        Commands::Account(account_cmd) => match account_cmd {
504            #[cfg(feature = "std")]
505            AccountCommands::Info { address, url } => {
506                use crate::clients::XRPLSyncClient;
507                use crate::models::requests::account_info::AccountInfo;
508
509                // Create client with standardized helper function
510                let client = create_json_rpc_client(url)?;
511
512                // Create request
513                let account_info = AccountInfo::new(
514                    None,                   // id
515                    address.clone().into(), // account
516                    None,                   // strict
517                    None,                   // ledger_index
518                    None,                   // ledger_hash
519                    None,                   // queue
520                    None,                   // signer_lists
521                );
522
523                // Execute request and handle response
524                handle_response(client.request(account_info.into()), "Account info")
525            }
526            #[cfg(feature = "std")]
527            AccountCommands::Tx {
528                address,
529                url,
530                limit,
531            } => {
532                use crate::clients::XRPLSyncClient;
533                use crate::models::requests::account_tx::AccountTx;
534
535                // Create client with standardized helper function
536                let client = create_json_rpc_client(url)?;
537
538                // Create request
539                let account_tx = AccountTx::new(
540                    None,
541                    address.clone().into(),
542                    None,
543                    None,
544                    None,
545                    None,
546                    Some(*limit),
547                    None,
548                    None,
549                    None,
550                );
551
552                // Execute request and handle response
553                handle_response(client.request(account_tx.into()), "Account transactions")
554            }
555            #[cfg(feature = "std")]
556            AccountCommands::Objects {
557                address,
558                url,
559                type_filter,
560                limit,
561            } => {
562                use std::str::FromStr;
563
564                use crate::clients::XRPLSyncClient;
565                use crate::models::requests::account_objects::{AccountObjectType, AccountObjects};
566
567                // Parse the type_filter into AccountObjectType if provided
568                let object_type = if let Some(filter) = type_filter.as_deref() {
569                    match AccountObjectType::from_str(filter) {
570                        Ok(obj_type) => Some(obj_type),
571                        Err(_) => {
572                            return Err(CliError::Other(format!(
573                                "Invalid object type: {}",
574                                filter
575                            )));
576                        }
577                    }
578                } else {
579                    None
580                };
581
582                // Create client with standardized helper function
583                let client = create_json_rpc_client(url)?;
584
585                // Create request
586                let account_objects = AccountObjects::new(
587                    None,
588                    address.clone().into(),
589                    None,
590                    None,
591                    object_type,
592                    None,
593                    Some(*limit),
594                    None,
595                );
596
597                // Execute request and handle response
598                handle_response(client.request(account_objects.into()), "Account objects")
599            }
600            #[cfg(feature = "std")]
601            AccountCommands::Channels {
602                address,
603                url,
604                destination_account,
605                limit,
606            } => {
607                use crate::clients::XRPLSyncClient;
608                use crate::models::requests::account_channels::AccountChannels;
609
610                // Create client with standardized helper function
611                let client = create_json_rpc_client(url)?;
612
613                // Create request
614                let account_channels = AccountChannels::new(
615                    None,
616                    address.clone().into(),
617                    destination_account.as_deref().map(Into::into),
618                    None,
619                    None,
620                    Some(*limit),
621                    None,
622                );
623
624                // Execute request and handle response
625                handle_response(client.request(account_channels.into()), "Account channels")
626            }
627            #[cfg(feature = "std")]
628            AccountCommands::Currencies { address, url } => {
629                use crate::clients::XRPLSyncClient;
630                use crate::models::requests::account_currencies::AccountCurrencies;
631
632                // Create client with standardized helper function
633                let client = create_json_rpc_client(url)?;
634
635                // Create request
636                let account_currencies =
637                    AccountCurrencies::new(None, address.clone().into(), None, None, None);
638
639                // Execute request and handle response
640                handle_response(
641                    client.request(account_currencies.into()),
642                    "Account currencies",
643                )
644            }
645            #[cfg(feature = "std")]
646            AccountCommands::Lines {
647                address,
648                url,
649                peer,
650                limit,
651            } => {
652                use crate::clients::XRPLSyncClient;
653                use crate::models::requests::account_lines::AccountLines;
654
655                // Create client with standardized helper function
656                let client = create_json_rpc_client(url)?;
657
658                // Create request
659                let account_lines = AccountLines::new(
660                    None,
661                    address.clone().into(),
662                    None,
663                    None,
664                    Some(*limit),
665                    peer.as_deref().map(Into::into),
666                );
667
668                // Execute request and handle response
669                handle_response(client.request(account_lines.into()), "Account trust lines")
670            }
671            #[cfg(feature = "std")]
672            AccountCommands::Nfts { address, url } => {
673                use crate::clients::XRPLSyncClient;
674                use crate::models::requests::account_nfts::AccountNfts;
675
676                let client = create_json_rpc_client(url)?;
677                let req = AccountNfts::new(
678                    None,                   // id
679                    address.clone().into(), // account
680                    None,                   // limit
681                    None,                   // marker
682                );
683
684                handle_response(client.request(req.into()), "Account NFTs")
685            }
686            #[cfg(feature = "std")]
687            AccountCommands::SetFlag { seed, flag, url } => {
688                use alloc::borrow::Cow;
689                use core::str::FromStr;
690
691                use crate::asynch::transaction::sign;
692                use crate::models::transactions::account_set::{AccountSet, AccountSetFlag};
693                use crate::wallet::Wallet;
694
695                let wallet = Wallet::new(seed, 0)?;
696                let flag_enum = AccountSetFlag::from_str(flag)
697                    .map_err(|_| CliError::Other(format!("Invalid flag: {}", flag)))?;
698
699                let mut tx = AccountSet::new(
700                    Cow::Owned(wallet.classic_address.clone()),
701                    None,            // account_txn_id
702                    None,            // fee
703                    None,            // flags
704                    None,            // last_ledger_sequence
705                    None,            // memos
706                    None,            // sequence
707                    None,            // signers
708                    None,            // source_tag
709                    None,            // ticket_sequence
710                    None,            // clear_flag
711                    None,            // domain
712                    None,            // email_hash
713                    None,            // message_key
714                    Some(flag_enum), // set_flag
715                    None,            // transfer_rate
716                    None,            // tick_size
717                    None,            // nftoken_minter
718                );
719
720                sign(&mut tx, &wallet, false)?;
721                let tx_blob = encode_and_print_tx(&tx)?;
722
723                alloc::println!(
724                    "To submit, use: xrpl transaction submit --tx-blob {} --url {}",
725                    tx_blob,
726                    url
727                );
728                Ok(())
729            }
730
731            #[cfg(feature = "std")]
732            AccountCommands::ClearFlag { seed, flag, url } => {
733                use alloc::borrow::Cow;
734                use core::str::FromStr;
735
736                use crate::asynch::transaction::sign;
737                use crate::models::transactions::account_set::{AccountSet, AccountSetFlag};
738                use crate::wallet::Wallet;
739
740                let wallet = Wallet::new(seed, 0)?;
741                let flag_enum = AccountSetFlag::from_str(flag)
742                    .map_err(|_| CliError::Other(format!("Invalid flag: {}", flag)))?;
743
744                let mut tx = AccountSet::new(
745                    Cow::Owned(wallet.classic_address.clone()),
746                    None,            // account_txn_id
747                    None,            // fee
748                    None,            // flags
749                    None,            // last_ledger_sequence
750                    None,            // memos
751                    None,            // sequence
752                    None,            // signers
753                    None,            // source_tag
754                    None,            // ticket_sequence
755                    None,            // clear_flag
756                    None,            // domain
757                    None,            // email_hash
758                    None,            // message_key
759                    Some(flag_enum), // set_flag
760                    None,            // transfer_rate
761                    None,            // tick_size
762                    None,            // nftoken_minter
763                );
764
765                sign(&mut tx, &wallet, false)?;
766                let tx_blob = encode_and_print_tx(&tx)?;
767
768                alloc::println!(
769                    "To submit, use: xrpl transaction submit --tx-blob {} --url {}",
770                    tx_blob,
771                    url
772                );
773                Ok(())
774            }
775        },
776
777        Commands::Transaction(tx_cmd) => match tx_cmd {
778            #[cfg(feature = "std")]
779            TransactionCommands::Sign { seed, r#type, json } => {
780                use serde_json::Value;
781
782                use crate::models::transactions::{
783                    account_set::AccountSet, offer_cancel::OfferCancel, offer_create::OfferCreate,
784                    payment::Payment, trust_set::TrustSet,
785                };
786                use crate::wallet::Wallet;
787
788                // Create wallet from seed
789                let wallet = Wallet::new(&seed, 0)?;
790
791                // Parse the JSON
792                let json_value: Value = serde_json::from_str(&json)?;
793
794                use crate::asynch::transaction::sign;
795
796                // Handle different transaction types
797                match r#type.to_lowercase().as_str() {
798                    "payment" => {
799                        let mut tx: Payment = serde_json::from_value(json_value)?;
800                        sign(&mut tx, &wallet, false)?;
801                        encode_and_print_tx(&tx)?;
802                    }
803                    "accountset" => {
804                        let mut tx: AccountSet = serde_json::from_value(json_value)?;
805                        sign(&mut tx, &wallet, false)?;
806                        encode_and_print_tx(&tx)?;
807                    }
808                    "offercreate" => {
809                        let mut tx: OfferCreate = serde_json::from_value(json_value)?;
810                        sign(&mut tx, &wallet, false)?;
811                        encode_and_print_tx(&tx)?;
812                    }
813                    "offercancel" => {
814                        let mut tx: OfferCancel = serde_json::from_value(json_value)?;
815                        sign(&mut tx, &wallet, false)?;
816                        encode_and_print_tx(&tx)?;
817                    }
818                    "trustset" => {
819                        let mut tx: TrustSet = serde_json::from_value(json_value)?;
820                        sign(&mut tx, &wallet, false)?;
821                        encode_and_print_tx(&tx)?;
822                    }
823                    _ => {
824                        return Err(CliError::Other(format!(
825                            "Unsupported transaction type: {}",
826                            r#type
827                        )));
828                    }
829                }
830
831                Ok(())
832            }
833            #[cfg(feature = "std")]
834            TransactionCommands::Submit { tx_blob, url } => {
835                use crate::clients::XRPLSyncClient;
836                use crate::models::requests::submit::Submit;
837
838                // Create client with standardized helper function
839                let client = create_json_rpc_client(url)?;
840
841                // Create request
842                let submit_request = Submit::new(None, tx_blob.into(), None);
843
844                // Execute request and handle response
845                handle_response(
846                    client.request(submit_request.into()),
847                    "Transaction submission result",
848                )
849            }
850            #[cfg(feature = "std")]
851            TransactionCommands::TrustSet {
852                seed,
853                issuer,
854                currency,
855                limit,
856                url,
857            } => {
858                use alloc::borrow::Cow;
859
860                use crate::models::IssuedCurrencyAmount;
861                use crate::models::transactions::trust_set::TrustSet;
862                use crate::wallet::Wallet;
863
864                // Create wallet from seed
865                let wallet = Wallet::new(seed, 0)?;
866
867                // Build IssuedCurrencyAmount for the trust line limit
868                let amount = IssuedCurrencyAmount::new(
869                    currency.clone().into(), // currency code
870                    issuer.clone().into(),   // issuer address
871                    limit.clone().into(),    // value as string
872                );
873
874                // Build TrustSet transaction
875                let mut tx = TrustSet::new(
876                    Cow::Owned(wallet.classic_address.clone()),
877                    None, // account_txn_id
878                    None, // fee
879                    None, // flags
880                    None, // last_ledger_sequence
881                    None, // memos
882                    None, // sequence
883                    None, // signers
884                    None, // source_tag
885                    None, // ticket_sequence
886                    amount,
887                    None, // quality_in
888                    None, // quality_out
889                );
890
891                // Sign the transaction
892                use crate::asynch::transaction::sign;
893                sign(&mut tx, &wallet, false)?;
894
895                // Encode and print the transaction blob
896                let tx_blob = encode_and_print_tx(&tx)?;
897
898                alloc::println!(
899                    "To submit, use: xrpl transaction submit --tx-blob {} --url {}",
900                    tx_blob,
901                    url
902                );
903
904                Ok(())
905            }
906            #[cfg(feature = "std")]
907            TransactionCommands::NftMint {
908                seed,
909                uri,
910                flags,
911                transfer_fee,
912                url,
913            } => {
914                use crate::asynch::transaction::sign;
915                use crate::models::transactions::nftoken_mint::{NFTokenMint, NFTokenMintFlag};
916                use crate::wallet::Wallet;
917                use alloc::borrow::Cow;
918
919                let wallet = Wallet::new(seed, 0)?;
920
921                // Support multiple flags via bitmask
922                let flag_collection = flags.map(|f| NFTokenMintFlag::from_bits(f).into());
923
924                let mut tx = NFTokenMint::new(
925                    Cow::Owned(wallet.classic_address.clone()),
926                    None, // account_txn_id
927                    None, // fee
928                    flag_collection,
929                    None, // last_ledger_sequence
930                    None, // memos
931                    None, // sequence
932                    None, // signers
933                    None, // source_tag
934                    None, // ticket_sequence
935                    0,    // nftoken_taxon (0 for default, or expose as CLI param)
936                    None, // issuer
937                    transfer_fee.map(|v| v as u32),
938                    Some(uri.clone().into()),
939                );
940
941                sign(&mut tx, &wallet, false)?;
942                let tx_blob = encode_and_print_tx(&tx)?;
943
944                alloc::println!(
945                    "To submit, use: xrpl transaction submit --tx-blob {} --url {}",
946                    tx_blob,
947                    url
948                );
949                Ok(())
950            }
951
952            #[cfg(feature = "std")]
953            TransactionCommands::NftBurn {
954                seed,
955                nftoken_id,
956                url,
957            } => {
958                use crate::asynch::transaction::sign;
959                use crate::models::transactions::nftoken_burn::NFTokenBurn;
960                use crate::wallet::Wallet;
961                use alloc::borrow::Cow;
962
963                let wallet = Wallet::new(seed, 0)?;
964
965                let mut tx = NFTokenBurn::new(
966                    Cow::Owned(wallet.classic_address.clone()),
967                    None, // account_txn_id
968                    None, // fee
969                    None, // last_ledger_sequence
970                    None, // memos
971                    None, // sequence
972                    None, // signers
973                    None, // source_tag
974                    None, // ticket_sequence
975                    nftoken_id.clone().into(),
976                    None, // owner (optional, only needed if burning on behalf of another)
977                );
978
979                sign(&mut tx, &wallet, false)?;
980                let tx_blob = encode_and_print_tx(&tx)?;
981
982                alloc::println!(
983                    "To submit, use: xrpl transaction submit --tx-blob {} --url {}",
984                    tx_blob,
985                    url
986                );
987                Ok(())
988            }
989        },
990
991        Commands::Server(server_cmd) => match server_cmd {
992            #[cfg(feature = "std")]
993            ServerCommands::Fee { url } => {
994                use crate::ledger::{FeeType, get_fee};
995
996                // Create a runtime and client
997                let rt = get_or_create_runtime()?;
998                let client = create_json_rpc_client(url)?;
999
1000                // Get the current fee within the Tokio runtime
1001                match rt.block_on(async { get_fee(&client, None, Some(FeeType::Open)) }) {
1002                    Ok(fee) => {
1003                        alloc::println!("Current network fee: {} drops", fee);
1004                        Ok(())
1005                    }
1006                    Err(e) => Err(CliError::HelperError(e)),
1007                }
1008            }
1009            #[cfg(feature = "std")]
1010            ServerCommands::Info { url } => {
1011                use crate::clients::XRPLSyncClient;
1012                use crate::models::requests::server_info::ServerInfo;
1013
1014                // Create client with standardized helper function
1015                let client = create_json_rpc_client(url)?;
1016
1017                // Create request
1018                let server_info = ServerInfo::new(None);
1019
1020                // Execute request and handle response
1021                handle_response(client.request(server_info.into()), "Server info")
1022            }
1023            #[cfg(feature = "std")]
1024            ServerCommands::Subscribe { url, stream, limit } => {
1025                use crate::clients::websocket::WebSocketClient;
1026                use crate::clients::{SingleExecutorMutex, XRPLSyncWebsocketIO};
1027                use crate::models::requests::subscribe::{StreamParameter, Subscribe};
1028
1029                // Parse the stream type
1030                let stream_param = match stream.to_lowercase().as_str() {
1031                    "ledger" => StreamParameter::Ledger,
1032                    "transactions" => StreamParameter::Transactions,
1033                    "validations" => StreamParameter::Validations,
1034                    _ => return Err(CliError::Other(format!("Unknown stream type: {}", stream))),
1035                };
1036
1037                // Open a websocket connection with consistent URL parsing
1038                let mut websocket: WebSocketClient<SingleExecutorMutex, _> =
1039                    WebSocketClient::open(parse_url(url)?)?;
1040
1041                // Subscribe to the stream
1042                let subscribe = Subscribe::new(
1043                    None,
1044                    None,
1045                    None,
1046                    None,
1047                    Some(vec![stream_param]),
1048                    None,
1049                    None,
1050                    None,
1051                );
1052
1053                websocket.xrpl_send(subscribe.into())?;
1054
1055                // Listen for messages
1056                let mut count = 0;
1057                loop {
1058                    if *limit > 0 && count >= *limit {
1059                        break;
1060                    }
1061
1062                    match websocket.xrpl_receive() {
1063                        Ok(Some(response)) => {
1064                            alloc::println!("Received: {:#?}", response);
1065                            count += 1;
1066                        }
1067                        Ok(None) => {
1068                            std::thread::sleep(std::time::Duration::from_millis(100));
1069                        }
1070                        Err(e) => {
1071                            return Err(CliError::ClientError(e));
1072                        }
1073                    }
1074                }
1075
1076                Ok(())
1077            }
1078        },
1079
1080        Commands::Ledger(ledger_cmd) => match ledger_cmd {
1081            #[cfg(feature = "std")]
1082            LedgerCommands::Data {
1083                url,
1084                ledger_index,
1085                ledger_hash,
1086                limit,
1087            } => {
1088                use crate::clients::XRPLSyncClient;
1089                use crate::models::requests::ledger_data::LedgerData;
1090
1091                // Create client with standardized helper function
1092                let client = create_json_rpc_client(url)?;
1093
1094                // Create request
1095                let ledger_data = LedgerData::new(
1096                    None,
1097                    None,
1098                    ledger_index.as_deref().map(Into::into),
1099                    ledger_hash.as_deref().map(Into::into),
1100                    Some(*limit),
1101                    None,
1102                );
1103
1104                // Execute request and handle response
1105                handle_response(client.request(ledger_data.into()), "Ledger data")
1106            }
1107        },
1108    }
1109}