zcash_client_sqlite: Add Orchard note selection.
This commit is contained in:
parent
a9aabb2aa0
commit
021128b106
|
@ -69,13 +69,13 @@ use zcash_client_backend::{
|
|||
},
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
||||
DecryptedOutput, ShieldedProtocol, TransferType,
|
||||
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
|
||||
};
|
||||
|
||||
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use zcash_client_backend::{data_api::ORCHARD_SHARD_HEIGHT, PoolType};
|
||||
use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
|
@ -111,6 +111,7 @@ pub(crate) const PRUNING_DEPTH: u32 = 100;
|
|||
pub(crate) const VERIFY_LOOKAHEAD: u32 = 10;
|
||||
|
||||
pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling";
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
pub(crate) const ORCHARD_TABLES_PREFIX: &str = "orchard";
|
||||
|
||||
|
@ -208,7 +209,20 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
|
|||
txid,
|
||||
index,
|
||||
),
|
||||
ShieldedProtocol::Orchard => Ok(None),
|
||||
ShieldedProtocol::Orchard => {
|
||||
#[cfg(feature = "orchard")]
|
||||
return wallet::orchard::get_spendable_orchard_note(
|
||||
self.conn.borrow(),
|
||||
&self.params,
|
||||
txid,
|
||||
index,
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
|
||||
ShieldedProtocol::Orchard,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,14 +234,25 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
|
|||
anchor_height: BlockHeight,
|
||||
exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
|
||||
wallet::sapling::select_spendable_sapling_notes(
|
||||
let received_iter = std::iter::empty();
|
||||
let received_iter = received_iter.chain(wallet::sapling::select_spendable_sapling_notes(
|
||||
self.conn.borrow(),
|
||||
&self.params,
|
||||
account,
|
||||
target_value,
|
||||
anchor_height,
|
||||
exclude,
|
||||
)
|
||||
)?);
|
||||
#[cfg(feature = "orchard")]
|
||||
let received_iter = received_iter.chain(wallet::orchard::select_spendable_orchard_notes(
|
||||
self.conn.borrow(),
|
||||
&self.params,
|
||||
account,
|
||||
target_value,
|
||||
anchor_height,
|
||||
exclude,
|
||||
)?);
|
||||
Ok(received_iter.collect())
|
||||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
|
|
@ -1,24 +1,38 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use incrementalmerkletree::Position;
|
||||
use rusqlite::{named_params, params, Connection};
|
||||
use orchard::{
|
||||
keys::Diversifier,
|
||||
note::{Note, Nullifier, RandomSeed},
|
||||
};
|
||||
use rusqlite::{named_params, params, types::Value, Connection, Row};
|
||||
|
||||
use zcash_client_backend::{
|
||||
data_api::NullifierQuery, wallet::WalletOrchardOutput, DecryptedOutput, TransferType,
|
||||
data_api::NullifierQuery,
|
||||
wallet::{ReceivedNote, WalletOrchardOutput},
|
||||
DecryptedOutput, ShieldedProtocol, TransferType,
|
||||
};
|
||||
use zcash_keys::keys::UnifiedFullViewingKey;
|
||||
use zcash_primitives::transaction::TxId;
|
||||
use zcash_protocol::{
|
||||
consensus::{self, BlockHeight},
|
||||
memo::MemoBytes,
|
||||
value::Zatoshis,
|
||||
};
|
||||
use zcash_protocol::memo::MemoBytes;
|
||||
use zip32::Scope;
|
||||
|
||||
use crate::{error::SqliteClientError, AccountId};
|
||||
use crate::{error::SqliteClientError, AccountId, ReceivedNoteId};
|
||||
|
||||
use super::{memo_repr, scope_code};
|
||||
use super::{memo_repr, parse_scope, scope_code, wallet_birthday};
|
||||
|
||||
/// This trait provides a generalization over shielded output representations.
|
||||
pub(crate) trait ReceivedOrchardOutput {
|
||||
fn index(&self) -> usize;
|
||||
fn account_id(&self) -> AccountId;
|
||||
fn note(&self) -> &orchard::note::Note;
|
||||
fn note(&self) -> &Note;
|
||||
fn memo(&self) -> Option<&MemoBytes>;
|
||||
fn is_change(&self) -> bool;
|
||||
fn nullifier(&self) -> Option<&orchard::note::Nullifier>;
|
||||
fn nullifier(&self) -> Option<&Nullifier>;
|
||||
fn note_commitment_tree_position(&self) -> Option<Position>;
|
||||
fn recipient_key_scope(&self) -> Option<Scope>;
|
||||
}
|
||||
|
@ -30,7 +44,7 @@ impl ReceivedOrchardOutput for WalletOrchardOutput<AccountId> {
|
|||
fn account_id(&self) -> AccountId {
|
||||
*WalletOrchardOutput::account_id(self)
|
||||
}
|
||||
fn note(&self) -> &orchard::note::Note {
|
||||
fn note(&self) -> &Note {
|
||||
WalletOrchardOutput::note(self)
|
||||
}
|
||||
fn memo(&self) -> Option<&MemoBytes> {
|
||||
|
@ -39,7 +53,7 @@ impl ReceivedOrchardOutput for WalletOrchardOutput<AccountId> {
|
|||
fn is_change(&self) -> bool {
|
||||
WalletOrchardOutput::is_change(self)
|
||||
}
|
||||
fn nullifier(&self) -> Option<&orchard::note::Nullifier> {
|
||||
fn nullifier(&self) -> Option<&Nullifier> {
|
||||
self.nf()
|
||||
}
|
||||
fn note_commitment_tree_position(&self) -> Option<Position> {
|
||||
|
@ -50,7 +64,7 @@ impl ReceivedOrchardOutput for WalletOrchardOutput<AccountId> {
|
|||
}
|
||||
}
|
||||
|
||||
impl ReceivedOrchardOutput for DecryptedOutput<orchard::note::Note, AccountId> {
|
||||
impl ReceivedOrchardOutput for DecryptedOutput<Note, AccountId> {
|
||||
fn index(&self) -> usize {
|
||||
self.index()
|
||||
}
|
||||
|
@ -66,7 +80,7 @@ impl ReceivedOrchardOutput for DecryptedOutput<orchard::note::Note, AccountId> {
|
|||
fn is_change(&self) -> bool {
|
||||
self.transfer_type() == TransferType::WalletInternal
|
||||
}
|
||||
fn nullifier(&self) -> Option<&orchard::note::Nullifier> {
|
||||
fn nullifier(&self) -> Option<&Nullifier> {
|
||||
None
|
||||
}
|
||||
fn note_commitment_tree_position(&self) -> Option<Position> {
|
||||
|
@ -81,6 +95,249 @@ impl ReceivedOrchardOutput for DecryptedOutput<orchard::note::Note, AccountId> {
|
|||
}
|
||||
}
|
||||
|
||||
fn to_spendable_note<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
row: &Row,
|
||||
) -> Result<
|
||||
Option<ReceivedNote<ReceivedNoteId, zcash_client_backend::wallet::Note>>,
|
||||
SqliteClientError,
|
||||
> {
|
||||
let note_id = ReceivedNoteId(ShieldedProtocol::Orchard, row.get(0)?);
|
||||
let txid = row.get::<_, [u8; 32]>(1).map(TxId::from_bytes)?;
|
||||
let action_index = row.get(2)?;
|
||||
let diversifier = {
|
||||
let d: Vec<_> = row.get(3)?;
|
||||
if d.len() != 11 {
|
||||
return Err(SqliteClientError::CorruptedData(
|
||||
"Invalid diversifier length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut tmp = [0; 11];
|
||||
tmp.copy_from_slice(&d);
|
||||
Diversifier::from_bytes(tmp)
|
||||
};
|
||||
|
||||
let note_value: u64 = row.get::<_, i64>(4)?.try_into().map_err(|_e| {
|
||||
SqliteClientError::CorruptedData("Note values must be nonnegative".to_string())
|
||||
})?;
|
||||
|
||||
let rho = {
|
||||
let rho_bytes: [u8; 32] = row.get(5)?;
|
||||
Option::from(Nullifier::from_bytes(&rho_bytes))
|
||||
.ok_or_else(|| SqliteClientError::CorruptedData("Invalid rho.".to_string()))
|
||||
}?;
|
||||
|
||||
let rseed = {
|
||||
let rseed_bytes: [u8; 32] = row.get(6)?;
|
||||
Option::from(RandomSeed::from_bytes(rseed_bytes, &rho)).ok_or_else(|| {
|
||||
SqliteClientError::CorruptedData("Invalid Orchard random seed.".to_string())
|
||||
})
|
||||
}?;
|
||||
|
||||
let note_commitment_tree_position =
|
||||
Position::from(u64::try_from(row.get::<_, i64>(7)?).map_err(|_| {
|
||||
SqliteClientError::CorruptedData("Note commitment tree position invalid.".to_string())
|
||||
})?);
|
||||
|
||||
let ufvk_str: Option<String> = row.get(8)?;
|
||||
let scope_code: Option<i64> = row.get(9)?;
|
||||
|
||||
ufvk_str
|
||||
.zip(scope_code)
|
||||
.map(|(ufvk_str, scope_code)| {
|
||||
let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str)
|
||||
.map_err(SqliteClientError::CorruptedData)?;
|
||||
|
||||
let spending_key_scope = parse_scope(scope_code).ok_or_else(|| {
|
||||
SqliteClientError::CorruptedData(format!("Invalid key scope code {}", scope_code))
|
||||
})?;
|
||||
let recipient = ufvk
|
||||
.orchard()
|
||||
.map(|fvk| fvk.to_ivk(spending_key_scope).address(diversifier))
|
||||
.ok_or_else(|| {
|
||||
SqliteClientError::CorruptedData("Diversifier invalid.".to_owned())
|
||||
})?;
|
||||
|
||||
let note = Option::from(Note::from_parts(
|
||||
recipient,
|
||||
orchard::value::NoteValue::from_raw(note_value),
|
||||
rho,
|
||||
rseed,
|
||||
))
|
||||
.ok_or_else(|| SqliteClientError::CorruptedData("Invalid Orchard note.".to_string()))?;
|
||||
|
||||
Ok(ReceivedNote::from_parts(
|
||||
note_id,
|
||||
txid,
|
||||
action_index,
|
||||
zcash_client_backend::wallet::Note::Orchard(note),
|
||||
spending_key_scope,
|
||||
note_commitment_tree_position,
|
||||
))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
// The `clippy::let_and_return` lint is explicitly allowed here because a bug in Clippy
|
||||
// (https://github.com/rust-lang/rust-clippy/issues/11308) means it fails to identify that the `result` temporary
|
||||
// is required in order to resolve the borrows involved in the `query_and_then` call.
|
||||
#[allow(clippy::let_and_return)]
|
||||
pub(crate) fn get_spendable_orchard_note<P: consensus::Parameters>(
|
||||
conn: &Connection,
|
||||
params: &P,
|
||||
txid: &TxId,
|
||||
index: u32,
|
||||
) -> Result<
|
||||
Option<ReceivedNote<ReceivedNoteId, zcash_client_backend::wallet::Note>>,
|
||||
SqliteClientError,
|
||||
> {
|
||||
let result = conn.query_row_and_then(
|
||||
"SELECT orchard_received_notes.id, txid, action_index,
|
||||
diversifier, value, rho, rseed, commitment_tree_position,
|
||||
accounts.ufvk, recipient_key_scope
|
||||
FROM orchard_received_notes
|
||||
INNER JOIN accounts on accounts.id = orchard_received_notes.account_id
|
||||
INNER JOIN transactions ON transactions.id_tx = orchard_received_notes.tx
|
||||
WHERE txid = :txid
|
||||
AND action_index = :action_index
|
||||
AND nf IS NOT NULL
|
||||
AND spent IS NULL",
|
||||
named_params![
|
||||
":txid": txid.as_ref(),
|
||||
":action_index": index,
|
||||
],
|
||||
|row| to_spendable_note(params, row),
|
||||
);
|
||||
|
||||
// `OptionalExtension` doesn't work here because the error type of `Result` is already
|
||||
// `SqliteClientError`
|
||||
match result {
|
||||
Ok(r) => Ok(r),
|
||||
Err(SqliteClientError::DbError(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility method for determining whether we have any spendable notes
|
||||
///
|
||||
/// If the tip shard has unscanned ranges below the anchor height and greater than or equal to
|
||||
/// the wallet birthday, none of our notes can be spent because we cannot construct witnesses at
|
||||
/// the provided anchor height.
|
||||
fn unscanned_tip_exists(
|
||||
conn: &Connection,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<bool, rusqlite::Error> {
|
||||
// v_orchard_shard_unscanned_ranges only returns ranges ending on or after wallet birthday, so
|
||||
// we don't need to refer to the birthday in this query.
|
||||
conn.query_row(
|
||||
"SELECT EXISTS (
|
||||
SELECT 1 FROM v_orchard_shard_unscanned_ranges range
|
||||
WHERE range.block_range_start <= :anchor_height
|
||||
AND :anchor_height BETWEEN
|
||||
range.subtree_start_height
|
||||
AND IFNULL(range.subtree_end_height, :anchor_height)
|
||||
)",
|
||||
named_params![":anchor_height": u32::from(anchor_height),],
|
||||
|row| row.get::<_, bool>(0),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn select_spendable_orchard_notes<P: consensus::Parameters>(
|
||||
conn: &Connection,
|
||||
params: &P,
|
||||
account: AccountId,
|
||||
target_value: Zatoshis,
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[ReceivedNoteId],
|
||||
) -> Result<Vec<ReceivedNote<ReceivedNoteId, zcash_client_backend::wallet::Note>>, SqliteClientError>
|
||||
{
|
||||
let birthday_height = match wallet_birthday(conn)? {
|
||||
Some(birthday) => birthday,
|
||||
None => {
|
||||
// the wallet birthday can only be unknown if there are no accounts in the wallet; in
|
||||
// such a case, the wallet has no notes to spend.
|
||||
return Ok(vec![]);
|
||||
}
|
||||
};
|
||||
|
||||
if unscanned_tip_exists(conn, anchor_height)? {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// The goal of this SQL statement is to select the oldest notes until the required
|
||||
// value has been reached.
|
||||
// 1) Use a window function to create a view of all notes, ordered from oldest to
|
||||
// newest, with an additional column containing a running sum:
|
||||
// - Unspent notes accumulate the values of all unspent notes in that note's
|
||||
// account, up to itself.
|
||||
// - Spent notes accumulate the values of all notes in the transaction they were
|
||||
// spent in, up to itself.
|
||||
//
|
||||
// 2) Select all unspent notes in the desired account, along with their running sum.
|
||||
//
|
||||
// 3) Select all notes for which the running sum was less than the required value, as
|
||||
// well as a single note for which the sum was greater than or equal to the
|
||||
// required value, bringing the sum of all selected notes across the threshold.
|
||||
//
|
||||
// 4) Match the selected notes against the witnesses at the desired height.
|
||||
let mut stmt_select_notes = conn.prepare_cached(
|
||||
"WITH eligible AS (
|
||||
SELECT
|
||||
orchard_received_notes.id AS id, txid, action_index,
|
||||
diversifier, value, rho, rseed, commitment_tree_position,
|
||||
SUM(value)
|
||||
OVER (PARTITION BY orchard_received_notes.account_id, spent ORDER BY orchard_received_notes.id) AS so_far,
|
||||
accounts.ufvk as ufvk, recipient_key_scope
|
||||
FROM orchard_received_notes
|
||||
INNER JOIN accounts on accounts.id = orchard_received_notes.account_id
|
||||
INNER JOIN transactions
|
||||
ON transactions.id_tx = orchard_received_notes.tx
|
||||
WHERE orchard_received_notes.account_id = :account
|
||||
AND commitment_tree_position IS NOT NULL
|
||||
AND spent IS NULL
|
||||
AND transactions.block <= :anchor_height
|
||||
AND orchard_received_notes.id NOT IN rarray(:exclude)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM v_orchard_shard_unscanned_ranges unscanned
|
||||
-- select all the unscanned ranges involving the shard containing this note
|
||||
WHERE orchard_received_notes.commitment_tree_position >= unscanned.start_position
|
||||
AND orchard_received_notes.commitment_tree_position < unscanned.end_position_exclusive
|
||||
-- exclude unscanned ranges that start above the anchor height (they don't affect spendability)
|
||||
AND unscanned.block_range_start <= :anchor_height
|
||||
-- exclude unscanned ranges that end below the wallet birthday
|
||||
AND unscanned.block_range_end > :wallet_birthday
|
||||
)
|
||||
)
|
||||
SELECT id, txid, action_index,
|
||||
diversifier, value, rho, rseed, commitment_tree_position,
|
||||
ufvk, recipient_key_scope
|
||||
FROM eligible WHERE so_far < :target_value
|
||||
UNION
|
||||
SELECT id, txid, action_index,
|
||||
diversifier, value, rho, rseed, commitment_tree_position,
|
||||
ufvk, recipient_key_scope
|
||||
FROM (SELECT * from eligible WHERE so_far >= :target_value LIMIT 1)",
|
||||
)?;
|
||||
|
||||
let excluded: Vec<Value> = exclude.iter().map(|n| Value::from(n.1)).collect();
|
||||
let excluded_ptr = Rc::new(excluded);
|
||||
|
||||
let notes = stmt_select_notes.query_and_then(
|
||||
named_params![
|
||||
":account": account.0,
|
||||
":anchor_height": &u32::from(anchor_height),
|
||||
":target_value": &u64::from(target_value),
|
||||
":exclude": &excluded_ptr,
|
||||
":wallet_birthday": u32::from(birthday_height)
|
||||
],
|
||||
|r| to_spendable_note(params, r),
|
||||
)?;
|
||||
|
||||
notes
|
||||
.filter_map(|r| r.transpose())
|
||||
.collect::<Result<_, _>>()
|
||||
}
|
||||
|
||||
/// Records the specified shielded output as having been received.
|
||||
///
|
||||
/// This implementation relies on the facts that:
|
||||
|
@ -94,27 +351,23 @@ pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
|||
) -> Result<(), SqliteClientError> {
|
||||
let mut stmt_upsert_received_note = conn.prepare_cached(
|
||||
"INSERT INTO orchard_received_notes
|
||||
(tx, action_index, account_id, diversifier, value, rseed, memo, nf,
|
||||
is_change, spent, commitment_tree_position,
|
||||
recipient_key_scope)
|
||||
(
|
||||
tx, action_index, account_id,
|
||||
diversifier, value, rho, rseed, memo, nf,
|
||||
is_change, spent, commitment_tree_position,
|
||||
recipient_key_scope
|
||||
)
|
||||
VALUES (
|
||||
:tx,
|
||||
:action_index,
|
||||
:account_id,
|
||||
:diversifier,
|
||||
:value,
|
||||
:rseed,
|
||||
:memo,
|
||||
:nf,
|
||||
:is_change,
|
||||
:spent,
|
||||
:commitment_tree_position,
|
||||
:tx, :action_index, :account_id,
|
||||
:diversifier, :value, :rho, :rseed, :memo, :nf,
|
||||
:is_change, :spent, :commitment_tree_position,
|
||||
:recipient_key_scope
|
||||
)
|
||||
ON CONFLICT (tx, action_index) DO UPDATE
|
||||
SET account_id = :account_id,
|
||||
diversifier = :diversifier,
|
||||
value = :value,
|
||||
rho = :rho,
|
||||
rseed = :rseed,
|
||||
nf = IFNULL(:nf, nf),
|
||||
memo = IFNULL(:memo, memo),
|
||||
|
@ -130,10 +383,11 @@ pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
|||
|
||||
let sql_args = named_params![
|
||||
":tx": &tx_ref,
|
||||
":output_index": i64::try_from(output.index()).expect("output indices are representable as i64"),
|
||||
":action_index": i64::try_from(output.index()).expect("output indices are representable as i64"),
|
||||
":account_id": output.account_id().0,
|
||||
":diversifier": diversifier.as_array(),
|
||||
":value": output.note().value().inner(),
|
||||
":rho": output.note().rho().to_bytes(),
|
||||
":rseed": &rseed.as_bytes(),
|
||||
":nf": output.nullifier().map(|nf| nf.to_bytes()),
|
||||
":memo": memo_repr(output.memo()),
|
||||
|
@ -159,7 +413,7 @@ pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
|||
pub(crate) fn get_orchard_nullifiers(
|
||||
conn: &Connection,
|
||||
query: NullifierQuery,
|
||||
) -> Result<Vec<(AccountId, orchard::note::Nullifier)>, SqliteClientError> {
|
||||
) -> Result<Vec<(AccountId, Nullifier)>, SqliteClientError> {
|
||||
// Get the nullifiers for the notes we are tracking
|
||||
let mut stmt_fetch_nullifiers = match query {
|
||||
NullifierQuery::Unspent => conn.prepare(
|
||||
|
@ -180,10 +434,7 @@ pub(crate) fn get_orchard_nullifiers(
|
|||
let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| {
|
||||
let account = AccountId(row.get(1)?);
|
||||
let nf_bytes: [u8; 32] = row.get(2)?;
|
||||
Ok::<_, rusqlite::Error>((
|
||||
account,
|
||||
orchard::note::Nullifier::from_bytes(&nf_bytes).unwrap(),
|
||||
))
|
||||
Ok::<_, rusqlite::Error>((account, Nullifier::from_bytes(&nf_bytes).unwrap()))
|
||||
})?;
|
||||
|
||||
let res: Vec<_> = nullifiers.collect::<Result<_, _>>()?;
|
||||
|
@ -198,7 +449,7 @@ pub(crate) fn get_orchard_nullifiers(
|
|||
pub(crate) fn mark_orchard_note_spent(
|
||||
conn: &Connection,
|
||||
tx_ref: i64,
|
||||
nf: &orchard::note::Nullifier,
|
||||
nf: &Nullifier,
|
||||
) -> Result<bool, SqliteClientError> {
|
||||
let mut stmt_mark_orchard_note_spent =
|
||||
conn.prepare_cached("UPDATE orchard_received_notes SET spent = ? WHERE nf = ?")?;
|
||||
|
@ -233,6 +484,7 @@ pub(crate) mod tests {
|
|||
use zcash_primitives::transaction::Transaction;
|
||||
use zcash_protocol::{consensus::BlockHeight, memo::MemoBytes, ShieldedProtocol};
|
||||
|
||||
use super::select_spendable_orchard_notes;
|
||||
use crate::{
|
||||
error::SqliteClientError,
|
||||
testing::{
|
||||
|
@ -321,7 +573,14 @@ pub(crate) mod tests {
|
|||
anchor_height: BlockHeight,
|
||||
exclude: &[crate::ReceivedNoteId],
|
||||
) -> Result<Vec<ReceivedNote<crate::ReceivedNoteId, Note>>, SqliteClientError> {
|
||||
todo!()
|
||||
select_spendable_orchard_notes(
|
||||
&st.wallet().conn,
|
||||
&st.wallet().params,
|
||||
account,
|
||||
target_value,
|
||||
anchor_height,
|
||||
exclude,
|
||||
)
|
||||
}
|
||||
|
||||
fn decrypted_pool_outputs_count(
|
||||
|
|
Loading…
Reference in New Issue