Skip to content

Commit aa97766

Browse files
authored
Merge pull request #1161 from fluidvanadium/grace_note-1
Send Grace Dust
2 parents 8fda8d2 + 355cbe9 commit aa97766

File tree

6 files changed

+146
-4
lines changed

6 files changed

+146
-4
lines changed

integration-tests/tests/chain_generic_tests.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use zcash_client_backend::ShieldedProtocol::Sapling;
55

66
use zingo_testutils::chain_generic_tests::fixtures::ignore_dust_inputs;
77
use zingo_testutils::chain_generic_tests::fixtures::propose_and_broadcast_value_to_pool;
8+
use zingo_testutils::chain_generic_tests::fixtures::send_grace_dust;
89
use zingo_testutils::chain_generic_tests::fixtures::send_required_dust;
910
use zingo_testutils::chain_generic_tests::fixtures::send_shield_cycle;
1011
use zingo_testutils::chain_generic_tests::fixtures::send_value_to_pool;
@@ -45,6 +46,10 @@ async fn libtonode_send_required_dust() {
4546
send_required_dust::<LibtonodeEnvironment>().await;
4647
}
4748
#[tokio::test]
49+
async fn libtonode_send_grace_dust() {
50+
send_grace_dust::<LibtonodeEnvironment>().await;
51+
}
52+
#[tokio::test]
4853
async fn libtonode_ignore_dust_inputs() {
4954
ignore_dust_inputs::<LibtonodeEnvironment>().await;
5055
}

zingo-testutils/src/chain_generic_tests.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,47 @@ pub mod fixtures {
200200
);
201201
}
202202

203+
/// uses a dust input to pad another input to finish a transaction
204+
pub async fn send_grace_dust<CC>()
205+
where
206+
CC: ConductChain,
207+
{
208+
let mut environment = CC::setup().await;
209+
let primary = environment.fund_client_orchard(120_000).await;
210+
let secondary = environment.create_client().await;
211+
212+
assert_eq!(
213+
with_assertions::propose_send_bump_sync_recipient(
214+
&mut environment,
215+
&primary,
216+
&secondary,
217+
vec![(Shielded(Orchard), 1), (Shielded(Orchard), 99_999)]
218+
)
219+
.await,
220+
3 * MARGINAL_FEE.into_u64()
221+
);
222+
223+
assert_eq!(
224+
with_assertions::propose_send_bump_sync_recipient(
225+
&mut environment,
226+
&secondary,
227+
&primary,
228+
vec![(Shielded(Orchard), 30_000)]
229+
)
230+
.await,
231+
2 * MARGINAL_FEE.into_u64()
232+
);
233+
234+
// since we used our dust as a freebie in the last send, we should only have 2
235+
assert_eq!(
236+
secondary
237+
.query_for_ids(OutputQuery::only_unspent())
238+
.await
239+
.len(),
240+
1
241+
);
242+
}
243+
203244
/// overlooks a bunch of dust inputs to find a pair of inputs marginally big enough to send
204245
pub async fn ignore_dust_inputs<CC>()
205246
where

zingolib/src/lightclient/describe.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ impl LightClient {
4141
.query_sum_value(include_notes)
4242
}
4343

44+
/// Uses a query to select all notes with specific properties and return a vector of their identifiers
45+
pub async fn query_for_ids(
46+
&self,
47+
include_notes: OutputQuery,
48+
) -> Vec<crate::wallet::notes::OutputId> {
49+
self.wallet
50+
.transaction_context
51+
.transaction_metadata_set
52+
.read()
53+
.await
54+
.transaction_records_by_id
55+
.query_for_ids(include_notes)
56+
}
57+
4458
/// TODO: Add Doc Comment Here!
4559
pub async fn do_addresses(&self) -> JsonValue {
4660
let mut objectified_addresses = Vec::new();

zingolib/src/wallet/notes/query.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ impl OutputQuery {
9696
pools: OutputPoolQuery::any(),
9797
}
9898
}
99+
/// a query that accepts all notes.
100+
pub fn only_unspent() -> Self {
101+
Self {
102+
spend_status: OutputSpendStatusQuery {
103+
unspent: true,
104+
pending_spent: false,
105+
spent: false,
106+
},
107+
pools: OutputPoolQuery::any(),
108+
}
109+
}
110+
99111
/// build a query, specifying each stipulation
100112
pub fn stipulations(
101113
unspent: bool,

zingolib/src/wallet/transaction_records_by_id.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ impl TransactionRecordsById {
6363
})
6464
}
6565

66+
/// Uses a query to select all notes across all transactions with specific properties and sum them
67+
pub fn query_for_ids(&self, include_notes: OutputQuery) -> Vec<crate::wallet::notes::OutputId> {
68+
self.0
69+
.iter()
70+
.flat_map(|transaction_record| transaction_record.1.query_for_ids(include_notes))
71+
.collect()
72+
}
73+
6674
/// TODO: Add Doc Comment Here!
6775
pub fn get_received_spendable_note_from_identifier<D: DomainWalletExt>(
6876
&self,

zingolib/src/wallet/transaction_records_by_id/trait_inputsource.rs

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ use zcash_client_backend::{
99
};
1010
use zcash_primitives::{
1111
legacy::Script,
12-
transaction::components::{amount::NonNegativeAmount, TxOut},
12+
transaction::{
13+
components::{amount::NonNegativeAmount, TxOut},
14+
fees::zip317::MARGINAL_FEE,
15+
},
1316
};
1417

1518
use crate::wallet::{
@@ -160,6 +163,25 @@ impl InputSource for TransactionRecordsById {
160163
}
161164
}
162165

166+
if selected.len() < 2 {
167+
// since we maxed out the target value with only one note, we have an option to grace a note.
168+
// we will rescue the biggest dust note
169+
unselected.reverse();
170+
if let Some(biggest_dust) = unselected
171+
.iter()
172+
.find(|(_id, value)| value <= &MARGINAL_FEE.into_u64())
173+
{
174+
selected.push(*biggest_dust);
175+
// we dont bother to pop this last selected note from unselected because we are done with unselected
176+
} else {
177+
// we have no extra dust, but we can still save a marginal fee by adding the next smallest note to change
178+
unselected.reverse();
179+
if let Some(id_value) = unselected.pop() {
180+
selected.push(id_value);
181+
};
182+
}
183+
}
184+
163185
let mut selected_sapling = Vec::<ReceivedNote<NoteId, sapling_crypto::Note>>::new();
164186
let mut selected_orchard = Vec::<ReceivedNote<NoteId, orchard::Note>>::new();
165187

@@ -310,11 +332,12 @@ mod tests {
310332

311333
use crate::wallet::{
312334
notes::{
313-
query::OutputSpendStatusQuery, transparent::mocks::TransparentOutputBuilder,
314-
OutputInterface,
335+
orchard::mocks::OrchardNoteBuilder, query::OutputSpendStatusQuery,
336+
transparent::mocks::TransparentOutputBuilder, OutputInterface,
315337
},
316338
transaction_record::mocks::{
317339
nine_note_transaction_record, nine_note_transaction_record_default,
340+
TransactionRecordBuilder,
318341
},
319342
transaction_records_by_id::TransactionRecordsById,
320343
};
@@ -388,7 +411,46 @@ mod tests {
388411
anchor_height,
389412
&[],
390413
).unwrap();
391-
let expected_len = if target_value > std::cmp::max(sapling_value, orchard_value) {2} else {1};
414+
prop_assert_eq!(spendable_notes.sapling().len(), 1);
415+
prop_assert_eq!(spendable_notes.orchard().len(), 1);
416+
}
417+
#[test]
418+
fn select_spendable_notes_2(
419+
target_value in 0..4_000_000u32,
420+
) {
421+
let mut transaction_records_by_id = TransactionRecordsById::new();
422+
transaction_records_by_id.insert_transaction_record(
423+
424+
TransactionRecordBuilder::default()
425+
.orchard_notes(OrchardNoteBuilder::default().value(1_000_000).clone())
426+
.orchard_notes(OrchardNoteBuilder::default().value(1_000_000).clone())
427+
.orchard_notes(OrchardNoteBuilder::default().value(1_000_000).clone())
428+
.orchard_notes(OrchardNoteBuilder::default().value(1_000_000).clone())
429+
.orchard_notes(OrchardNoteBuilder::default().value(0).clone())
430+
.orchard_notes(OrchardNoteBuilder::default().value(1).clone())
431+
.orchard_notes(OrchardNoteBuilder::default().value(10).clone())
432+
.randomize_txid()
433+
.set_output_indexes()
434+
.build()
435+
);
436+
437+
let target_amount = NonNegativeAmount::const_from_u64(target_value as u64);
438+
let anchor_height: BlockHeight = 10.into();
439+
let spendable_notes =
440+
zcash_client_backend::data_api::InputSource::select_spendable_notes(
441+
&transaction_records_by_id,
442+
AccountId::ZERO,
443+
target_amount,
444+
&[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard],
445+
anchor_height,
446+
&[],
447+
).unwrap();
448+
let expected_len = match target_value {
449+
target_value if target_value <= 2_000_000 => 2,
450+
target_value if target_value <= 3_000_000 => 3,
451+
_ => 4
452+
};
453+
392454
prop_assert_eq!(spendable_notes.sapling().len() + spendable_notes.orchard().len(), expected_len);
393455
}
394456
}

0 commit comments

Comments
 (0)