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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,12 @@ quantus wallet import --name recovered_wallet --mnemonic-file ./mnemonic.txt
# Import as ML-DSA-87 (same secure prompt)
quantus wallet import --name recovered_87 --scheme ml-dsa-87

# Recovering an existing wallet? Show every address the phrase maps to and pick one
quantus wallet import --name recovered_wallet --scan

# Air-gapped machine: never contact a node
quantus wallet import --name recovered_wallet --scan --offline

# Create from raw 32-byte seed
quantus wallet from-seed --name raw_wallet --seed <64-hex-chars>

Expand All @@ -597,6 +603,29 @@ quantus wallet view --name my_wallet
quantus wallet export --name my_wallet --format mnemonic
```

#### Recovering a wallet that shows no funds

The same recovery phrase maps to a different address for each signature scheme
and derivation path, and the CLI's defaults have changed:

| Created with | Scheme | HD path |
|---|---|---|
| CLI v2.0 and later (current default) | ML-DSA-65 | `m/44'/189189'/0'/0'/1'` |
| CLI v1.x | ML-DSA-87 | `m/44'/189189'/0'/0'/0'` |
| `--no-derivation` (either scheme) | — | master seed |

A plain `wallet import` uses the current default, so a phrase from an older
wallet imports "successfully" onto a brand-new, empty address. After a plain
import the CLI checks the address on-chain (when a node is reachable) and warns
if it has no current balance or nonce. `--scan` derives all of the combinations
above, shows the current balance and nonce of each, and lets you pick; it also prints the
explicit `--scheme`/`--derivation-path` flags for next time. For even older
testnet addresses, use `quantus airdrop check`.

Wallet commands never require a node. The on-chain check waits at most a few
seconds and is skipped when nothing answers; pass `--offline` to not attempt it
at all.

---

### Cold Wallets (Keystone / Quantus Cold Wallet App)
Expand Down
56 changes: 48 additions & 8 deletions src/chain/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,9 @@ impl subxt::tx::Signer<ChainConfig> for QuantusSigner {
}
}

/// In-process mock node for tests in this crate that need a real `QuantusClient`.
#[cfg(test)]
mod tests {
pub(crate) mod test_support {
use super::*;
use codec::Encode;
use jsonrpsee::{
Expand All @@ -453,13 +454,13 @@ mod tests {
use serde_json::json;
use std::sync::Mutex;

const OLD_RUNTIME: RuntimeVersion =
pub(crate) const OLD_RUNTIME: RuntimeVersion =
RuntimeVersion { spec_version: 144, transaction_version: 3 };
const NEW_RUNTIME: RuntimeVersion =
pub(crate) const NEW_RUNTIME: RuntimeVersion =
RuntimeVersion { spec_version: 148, transaction_version: 6 };
const GENESIS: H256 = H256([0x01; 32]);
const FINALIZED: H256 = H256([0x44; 32]);
const HEAD: H256 = H256([0x48; 32]);
pub(crate) const GENESIS: H256 = H256([0x01; 32]);
pub(crate) const FINALIZED: H256 = H256([0x44; 32]);
pub(crate) const HEAD: H256 = H256([0x48; 32]);

fn runtime_at(hash: H256) -> RuntimeVersion {
if hash == HEAD {
Expand All @@ -469,14 +470,38 @@ mod tests {
}
}

pub(crate) struct MockNode {
pub(crate) url: String,
pub(crate) metadata_requests: Arc<Mutex<Vec<H256>>>,
_handle: ServerHandle,
}

/// A node caught mid-upgrade the way Heisenberg is for ~20 minutes after every enactment:
/// the head runs the new runtime, the finalized block still runs the old one. Records the
/// block named by every metadata request.
async fn mock_node() -> (String, Arc<Mutex<Vec<H256>>>, ServerHandle) {
///
/// With `stall_storage`, the node accepts connections and answers everything
/// `QuantusClient::new` needs, but never responds to `state_getStorage`: a node that
/// connects fine and then stops answering.
pub(crate) async fn mock_node(stall_storage: bool) -> MockNode {
let metadata_requests = Arc::new(Mutex::new(Vec::new()));
let server = Server::builder().build("127.0.0.1:0").await.expect("bind mock node");
let url = format!("ws://{}", server.local_addr().expect("mock node address"));
let mut module = RpcModule::new(metadata_requests.clone());
if stall_storage {
module
.register_async_method("state_getStorage", |_, _, _| async {
std::future::pending::<Result<Option<String>, ErrorObjectOwned>>().await
})
.expect("register");
} else {
// Empty state: every storage read is "no value".
module
.register_method("state_getStorage", |_, _, _| {
Ok::<Option<String>, ErrorObjectOwned>(None)
})
.expect("register");
}
module
.register_method("chain_getBlockHash", |params, _, _| {
let number: Option<u32> = params.sequence().optional_next()?;
Expand Down Expand Up @@ -517,7 +542,22 @@ mod tests {
))
})
.expect("register");
(url, metadata_requests, server.start(module))
MockNode { url, metadata_requests, _handle: server.start(module) }
}
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::sync::Mutex;
use test_support::{
mock_node as start_mock_node, MockNode, FINALIZED, GENESIS, HEAD, NEW_RUNTIME, OLD_RUNTIME,
};

async fn mock_node() -> (String, Arc<Mutex<Vec<H256>>>, MockNode) {
let node = start_mock_node(false).await;
(node.url.clone(), node.metadata_requests.clone(), node)
}

#[tokio::test]
Expand Down
Loading
Loading