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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ be considered breaking changes.
- `decodescript`
- `verifymessage`
- `z_converttex`
- `z_exportkey` (Sapling extended spending keys only)
- `z_importaddress`
- `z_importkey` (Sapling extended spending keys only)
- `z_shieldcoinbase`

### Changed
Expand Down
65 changes: 65 additions & 0 deletions zallet/src/components/json_rpc/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use {
mod convert_tex;
mod decode_raw_transaction;
mod decode_script;
#[cfg(zallet_build = "wallet")]
mod export_key;
mod get_account;
mod get_address_for_account;
#[cfg(zallet_build = "wallet")]
Expand All @@ -36,6 +38,8 @@ mod get_raw_transaction;
mod get_wallet_info;
#[cfg(zallet_build = "wallet")]
mod help;
#[cfg(zallet_build = "wallet")]
mod import_key;
mod list_accounts;
mod list_addresses;
#[cfg(zallet_build = "wallet")]
Expand Down Expand Up @@ -495,6 +499,46 @@ pub(crate) trait WalletRpc {
as_of_height: Option<i64>,
) -> get_notes_count::Response;

/// Exports the spending key for a Sapling payment address.
///
/// The wallet must be unlocked to use this method.
///
/// # Warning
///
/// This exports **only** the Sapling spending key. It is **not** a complete backup of
/// the funds reachable from this account's root of spending authority: in particular,
/// any Orchard funds derived from the same seed are **not** represented by the exported
/// key. Do not rely on `z_exportkey` as a wallet backup — use a full seed/wallet backup
/// instead, or you may lose access to funds.
///
/// # Arguments
///
/// - `address` (string, required) The Sapling payment address corresponding to the
/// spending key to export.
#[method(name = "z_exportkey")]
async fn export_key(&self, address: &str) -> export_key::Response;

/// Imports a spending key into the wallet.
///
/// Only Sapling extended spending keys are supported.
///
/// # Arguments
///
/// - `key` (string, required) The spending key to import.
/// - `rescan` (string, optional, default="whenkeyisnew") Whether to rescan the
/// blockchain for transactions ("yes", "no", or "whenkeyisnew"). When rescan is
/// enabled, the wallet's background sync engine will scan for historical
/// transactions from the given start height.
/// - `startHeight` (numeric, optional, default=0) Block height from which to begin
/// the rescan. Only used when rescan is "yes" or "whenkeyisnew" (for a new key).
#[method(name = "z_importkey")]
async fn import_key(
&self,
key: &str,
rescan: Option<&str>,
start_height: Option<u64>,
) -> import_key::Response;

/// Send a transaction with multiple recipients.
///
/// This is an async operation; it returns an operation ID string that you can pass to
Expand Down Expand Up @@ -945,6 +989,27 @@ impl WalletRpcServer for WalletRpcImpl {
get_notes_count::call(self.wallet().await?.as_ref(), minconf, as_of_height)
}

async fn export_key(&self, address: &str) -> export_key::Response {
export_key::call(self.wallet().await?.as_ref(), &self.keystore, address).await
}

async fn import_key(
&self,
key: &str,
rescan: Option<&str>,
start_height: Option<u64>,
) -> import_key::Response {
import_key::call(
self.wallet().await?.as_mut(),
&self.keystore,
self.chain().await?,
key,
rescan,
start_height,
)
.await
}

async fn z_send_many(
&self,
fromaddress: String,
Expand Down
111 changes: 111 additions & 0 deletions zallet/src/components/json_rpc/methods/export_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use documented::Documented;
use jsonrpsee::core::RpcResult;
use schemars::JsonSchema;
use serde::Serialize;
use zcash_keys::encoding::{decode_payment_address, encode_extended_spending_key};
use zcash_protocol::consensus::NetworkConstants;

use crate::components::{
database::DbConnection,
json_rpc::{server::LegacyCode, utils::ensure_wallet_is_unlocked},
keystore::KeyStore,
};

/// Response to a `z_exportkey` RPC request.
pub(crate) type Response = RpcResult<ResultType>;

/// The exported Sapling extended spending key, encoded as a Bech32 string.
#[derive(Clone, Debug, Serialize, Documented, JsonSchema)]
#[serde(transparent)]
pub(crate) struct ResultType(String);

pub(super) const PARAM_ADDRESS_DESC: &str =
"The Sapling payment address corresponding to the spending key to export.";

pub(crate) async fn call(wallet: &DbConnection, keystore: &KeyStore, address: &str) -> Response {
ensure_wallet_is_unlocked(keystore).await?;

// Decode the Sapling payment address.
let payment_address = decode_payment_address(
wallet.params().hrp_sapling_payment_address(),
address,
)
.map_err(|e| {
LegacyCode::InvalidAddressOrKey.with_message(format!("Invalid Sapling address: {e}"))
})?;

// Look up and decrypt the standalone spending key for this address.
let extsk = keystore
.decrypt_standalone_sapling_key(&payment_address)
.await
.map_err(|e| LegacyCode::Wallet.with_message(e.to_string()))?
.ok_or_else(|| {
LegacyCode::InvalidAddressOrKey
.with_static("Wallet does not hold the spending key for this Sapling address")
})?;

let encoded =
encode_extended_spending_key(wallet.params().hrp_sapling_extended_spending_key(), &extsk);

Ok(ResultType(encoded))
}

#[cfg(test)]
mod tests {
use zcash_keys::encoding::{decode_payment_address, encode_payment_address};
use zcash_protocol::constants;

#[test]
fn decode_valid_mainnet_sapling_address() {
// From zcash_keys::encoding tests — address derived from seed [0; 32].
let addr = decode_payment_address(
constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS,
"zs1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75c8v35z",
);
assert!(addr.is_ok());
}

#[test]
fn decode_valid_testnet_sapling_address() {
let addr = decode_payment_address(
constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS,
"ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk",
);
assert!(addr.is_ok());
}

#[test]
fn decode_invalid_address() {
let addr = decode_payment_address(
constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS,
"not-a-valid-address",
);
assert!(addr.is_err());
}

#[test]
fn decode_wrong_network_address() {
// Testnet address decoded with mainnet HRP should fail.
let addr = decode_payment_address(
constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS,
"ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk",
);
assert!(addr.is_err());
}

#[test]
fn address_encode_decode_roundtrip() {
let addr = decode_payment_address(
constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS,
"zs1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75c8v35z",
)
.unwrap();

let re_encoded =
encode_payment_address(constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS, &addr);
assert_eq!(
re_encoded,
"zs1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75c8v35z"
);
}
}
Loading
Loading