From f518b084f3fabdb4abf5d64691f7d3253c30a8e6 Mon Sep 17 00:00:00 2001 From: Tal Derei Date: Mon, 29 Apr 2024 11:46:44 -0700 Subject: [PATCH] frankenstein is not happy --- crates/view/src/planner.rs | 825 +++++++++++++++++++++++++++---------- 1 file changed, 616 insertions(+), 209 deletions(-) diff --git a/crates/view/src/planner.rs b/crates/view/src/planner.rs index 01d0b0862b..deaf6c3843 100644 --- a/crates/view/src/planner.rs +++ b/crates/view/src/planner.rs @@ -1,12 +1,16 @@ + use std::{ collections::BTreeMap, fmt::{self, Debug, Formatter}, mem, }; +use anyhow::anyhow; use anyhow::Result; +use ark_std::iterable::Iterable; use penumbra_sct::epoch::Epoch; use rand::{CryptoRng, RngCore}; +use rand_core::OsRng; use tracing::instrument; use penumbra_asset::{asset, Balance, Value, STAKING_TOKEN_ASSET_ID}; @@ -27,7 +31,7 @@ use penumbra_dex::{ swap_claim::SwapClaimPlan, TradingPair, }; -use penumbra_fee::{Fee, FeeTier, GasPrices}; +use penumbra_fee::{Fee, FeeTier, Gas, GasPrices}; use penumbra_governance::{ proposal_state, DelegatorVotePlan, Proposal, ProposalDepositClaim, ProposalSubmit, ProposalWithdraw, ValidatorVote, Vote, @@ -36,13 +40,14 @@ use penumbra_ibc::IbcRelay; use penumbra_keys::{keys::AddressIndex, Address}; use penumbra_num::Amount; use penumbra_proto::view::v1::{NotesForVotingRequest, NotesRequest}; -use penumbra_shielded_pool::{fmd, Ics20Withdrawal, Note, OutputPlan, SpendPlan}; +use penumbra_shielded_pool::{Ics20Withdrawal, Note, OutputPlan, SpendPlan}; use penumbra_stake::{rate::RateData, validator, IdentityKey, UndelegateClaimPlan}; use penumbra_tct as tct; use penumbra_transaction::{ - gas::{self, GasCost}, + gas::GasCost, memo::MemoPlaintext, plan::{ActionPlan, MemoPlan, TransactionPlan}, + TransactionParameters, }; use crate::{SpendableNoteRecord, ViewClient}; @@ -54,9 +59,10 @@ pub struct Planner { balance: Balance, vote_intents: BTreeMap, plan: TransactionPlan, - ibc_actions: Vec, gas_prices: GasPrices, fee_tier: FeeTier, + actions: Vec, + change_outputs: BTreeMap, // IMPORTANT: if you add more fields here, make sure to clear them when the planner is finished } @@ -78,17 +84,237 @@ impl Debug for Planner { } impl Planner { - /// Create a new planner. + /// Creates a new `Planner` instance with default settings. + /// The planner is used to assemble and manage transaction plans, incorporating + /// various actions like spending and receiving, as well as handling gas and fees. pub fn new(rng: R) -> Self { Self { rng, balance: Balance::default(), vote_intents: BTreeMap::default(), plan: TransactionPlan::default(), - ibc_actions: Vec::new(), gas_prices: GasPrices::zero(), fee_tier: FeeTier::default(), + actions: Vec::new(), + change_outputs: BTreeMap::new(), + } + } + + /// Calculates the total balance by summing up the balance of all actions and change outputs. + fn calculate_balance(&self) -> Balance { + let mut balance = Balance::zero(); + for action in &self.actions { + balance += action.balance(); + } + for action in self.change_outputs.values() { + balance += action.balance(); + } + + balance + } + + /// Calculates the balance after accounting for the base fee estimation. + /// This helps understand the net balance available after fees are applied. + fn calculate_balance_with_fees(&self, base_fee_estimation: Fee) -> Balance { + self.calculate_balance() - base_fee_estimation.0 + } + + /// Adds an action plan to the list of actions within the planner. + /// This is used when assembling the components of a transaction. + fn push(&mut self, action: ActionPlan) { + self.actions.push(action); + } + + /// Estimates the total gas usage of the transaction based on all actions and change outputs. + fn gas_estimate(&self) -> Gas { + let mut gas = Gas::zero(); + for action in &self.actions { + gas += action.gas_cost(); + } + for action in self.change_outputs.values() { + gas += ActionPlan::from(action.clone()).gas_cost(); } + + gas + } + + /// Estimates the total fees for the transaction based on the estimated gas usage + /// and the current gas prices and fee tier. + fn fee_estimate(&self, gas_prices: &GasPrices, fee_tier: &FeeTier) -> Fee { + let base_fee: Fee = Fee::from_staking_token_amount(gas_prices.fee(&self.gas_estimate())); + + base_fee.apply_tier(*fee_tier) + } + + /// Refreshes the change outputs based on the current balance and specified change address. + /// This creates new change outputs for any excess value after actions are accounted for. + fn refresh_change(&mut self, change_address: Address) { + println!("entered refresh_change!"); + println!("refresh change before: {:?}", self.change_outputs); + + self.change_outputs = BTreeMap::new(); + // For each "provided" balance component, create a change note. + for value in self.calculate_balance().provided() { + self.change_outputs.insert( + value.asset_id, + OutputPlan::new(&mut OsRng, value, change_address), + ); + } + + println!("refresh change after: {:?}", self.change_outputs); + } + + /// Adjusts the change outputs to account for transaction fees. + /// This reduces the change amount by the estimated fee to ensure the transaction + /// balances correctly after fees are considered. + fn adjust_change_for_fee(&mut self, fee: Fee) { + println!("entered adjust_change_for_fee!"); + + if !(self.change_outputs.is_empty()) { + self.change_outputs.entry(fee.0.asset_id).and_modify(|e| { + e.value.amount = e.value.amount.saturating_sub(&fee.0.amount); + }); + } + + println!("change outputs after fee: {:?}", self.change_outputs); + } + + // /// Calculates the total balance by summing up the balance of all actions and change outputs. + // fn calculate_balance(&self) -> Balance { + // println!("entered calculate_balance!"); + // let mut balance = Balance::zero(); + // println!("actions 1: {:?}", self.actions); + // for action in &self.actions { + // balance += action.balance(); + // } + // println!("baa;lance after action 1: {:?}", balance); + // println!("actions 2: {:?}", self.change_outputs.values()); + // for action in self.change_outputs.values() { + // balance += action.balance(); + // } + + // println!("balance after action 2: {:?}", balance); + + // balance + // } + + // fn calculate_balance_with_fees(&self, base_fee_estimation: Fee) -> Balance { + // println!("entered calculate_balance_with_fee!"); + // let mut balance = Balance::zero(); + // println!("actions 1: {:?}", self.actions); + + // // we'll add another spend note here. + // for action in &self.actions { + // balance += action.balance(); + // } + + // println!("baa;lance after action 1: {:?}", balance); + // println!("actions 2: {:?}", self.change_outputs.values()); + // for action in self.change_outputs.values() { + // balance += action.balance(); + // } + + // println!("balance after action 2: {:?}", balance); + + // println!("base_fee_estimation.0: {:?}", base_fee_estimation.0); + + // balance -= base_fee_estimation.0; + // println!("balance after fee subtraction: {:?}", balance); + + // balance + // } + + + // fn push(&mut self, action: ActionPlan) { + // self.actions.push(action); + // } + + // fn gas_estimate(&self) -> Gas { + // // TODO: this won't include the gas cost for the bytes of the tx itself + // // so this gas estimate will be an underestimate, but since the tx-bytes contribution + // // to the fee is ideally small, hopefully it doesn't matter. + // let mut gas = Gas::zero(); + // for action in &self.actions { + // // TODO missing AddAssign + // gas = gas + action.gas_cost(); + // } + // for action in self.change_outputs.values() { + // // TODO missing AddAssign + // // TODO missing GasCost impl on OutputPlan + // gas = gas + ActionPlan::from(action.clone()).gas_cost(); + // } + + // println!("gas is: {:?}", gas); + // println!("self.actions is: {:?}", self.actions); + // println!("self.change_outputs is: {:?}", self.change_outputs); + // println!(")))))))))))))))))"); + + // gas + // } + + // fn fee_estimate(&self, gas_prices: &GasPrices, fee_tier: &FeeTier) -> Fee { + // println!("!!!!!!!!!!!!!!!!! fee_estimate!"); + // println!("gas_prices in fee_estomate: {:?}", gas_prices); + // let base_fee: Fee = Fee::from_staking_token_amount(gas_prices.fee(&self.gas_estimate())); + // println!("base fee: {:?}", base_fee); + // base_fee.apply_tier(*fee_tier) + // } + + // fn refresh_change(&mut self, change_address: Address) { + // println!("entered refresh_chnage!"); + // self.change_outputs = BTreeMap::new(); + // // For each "provided" balance component, create a change note. + // for value in self.calculate_balance().provided() { + // println!("value is: {:?}", value); + // self.change_outputs.insert( + // value.asset_id, + // OutputPlan::new(&mut OsRng, value, change_address), + // ); + // } + + // println!("self.change_outputs is: {:?}", self.change_outputs); + // } + + // fn adjust_change_for_fee(&mut self, fee: Fee) { + // println!("self.change_outputs.is_empty(): {:?}", self.change_outputs.is_empty()); + // if !(self.change_outputs.is_empty()) { + // self.change_outputs.entry(fee.0.asset_id).and_modify(|e| { + // e.value.amount = e.value.amount.saturating_sub(&fee.0.amount); + // }); + // } + // } + + /// Prioritize notes to spend to release value of a specific transaction. + /// + /// Various logic is possible for note selection. Currently, this method + /// prioritizes notes sent to a one-time address, then notes with the largest + /// value: + /// + /// - Prioritizing notes sent to one-time addresses optimizes for a future in + /// which we implement DAGSync keyed by fuzzy message detection (which will not + /// be able to detect notes sent to one-time addresses). Spending these notes + /// immediately converts them into change notes, sent to the default address for + /// the users' account, which are detectable. + /// + /// - Prioritizing notes with the largest value optimizes for gas used by the + /// transaction. + /// + /// We may want to make note prioritization configurable in the future. For + /// instance, a user might prefer a note prioritization strategy that harvested + /// capital losses when possible, using cost basis information retained by the + /// view server. + fn prioritize_and_filter_spendable_notes( + records: Vec, + ) -> Vec { + // Filter out zero valued notes. + let mut filtered = records + .into_iter() + .filter(|record| record.note.amount() > Amount::zero()) + .collect::>(); + + filtered.sort_by(|a, b| b.note.amount().cmp(&a.note.amount())); + + filtered } /// Set the current gas prices for fee prediction. @@ -168,40 +394,14 @@ impl Planner { self } - /// Calculate gas cost-based fees and add to the transaction plan. - /// - /// This function should be called once. - // TODO: clarify why we have both `add_gas_fees` and `fee` - // should one be `auto_fee` and the other `set_fee`? - #[instrument(skip(self))] - pub fn add_gas_fees(&mut self) -> &mut Self { - // Add a single Spend + Output to the minimum fee to cover paying the fee - let minimum_fee = self - .gas_prices - .fee(&(self.plan.gas_cost() + gas::output_gas_cost() + gas::spend_gas_cost())); - - // Since paying the fee possibly requires adding additional Spends and Outputs - // to the transaction, which would then change the fee calculation, we multiply - // the fee here by a factor of 128 and then recalculate and capture the excess as - // change outputs. - // - // TODO: this is gross and depending on gas costs could make the gas overpayment - // ridiculously large (so large that the account may not have notes available to cover it) - // or too small. We may need a cyclical calculation of fees on the transaction plan, - // or a "simulated" transaction plan with infinite assets to calculate fees on before - // copying the exact fees to the real transaction. - let fee = Fee::from_staking_token_amount(minimum_fee * Amount::from(128u32)); - self.balance -= fee.0; - self.plan.transaction_parameters.fee = fee.clone(); - self - } - /// Spend a specific positioned note in the transaction. /// /// If you don't use this method to specify spends, they will be filled in automatically from /// the view service when the plan is [`finish`](Planner::finish)ed. #[instrument(skip(self))] pub fn spend(&mut self, note: Note, position: tct::Position) -> &mut Self { + println!("entered spend!"); + let spend = SpendPlan::new(&mut self.rng, note, position).into(); self.action(spend); self @@ -244,6 +444,8 @@ impl Planner { /// Perform a swap claim based on an input swap NFT with a pre-paid fee. #[instrument(skip(self))] pub fn swap_claim(&mut self, plan: SwapClaimPlan) -> &mut Self { + println!("entered swap_claim!"); + // Nothing needs to be spent, since the fee is pre-paid and the // swap NFT will be automatically consumed when the SwapClaim action // is processed by the validators. @@ -262,6 +464,8 @@ impl Planner { swap_claim_fee: Fee, claim_address: Address, ) -> Result<&mut Self> { + println!("entered swap!"); + // Determine the canonical order for the assets being swapped. // This will determine whether the input amount is assigned to delta_1 or delta_2. let trading_pair = TradingPair::new(input_value.asset_id, into_asset); @@ -467,66 +671,11 @@ impl Planner { unbonded_amount, ) .into(); - self.action(vote); + + self.push(vote); self } - // /// Initiates a Dutch auction using protocol-controlled liquidity. - // #[instrument(skip(self))] - // pub fn dutch_auction_schedule( - // &mut self, - // input: Value, - // output_id: asset::Id, - // max_output: Amount, - // min_output: Amount, - // start_height: u64, - // end_height: u64, - // step_count: u64, - // nonce: [u8; 32], - // ) -> &mut Self { - // self.action(ActionPlan::ActionDutchAuctionSchedule( - // ActionDutchAuctionSchedule { - // description: DutchAuctionDescription { - // input, - // output_id, - // max_output, - // min_output, - // start_height, - // end_height, - // step_count, - // nonce, - // }, - // }, - // )) - // } - - // /// Ends a Dutch auction using protocol-controlled liquidity. - // #[instrument(skip(self))] - // pub fn dutch_auction_end(&mut self, auction_id: AuctionId) -> &mut Self { - // self.action(ActionPlan::ActionDutchAuctionEnd(ActionDutchAuctionEnd { - // auction_id, - // })) - // } - - // /// Withdraws the reserves of the Dutch auction. - // #[instrument(skip(self))] - // pub fn dutch_auction_withdraw( - // &mut self, - // auction_id: AuctionId, - // seq: u64, - // reserves_input: Value, - // reserves_output: Value, - // ) -> &mut Self { - // self.action(ActionPlan::ActionDutchAuctionWithdraw( - // ActionDutchAuctionWithdrawPlan { - // auction_id, - // seq, - // reserves_input, - // reserves_output, - // }, - // )) - // } - fn action(&mut self, action: ActionPlan) -> &mut Self { // Track the contribution of the action to the transaction's balance self.balance += action.balance(); @@ -545,75 +694,31 @@ impl Planner { view: &mut V, source: AddressIndex, ) -> anyhow::Result { - // Gather all the information needed from the view service + println!("self.plan.actions.clone() original: {:?}", self.plan.actions.clone()); + println!("self.balance: {:?}", self.balance); + + // Gather all the information needed from the view service. let app_params = view.app_params().await?; let chain_id = app_params.chain_id.clone(); let fmd_params = view.fmd_parameters().await?; - // Calculate the gas that needs to be paid for the transaction based on the configured gas prices. - // Note that _paying the fee might incur an additional `Spend` action_, thus increasing the fee, - // so we slightly overpay here and then capture the excess as change later during `plan_with_spendable_and_votable_notes`. - // Add the fee to the planner's internal balance. - self.add_gas_fees(); + // Caller has already processed all the user-supplied intents into complete action plans. + self.actions = self.plan.actions.clone(); - let mut spendable_notes = Vec::new(); + // Change address represents the sender's address. + let change_address = view.address_by_index(source).await?; + + // Query voting notes. let mut voting_notes = Vec::new(); + let mut spendable_notes = Vec::new(); let (spendable_requests, voting_requests) = self.notes_requests(source); - for request in spendable_requests { - let notes = view.notes(request).await?; - spendable_notes.extend(notes); - } for request in voting_requests { let notes = view.notes_for_voting(request).await?; voting_notes.push(notes); } - - // Plan the transaction using the gathered information - - let self_address = view.address_by_index(source).await?; - self.plan_with_spendable_and_votable_notes( - chain_id, - &fmd_params, - spendable_notes, - voting_notes, - self_address, - ) - } - - /// Add spends and change outputs as required to balance the transaction, using the spendable - /// notes provided. It is the caller's responsibility to ensure that the notes are the result of - /// collected responses to the requests generated by an immediately preceding call to - /// [`Planner::note_requests`]. - /// - /// Clears the contents of the planner, which can be re-used. - #[instrument(skip( - self, - chain_id, - fmd_params, - self_address, - spendable_notes, - votable_notes, - ))] - pub fn plan_with_spendable_and_votable_notes( - &mut self, - chain_id: String, - fmd_params: &fmd::Parameters, - spendable_notes: Vec, - votable_notes: Vec>, - self_address: Address, - ) -> anyhow::Result { - tracing::debug!(plan = ?self.plan, balance = ?self.balance, "finalizing transaction"); - - // Fill in the chain id based on the view service - self.plan.transaction_parameters.chain_id = chain_id; - - // Add the required spends to the planner - for record in spendable_notes { - self.spend(record.note, record.position); - } - // Add any IBC actions to the planner - for ibc_action in self.ibc_actions.clone() { - self.ibc_action(ibc_action); + for request in spendable_requests { + let notes = view.notes(request).await?; + spendable_notes.extend(notes); } // Add the required votes to the planner @@ -628,7 +733,7 @@ impl Planner { .. }, ), - ) in votable_notes + ) in voting_notes .into_iter() .chain(std::iter::repeat(vec![])) // Chain with infinite repeating no notes, so the zip doesn't stop early .zip(mem::take(&mut self.vote_intents).into_iter()) @@ -651,7 +756,9 @@ impl Planner { // result in change sent back to us). This unlinks nullifiers used for voting on // multiple non-overlapping proposals, increasing privacy. if record.height_spent.is_none() { - self.spend(record.note.clone(), record.position); + self.push( + SpendPlan::new(&mut OsRng, record.note.clone(), record.position).into(), + ); } self.delegator_vote_precise( @@ -677,88 +784,388 @@ impl Planner { } } - // Since we over-estimate the fees to be paid upfront by a fixed multiple to account - // for the cost of any additional `Spend` and `Output` actions necessary to pay the fee, - // we need to now calculate the transaction's fee again and capture the excess as change - // by subtracting the excess from the required value balance. - // - // Here, tx_real_fee is the minimum fee to be paid for the transaction, with no tip. - let mut tx_real_fee = self.gas_prices.fee(&self.plan.gas_cost()); - - // Since the excess fee paid will create an additional Output action, we need to - // account for the necessary fee for that action as well. - tx_real_fee += self.gas_prices.fee(&gas::output_gas_cost()); - - // For any remaining provided balance, add the necessary fee for collecting: - tx_real_fee += Amount::from(self.balance.provided().count() as u64) - * self.gas_prices.fee(&gas::output_gas_cost()); - - // Apply the fee tier to tx_real_fee so the block proposer can receive a tip: - tx_real_fee = Fee::from_staking_token_amount(tx_real_fee) - .apply_tier(self.fee_tier) - .amount(); - - println!("tx_real_fee: {:?}", tx_real_fee); - println!("balance: {:?}", self.balance); - - assert!( - tx_real_fee <= self.plan.transaction_parameters.fee.amount(), - "tx real fee {:?} must be less than planned fee {:?}", - tx_real_fee, - self.plan.transaction_parameters.fee.amount(), - ); - let excess_fee_spent = self.plan.transaction_parameters.fee.amount() - tx_real_fee; - self.balance += Value { - amount: excess_fee_spent, - asset_id: *STAKING_TOKEN_ASSET_ID, - }; + // Check enum for voting-based action + let mut is_voting = false; + for action in self.actions.iter() { + if matches!(action, ActionPlan::Spend(_)) { + is_voting = true; + } + } - self.plan.transaction_parameters.fee = Fee::from_staking_token_amount(tx_real_fee); + println!("self.calculate_balance(): {:?}", self.calculate_balance()); - println!("self.actions before: {:?}", self.plan.actions); + let mut staking_token_notes_for_fees = BTreeMap::new(); - // For any remaining provided balance, make a single change note for each - for value in self.balance.provided().collect::>() { - println!("value: {:?}", value); - self.output(value, self_address); - } + let mut notes_by_asset_id = BTreeMap::new(); - println!("self.actions before: {:?}", self.plan.actions); + // Cache the balance calculations to avoid multiple calls + let balance = self.calculate_balance(); + let mut required_iter = balance.required().peekable(); + let mut provided_iter = balance.provided().peekable(); - // All actions have now been added, so check to make sure that you don't build and submit an - // empty transaction - if self.plan.actions.is_empty() { - anyhow::bail!("planned transaction would be empty, so should not be submitted"); - } + // Determine which iterator to use based on the presence of elements + let balance_iter: Box + Send> = + if required_iter.peek().is_some() { + println!("+++++++++++++++++++++++++++++++++++++++++++"); + Box::new(required_iter) + } else if provided_iter.peek().is_some() { + println!("???????????????????????????????"); + Box::new(provided_iter) + } else { + // Handle the case where neither iterator has elements + println!("------------------------------------"); + Box::new(std::iter::empty::()) as Box + Send> + }; - // Now the transaction should be fully balanced, unless we didn't have enough to spend - if !self.balance.is_zero() { - anyhow::bail!( - "balance is non-zero after attempting to balance transaction: {:?}", - self.balance + for required in balance_iter { + println!("iter 1 is: {:?}", required); + // Find all the notes of this asset in the source account. + let records: Vec = view + .notes(NotesRequest { + include_spent: false, + asset_id: Some(required.asset_id.into()), + address_index: Some(source.into()), + amount_to_spend: None, + }) + .await?; + + println!("records is: {:?}", records); + + for record in &records { + println!("record.note.value().amount: {:?}", record.note.value().amount); + // if record.note.value().amount == 0 { + // println!("zero note detected ======================================================================================================"); + // } + } + + notes_by_asset_id.insert( + required.asset_id, + Self::prioritize_and_filter_spendable_notes(records), ); } + // Calculate initial transaction fees. + // let mut fee = self.fee_estimate(&self.gas_prices, &self.fee_tier); + // Set non-zero gas price. + let mut gas_price = GasPrices::default(); + gas_price.block_space_price = 5u64; + gas_price.compact_block_space_price = 5u64; + gas_price.execution_price = 5u64; + gas_price.verification_price = 5u64; + let fee_tier = FeeTier::High; + + self.set_gas_prices(gas_price).set_fee_tier(fee_tier); + + let mut fee: Fee = self.fee_estimate(&self.gas_prices, &self.fee_tier); + + println!("fee: {:?}", fee); + + // Add fee notes + // Find all the notes of this asset in the source account. + let records: Vec = view + .notes(NotesRequest { + include_spent: false, + asset_id: Some(fee.asset_id().into()), + address_index: Some(source.into()), + amount_to_spend: None, + }) + .await?; + + println!("fee ecords is: {:?}", records); + + for record in &records { + println!("fee record.note.value().amount: {:?}", record.note.value().amount); + // if record.note.value().amount == 0 { + // println!("zero note detected ======================================================================================================"); + // } + } + + staking_token_notes_for_fees.insert( + fee.asset_id(), + Self::prioritize_and_filter_spendable_notes(records), + ); + + // Check enum for swap-claim based action + // let mut is_swap_claim = false; + // for action in self.actions.iter() { + // if matches!(action, ActionPlan::SwapClaim(_)) { + // is_swap_claim = true; + // } + // } + + // Add spends and change outputs as required to balance the transaction, using the spendable + // notes provided. It is the caller's responsibility to ensure that the notes are the result of + // collected responses to the requests generated by an immediately preceding call to + // [`Planner::note_requests`]. + let mut iterations = 0usize; + while let Some(required) = self.calculate_balance_with_fees(fee).required().next() { + println!("self.actions 1: {:?}", self.actions); + println!("iter 2 is: {:?}", required); + // Spend a single note towards the required balance, if possible. + // This adds the required spends to the planner. + println!("required.asset_id: {:?}", required.asset_id); + + // If it's a swap claim, handle it differently + // if is_swap_claim { + // let records: Vec = view + // .notes(NotesRequest { + // include_spent: false, + // asset_id: Some(required.asset_id.into()), + // address_index: Some(source.into()), + // amount_to_spend: None, + // }) + // .await?; + + // println!("records is: {:?}", records); + + // notes_by_asset_id.insert( + // required.asset_id, + // Self::prioritize_and_filter_spendable_notes(records), + // ); + // } + + // this will fail for swap_claims! + // let mut zero_amount_records = Vec::new(); + // if !is_swap_claim { + let Some((asset_id, mut note)) = notes_by_asset_id.pop_first() + // let Some(note) = notes_by_asset_id + // .get_mut(&required.asset_id) + // .expect("we already queried") + // .pop() + else { + return Err(anyhow!( + "ran out of notes to spend while planning transaction, need {} of asset {}", + required.amount, + required.asset_id, + ) + .into()); + }; + + // zero_amount_records.push(note.clone()); + // zero_amount_records.push(note[0].clone()); + // } + + // push a staking token note + // let Some((asset_id_fee, mut note_fee)) = staking_token_notes_for_fees.pop_first() + // // .get_mut(&required.asset_id) + // // .expect("we already queried") + // // .pop() + // else { + // return Err(anyhow!( + // "ran out of notes to spend while planning transaction, need {} of asset {}", + // required.amount, + // required.asset_id, + // ) + // .into()); + // }; + + // Add the required spends to the planner. + // if !is_swap_claim { + self.push(SpendPlan::new(&mut OsRng, note[0].clone().note, note[0].clone().position).into()); + // } + + // self.push(SpendPlan::new(&mut OsRng, note_fee[0].clone().note, note_fee[0].clone().position).into()); + + + // Recompute the change outputs, without accounting for fees. + self.refresh_change(change_address); + + // Now re-estimate the fee of the updated transaction and adjust the change if possible. + fee = self.fee_estimate(&self.gas_prices, &self.fee_tier); + println!("fee estimate: {:?}", fee); + + self.adjust_change_for_fee(fee); + + // Need to account to balance after applying fees. + self.balance = self.calculate_balance_with_fees(fee); + // self.balance = self.calculate_balance(); + + println!("self.actions: {:?}", self.actions); + println!("self.balance is: {:?}", self.balance); + + // println!("elf.balance.provided().next() is: {:?}", self.balance.provided().next().unwrap().amount); + + // We've successfully balanced the equation. + // if self.balance.provided().next().unwrap().amount == 0u64.into() { + // break; + // } + if self.balance.is_zero() { + println!("self.balance is zero!"); + break; + } + + iterations += 1; + if iterations > 100 { + return Err(anyhow!("failed to plan transaction after 100 iterations").into()); + } + } + + println!("continue hell!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + // let mut iterations2 = 0usize; + // while let Some(required) = self.calculate_balance_with_fees(fee).required().next() { + // println!("iter 2 is: {:?}", required); + // // Spend a single note towards the required balance, if possible. + // // This adds the required spends to the planner. + // println!("required.asset_id: {:?}", required.asset_id); + + // // If it's a swap claim, handle it differently + // // if is_swap_claim { + // // let records: Vec = view + // // .notes(NotesRequest { + // // include_spent: false, + // // asset_id: Some(required.asset_id.into()), + // // address_index: Some(source.into()), + // // amount_to_spend: None, + // // }) + // // .await?; + + // // println!("records is: {:?}", records); + + // // notes_by_asset_id.insert( + // // required.asset_id, + // // Self::prioritize_and_filter_spendable_notes(records), + // // ); + // // } + + // // this will fail for swap_claims! + // // let mut zero_amount_records = Vec::new(); + // // if !is_swap_claim { + // // let Some((asset_id, mut note)) = notes_by_asset_id.pop_first() + // // let Some(note) = notes_by_asset_id + // // .get_mut(&required.asset_id) + // // .expect("we already queried") + // // .pop() + // // else { + // // return Err(anyhow!( + // // "ran out of notes to spend while planning transaction, need {} of asset {}", + // // required.amount, + // // required.asset_id, + // // ) + // // .into()); + // // }; + + // // zero_amount_records.push(note.clone()); + // // zero_amount_records.push(note[0].clone()); + // // } + + // // push a staking token note + // println!(":))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) self.balance: {:?}", self.balance); + // let Some((asset_id_fee, mut note_fee)) = staking_token_notes_for_fees.pop_first() + // // .get_mut(&required.asset_id) + // // .expect("we already queried") + // // .pop() + // else { + // return Err(anyhow!( + // "ran out of notes to spend while planning transaction, need {} of asset {}", + // required.amount, + // required.asset_id, + // ) + // .into()); + // }; + + // // Add the required spends to the planner. + // // if !is_swap_claim { + // // self.push(SpendPlan::new(&mut OsRng, note.clone().note, note.clone().position).into()); + // // } + + // // if (!self.change_outputs.contains_key(&*STAKING_TOKEN_ASSET_ID)) { + // self.push(SpendPlan::new(&mut OsRng, note_fee[0].clone().note, note_fee[0].clone().position).into()); + // // } + + // // Recompute the change outputs, without accounting for fees. + // self.refresh_change(change_address); + + // // Now re-estimate the fee of the updated transaction and adjust the change if possible. + // fee = self.fee_estimate(&self.gas_prices, &self.fee_tier); + // println!("fee estimate: {:?}", fee); + + // self.adjust_change_for_fee(fee); + + // // Need to account to balance after applying fees. + // // self.balance = self.calculate_balance_with_fees(fee); + // self.balance = self.calculate_balance_with_fees(fee); + + // println!("self.actions: {:?}", self.actions); + // println!("self.balance is: {:?}", self.balance); + + // // We've successfully balanced the equation. + // // if self.balance.provided().next().unwrap().amount == 0u64.into() { + // // break; + // // } + // if self.balance.is_zero() { + // println!("self.balance is zero!"); + // break; + // } + + // iterations2 += 1; + // if iterations2 > 100 { + // return Err(anyhow!("failed to plan transaction after 100 iterations").into()); + // } + // } + + println!("we've balanced the fees!"); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // everything here is great + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + let fee = self.fee_estimate(&self.gas_prices, &self.fee_tier); + + // Assemble the fully-formed transaction plan. + self.plan = TransactionPlan { + actions: self + .actions + .clone() + .into_iter() + .chain(self.change_outputs.clone().into_values().map(Into::into)) + .collect(), + transaction_parameters: TransactionParameters { + expiry_height: self.plan.transaction_parameters.expiry_height, + chain_id: chain_id.clone(), + fee: fee, + }, + detection_data: None, + memo: self.plan.memo.clone(), + }; + // If there are outputs, we check that a memo has been added. If not, we add a blank memo. if self.plan.num_outputs() > 0 && self.plan.memo.is_none() { - self.memo(MemoPlaintext::blank_memo(self_address.clone())) + self.memo(MemoPlaintext::blank_memo(change_address.clone())) .expect("empty string is a valid memo"); } else if self.plan.num_outputs() == 0 && self.plan.memo.is_some() { anyhow::bail!("if no outputs, no memo should be added"); } // Add clue plans for `Output`s. - let precision_bits = fmd_params.precision_bits; self.plan - .populate_detection_data(&mut self.rng, precision_bits.into()); + .populate_detection_data(&mut OsRng, fmd_params.precision_bits.into()); + + // All actions have now been added, so check to make sure that you don't build and submit an + // empty transaction. + if self.actions.is_empty() { + anyhow::bail!("planned transaction would be empty, so should not be submitted"); + } + + // Now the transaction should be fully balanced, unless we didn't have enough to spend + if !self.calculate_balance_with_fees(fee.clone()).is_zero() { + anyhow::bail!( + "balance is non-zero after attempting to balance transaction: {:?}", + self.balance + ); + } tracing::debug!(plan = ?self.plan, "finished balancing transaction"); - // Clear the planner and pull out the plan to return + // Clear the contents of the planner, which can be re-used. self.balance = Balance::zero(); self.vote_intents = BTreeMap::new(); - self.ibc_actions = Vec::new(); self.gas_prices = GasPrices::zero(); + self.actions = Vec::new(); + self.change_outputs = BTreeMap::new(); + + // clean note by asset id + notes_by_asset_id = BTreeMap::new(); let plan = mem::take(&mut self.plan); Ok(plan)