This document explains how to use quantus-cli as a library in your Rust applications.
[dependencies]
quantus-cli = { path = "." } # For local development
# or
quantus-cli = "0.1.0" # When published to crates.iouse quantus_cli::wallet::WalletManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let wallet_manager = WalletManager::new()?;
// Create a new wallet
let wallet_info = wallet_manager
.create_wallet("my_wallet", Some("secure_password"))
.await?;
println!("Created wallet: {}", wallet_info.name);
println!("Address: {}", wallet_info.address);
Ok(())
}use quantus_cli::chain::client::QuantusClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
// Get system information
let runtime_version = client.get_runtime_version().await?;
println!("Runtime version: {:?}", runtime_version);
Ok(())
}use quantus_cli::{
wallet::{WalletManager, QuantumKeyPair},
chain::client::QuantusClient,
};
async fn load_wallet_for_transactions() -> Result<(), Box<dyn std::error::Error>> {
let wallet_manager = WalletManager::new()?;
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
// Load wallet data (includes private key)
let wallet_data = wallet_manager.load_wallet("my_wallet", "secure_password")?;
let keypair = wallet_data.keypair;
// Now you can use the keypair for transactions
let account_id = keypair.to_account_id_32();
println!("Account ID: {:?}", account_id);
Ok(())
}use quantus_cli::wallet::WalletManager;
async fn create_wallets() -> Result<(), Box<dyn std::error::Error>> {
let wallet_manager = WalletManager::new()?;
// Create a regular wallet
let wallet_info = wallet_manager
.create_wallet("regular_wallet", Some("password"))
.await?;
// Create a developer/test wallet (crystal_alice, crystal_bob, crystal_charlie)
let dev_wallet = wallet_manager
.create_developer_wallet("crystal_alice")
.await?;
// Import wallet from mnemonic
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
let imported_wallet = wallet_manager
.import_wallet("imported_wallet", mnemonic, Some("password"))
.await?;
Ok(())
}async fn manage_wallets() -> Result<(), Box<dyn std::error::Error>> {
let wallet_manager = WalletManager::new()?;
// List all wallets
let wallets = wallet_manager.list_wallets()?;
for wallet in wallets {
println!("Wallet: {} - {}", wallet.name, wallet.address);
}
// Get specific wallet info
if let Some(wallet_info) = wallet_manager.get_wallet("my_wallet", Some("password"))? {
println!("Wallet details: {:?}", wallet_info);
}
// Delete a wallet
let deleted = wallet_manager.delete_wallet("old_wallet")?;
println!("Wallet deleted: {}", deleted);
Ok(())
}use quantus_cli::{
chain::client::QuantusClient,
wallet::WalletManager,
};
async fn query_balance() -> Result<(), Box<dyn std::error::Error>> {
let wallet_manager = WalletManager::new()?;
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
// Load wallet
let wallet_data = wallet_manager.load_wallet("my_wallet", "password")?;
let account_id = wallet_data.keypair.to_account_id_32();
// Query balance
use quantus_cli::chain::quantus_subxt::api;
let account_bytes: [u8; 32] = *account_id.as_ref();
let subxt_account_id = subxt::utils::AccountId32::from(account_bytes);
let storage_addr = api::storage().system().account(subxt_account_id);
let account_info = client.client().storage().at(None).fetch_or_default(&storage_addr).await?;
println!("Balance: {} DEV", account_info.data.free);
Ok(())
}use quantus_cli::{
chain::client::QuantusClient,
wallet::WalletManager,
AccountId32,
};
async fn send_transaction() -> Result<(), Box<dyn std::error::Error>> {
let wallet_manager = WalletManager::new()?;
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
// Load sender wallet
let wallet_data = wallet_manager.load_wallet("my_wallet", "password")?;
let keypair = wallet_data.keypair;
// Parse recipient address
let to_address = "qzkeicNBtW2AG2E7USjDcLzAL8d9WxTZnV2cbtXoDzWxzpHC2";
let to_account_id = AccountId32::from_ss58check(to_address)?;
// Create transfer call
use quantus_cli::chain::quantus_subxt::api;
use subxt::tx::TxClient;
let to_account_bytes: [u8; 32] = *to_account_id.as_ref();
let to_subxt_account_id = subxt::utils::AccountId32::from(to_account_bytes);
let transfer_call = api::tx().balances().transfer(
to_subxt_account_id.into(),
1000000000000, // 1 DEV
);
// Submit transaction
let tx_hash = client
.client()
.tx()
.sign_and_submit_then_watch_default(&transfer_call, &keypair)
.await?
.wait_for_finalized_success()
.await?
.extrinsic_hash();
println!("Transaction hash: {:?}", tx_hash);
Ok(())
}For web services or applications that need to manage multiple wallets:
use quantus_cli::{
wallet::WalletManager,
chain::client::QuantusClient,
};
use std::sync::Arc;
use tokio::sync::RwLock;
pub struct WalletService {
wallet_manager: Arc<WalletManager>,
client: Arc<RwLock<QuantusClient>>,
}
impl WalletService {
pub async fn new(node_url: &str) -> Result<Self, Box<dyn std::error::Error>> {
let wallet_manager = Arc::new(WalletManager::new()?);
let client = Arc::new(RwLock::new(QuantusClient::new(node_url).await?));
Ok(Self {
wallet_manager,
client,
})
}
pub async fn create_wallet(&self, name: &str, password: &str) -> Result<String, Box<dyn std::error::Error>> {
let wallet_info = self.wallet_manager
.create_wallet(name, Some(password))
.await?;
Ok(wallet_info.address)
}
pub async fn get_balance(&self, name: &str, password: &str) -> Result<u128, Box<dyn std::error::Error>> {
let wallet_data = self.wallet_manager.load_wallet(name, password)?;
let account_id = wallet_data.keypair.to_account_id_32();
// Query balance logic here...
Ok(0) // Placeholder
}
}The library uses custom error types for better error handling:
use quantus_cli::error::{QuantusError, Result};
async fn handle_errors() -> Result<()> {
let wallet_manager = WalletManager::new()?;
match wallet_manager.create_wallet("existing_wallet", Some("password")).await {
Ok(wallet) => println!("Created wallet: {}", wallet.name),
Err(QuantusError::Wallet(quantus_cli::wallet::WalletError::AlreadyExists)) => {
println!("Wallet already exists");
},
Err(e) => {
println!("Other error: {}", e);
}
}
Ok(())
}The library is designed to be thread-safe:
WalletManagercan be shared across threads usingArc<WalletManager>QuantusClientcan be shared usingArc<RwLock<QuantusClient>>- Wallet operations are safe to call concurrently
The library provides full programmatic access to multisig functionality.
Multisig addresses are deterministically calculated from signers, threshold, and nonce. You can predict the address before creating:
use quantus_cli::predict_multisig_address;
fn predict_address_example() -> Result<(), Box<dyn std::error::Error>> {
let alice_account = parse_address("qzkaf...")?;
let bob_account = parse_address("qzmqr...")?;
let charlie_account = parse_address("qzo4j...")?;
let signers = vec![alice_account, bob_account, charlie_account];
let threshold = 2;
let nonce = 0; // Default nonce
// Calculate predicted address
let predicted_address = predict_multisig_address(signers.clone(), threshold, nonce);
println!("Predicted address: {}", predicted_address);
// Now create with the same parameters - address will match!
Ok(())
}
fn parse_address(ss58: &str) -> Result<subxt::utils::AccountId32, Box<dyn std::error::Error>> {
use sp_core::crypto::{AccountId32, Ss58Codec};
let (account_id, _) = AccountId32::from_ss58check_with_version(ss58)?;
let bytes: [u8; 32] = *account_id.as_ref();
Ok(subxt::utils::AccountId32::from(bytes))
}Key points:
- Same signers + threshold + nonce = same address (deterministic)
- Order of signers doesn't matter (automatically sorted)
- Use different nonce to create multiple multisigs with same signers
use quantus_cli::{create_multisig, predict_multisig_address, QuantusClient};
async fn create_multisig_example() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
let keypair = quantus_cli::wallet::load_keypair_from_wallet("alice", None, None)?;
// Parse signer addresses
let alice_account = parse_address("qzkaf...")?;
let bob_account = parse_address("qzmqr...")?;
let charlie_account = parse_address("qzo4j...")?;
let signers = vec![alice_account, bob_account, charlie_account];
let threshold = 2; // 2-of-3
let nonce = 0; // Default: 0. Use different values to create multiple multisigs
// Optional: Predict address before creating
let predicted = predict_multisig_address(signers.clone(), threshold, nonce);
println!("Will create at: {}", predicted);
// Create multisig (wait_for_inclusion=true to get address from event)
let (tx_hash, multisig_address) = create_multisig(
&client,
&keypair,
signers,
threshold,
nonce, // NEW: nonce parameter for deterministic addresses
true // wait for address from event
).await?;
println!("Multisig created at: {:?}", multisig_address);
assert_eq!(multisig_address.unwrap(), predicted); // Should match!
Ok(())
}use quantus_cli::{get_multisig_info, MultisigInfo};
async fn query_multisig() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
let multisig_account = parse_address("qz...")?;
if let Some(info) = get_multisig_info(&client, multisig_account).await? {
println!("Address: {}", info.address);
println!("Balance: {} (raw units)", info.balance);
println!("Threshold: {}", info.threshold);
println!("Creator: {}", info.creator);
println!("Signers: {:?}", info.signers);
println!("Active Proposals: {}", info.active_proposals);
println!("Deposit: {} (returned to creator on dissolve)", info.deposit);
println!("💡 INFO: Deposit will be returned to creator when multisig is dissolved");
}
Ok(())
}use quantus_cli::{propose_transfer, parse_multisig_amount};
async fn create_proposal() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
let keypair = quantus_cli::wallet::load_keypair_from_wallet("alice", None, None)?;
let multisig_account = parse_address("qz...")?;
let recipient = parse_address("qzmqr...")?;
// Parse amount (supports "10", "10.5", "0.001" format)
let amount = parse_multisig_amount("10")?; // 10 QUAN
let expiry = 1000; // Block number
let tx_hash = propose_transfer(
&client,
&keypair,
multisig_account,
recipient,
amount,
expiry
).await?;
println!("Proposal created: 0x{}", hex::encode(tx_hash));
Ok(())
}When a proposal reaches the approval threshold, its status becomes Approved. Execution is not automatic: any signer must then call execute to dispatch the call (CLI: quantus multisig execute --address <addr> --proposal-id <id> --from <signer>).
use quantus_cli::approve_proposal;
async fn approve_example() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
let keypair = quantus_cli::wallet::load_keypair_from_wallet("bob", None, None)?;
let multisig_account = parse_address("qz...")?;
let proposal_id = 0u32;
let tx_hash = approve_proposal(
&client,
&keypair,
multisig_account,
proposal_id
).await?;
println!("Approval submitted: 0x{}", hex::encode(tx_hash));
// Once threshold is reached, status becomes Approved; any signer must call execute to dispatch
Ok(())
}After enough signers have approved, the proposal status is Approved. Any signer must then submit an execute transaction to actually run the call. From the CLI:
quantus multisig execute --address <MULTISIG_SS58> --proposal-id <ID> --from <SIGNER_WALLET>Proposal statuses: Active (collecting approvals), Approved (threshold reached, ready to execute), Executed, Cancelled.
use quantus_cli::{list_proposals, ProposalInfo, ProposalStatus};
async fn list_all_proposals() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
let multisig_account = parse_address("qz...")?;
let proposals = list_proposals(&client, multisig_account).await?;
println!("Found {} proposal(s)", proposals.len());
for proposal in proposals {
println!("Proposal #{}:", proposal.id);
println!(" Proposer: {}", proposal.proposer);
println!(" Expiry: block {}", proposal.expiry);
println!(" Status: {:?}", proposal.status);
println!(" Approvals: {}", proposal.approvals.len());
}
Ok(())
}use quantus_cli::get_proposal_info;
async fn query_proposal() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
let multisig_account = parse_address("qz...")?;
let proposal_id = 0u32;
if let Some(proposal) = get_proposal_info(&client, multisig_account, proposal_id).await? {
println!("Proposer: {}", proposal.proposer);
println!("Call data size: {} bytes", proposal.call_data.len());
println!("Expiry: block {}", proposal.expiry);
println!("Approvals: {:?}", proposal.approvals);
println!("Status: {:?}", proposal.status);
}
Ok(())
}use quantus_cli::cancel_proposal;
async fn cancel_example() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
let keypair = quantus_cli::wallet::load_keypair_from_wallet("alice", None, None)?;
let multisig_account = parse_address("qz...")?;
let proposal_id = 0u32;
let tx_hash = cancel_proposal(
&client,
&keypair,
multisig_account,
proposal_id
).await?;
println!("Proposal canceled: 0x{}", hex::encode(tx_hash));
Ok(())
}IMPORTANT: Dissolution now requires threshold approvals and the deposit is RETURNED to the creator.
use quantus_cli::approve_dissolve_multisig;
async fn dissolve_example() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
// Each signer must approve dissolution
let multisig_account = parse_address("qz...")?;
// Alice approves (1/2)
let alice_keypair = quantus_cli::wallet::load_keypair_from_wallet("alice", None, None)?;
let tx_hash1 = approve_dissolve_multisig(
&client,
&alice_keypair,
multisig_account.clone()
).await?;
println!("Alice approved dissolution: 0x{}", hex::encode(tx_hash1));
// Bob approves (2/2) - threshold reached, multisig dissolved automatically
let bob_keypair = quantus_cli::wallet::load_keypair_from_wallet("bob", None, None)?;
let tx_hash2 = approve_dissolve_multisig(
&client,
&bob_keypair,
multisig_account
).await?;
println!("Bob approved - Multisig dissolved: 0x{}", hex::encode(tx_hash2));
Ok(())
}Requirements for dissolution:
- ✅ No proposals (any status: active, executed, or cancelled)
- ✅ Balance must be zero
- ✅ Threshold approvals required
- 💡 Deposit is RETURNED to creator on successful dissolution
Note: If proposals exist, you must first cancel or claim them before dissolution can proceed.
When a multisig extrinsic fails, the CLI (and any code using the chain) receives a dispatch error. The runtime returns named errors; after metadata is up to date, you will see messages such as:
| Error | When |
|---|---|
ExpiryTooFar |
Proposal expiry is beyond the chain's MaxExpiryDuration (e.g. ~2 weeks in blocks). |
TooManyProposalsInStorage |
Multisig has reached the max total proposals in storage; cleanup via claim_deposits or remove_expired first. |
TooManyProposalsPerSigner |
This signer has too many proposals (per-signer limit for filibuster protection). |
ProposalNotApproved |
execute was called but the proposal is not in Approved status (e.g. still Active or already removed). |
ProposalNotFound |
No proposal with the given ID for this multisig. |
CallNotAllowedForHighSecurityMultisig |
Multisig is high-security and the proposed call is not whitelisted (e.g. use schedule_transfer instead of transfer_allow_death). |
ProposalsExist |
Cannot dissolve: there are still proposals; clear them first. |
MultisigAccountNotZero |
Cannot dissolve: multisig balance is not zero. |
Other errors (e.g. NotASigner, AlreadyApproved, ExpiryInPast, ProposalExpired) are self-explanatory. Error text is resolved from runtime metadata when available.
Multisig accounts can be configured with high-security mode, which delays all transfers and allows a guardian to intercept suspicious transactions.
# CLI usage
quantus multisig high-security status --address qz...Example output:
🔍 MULTISIG Checking High-Security status...
📋 Multisig: qz...
✅ High-Security: ENABLED
🛡️ Guardian/Interceptor: qzmqr...
⏱️ Delay: 100 blocks
💡 INFO All transfers from this multisig will be delayed and reversible
The guardian can intercept suspicious transactions during the delay period
To enable high-security for a multisig, you need to create a proposal that will call reversible_transfers.set_high_security. This requires approval from threshold signers.
# CLI usage - Create proposal to enable high-security
quantus multisig propose high-security \
--address qz... \
--interceptor qzmqr... \
--delay-blocks 100 \
--expiry 2000 \
--from alice \
-p password
# Alternative: delay in seconds instead of blocks
quantus multisig propose high-security \
--address qz... \
--interceptor qzmqr... \
--delay-seconds 600 \
--expiry 2000 \
--from alice \
-p passwordExample workflow:
# 1. Alice (signer) proposes high-security
quantus multisig propose high-security \
--address qz123... \
--interceptor qzguardian... \
--delay-blocks 100 \
--expiry 2000 \
--from alice
# 2. Check proposals to find the ID
quantus multisig list-proposals --address qz123...
# 3. Bob (another signer) approves
quantus multisig approve \
--address qz123... \
--proposal-id 0 \
--from bob
# 4. Once threshold is reached, high-security is automatically enabled
# 5. Verify it's enabled
quantus multisig high-security status --address qz123...- Guardian/Interceptor: An account that can intercept (reverse) transactions during the delay period
- Delay: Time window during which transactions are reversible (in blocks or seconds)
- Delayed Transfers: All transfers from a high-security multisig are scheduled for delayed execution
- Interception: Guardian can cancel suspicious transactions and recover funds
Note: There is currently no remove command for disabling high-security mode. The runtime does not expose a remove_high_security extrinsic.
If you need to disable high-security for a multisig:
- Create a new multisig without high-security
- Transfer funds from the HS multisig to the new one
- Dissolve the old HS multisig (after cleanup)
Alternatively, request a runtime upgrade to add remove_high_security functionality.
- Choose a trusted guardian account (can be another multisig)
- Set an appropriate delay period (longer = more secure, but less convenient)
- Guardian has full control to intercept transactions during delay
- Once enabled, only whitelisted calls are allowed from high-security multisigs
- High-security cannot be disabled - consider this permanent for the multisig account
Currently, high-security operations are best performed via CLI. For programmatic access, you can build the runtime call manually:
use quantus_cli::{chain::client::QuantusClient, chain::quantus_subxt};
async fn enable_hs_via_proposal() -> Result<(), Box<dyn std::error::Error>> {
let client = QuantusClient::new("ws://127.0.0.1:9944").await?;
// Build set_high_security call
use quantus_subxt::api::reversible_transfers::calls::types::set_high_security::Delay;
let delay = Delay::BlockNumber(100);
let interceptor = parse_address("qzguardian...")?;
let set_hs_call = quantus_subxt::api::tx()
.reversible_transfers()
.set_high_security(delay, interceptor);
// Encode as call data
use subxt::tx::Payload;
let call_data = set_hs_call.encode_call_data(&client.client().metadata())?;
// Create multisig proposal with this call
let multisig_account = parse_address("qz...")?;
let expiry = 2000;
let propose_tx = quantus_subxt::api::tx()
.multisig()
.propose(multisig_account, call_data, expiry);
// Submit via your signer keypair
// ... (submit transaction)
Ok(())
}See the examples/ directory for complete working examples:
examples/basic_usage.rs- Basic library usageexamples/wallet_ops.rs- Advanced wallet operationsexamples/service.rs- Service architecture exampleexamples/multisig_library_usage.rs- Multisig operationsexamples/multisig_usage.rs- Multisig CLI usage reference
# Run basic usage example
cargo run --example basic_usage
# Run wallet operations example
cargo run --example wallet_ops
# Run service example
cargo run --example service
# Run multisig library usage example
cargo run --example multisig_library_usage- Quantum-safe cryptography: Uses Dilithium ML-DSA-87 for all cryptographic operations
- Wallet management: Create, import, export, and manage multiple wallets
- Blockchain interaction: Query balances, send transactions, get system info
- Thread-safe: Safe to use in multi-threaded applications
- Async/await: Full async support for non-blocking operations
- Error handling: Comprehensive error types for better error handling
- Developer wallets: Built-in support for test wallets (crystal_alice, crystal_bob, crystal_charlie)
- Always use strong passwords for wallet encryption
- Store passwords securely in production applications
- Use environment variables or secure key management for passwords
- The library uses quantum-safe encryption (AES-256-GCM + Argon2) for wallet storage
- Private keys are never stored in plain text