Skip to content

Import a cold wallet's NEAR public key and accept cold wallets in all near commands - #175

Merged
illuzen merged 2 commits into
mainfrom
near-cold-key-import
Oct 1, 2026
Merged

illuzen merged 2 commits into
mainfrom
near-cold-key-import

Conversation

@illuzen

@illuzen illuzen commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Step 5 of the cold-wallet NEAR plan: the CLI side of the NEAR public key export QR added in Quantus-Network/quantus-apps#677.

A cold wallet file holds only an SS58 address, which is a hash of the key and cannot be turned back into the 1952-byte ML-DSA-65 public key NEAR needs for AddKey or to name a transaction's signer. Until now near show-key, create-account, keys, send and dao were hot-wallet only.

  • quantus near import-cold-key --wallet <cold> [--key ml-dsa-65:<b58>] — scans the export QR the cold wallet app shows (account → Show public key → NEAR), or takes the key as text. Refused unless the key hashes to the wallet's stored address and (when scanned) the export's address matches, so another device's QR cannot be attached to the wallet. Saved on the wallet record via save_wallet_if_current.
  • EncryptedWallet.near_public_key: Option<String> — #[serde(default, skip_serializing_if = "Option::is_none")]; absent for hot wallets and for cold wallets written before this change.
  • qr::key_export::NearPublicKeyExport — strict decoder for {v: 1, kind: "near-public-key", address, near_public_key} (deny_unknown_fields), mirrors NearPublicKeyExport in quantus_sdk. public_key() re-derives the AccountId32 from the key and compares it to address.
  • Every quantus near command now accepts a cold wallet. load_ml_dsa_65_wallet became load_near_signer returning a NearSigner::{Hot, Cold}; send and the DAO calls sign through it, so a cold wallet runs the same QR roundtrip as sign-cold. The key is re-checked against the address every time it is read from the (unencrypted) wallet file. Cold wallets with no imported key get an error pointing at import-cold-key.
  • developer cold-sign-sim --export-near-key — emits the same payload a device would, for end-to-end testing without hardware.
  • Fixed help text that referred to a non-existent quantus wallet create-cold (the command is import-cold).

Testing

  • ./clippy.sh clean (nightly fmt, taplo, clippy -D warnings).
  • cargo test --all-features: 419 passed. New: 7 tests in qr::key_export (round trip, SDK key set, unknown keys / version / kind, signing request rejected, key–address mismatch, non-ML-DSA key, bad address), test_cold_wallet_near_public_key_round_trip in wallet.
  • End-to-end with an isolated $HOME: create ML-DSA-65 hot wallet → import-cold its address → cold-sign-sim --export-near-key → near import-cold-key --cold-response-in → near show-key --wallet <cold> prints the same key and handle as the hot wallet; re-import reports "already holds"; import into a cold wallet with a different address, scanning another device's export, and importing into a hot wallet are all refused with specific errors; near keys --wallet <cold> works without a password prompt.

Notes

  • near create-account --wallet <cold> needs no QR: the parent signs; only the public key is read.
  • The on-chain path for cold send/dao (sign_transaction_cold) is the one already exercised by sign-cold in Add cold-wallet signing for NEAR transactions #174; not re-run against testnet here as it requires a funded account with the key registered.

A cold wallet file holds only an SS58 address, which cannot be turned back
into the ML-DSA-65 key NEAR needs. `quantus near import-cold-key` reads the
key export the cold wallet app shows (v1 {kind: near-public-key, address,
near_public_key}), refuses it unless the key hashes to the wallet's stored
address, and saves it on the wallet record. Every `quantus near` command
then accepts the cold wallet: hot wallets sign locally, cold wallets run the
QR roundtrip of sign-cold.

The simulator gains --export-near-key to emit the same payload a device
would, for end-to-end testing.
@illuzen illuzen added the bot-review Request automated review from review-bot label Oct 1, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer model: GPT-6 Sol

Verdict: Request changes. The import can report success after saving a public key to a different cold wallet address.

  • [P2] Bind the save to the address that was validated (src/cli/near.rs:377-411, src/wallet/mod.rs:362-369). handle_import_cold_key reads the wallet address before waiting for the QR scan. If another process deletes and reimports that wallet name with a different address during the scan, set_cold_near_public_key reloads the replacement record and saves the previously validated key to it. Its compare-and-save check only protects changes after that reload, so the command reports success with a mismatched key; later show-key and send reject the wallet. Pass the validated address or wallet snapshot into the setter and reject the update if the current record differs, or validate the key against the current address as part of the guarded save.

Validation: reviewed the full diff and the cold wallet QR export at Quantus-Network/quantus-apps#677; git diff --check and cargo +nightly fmt --all -- --check passed locally. All seven PR checks passed, including macOS and Ubuntu build/test jobs. I did not rerun the full Rust test suite locally.

@n13 n13 removed the bot-review Request automated review from review-bot label Oct 1, 2026
The address is read before the QR scan waits; if the wallet name is deleted
and re-imported with another address meanwhile, the guarded save would have
attached the validated key to the replacement record.
@illuzen

illuzen commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in a9510f2.

[P2] Bind the save to the validated address — set_cold_near_public_key now takes the address the key was checked against and refuses the save ("nothing was saved — retry") if the record on disk has a different address, before the existing compare-and-save. handle_import_cold_key passes the address it validated the export against. New test test_cold_wallet_near_public_key_save_is_bound_to_the_validated_address deletes and re-imports the wallet name with another address between validation and save and checks that the key is not written.

./clippy.sh clean; cargo test --all-features green.

@illuzen illuzen added the bot-review Request automated review from review-bot label Oct 1, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer model: GPT-6 Sol

Verdict: Approve. The new address check resolves my earlier finding: the import refuses to save a validated key if the wallet name now points to a different address. I found no blocking issues at head a9510f2.

I reviewed the full diff against base 72a89a8, including the cold-signing path and the cold wallet app's export format. The decoder checks the key scheme, derived Quantus address, export address, and exact wire fields; cold transaction signing verifies the returned signature and wallet address.

Validation: git diff --check and cargo +nightly fmt --all -- --check passed. Focused Rust tests passed (7 export, 2 wallet persistence, 4 cold NEAR signing). An isolated CLI roundtrip passed for simulated QR export, import, show-key, repeat import, and hot-wallet rejection. Ubuntu build/tests, Clippy/docs, security audit, format, dependency checks, and examples are green; the macOS CI build/test job was still running when I posted this review.

@n13 n13 removed the bot-review Request automated review from review-bot label Oct 1, 2026
@illuzen
illuzen merged commit 97b6049 into main Oct 1, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants