Skip to content

rpc: Implement z_importviewingkey for Sapling#431

Open
mnm458 wants to merge 1 commit into
zcash:mainfrom
mnm458:z_importviewingkey
Open

rpc: Implement z_importviewingkey for Sapling#431
mnm458 wants to merge 1 commit into
zcash:mainfrom
mnm458:z_importviewingkey

Conversation

@mnm458

@mnm458 mnm458 commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements z_importviewingkey RPC method for Sapling extended full viewing keys (issue #80).

  • Imports a Sapling extfvk (zxviews/zxviewtestsapling) so the wallet can track transactions without holding spending authority. Accounts are stored with AccountPurpose::ViewOnly.

    • Errors if the wallet already holds the spending key for the given viewing key (matching zcashd behavior).
    • Adds fetch_account_birthday to json_rpc::utils as a shared helper (also needed by Implement z_importkey and z_exportkey for Sapling #400 — whichever merges first provides it).
    • Includes unit tests for parameter validation, key decoding, address derivation, encoding roundtrips, and rejection of invalid/wrong-network/spending keys.

    Why implement this despite zero usage in the survey? Watch-only wallets are a fundamental capability for exchanges, auditors, and operators who need to monitor balances without holding spending keys. As zcashd is deprecated, this becomes the only path for users to import existing viewing keys into the new stack. It also supports the separation-of-concerns model where spending keys stay on cold storage while
    a hot node tracks balances.

    Test plan

    Close rpc: Implement z_importviewingkey #80

@mnm458 mnm458 marked this pull request as draft April 25, 2026 02:05
@mnm458 mnm458 changed the title rpc: Implement z_importviewingkey for Sapling [WIP] rpc: Implement z_importviewingkey for Sapling Apr 25, 2026
@mnm458 mnm458 marked this pull request as ready for review May 10, 2026 11:07
@mnm458 mnm458 changed the title [WIP] rpc: Implement z_importviewingkey for Sapling rpc: Implement z_importviewingkey for Sapling May 10, 2026
Add support for importing Sapling extended full viewing keys via the
z_importviewingkey JSON-RPC method. The wallet will track incoming
and outgoing transactions for addresses derived from imported keys
but will not have spending authority.

Supports the "whenkeyisnew", "yes", and "no" rescan options with
a configurable start height, matching zcashd's interface.

Closes zcash#80

@nullcopy nullcopy 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.

Thanks for taking this on! I've left a few requests before this can merge.

Also, I think we need an integration test, before we merge this. I've created an issue (zcash/integration-tests#99). It'd be helpful if you wanted to take that on too, but no obligation ofc 😅

Reviewed as of dca4cbe

#[derive(Clone, Debug, Serialize, Documented, JsonSchema)]
pub(crate) struct ResultType {
/// The type of the imported address (always "sapling").
address_type: String,

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.

Suggested change
address_type: String,
type: String,

Match zcashd naming

/// Validates the `rescan` parameter.
///
/// Returns the validated rescan value, or an RPC error if the value is invalid.
fn validate_rescan(rescan: Option<&str>) -> RpcResult<&str> {

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.

Please create an enum for the rescan value, so that we don't rely on stringly-typed data

/// commitment trees are empty. For non-zero heights, fetches the real treestate from
/// the chain indexer so the sync engine can validate note commitment tree continuity.
#[cfg(zallet_build = "wallet")]
pub(super) async fn fetch_account_birthday(

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.

This is a useful helper. There's a few places in the codebase that should use this helper now:

  • get_new_account.rs:58-92
  • recover_accounts.rs:85-125


wallet
.import_account_ufvk(
&format!("Imported Sapling viewing key {}", &address[..16]),

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.

Suggested change
&format!("Imported Sapling viewing key {}", &address[..16]),
&format!("Imported Sapling viewing key {}", &address),

.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?;

if let Some(tip) = chain_tip {
if start_height > tip {

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.

This check should only apply if rescan="yes"

//
// TODO: When rescan is "yes" and the key already exists, zcashd would force a
// rescan from start_height. We currently skip this because zcash_client_sqlite
// does not expose a way to reset scan ranges for an existing account.

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.

This TODO is out of date. zcash_client_backend recently added WalletWrite::rewind_to_chain_state which does the rescan you need here. You'll need to update the crate patch in Cargo.toml for zcash_client_backend to point to latest main.

match existing_account {
Some(account) => {
if matches!(account.purpose(), AccountPurpose::Spending { .. }) {
return Err(LegacyCode::Wallet.with_message(format!(

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.

Suggested change
return Err(LegacyCode::Wallet.with_message(format!(
"The wallet already contains the private key for this viewing key (address: {address})",

///
/// # Arguments
///
/// - `vkey` (string, required) The viewing key (see `z_exportviewingkey`).

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.

Suggested change
/// - `vkey` (string, required) The viewing key (see `z_exportviewingkey`).
/// - `vkey` (string, required) The viewing key.

z_exportviewingkey hasn't been implemented yet, so this reference doesn't make sense. IMO just drop the reference for now

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.

rpc: Implement z_importviewingkey

2 participants