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