Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,398 changes: 1,323 additions & 75 deletions Cargo.lock

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,29 @@ path = "src/lib.rs"
name = "quantus"
path = "src/main.rs"

[features]
default = ["camera"]
# Camera capture + QR decoding for cold-wallet signing. Disable for headless
# builds; UR input via --cold-response-in <file|-> keeps working without it.
camera = ["dep:minifb", "dep:nokhwa", "dep:rxing"]

[dependencies]
# CLI and async runtime
clap = { version = "4.5", features = ["derive"] }
tokio = { version = "1.46", features = ["full"] }

# Cold-wallet QR signing: UR encoding shared with the mobile/cold wallet apps
# (1.6.0). Keystone firmware still encodes 1.4.0; the wire format is compatible.
quantus_ur = { git = "https://github.com/Quantus-Network/quantus_ur.git", tag = "1.6.0" }
# Terminal QR rendering (string renderers only; image/svg not needed)
qrcode = { version = "0.14", default-features = false }
# Camera capture (AVFoundation / MSMF / V4L2) + pure-Rust QR decoding.
# rxing (ZXing port) over rqrr: its adaptive binarizer decodes the defocused
# frames a fixed-focus Mac camera produces at phone-scanning distance.
minifb = { version = "0.28", optional = true }
nokhwa = { version = "0.10", features = ["input-native"], optional = true }
rxing = { version = "0.8", default-features = false, features = ["encoding_rs"], optional = true }

# Serialization and configuration
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
28 changes: 13 additions & 15 deletions LIBRARY_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,20 +175,18 @@ use quantus_cli::{
};

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;


// Hot wallets decrypt locally; cold (watch-only) wallets sign over QR.
let signer = quantus_cli::wallet::load_signer_from_wallet("my_wallet", None, None)?;

// Recipient address
let to_address = "qzkeicNBtW2AG2E7USjDcLzAL8d9WxTZnV2cbtXoDzWxzpHC2";

// Submit and wait for inclusion in a best block
let tx_hash = transfer(
&client,
&keypair,
&signer,
to_address,
1_000_000_000_000, // raw units, e.g. 1 token on a 12-decimal chain
None,
Expand Down Expand Up @@ -338,7 +336,7 @@ 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)?;
let signer = quantus_cli::wallet::load_signer_from_wallet("alice", None, None)?;

// Parse signer addresses
let alice_account = parse_address("qzkaf...")?;
Expand All @@ -356,7 +354,7 @@ async fn create_multisig_example() -> Result<(), Box<dyn std::error::Error>> {
// Create multisig (wait_for_inclusion=true to get address from event)
let (tx_hash, multisig_address) = create_multisig(
&client,
&keypair,
&signer,
signers,
threshold,
nonce, // NEW: nonce parameter for deterministic addresses
Expand Down Expand Up @@ -400,7 +398,7 @@ 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 signer = quantus_cli::wallet::load_signer_from_wallet("alice", None, None)?;

let multisig_account = parse_address("qz...")?;
let recipient = parse_address("qzmqr...")?;
Expand All @@ -412,7 +410,7 @@ async fn create_proposal() -> Result<(), Box<dyn std::error::Error>> {

let tx_hash = propose_transfer(
&client,
&keypair,
&signer,
multisig_account,
recipient,
amount,
Expand All @@ -433,14 +431,14 @@ 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 signer = quantus_cli::wallet::load_signer_from_wallet("bob", None, None)?;

let multisig_account = parse_address("qz...")?;
let proposal_id = 0u32;

let tx_hash = approve_proposal(
&client,
&keypair,
&signer,
multisig_account,
proposal_id
).await?;
Expand Down Expand Up @@ -514,14 +512,14 @@ 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 signer = quantus_cli::wallet::load_signer_from_wallet("alice", None, None)?;

let multisig_account = parse_address("qz...")?;
let proposal_id = 0u32;

let tx_hash = cancel_proposal(
&client,
&keypair,
&signer,
multisig_account,
proposal_id
).await?;
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ A modern command line interface for interacting with the Quantus Network, featur
## 🌟 Features

- **Quantum-Safe Wallets**: Built with Dilithium post-quantum cryptography
- **Cold Wallet Signing**: Air-gapped signing over QR codes with Keystone or the Quantus cold wallet app
- **SubXT Integration**: Modern Substrate client with type-safe API
- **Generic Pallet Calls**: Call ANY blockchain function using metadata-driven parsing
- **Real Chain Operations**: Send tokens, query balances, explore metadata
Expand Down Expand Up @@ -398,6 +399,63 @@ quantus wallet export --name my_wallet --format mnemonic

---

### Cold Wallets (Keystone / Quantus Cold Wallet App)

Pair the CLI with an air-gapped signer — a Keystone 3 hardware wallet or the
Quantus cold wallet app. The CLI stores only the address (watch-only); every
transaction is signed on the device by exchanging QR codes.

```bash
# Import by scanning the device's address QR with the laptop camera
quantus wallet import-cold --name my_cold

# Or paste the address directly (no camera needed)
quantus wallet import-cold --name my_cold --address qz...

# Any extrinsic command works with a cold wallet: the CLI shows the
# transaction as a QR, then scans the device's animated signature QR
quantus send --from my_cold --to <address> --amount 10.5 --wait-for-transaction
quantus multisig approve --from my_cold --address qz... --proposal-id 0
```

The signing flow (identical for every command — commands don't know whether
the wallet is hot or cold; the shared submit stage branches on the wallet
type):

1. The CLI displays the transaction as a `ur:quantus-sign-request` QR
(animated if the payload is large). Scan it with the cold wallet, review
the details on the device, and press Enter in the CLI.
2. Sign on the device — it shows an animated QR containing the signature.
3. Confirm in the CLI, then point the laptop camera at the device's screen.
The CLI verifies the signature against the stored address before
submitting; a response signed by any other key is rejected.

Notes:

- The transaction stays valid for 256 blocks after the QR is generated; if it
expires or the account's nonce changes before submission, re-run the
command to sign a fresh QR.
- Commands that submit several extrinsics (e.g. `runtime update`,
`tech-referenda submit-with-preimage`) do one QR roundtrip per extrinsic.
- Wormhole operations are not cold-compatible: they derive secrets from the
wallet's mnemonic and submit unsigned extrinsics.
- **macOS camera permission**: the permission prompt is attributed to your
terminal app (Terminal, iTerm, VS Code, …) — grant it under System
Settings > Privacy & Security > Camera.
- **Headless / no camera**: `--cold-response-in <file>` (or `-` for stdin)
reads the response UR parts (one per line) instead of scanning, and the
hidden `--cold-request-out <file>` writes the request UR parts for
scripted flows. Both are global flags that work with any command. Builds
without the default `camera` feature (`cargo build --no-default-features`)
support only this path.
- Real devices only sign for known networks (Planck / Heisenberg genesis
hashes) and whitelisted calls — balance transfers, reversible transfers,
and (cold wallet app only) multisig. Test against a dev node with the
hidden `quantus developer cold-sign-sim` command, which plays the cold
wallet side using a local hot wallet.

---

### Sending Tokens

```bash
Expand Down
44 changes: 12 additions & 32 deletions examples/basic_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
//! 4. Query blockchain data

use quantus_cli::{
chain::client::QuantusClient,
error::Result,
wallet::{QuantumKeyPair, WalletManager},
AccountId32,
chain::client::QuantusClient, cli::common::ExecutionMode, error::Result, transfer,
wallet::WalletManager, AccountId32,
};
use sp_core::crypto::Ss58Codec;

#[tokio::main]
async fn main() -> Result<()> {
Expand Down Expand Up @@ -79,36 +76,19 @@ async fn get_account_balance(client: &QuantusClient, account_id: &AccountId32) -
#[allow(dead_code)]
async fn send_tokens(
client: &QuantusClient,
from_keypair: &QuantumKeyPair,
from_keypair: &quantus_cli::wallet::QuantumKeyPair,
to_address: &str,
amount: u128,
) -> Result<subxt::utils::H256> {
use quantus_cli::chain::quantus_subxt::api;

// Parse recipient address
let to_account_id = AccountId32::from_ss58check(to_address)
.map_err(|e| quantus_cli::error::QuantusError::Generic(format!("Invalid address: {e}")))?;
let to_account_bytes: [u8; 32] = *to_account_id.as_ref();
let to_subxt_account_id = subxt::utils::AccountId32::from(to_account_bytes);

// Create transfer call
let transfer_call =
api::tx().balances().transfer_allow_death(to_subxt_account_id.into(), amount);

// Convert QuantumKeyPair to DilithiumPair for signing
let dilithium_pair = from_keypair.to_subxt_signer()?;

// Submit transaction
let tx_hash = client
.client()
.tx()
.sign_and_submit_then_watch_default(&transfer_call, &dilithium_pair)
.await?
.wait_for_finalized_success()
.await?
.extrinsic_hash();

Ok(tx_hash)
transfer(
client,
&from_keypair.as_signer(),
to_address,
amount,
None,
ExecutionMode { wait_for_transaction: true, finalized: true },
)
.await
}

/// Example of creating a wallet from mnemonic
Expand Down
15 changes: 11 additions & 4 deletions examples/multisig_library_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,15 @@ async fn main() -> Result<()> {
let predicted_address = predict_multisig_address(signers.clone(), threshold, nonce);
println!("📍 Predicted address: {}", predicted_address);

let (tx_hash, multisig_address) =
create_multisig(&quantus_client, &alice_keypair, signers, threshold, nonce, true).await?;
let (tx_hash, multisig_address) = create_multisig(
&quantus_client,
&alice_keypair.as_signer(),
signers,
threshold,
nonce,
true,
)
.await?;

println!("✅ Multisig created!");
println!(" Tx hash: 0x{}", hex::encode(tx_hash));
Expand Down Expand Up @@ -109,7 +116,7 @@ async fn main() -> Result<()> {

let propose_tx_hash = propose_transfer(
&quantus_client,
&alice_keypair,
&alice_keypair.as_signer(),
multisig_account.clone(),
bob_account.clone(),
amount,
Expand Down Expand Up @@ -157,7 +164,7 @@ async fn main() -> Result<()> {
println!("✅ Approving proposal #{}...", proposal_id);
let approve_tx_hash = approve_proposal(
&quantus_client,
&bob_keypair,
&bob_keypair.as_signer(),
multisig_account.clone(),
proposal_id,
)
Expand Down
2 changes: 1 addition & 1 deletion examples/multisig_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async fn main() -> Result<()> {

let tx_hash = quantus_cli::cli::common::submit_transaction(
&quantus_client,
&alice_keypair,
&quantus_cli::wallet::WalletSigner::Hot(alice_keypair),
create_tx,
None,
execution_mode,
Expand Down
36 changes: 12 additions & 24 deletions examples/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

use quantus_cli::{
chain::client::QuantusClient,
cli::common::ExecutionMode,
error::{QuantusError, Result},
transfer,
wallet::{QuantumKeyPair, WalletManager},
AccountId32,
};
Expand Down Expand Up @@ -195,30 +197,16 @@ impl WalletService {
to_account_id: &AccountId32,
amount: u128,
) -> Result<subxt::utils::H256> {
use quantus_cli::chain::quantus_subxt::api;

// Convert recipient to subxt format
let to_account_bytes: [u8; 32] = *to_account_id.as_ref();
let to_subxt_account_id = subxt::utils::AccountId32::from(to_account_bytes);

// Create transfer call
let transfer_call =
api::tx().balances().transfer_allow_death(to_subxt_account_id.into(), amount);

// Convert QuantumKeyPair to DilithiumPair for signing
let dilithium_pair = from_keypair.to_subxt_signer()?;

// Submit transaction
let tx_hash = client
.client()
.tx()
.sign_and_submit_then_watch_default(&transfer_call, &dilithium_pair)
.await?
.wait_for_finalized_success()
.await?
.extrinsic_hash();

Ok(tx_hash)
let to_address = to_account_id.to_ss58check();
transfer(
client,
&from_keypair.as_signer(),
&to_address,
amount,
None,
ExecutionMode { wait_for_transaction: true, finalized: true },
)
.await
}
}

Expand Down
Loading
Loading