Skip to content
Open
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
32 changes: 32 additions & 0 deletions src/cli/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,38 @@ fn format_dispatch_error(
}
}

/// Submit a preimage, treating AlreadyNoted as success (idempotent).
/// Always waits for inclusion so subsequent txs from the same sender get a fresh nonce.
pub async fn submit_preimage(
quantus_client: &crate::chain::client::QuantusClient,
keypair: &crate::wallet::QuantumKeyPair,
encoded_call: Vec<u8>,
execution_mode: ExecutionMode,
) -> Result<()> {
type PreimageBytes =
crate::chain::quantus_subxt::api::preimage::calls::types::note_preimage::Bytes;
let bounded_bytes: PreimageBytes = encoded_call;

crate::log_print!("📝 Submitting preimage...");
let note_preimage_tx =
crate::chain::quantus_subxt::api::tx().preimage().note_preimage(bounded_bytes);
let wait_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };

match submit_transaction(quantus_client, keypair, note_preimage_tx, None, wait_mode).await {
Ok(_) => {
crate::log_success!("Preimage submitted");
},
Err(e) if e.to_string().contains("AlreadyNoted") => {
crate::log_print!(
"✅ {} Preimage already exists on-chain, continuing",
"OK".bright_green().bold()
);
},
Err(e) => return Err(e),
}
Ok(())
}

async fn check_execution_success(
client: &OnlineClient<ChainConfig>,
block_hash: &subxt::utils::H256,
Expand Down
124 changes: 90 additions & 34 deletions src/cli/multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};

// Base unit (QUAN) decimals for amount conversions
const QUAN_DECIMALS: u128 = 1_000_000_000_000; // 10^12
const DEFAULT_TRANSFER_EXPIRY_BLOCKS: u32 = (2 * 60 * 60) / 10; // ~2h at 10s/block

// ============================================================================
// PUBLIC LIBRARY API - Data Structures
Expand Down Expand Up @@ -110,25 +111,26 @@ pub fn parse_amount(amount: &str) -> crate::error::Result<u128> {
#[derive(Subcommand, Debug)]
pub enum ProposeSubcommand {
/// Propose a simple transfer (most common case)
#[command(arg_required_else_help = true)]
Transfer {
/// Multisig account address (SS58 format)
#[arg(long)]
/// The multisig account address (SS58 format)
#[arg(long, value_name = "MULTISIG_ADDRESS")]
address: String,

/// Recipient address (SS58 format)
#[arg(long)]
/// The recipient address (SS58 format or wallet name)
#[arg(long, value_name = "RECIPIENT")]
to: String,

/// Amount to transfer (e.g., "10", "10.5", or raw "10000000000000")
#[arg(long)]
/// The amount to transfer (e.g. "10", "10.5", or raw base units)
#[arg(long, value_name = "AMOUNT")]
amount: String,

/// Expiry block number (when this proposal expires)
#[arg(long)]
expiry: u32,
/// Expiry block number for this proposal. If omitted, defaults to head + ~2h
#[arg(long, value_name = "EXPIRY_BLOCK")]
expiry: Option<u32>,

/// Proposer wallet name (must be a signer)
#[arg(long)]
/// The proposer wallet name (must be a signer in this multisig)
#[arg(long, value_name = "PROPOSER_WALLET")]
from: String,

/// Password for the wallet
Expand Down Expand Up @@ -215,9 +217,13 @@ pub enum ProposeSubcommand {
#[derive(Subcommand, Debug)]
pub enum MultisigCommands {
/// Create a new multisig account
#[command(
arg_required_else_help = true,
after_help = "Examples:\n quantus multisig create --signers \"5F3sa2TJ...abc,5DAAnrj7...xyz,5HGjWAeF...123\" --threshold 2 --from alice\n quantus multisig create --signers \"alice,bob,charlie\" --threshold 2 --from alice"
)]
Create {
/// List of signer addresses (SS58 or wallet names), comma-separated
#[arg(long)]
#[arg(long, value_name = "SIGNERS_CSV")]
signers: String,

/// Number of approvals required to execute transactions
Expand Down Expand Up @@ -476,14 +482,11 @@ pub fn predict_multisig_address(
data.extend_from_slice(&threshold.encode());
data.extend_from_slice(&nonce.encode());

// Hash the data and map it deterministically into an AccountId
// CRITICAL: Use PoseidonHasher (same as runtime!) and TrailingZeroInput
use codec::Decode;
use qp_poseidon::PoseidonHasher;
use sp_core::crypto::AccountId32;
use sp_runtime::traits::{Hash as HashT, TrailingZeroInput};
use sp_runtime::traits::{BlakeTwo256, Hash as HashT, TrailingZeroInput};

let hash = PoseidonHasher::hash(&data);
let hash = BlakeTwo256::hash(&data);
let account_id = AccountId32::decode(&mut TrailingZeroInput::new(hash.as_ref()))
.expect("TrailingZeroInput provides sufficient bytes; qed");

Expand Down Expand Up @@ -1194,15 +1197,16 @@ async fn handle_create_multisig(
if let Some(address) = actual_address {
log_print!("");

// Verify address matches prediction
// Use the on-chain event address as the source of truth.
log_success!("✅ Confirmed multisig address: {}", address.bright_cyan().bold());
if address == predicted_address {
log_success!("✅ Confirmed multisig address: {}", address.bright_cyan().bold());
log_print!(" {} Matches predicted address!", "✓".bright_green().bold());
} else {
log_error!("⚠️ Address mismatch!");
log_print!(" Expected: {}", predicted_address.bright_yellow());
log_print!(" Got: {}", address.bright_red());
log_print!(" This should never happen with deterministic addresses!");
log_verbose!(
"Predicted address differed from emitted address: predicted={}, emitted={}",
predicted_address,
address
);
}

log_print!("");
Expand Down Expand Up @@ -1305,7 +1309,7 @@ async fn handle_propose_transfer(
multisig_address: String,
to: String,
amount: String,
expiry: u32,
expiry: Option<u32>,
from: String,
password: Option<String>,
password_file: Option<String>,
Expand All @@ -1332,8 +1336,26 @@ async fn handle_propose_transfer(
// Connect to chain and check if multisig has High Security enabled
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
let current_block_number = latest_block.number();
let storage_at = quantus_client.client().storage().at(latest_block_hash);

let expiry = match expiry {
Some(expiry) => expiry,
None => {
let default_expiry =
current_block_number.saturating_add(DEFAULT_TRANSFER_EXPIRY_BLOCKS);
log_print!(
"⏱️ {} No --expiry provided; using block {} (head {} + {} blocks, ~2h)",
"DEFAULT".bright_blue().bold(),
default_expiry,
current_block_number,
DEFAULT_TRANSFER_EXPIRY_BLOCKS
);
default_expiry
},
};

let hs_query = quantus_subxt::api::storage()
.reversible_transfers()
.high_security_accounts(multisig_account_id);
Expand Down Expand Up @@ -1403,6 +1425,43 @@ async fn handle_propose_transfer(
}
}

async fn fetch_proposal_id(
quantus_client: &crate::chain::client::QuantusClient,
multisig_ss58: &str,
) -> Option<u32> {
let latest_block_hash = quantus_client.get_latest_block().await.ok()?;
let events = quantus_client.client().events().at(latest_block_hash).await.ok()?;
for ev in events.find::<quantus_subxt::api::multisig::events::ProposalCreated>() {
if let Ok(created) = ev {
let addr_bytes: &[u8; 32] = created.multisig_address.as_ref();
let addr = SpAccountId32::from(*addr_bytes);
let addr_ss58 =
addr.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
if addr_ss58 == multisig_ss58 {
return Some(created.proposal_id);
}
}
}
None
}

fn log_proposal_result(multisig_ss58: &str, proposal_id: Option<u32>) {
if let Some(id) = proposal_id {
log_print!("");
log_success!("✅ Proposal #{} confirmed on-chain", id.to_string().bright_cyan().bold());
log_print!("");
log_print!("🚀 {} To approve this proposal, signers run:", "NEXT".bright_blue().bold());
log_print!(
" quantus multisig approve --address {} --proposal-id {} --from <SIGNER_WALLET>",
multisig_ss58.bright_cyan(),
id
);
} else {
log_success!("✅ Proposal confirmed on-chain");
log_print!(" Run `quantus multisig list-proposals --address {}` to find the proposal ID", multisig_ss58);
}
}

/// Propose a custom transaction
async fn handle_propose(
multisig_address: String,
Expand Down Expand Up @@ -1542,7 +1601,8 @@ async fn handle_propose(
)
.await?;

log_success!("✅ Proposal confirmed on-chain");
let proposal_id = fetch_proposal_id(&quantus_client, &multisig_ss58).await;
log_proposal_result(&multisig_ss58, proposal_id);

Ok(())
}
Expand Down Expand Up @@ -1609,7 +1669,8 @@ async fn handle_propose_with_call_data(
)
.await?;

log_success!("✅ Proposal confirmed on-chain");
let proposal_id = fetch_proposal_id(quantus_client, &multisig_ss58).await;
log_proposal_result(&multisig_ss58, proposal_id);

Ok(())
}
Expand Down Expand Up @@ -3152,18 +3213,13 @@ async fn handle_high_security_set(
)
.await?;

log_print!("");
log_success!("✅ High-Security proposal confirmed on-chain!");
log_print!("");
let proposal_id = fetch_proposal_id(&quantus_client, &multisig_ss58).await;
log_proposal_result(&multisig_ss58, proposal_id);
log_print!(
"💡 {} Once this proposal reaches threshold, High-Security will be enabled",
"NEXT STEPS".bright_blue().bold()
);
log_print!(
" - Other signers need to approve: quantus multisig approve --address {} --proposal-id <ID> --from <SIGNER>",
multisig_ss58.bright_cyan()
);
log_print!(" - After threshold is reached, all transfers will be delayed and reversible");
log_print!(" After threshold is reached, all transfers will be delayed and reversible");
log_print!("");

Ok(())
Expand Down
5 changes: 1 addition & 4 deletions src/cli/preimage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,6 @@ async fn create_preimage(
password_file: Option<String>,
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<()> {
use qp_poseidon::PoseidonHasher;

log_print!("📦 Creating preimage from WASM file: {}", wasm_file.display());
log_print!(" 👤 From: {}", from_str.bright_yellow());

Expand All @@ -405,9 +403,8 @@ async fn create_preimage(

log_verbose!("📝 Encoded call size: {} bytes", encoded_call.len());

// Compute preimage hash using Poseidon (runtime uses PoseidonHasher)
let preimage_hash: sp_core::H256 =
<PoseidonHasher as sp_runtime::traits::Hash>::hash(&encoded_call);
<sp_runtime::traits::BlakeTwo256 as sp_runtime::traits::Hash>::hash(&encoded_call);

log_print!("🔗 Preimage hash: {:?}", preimage_hash);

Expand Down
27 changes: 9 additions & 18 deletions src/cli/referenda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{
chain::quantus_subxt, cli::common::submit_transaction, error::QuantusError, log_error,
log_print, log_success, log_verbose,
};
use crate::cli::tech_collective::VoteChoice;
use clap::Subcommand;
use colored::Colorize;
use std::str::FromStr;
Expand Down Expand Up @@ -102,9 +103,9 @@ pub enum ReferendaCommands {
#[arg(short, long)]
index: u32,

/// Vote aye (true) or nay (false)
/// Vote: "aye" or "nay"
#[arg(long)]
aye: bool,
vote: VoteChoice,

/// Conviction (0=None, 1=Locked1x, 2=Locked2x, up to 6=Locked6x)
#[arg(long, default_value = "0")]
Expand Down Expand Up @@ -216,7 +217,7 @@ pub async fn handle_referenda_command(
.await,
ReferendaCommands::Vote {
index,
aye,
vote,
conviction,
amount,
from,
Expand All @@ -226,7 +227,7 @@ pub async fn handle_referenda_command(
vote_on_referendum(
&quantus_client,
index,
aye,
matches!(vote, VoteChoice::Aye),
conviction,
&amount,
&from,
Expand Down Expand Up @@ -293,19 +294,9 @@ async fn submit_remark_proposal(

log_print!("🔗 Preimage hash: {:?}", preimage_hash);

// Submit Preimage::note_preimage
type PreimageBytes = quantus_subxt::api::preimage::calls::types::note_preimage::Bytes;
let bounded_bytes: PreimageBytes = encoded_call.clone();

log_print!("📝 Submitting preimage...");
let note_preimage_tx = quantus_subxt::api::tx().preimage().note_preimage(bounded_bytes);
let preimage_tx_hash =
submit_transaction(quantus_client, &keypair, note_preimage_tx, None, execution_mode)
.await?;
log_print!("✅ Preimage transaction submitted: {:?}", preimage_tx_hash);

// Wait for preimage transaction confirmation
log_print!("⏳ Waiting for preimage transaction confirmation...");
let call_len = encoded_call.len() as u32;
crate::cli::common::submit_preimage(quantus_client, &keypair, encoded_call, execution_mode)
.await?;

// Build Referenda::submit call using Lookup preimage reference
type ProposalBounded =
Expand All @@ -316,7 +307,7 @@ async fn submit_remark_proposal(

let preimage_hash_subxt: subxt::utils::H256 = preimage_hash;
let proposal: ProposalBounded =
ProposalBounded::Lookup { hash: preimage_hash_subxt, len: encoded_call.len() as u32 };
ProposalBounded::Lookup { hash: preimage_hash_subxt, len: call_len };

// Create origin based on origin_type parameter
let account_id_sp = keypair.to_account_id_32();
Expand Down
Loading
Loading