Skip to main content

miden_client/test_utils/
fee.rs

1//! Funding support for running the test helpers against a fee-charging chain.
2
3use alloc::boxed::Box;
4use alloc::sync::Arc;
5use alloc::vec;
6use alloc::vec::Vec;
7use core::fmt;
8
9use anyhow::{Context, Result};
10use miden_protocol::Felt;
11use miden_protocol::account::AccountId;
12use miden_protocol::block::BlockNumber;
13
14use super::common::TestClient;
15use crate::note::Note;
16use crate::transaction::{TransactionId, TransactionRequestBuilder};
17
18/// Makes accounts able to pay their own transaction fees.
19#[async_trait::async_trait(?Send)]
20pub trait FeeFunder: Send + Sync + fmt::Debug {
21    /// Pays every account in `account_ids` enough to cover its own fees, returning each paired with
22    /// the note carrying its funds.
23    ///
24    /// Taken together so one transaction can pay them all; returned rather than consumed so each
25    /// account's own next transaction spends its note.
26    async fn fund(&self, account_ids: &[AccountId]) -> Result<Vec<(AccountId, Note)>>;
27
28    /// Waits until a block carries every payment this funder has submitted.
29    async fn flush(&self) -> Result<()> {
30        Ok(())
31    }
32}
33
34impl TestClient {
35    /// Pays `account_ids` what they need to cover their own fees, if the chain charges any.
36    ///
37    /// Each note is held until the account's next transaction, which consumes it and is thereby
38    /// also its deploy. Does nothing on a fee-free chain.
39    pub async fn fund_if_needed(&mut self, account_ids: &[AccountId]) -> Result<()> {
40        if !self.chain_charges_fees().await? {
41            return Ok(());
42        }
43
44        let funded = self.funder()?.fund(account_ids).await?;
45        self.stash_funding(funded);
46
47        Ok(())
48    }
49
50    /// Waits until a block carries every payment this client's funder has submitted.
51    pub async fn flush_funder(&self) -> Result<()> {
52        match self.fee_funder() {
53            Some(funder) => funder.flush().await,
54            None => Ok(()),
55        }
56    }
57
58    /// Returns the funder, or an error naming what to supply when the chain needs one.
59    fn funder(&self) -> Result<Arc<dyn FeeFunder>> {
60        self.fee_funder().cloned().context(
61            "this chain charges a transaction fee, so every account a test creates has to be \
62             funded before it can transact, but this client has no fee funder. Supply the funder \
63             wallets to draw from (see the integration tests' `--funders` argument)",
64        )
65    }
66
67    /// Deploys `account_id` on-chain, whether or not the chain charges fees.
68    pub async fn deploy_account(&mut self, account_id: AccountId) -> Result<()> {
69        self.deploy_accounts(&[account_id]).await
70    }
71
72    /// Deploys `account_ids` on-chain, whether or not the chain charges fees. Already-deployed
73    /// accounts are left alone.
74    ///
75    /// Taken together so the funder pays once and the deploys share a single wait.
76    pub async fn deploy_accounts(&mut self, account_ids: &[AccountId]) -> Result<()> {
77        let mut undeployed = Vec::with_capacity(account_ids.len());
78        for account_id in account_ids.iter().copied() {
79            // A zero nonce is what marks an account as never having transacted, so it reads the
80            // nonce alone rather than reconstructing the account.
81            let nonce =
82                self.account_reader(account_id).nonce().await.with_context(|| {
83                    format!("account {account_id} is not tracked by the client")
84                })?;
85            if nonce == Felt::ZERO {
86                undeployed.push(account_id);
87            }
88        }
89        if undeployed.is_empty() {
90            return Ok(());
91        }
92
93        if self.chain_charges_fees().await? {
94            // Deploying on demand means there is no later transaction to fold the funding into, so
95            // the notes are consumed here.
96            let mut funded = Vec::with_capacity(undeployed.len());
97            let mut unfunded = Vec::new();
98            for account_id in undeployed.iter().copied() {
99                match self.take_funding(account_id) {
100                    Some(note) => funded.push((account_id, note)),
101                    None => unfunded.push(account_id),
102                }
103            }
104
105            // Paid in one transaction rather than one apiece, which each cost a fee and a proof.
106            if !unfunded.is_empty() {
107                funded.extend(self.funder()?.fund(&unfunded).await?);
108            }
109
110            return self.deploy_by_consuming(&funded).await;
111        }
112
113        let mut tx_ids = Vec::with_capacity(undeployed.len());
114        for account_id in undeployed {
115            let request = TransactionRequestBuilder::new()
116                .build()
117                .context("failed to build the deploy transaction request")?;
118            let tx_id =
119                Box::pin(self.submit_new_transaction(account_id, request)).await.with_context(
120                    || format!("failed to submit the deploy transaction of {account_id}"),
121                )?;
122            tx_ids.push((account_id, tx_id));
123        }
124
125        self.wait_for_deploys(&tx_ids).await
126    }
127
128    /// Deploys each account by consuming the note paired with it, a note carrying enough of the
129    /// native fee asset for the deploy to settle its own fee.
130    pub async fn deploy_by_consuming(&mut self, funded: &[(AccountId, Note)]) -> Result<()> {
131        // Every deploy is submitted before any of them is waited on, so they settle in as few
132        // blocks as the node packs them into rather than one block apiece.
133        let mut tx_ids = Vec::with_capacity(funded.len());
134        for (account_id, note) in funded {
135            let (account_id, note_id) = (*account_id, note.id());
136
137            // Consumed as an unauthenticated input, so the funder's transaction only has to have
138            // reached the mempool. This doubles as the deploy, paying its fee out of the note it
139            // just consumed.
140            let request = TransactionRequestBuilder::new()
141                .build_consume_notes(vec![note.clone()])
142                .context("failed to build the funding note consumption request")?;
143            let tx_id =
144                Box::pin(self.submit_new_transaction(account_id, request)).await.with_context(
145                    || format!("account {account_id} failed to consume funding note {note_id}"),
146                )?;
147            tx_ids.push((account_id, tx_id));
148        }
149
150        self.wait_for_deploys(&tx_ids).await
151    }
152
153    /// Waits for every deploy transaction to commit, so the test that follows does not see the
154    /// deploys and funding notes in its own sync.
155    async fn wait_for_deploys(&mut self, tx_ids: &[(AccountId, TransactionId)]) -> Result<()> {
156        for (account_id, tx_id) in tx_ids.iter().copied() {
157            self.wait_for_tx(tx_id).await.with_context(|| {
158                format!("the deploy transaction of account {account_id} never committed")
159            })?;
160        }
161
162        Ok(())
163    }
164
165    /// Returns whether the chain charges a non-zero fee per transaction, read from the genesis
166    /// header.
167    ///
168    /// Exposed because a few invariants only hold fee-free: paying a fee is itself an account state
169    /// change, so asserting a transaction left a commitment untouched only holds on a fee-free
170    /// chain.
171    pub async fn chain_charges_fees(&self) -> Result<bool> {
172        let (genesis, _) = self
173            .get_block_header_by_num(BlockNumber::GENESIS)
174            .await?
175            .context("the genesis block header is not in the client's store")?;
176
177        Ok(genesis.fee_parameters().verification_base_fee() != 0)
178    }
179}