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:
Francisco Gindre 2020-12-22 11:10:13 -03:00 committed by Kris Nuttycombe
parent 3b02c8b26e
commit cff457ff15
12 changed files with 415 additions and 43 deletions

View File

@ -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]

View File

@ -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 {

View File

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

View File

@ -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(),
})
}

View File

@ -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(
&params.b58_pubkey_address_prefix(),
&params.b58_script_address_prefix(),
self,
)
}
fn decode(params: &P, address: &str) -> Result<TransparentAddress, TransparentCodecError> {
decode_transparent_address(
&params.b58_pubkey_address_prefix(),
&params.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

View File

@ -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;

View File

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

View File

@ -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),

View File

@ -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(

View File

@ -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;

View File

@ -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(())
}

View File

@ -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,