penumbra_sdk_wallet/
plan.rs1use std::collections::BTreeMap;
2
3use anyhow::Context;
4use decaf377::Fq;
5use rand_core::{CryptoRng, RngCore};
6use tracing::instrument;
7
8use penumbra_sdk_dex::swap_claim::SwapClaimPlan;
9use penumbra_sdk_keys::keys::AddressIndex;
10use penumbra_sdk_proto::view::v1::NotesRequest;
11use penumbra_sdk_transaction::{TransactionParameters, TransactionPlan};
12pub use penumbra_sdk_view::Planner;
13use penumbra_sdk_view::{SpendableNoteRecord, ViewClient};
14
15pub const SWEEP_COUNT: usize = 8;
16
17#[instrument(skip(view, rng))]
18pub async fn sweep<V, R>(view: &mut V, mut rng: R) -> anyhow::Result<Vec<TransactionPlan>>
19where
20 V: ViewClient,
21 R: RngCore + CryptoRng,
22{
23 let mut plans = Vec::new();
24
25 plans.extend(claim_unclaimed_swaps(view, &mut rng).await?);
27
28 plans.extend(sweep_notes(view, &mut rng).await?);
31
32 Ok(plans)
33}
34
35#[instrument(skip(view, rng))]
36async fn claim_unclaimed_swaps<V, R>(
37 view: &mut V,
38 mut rng: R,
39) -> anyhow::Result<Vec<TransactionPlan>>
40where
41 V: ViewClient,
42 R: RngCore + CryptoRng,
43{
44 let mut plans = Vec::new();
45 let app_params = view.app_params().await?;
51 let epoch_duration = app_params.sct_params.epoch_duration;
52
53 let unclaimed_swaps = view.unclaimed_swaps().await?;
54
55 for swap in unclaimed_swaps {
56 let swap_plaintext = swap.swap;
58
59 let output_data = swap.output_data;
60
61 let mut plan = TransactionPlan {
62 transaction_parameters: TransactionParameters {
63 chain_id: app_params.clone().chain_id,
64 fee: swap_plaintext.claim_fee.clone(),
65 ..Default::default()
66 },
67 memo: None,
69 ..Default::default()
70 };
71
72 let action_plan = SwapClaimPlan {
73 swap_plaintext,
74 position: swap.position,
75 output_data,
76 epoch_duration,
77 proof_blinding_r: Fq::rand(&mut rng),
78 proof_blinding_s: Fq::rand(&mut rng),
79 };
80 plan.actions.push(action_plan.into());
81 plans.push(plan);
82 }
83
84 Ok(plans)
85}
86
87#[instrument(skip(view, rng))]
88async fn sweep_notes<V, R>(view: &mut V, mut rng: R) -> anyhow::Result<Vec<TransactionPlan>>
89where
90 V: ViewClient,
91 R: RngCore + CryptoRng,
92{
93 let gas_prices = view.gas_prices().await?;
94
95 let all_notes = view
96 .notes(NotesRequest {
97 ..Default::default()
98 })
99 .await?;
100
101 let mut notes_by_addr_and_denom: BTreeMap<AddressIndex, BTreeMap<_, Vec<SpendableNoteRecord>>> =
102 BTreeMap::new();
103
104 for record in all_notes {
105 notes_by_addr_and_denom
106 .entry(record.address_index)
107 .or_default()
108 .entry(record.note.asset_id())
109 .or_default()
110 .push(record);
111 }
112
113 let mut plans = Vec::new();
114
115 for (index, notes_by_denom) in notes_by_addr_and_denom {
116 tracing::info!(?index, "processing address");
117
118 for (asset_id, mut records) in notes_by_denom {
119 tracing::debug!(?asset_id, "processing asset");
120
121 records.sort_by(|a, b| a.note.value().amount.cmp(&b.note.value().amount));
123 for group in records.chunks_exact(SWEEP_COUNT) {
126 let mut planner = Planner::new(&mut rng);
127 planner.set_gas_prices(gas_prices);
128
129 for record in group {
130 planner.spend(record.note.clone(), record.position);
131 }
132
133 let plan = planner
134 .plan(view, index)
135 .await
136 .context("can't build sweep transaction")?;
137
138 tracing::debug!(?plan);
139 plans.push(plan);
140 }
141 }
142 }
143
144 Ok(plans)
145}