PoC Auto-Shielding
Add retrieval of transparent UTXOs to WalletRead Co-authored-by: Kris Nuttycombe <kris@electriccoin.co> Co-authored-by: Kevin Gorham <anothergmale@gmail.com>
This commit is contained in:
parent
3b02c8b26e
commit
cff457ff15
|
@ -26,6 +26,9 @@ percent-encoding = "2.1.0"
|
|||
proptest = { version = "0.10.1", optional = true }
|
||||
protobuf = "2.20"
|
||||
rand_core = "0.5.1"
|
||||
ripemd160 = { version = "0.9.1", optional = true }
|
||||
secp256k1 = { version = "0.20", optional = true }
|
||||
sha2 = "0.9"
|
||||
subtle = "2.2.3"
|
||||
time = "0.2"
|
||||
zcash_note_encryption = { version = "0.0", path = "../components/zcash_note_encryption" }
|
||||
|
@ -43,6 +46,7 @@ zcash_client_sqlite = { version = "0.3", path = "../zcash_client_sqlite" }
|
|||
zcash_proofs = { version = "0.5", path = "../zcash_proofs" }
|
||||
|
||||
[features]
|
||||
transparent-inputs = ["ripemd160", "secp256k1"]
|
||||
test-dependencies = ["proptest", "zcash_primitives/test-dependencies"]
|
||||
|
||||
[badges]
|
||||
|
|
|
@ -7,10 +7,14 @@ use std::fmt::Debug;
|
|||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::BlockHeight,
|
||||
legacy::TransparentAddress,
|
||||
memo::{Memo, MemoBytes},
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
sapling::{Node, Nullifier, PaymentAddress},
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
transaction::{
|
||||
components::{Amount, OutPoint},
|
||||
Transaction, TxId,
|
||||
},
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
|
@ -19,7 +23,7 @@ use crate::{
|
|||
data_api::wallet::ANCHOR_OFFSET,
|
||||
decrypt::DecryptedOutput,
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{AccountId, SpendableNote, WalletTx},
|
||||
wallet::{AccountId, SpendableNote, WalletTransparentOutput, WalletTx},
|
||||
};
|
||||
|
||||
pub mod chain;
|
||||
|
@ -160,7 +164,7 @@ pub trait WalletRead {
|
|||
fn get_nullifiers(&self) -> Result<Vec<(AccountId, Nullifier)>, Self::Error>;
|
||||
|
||||
/// Return all spendable notes.
|
||||
fn get_spendable_notes(
|
||||
fn get_spendable_sapling_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
|
@ -168,12 +172,18 @@ pub trait WalletRead {
|
|||
|
||||
/// Returns a list of spendable notes sufficient to cover the specified
|
||||
/// target value, if possible.
|
||||
fn select_spendable_notes(
|
||||
fn select_spendable_sapling_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<SpendableNote>, Self::Error>;
|
||||
|
||||
fn get_spendable_transparent_utxos(
|
||||
&self,
|
||||
address: &TransparentAddress,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error>;
|
||||
}
|
||||
|
||||
/// The subset of information that is relevant to this wallet that has been
|
||||
|
@ -215,6 +225,7 @@ pub struct SentTransaction<'a> {
|
|||
pub recipient_address: &'a RecipientAddress,
|
||||
pub value: Amount,
|
||||
pub memo: Option<MemoBytes>,
|
||||
pub utxos_spent: Vec<OutPoint>,
|
||||
}
|
||||
|
||||
/// This trait encapsulates the write capabilities required to update stored
|
||||
|
@ -274,6 +285,7 @@ pub mod testing {
|
|||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::BlockHeight,
|
||||
legacy::TransparentAddress,
|
||||
memo::Memo,
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
sapling::{Node, Nullifier, PaymentAddress},
|
||||
|
@ -283,7 +295,7 @@ pub mod testing {
|
|||
|
||||
use crate::{
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{AccountId, SpendableNote},
|
||||
wallet::{AccountId, SpendableNote, WalletTransparentOutput},
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
@ -380,7 +392,7 @@ pub mod testing {
|
|||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn get_spendable_notes(
|
||||
fn get_spendable_sapling_notes(
|
||||
&self,
|
||||
_account: AccountId,
|
||||
_anchor_height: BlockHeight,
|
||||
|
@ -388,7 +400,7 @@ pub mod testing {
|
|||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn select_spendable_notes(
|
||||
fn select_spendable_sapling_notes(
|
||||
&self,
|
||||
_account: AccountId,
|
||||
_target_value: Amount,
|
||||
|
@ -396,6 +408,14 @@ pub mod testing {
|
|||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn get_spendable_transparent_utxos(
|
||||
&self,
|
||||
_address: &TransparentAddress,
|
||||
_anchor_height: BlockHeight,
|
||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletWrite for MockWalletDb {
|
||||
|
|
|
@ -25,6 +25,8 @@ pub enum ChainInvalid {
|
|||
#[derive(Debug)]
|
||||
pub enum Error<NoteId> {
|
||||
/// Unable to create a new spend because the wallet balance is not sufficient.
|
||||
/// The first argument is the amount available, the second is the amount needed
|
||||
/// to construct a valid transaction.
|
||||
InsufficientBalance(Amount, Amount),
|
||||
|
||||
/// Chain validation detected an error in the block at the specified block height.
|
||||
|
|
|
@ -20,6 +20,12 @@ use crate::{
|
|||
wallet::{AccountId, OvkPolicy},
|
||||
};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use zcash_primitives::{legacy::Script, transaction::components::TxOut};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use crate::keys::derive_transparent_address_from_secret_key;
|
||||
|
||||
pub const ANCHOR_OFFSET: u32 = 10;
|
||||
|
||||
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
|
||||
|
@ -184,7 +190,8 @@ where
|
|||
.and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?;
|
||||
|
||||
let target_value = value + DEFAULT_FEE;
|
||||
let spendable_notes = wallet_db.select_spendable_notes(account, target_value, anchor_height)?;
|
||||
let spendable_notes =
|
||||
wallet_db.select_spendable_sapling_notes(account, target_value, anchor_height)?;
|
||||
|
||||
// Confirm we were able to select sufficient value
|
||||
let selected_value = spendable_notes.iter().map(|n| n.note_value).sum();
|
||||
|
@ -254,5 +261,96 @@ where
|
|||
recipient_address: to,
|
||||
value,
|
||||
memo,
|
||||
utxos_spent: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
pub fn shield_funds<E, N, P, D, R>(
|
||||
wallet_db: &mut D,
|
||||
params: &P,
|
||||
prover: impl TxProver,
|
||||
account: AccountId,
|
||||
sk: &secp256k1::SecretKey,
|
||||
extsk: &ExtendedSpendingKey,
|
||||
memo: &MemoBytes,
|
||||
) -> Result<D::TxRef, E>
|
||||
where
|
||||
E: From<Error<N>>,
|
||||
P: consensus::Parameters,
|
||||
R: Copy + Debug,
|
||||
D: WalletWrite<Error = E, TxRef = R>,
|
||||
{
|
||||
let (latest_scanned_height, latest_anchor) = wallet_db
|
||||
.get_target_and_anchor_heights()
|
||||
.and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?;
|
||||
|
||||
// derive the corresponding t-address
|
||||
let taddr = derive_transparent_address_from_secret_key(*sk);
|
||||
|
||||
// derive own shielded address from the provided extended spending key
|
||||
let z_address = extsk.default_address().unwrap().1;
|
||||
|
||||
let exfvk = ExtendedFullViewingKey::from(extsk);
|
||||
|
||||
let ovk = exfvk.fvk.ovk;
|
||||
|
||||
// get UTXOs from DB
|
||||
let utxos = wallet_db.get_spendable_transparent_utxos(&taddr, latest_anchor)?;
|
||||
let total_amount = utxos.iter().map(|utxo| utxo.value).sum::<Amount>();
|
||||
|
||||
let fee = DEFAULT_FEE;
|
||||
if fee >= total_amount {
|
||||
return Err(E::from(Error::InsufficientBalance(total_amount, fee)));
|
||||
}
|
||||
|
||||
let amount_to_shield = total_amount - fee;
|
||||
|
||||
let mut builder = Builder::new(params.clone(), latest_scanned_height);
|
||||
|
||||
for utxo in &utxos {
|
||||
let coin = TxOut {
|
||||
value: utxo.value,
|
||||
script_pubkey: Script {
|
||||
0: utxo.script.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
builder
|
||||
.add_transparent_input(*sk, utxo.outpoint.clone(), coin)
|
||||
.map_err(Error::Builder)?;
|
||||
}
|
||||
|
||||
// there are no sapling notes so we set the change manually
|
||||
builder.send_change_to(ovk, z_address.clone());
|
||||
|
||||
// add the sapling output to shield the funds
|
||||
builder
|
||||
.add_sapling_output(
|
||||
Some(ovk),
|
||||
z_address.clone(),
|
||||
amount_to_shield,
|
||||
Some(memo.clone()),
|
||||
)
|
||||
.map_err(Error::Builder)?;
|
||||
|
||||
let consensus_branch_id = BranchId::for_height(params, latest_anchor);
|
||||
|
||||
let (tx, tx_metadata) = builder
|
||||
.build(consensus_branch_id, &prover)
|
||||
.map_err(Error::Builder)?;
|
||||
let output_index = tx_metadata.output_index(0).expect(
|
||||
"No sapling note was created in autoshielding transaction. This is a programming error.",
|
||||
);
|
||||
|
||||
wallet_db.store_sent_tx(&SentTransaction {
|
||||
tx: &tx,
|
||||
created: time::OffsetDateTime::now_utc(),
|
||||
output_index,
|
||||
account,
|
||||
recipient_address: &RecipientAddress::Shielded(z_address),
|
||||
value: amount_to_shield,
|
||||
memo: Some(memo.clone()),
|
||||
utxos_spent: utxos.iter().map(|utxo| utxo.outpoint.clone()).collect(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
use bech32::{self, Error, FromBase32, ToBase32, Variant};
|
||||
use bs58::{self, decode::Error as Bs58Error};
|
||||
use std::convert::TryInto;
|
||||
use std::fmt;
|
||||
use std::io::{self, Write};
|
||||
use zcash_primitives::{
|
||||
consensus,
|
||||
legacy::TransparentAddress,
|
||||
sapling::PaymentAddress,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
|
@ -36,6 +38,61 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub trait AddressCodec<P>
|
||||
where
|
||||
Self: std::marker::Sized,
|
||||
{
|
||||
type Error;
|
||||
|
||||
fn encode(&self, params: &P) -> String;
|
||||
fn decode(params: &P, address: &str) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TransparentCodecError {
|
||||
UnsupportedAddressType(String),
|
||||
Base58(Bs58Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for TransparentCodecError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
TransparentCodecError::UnsupportedAddressType(s) => write!(
|
||||
f,
|
||||
"Could not recognize {} as a supported p2sh or p2pkh address.",
|
||||
s
|
||||
),
|
||||
TransparentCodecError::Base58(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TransparentCodecError {}
|
||||
|
||||
impl<P: consensus::Parameters> AddressCodec<P> for TransparentAddress {
|
||||
type Error = TransparentCodecError;
|
||||
|
||||
fn encode(&self, params: &P) -> String {
|
||||
encode_transparent_address(
|
||||
¶ms.b58_pubkey_address_prefix(),
|
||||
¶ms.b58_script_address_prefix(),
|
||||
self,
|
||||
)
|
||||
}
|
||||
|
||||
fn decode(params: &P, address: &str) -> Result<TransparentAddress, TransparentCodecError> {
|
||||
decode_transparent_address(
|
||||
¶ms.b58_pubkey_address_prefix(),
|
||||
¶ms.b58_script_address_prefix(),
|
||||
address,
|
||||
)
|
||||
.map_err(TransparentCodecError::Base58)
|
||||
.and_then(|opt| {
|
||||
opt.ok_or_else(|| TransparentCodecError::UnsupportedAddressType(address.to_string()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes an [`ExtendedSpendingKey`] as a Bech32-encoded string.
|
||||
///
|
||||
/// # Examples
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
//! Helper functions for managing light client key material.
|
||||
#![cfg(feature = "transparent-inputs")]
|
||||
|
||||
use zcash_primitives::zip32::{ChildIndex, ExtendedSpendingKey};
|
||||
use zcash_primitives::{
|
||||
legacy::TransparentAddress,
|
||||
zip32::{ChildIndex, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use secp256k1::{key::PublicKey, Secp256k1};
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Derives the ZIP 32 [`ExtendedSpendingKey`] for a given coin type and account from the
|
||||
/// given seed.
|
||||
|
@ -33,6 +41,16 @@ pub fn spending_key(seed: &[u8], coin_type: u32, account: u32) -> ExtendedSpendi
|
|||
)
|
||||
}
|
||||
|
||||
pub fn derive_transparent_address_from_secret_key(
|
||||
secret_key: secp256k1::key::SecretKey,
|
||||
) -> TransparentAddress {
|
||||
let secp = Secp256k1::new();
|
||||
let pk = PublicKey::from_secret_key(&secp, &secret_key);
|
||||
let mut hash160 = ripemd160::Ripemd160::new();
|
||||
hash160.update(Sha256::digest(&pk.serialize()[..].to_vec()));
|
||||
TransparentAddress::PublicKey(*hash160.finalize().as_ref())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::spending_key;
|
||||
|
|
|
@ -4,11 +4,16 @@
|
|||
use subtle::{Choice, ConditionallySelectable};
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::BlockHeight,
|
||||
legacy::TransparentAddress,
|
||||
merkle_tree::IncrementalWitness,
|
||||
sapling::{
|
||||
keys::OutgoingViewingKey, Diversifier, Node, Note, Nullifier, PaymentAddress, Rseed,
|
||||
},
|
||||
transaction::{components::Amount, TxId},
|
||||
transaction::{
|
||||
components::{Amount, OutPoint},
|
||||
TxId,
|
||||
},
|
||||
};
|
||||
|
||||
/// A type-safe wrapper for account identifiers.
|
||||
|
@ -39,6 +44,14 @@ pub struct WalletTx<N> {
|
|||
pub shielded_outputs: Vec<WalletShieldedOutput<N>>,
|
||||
}
|
||||
|
||||
pub struct WalletTransparentOutput {
|
||||
pub address: TransparentAddress,
|
||||
pub outpoint: OutPoint,
|
||||
pub script: Vec<u8>,
|
||||
pub value: Amount,
|
||||
pub height: BlockHeight,
|
||||
}
|
||||
|
||||
/// A subset of a [`SpendDescription`] relevant to wallets and light clients.
|
||||
///
|
||||
/// [`SpendDescription`]: zcash_primitives::transaction::components::SpendDescription
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use std::error;
|
||||
use std::fmt;
|
||||
|
||||
use zcash_client_backend::data_api;
|
||||
use zcash_client_backend::{data_api, encoding::TransparentCodecError};
|
||||
|
||||
use crate::NoteId;
|
||||
|
||||
|
@ -32,6 +32,9 @@ pub enum SqliteClientError {
|
|||
/// Base58 decoding error
|
||||
Base58(bs58::decode::Error),
|
||||
|
||||
/// Base58 decoding error
|
||||
TransparentAddress(TransparentCodecError),
|
||||
|
||||
/// Wrapper for rusqlite errors.
|
||||
DbError(rusqlite::Error),
|
||||
|
||||
|
@ -68,6 +71,7 @@ impl fmt::Display for SqliteClientError {
|
|||
SqliteClientError::InvalidNoteId => write!(f, "The note ID associated with an inserted witness must correspond to a received note."),
|
||||
SqliteClientError::Bech32(e) => write!(f, "{}", e),
|
||||
SqliteClientError::Base58(e) => write!(f, "{}", e),
|
||||
SqliteClientError::TransparentAddress(e) => write!(f, "{}", e),
|
||||
SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"),
|
||||
SqliteClientError::DbError(e) => write!(f, "{}", e),
|
||||
SqliteClientError::Io(e) => write!(f, "{}", e),
|
||||
|
|
|
@ -41,6 +41,7 @@ use rusqlite::{Connection, Statement, NO_PARAMS};
|
|||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight},
|
||||
legacy::TransparentAddress,
|
||||
memo::Memo,
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
sapling::{Node, Nullifier, PaymentAddress},
|
||||
|
@ -54,7 +55,7 @@ use zcash_client_backend::{
|
|||
},
|
||||
encoding::encode_payment_address,
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{AccountId, SpendableNote},
|
||||
wallet::{AccountId, SpendableNote, WalletTransparentOutput},
|
||||
};
|
||||
|
||||
use crate::error::SqliteClientError;
|
||||
|
@ -80,6 +81,11 @@ impl fmt::Display for NoteId {
|
|||
}
|
||||
}
|
||||
|
||||
/// A newtype wrapper for sqlite primary key values for the utxos
|
||||
/// table.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct UtxoId(i64);
|
||||
|
||||
/// A wrapper for the SQLite connection to the wallet database.
|
||||
pub struct WalletDb<P> {
|
||||
conn: Connection,
|
||||
|
@ -91,7 +97,9 @@ impl<P: consensus::Parameters> WalletDb<P> {
|
|||
pub fn for_path<F: AsRef<Path>>(path: F, params: P) -> Result<Self, rusqlite::Error> {
|
||||
Connection::open(path).map(move |conn| WalletDb { conn, params })
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: consensus::Parameters> WalletDb<P> {
|
||||
/// Given a wallet database connection, obtain a handle for the write operations
|
||||
/// for that database. This operation may eagerly initialize and cache sqlite
|
||||
/// prepared statements that are used in write operations.
|
||||
|
@ -122,9 +130,18 @@ impl<P: consensus::Parameters> WalletDb<P> {
|
|||
stmt_select_tx_ref: self.conn.prepare(
|
||||
"SELECT id_tx FROM transactions WHERE txid = ?",
|
||||
)?,
|
||||
stmt_mark_recived_note_spent: self.conn.prepare(
|
||||
stmt_mark_sapling_note_spent: self.conn.prepare(
|
||||
"UPDATE received_notes SET spent = ? WHERE nf = ?"
|
||||
)?,
|
||||
stmt_mark_transparent_utxo_spent: self.conn.prepare(
|
||||
"UPDATE utxos SET spent_in_tx = :spent_in_tx
|
||||
WHERE prevout_txid = :prevout_txid
|
||||
AND prevout_idx = :prevout_idx"
|
||||
)?,
|
||||
stmt_insert_received_transparent_utxo: self.conn.prepare(
|
||||
"INSERT INTO utxos (address, prevout_txid, prevout_idx, script, value_zat, height)
|
||||
VALUES (:address, :prevout_txid, :prevout_idx, :script, :value_zat, :height)"
|
||||
)?,
|
||||
stmt_insert_received_note: self.conn.prepare(
|
||||
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo, nf, is_change)
|
||||
VALUES (:tx, :output_index, :account, :diversifier, :value, :rcm, :memo, :nf, :is_change)",
|
||||
|
@ -176,25 +193,25 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
type TxRef = i64;
|
||||
|
||||
fn block_height_extrema(&self) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
|
||||
wallet::block_height_extrema(self).map_err(SqliteClientError::from)
|
||||
wallet::block_height_extrema(&self).map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
|
||||
wallet::get_block_hash(self, block_height).map_err(SqliteClientError::from)
|
||||
wallet::get_block_hash(&self, block_height).map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
|
||||
wallet::get_tx_height(self, txid).map_err(SqliteClientError::from)
|
||||
wallet::get_tx_height(&self, txid).map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
fn get_extended_full_viewing_keys(
|
||||
&self,
|
||||
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, Self::Error> {
|
||||
wallet::get_extended_full_viewing_keys(self)
|
||||
wallet::get_extended_full_viewing_keys(&self)
|
||||
}
|
||||
|
||||
fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error> {
|
||||
wallet::get_address(self, account)
|
||||
wallet::get_address(&self, account)
|
||||
}
|
||||
|
||||
fn is_valid_account_extfvk(
|
||||
|
@ -202,7 +219,7 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
account: AccountId,
|
||||
extfvk: &ExtendedFullViewingKey,
|
||||
) -> Result<bool, Self::Error> {
|
||||
wallet::is_valid_account_extfvk(self, account, extfvk)
|
||||
wallet::is_valid_account_extfvk(&self, account, extfvk)
|
||||
}
|
||||
|
||||
fn get_balance_at(
|
||||
|
@ -210,7 +227,7 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Amount, Self::Error> {
|
||||
wallet::get_balance_at(self, account, anchor_height)
|
||||
wallet::get_balance_at(&self, account, anchor_height)
|
||||
}
|
||||
|
||||
fn get_memo(&self, id_note: Self::NoteRef) -> Result<Memo, Self::Error> {
|
||||
|
@ -224,7 +241,7 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
&self,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<Option<CommitmentTree<Node>>, Self::Error> {
|
||||
wallet::get_commitment_tree(self, block_height)
|
||||
wallet::get_commitment_tree(&self, block_height)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
|
@ -232,28 +249,41 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
&self,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<Vec<(Self::NoteRef, IncrementalWitness<Node>)>, Self::Error> {
|
||||
wallet::get_witnesses(self, block_height)
|
||||
wallet::get_witnesses(&self, block_height)
|
||||
}
|
||||
|
||||
fn get_nullifiers(&self) -> Result<Vec<(AccountId, Nullifier)>, Self::Error> {
|
||||
wallet::get_nullifiers(self)
|
||||
wallet::get_nullifiers(&self)
|
||||
}
|
||||
|
||||
fn get_spendable_notes(
|
||||
fn get_spendable_sapling_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
||||
wallet::transact::get_spendable_notes(self, account, anchor_height)
|
||||
wallet::transact::get_spendable_sapling_notes(&self, account, anchor_height)
|
||||
}
|
||||
|
||||
fn select_spendable_notes(
|
||||
fn select_spendable_sapling_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
||||
wallet::transact::select_spendable_notes(self, account, target_value, anchor_height)
|
||||
wallet::transact::select_spendable_sapling_notes(
|
||||
&self,
|
||||
account,
|
||||
target_value,
|
||||
anchor_height,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_spendable_transparent_utxos(
|
||||
&self,
|
||||
address: &TransparentAddress,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
||||
wallet::get_spendable_transparent_utxos(&self, address, anchor_height)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,8 +305,10 @@ pub struct DataConnStmtCache<'a, P> {
|
|||
stmt_update_tx_data: Statement<'a>,
|
||||
stmt_select_tx_ref: Statement<'a>,
|
||||
|
||||
stmt_mark_recived_note_spent: Statement<'a>,
|
||||
stmt_mark_sapling_note_spent: Statement<'a>,
|
||||
stmt_mark_transparent_utxo_spent: Statement<'a>,
|
||||
|
||||
stmt_insert_received_transparent_utxo: Statement<'a>,
|
||||
stmt_insert_received_note: Statement<'a>,
|
||||
stmt_update_received_note: Statement<'a>,
|
||||
stmt_select_received_note: Statement<'a>,
|
||||
|
@ -355,22 +387,32 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
|
|||
self.wallet_db.get_nullifiers()
|
||||
}
|
||||
|
||||
fn get_spendable_notes(
|
||||
fn get_spendable_sapling_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
||||
self.wallet_db.get_spendable_notes(account, anchor_height)
|
||||
self.wallet_db
|
||||
.get_spendable_sapling_notes(account, anchor_height)
|
||||
}
|
||||
|
||||
fn select_spendable_notes(
|
||||
fn select_spendable_sapling_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
||||
self.wallet_db
|
||||
.select_spendable_notes(account, target_value, anchor_height)
|
||||
.select_spendable_sapling_notes(account, target_value, anchor_height)
|
||||
}
|
||||
|
||||
fn get_spendable_transparent_utxos(
|
||||
&self,
|
||||
address: &TransparentAddress,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
||||
self.wallet_db
|
||||
.get_spendable_transparent_utxos(address, anchor_height)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -426,9 +468,12 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
|
||||
// Mark notes as spent and remove them from the scanning cache
|
||||
for spend in &tx.shielded_spends {
|
||||
wallet::mark_spent(up, tx_row, &spend.nf)?;
|
||||
wallet::mark_sapling_note_spent(up, tx_row, &spend.nf)?;
|
||||
}
|
||||
|
||||
//TODO
|
||||
//wallet::mark_transparent_utxo_spent(up, tx_ref, &utxo.outpoint)?;
|
||||
|
||||
for output in &tx.shielded_outputs {
|
||||
let received_note_id = wallet::put_received_note(up, output, tx_row)?;
|
||||
|
||||
|
@ -490,7 +535,11 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
// Assumes that create_spend_to_address() will never be called in parallel, which is a
|
||||
// reasonable assumption for a light client such as a mobile phone.
|
||||
for spend in &sent_tx.tx.shielded_spends {
|
||||
wallet::mark_spent(up, tx_ref, &spend.nullifier)?;
|
||||
wallet::mark_sapling_note_spent(up, tx_ref, &spend.nullifier)?;
|
||||
}
|
||||
|
||||
for utxo_outpoint in &sent_tx.utxos_spent {
|
||||
wallet::mark_transparent_utxo_spent(up, tx_ref, &utxo_outpoint)?;
|
||||
}
|
||||
|
||||
wallet::insert_sent_note(
|
||||
|
|
|
@ -15,10 +15,14 @@ use std::convert::TryFrom;
|
|||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
legacy::TransparentAddress,
|
||||
memo::{Memo, MemoBytes},
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
sapling::{Node, Note, Nullifier, PaymentAddress},
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
transaction::{
|
||||
components::{Amount, OutPoint},
|
||||
Transaction, TxId,
|
||||
},
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
|
@ -27,13 +31,13 @@ use zcash_client_backend::{
|
|||
data_api::error::Error,
|
||||
encoding::{
|
||||
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key,
|
||||
encode_payment_address,
|
||||
encode_payment_address, AddressCodec,
|
||||
},
|
||||
wallet::{AccountId, WalletShieldedOutput, WalletTx},
|
||||
wallet::{AccountId, WalletShieldedOutput, WalletTransparentOutput, WalletTx},
|
||||
DecryptedOutput,
|
||||
};
|
||||
|
||||
use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb};
|
||||
use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, UtxoId, WalletDb};
|
||||
|
||||
pub mod init;
|
||||
pub mod transact;
|
||||
|
@ -588,6 +592,57 @@ pub fn get_nullifiers<P>(
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn get_spendable_transparent_utxos<P: consensus::Parameters>(
|
||||
wdb: &WalletDb<P>,
|
||||
address: &TransparentAddress,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<WalletTransparentOutput>, SqliteClientError> {
|
||||
let mut stmt_blocks = wdb.conn.prepare(
|
||||
"SELECT address, prevout_txid, prevout_idx, script, value_zat, height
|
||||
FROM utxos
|
||||
WHERE address = ?
|
||||
AND height <= ?
|
||||
AND spent_in_tx IS NULL",
|
||||
)?;
|
||||
|
||||
let addr_str = address.encode(&wdb.params);
|
||||
|
||||
let rows = stmt_blocks.query_map(params![addr_str, u32::from(anchor_height)], |row| {
|
||||
let addr: String = row.get(0)?;
|
||||
let address = TransparentAddress::decode(&wdb.params, &addr).map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(
|
||||
addr.len(),
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let id: Vec<u8> = row.get(1)?;
|
||||
|
||||
let mut txid_bytes = [0u8; 32];
|
||||
txid_bytes.copy_from_slice(&id);
|
||||
let index: i32 = row.get(2)?;
|
||||
let script: Vec<u8> = row.get(3)?;
|
||||
let value: i64 = row.get(4)?;
|
||||
let height: u32 = row.get(5)?;
|
||||
|
||||
Ok(WalletTransparentOutput {
|
||||
address,
|
||||
outpoint: OutPoint::new(txid_bytes, index as u32),
|
||||
script,
|
||||
value: Amount::from_i64(value).unwrap(),
|
||||
height: BlockHeight::from(height),
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut utxos = Vec::<WalletTransparentOutput>::new();
|
||||
|
||||
for utxo in rows {
|
||||
utxos.push(utxo.unwrap())
|
||||
}
|
||||
Ok(utxos)
|
||||
}
|
||||
|
||||
/// Inserts information about a scanned block into the database.
|
||||
pub fn insert_block<'a, P>(
|
||||
stmts: &mut DataConnStmtCache<'a, P>,
|
||||
|
@ -676,18 +731,56 @@ pub fn put_tx_data<'a, P>(
|
|||
///
|
||||
/// Marking a note spent in this fashion does NOT imply that the
|
||||
/// spending transaction has been mined.
|
||||
pub fn mark_spent<'a, P>(
|
||||
pub fn mark_sapling_note_spent<'a, P>(
|
||||
stmts: &mut DataConnStmtCache<'a, P>,
|
||||
tx_ref: i64,
|
||||
nf: &Nullifier,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
stmts
|
||||
.stmt_mark_recived_note_spent
|
||||
.stmt_mark_sapling_note_spent
|
||||
.execute(&[tx_ref.to_sql()?, nf.0.to_sql()?])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Records the specified shielded output as having been received.
|
||||
pub fn mark_transparent_utxo_spent<'a, P>(
|
||||
stmts: &mut DataConnStmtCache<'a, P>,
|
||||
tx_ref: i64,
|
||||
outpoint: &OutPoint,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let sql_args: &[(&str, &dyn ToSql)] = &[
|
||||
(&":spent_in_tx", &tx_ref),
|
||||
(&":prevout_txid", &outpoint.hash().to_vec()),
|
||||
(&":prevout_idx", &outpoint.n()),
|
||||
];
|
||||
|
||||
stmts
|
||||
.stmt_mark_transparent_utxo_spent
|
||||
.execute_named(&sql_args)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn put_received_transparent_utxo<'a, P: consensus::Parameters>(
|
||||
stmts: &mut DataConnStmtCache<'a, P>,
|
||||
output: &WalletTransparentOutput,
|
||||
) -> Result<UtxoId, SqliteClientError> {
|
||||
let sql_args: &[(&str, &dyn ToSql)] = &[
|
||||
(&":address", &output.address.encode(&stmts.wallet_db.params)),
|
||||
(&":prevout_txid", &output.outpoint.hash().to_vec()),
|
||||
(&":prevout_idx", &output.outpoint.n()),
|
||||
(&":script", &output.script),
|
||||
(&":value_zat", &i64::from(output.value)),
|
||||
(&":height", &u32::from(output.height)),
|
||||
];
|
||||
|
||||
stmts
|
||||
.stmt_insert_received_transparent_utxo
|
||||
.execute_named(&sql_args)?;
|
||||
|
||||
Ok(UtxoId(stmts.wallet_db.conn.last_insert_rowid()))
|
||||
}
|
||||
|
||||
// Assumptions:
|
||||
// - A transaction will not contain more than 2^63 shielded outputs.
|
||||
// - A note value will never exceed 2^63 zatoshis.
|
||||
|
@ -847,7 +940,6 @@ pub fn insert_sent_note<'a, P: consensus::Parameters>(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::NamedTempFile;
|
||||
|
|
|
@ -106,6 +106,21 @@ pub fn init_wallet_db<P>(wdb: &WalletDb<P>) -> Result<(), rusqlite::Error> {
|
|||
)",
|
||||
NO_PARAMS,
|
||||
)?;
|
||||
wdb.conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS utxos (
|
||||
id_utxo INTEGER PRIMARY KEY,
|
||||
address TEXT NOT NULL,
|
||||
prevout_txid BLOB NOT NULL,
|
||||
prevout_idx INTEGER NOT NULL,
|
||||
script BLOB NOT NULL,
|
||||
value_zat INTEGER NOT NULL,
|
||||
height INTEGER NOT NULL,
|
||||
spent_in_tx INTEGER,
|
||||
FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
|
||||
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
||||
)",
|
||||
NO_PARAMS,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ fn to_spendable_note(row: &Row) -> Result<SpendableNote, SqliteClientError> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn get_spendable_notes<P>(
|
||||
pub fn get_spendable_sapling_notes<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
|
@ -87,7 +87,7 @@ pub fn get_spendable_notes<P>(
|
|||
notes.collect::<Result<_, _>>()
|
||||
}
|
||||
|
||||
pub fn select_spendable_notes<P>(
|
||||
pub fn select_spendable_sapling_notes<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
|
|
Loading…
Reference in New Issue