fuel_core_e2e_client/
test_context.rs

1//! Utilities and helper methods for writing tests
2
3use anyhow::{
4    anyhow,
5    Context,
6};
7use fuel_core_chain_config::ContractConfig;
8use fuel_core_client::client::{
9    types::{
10        CoinType,
11        TransactionStatus,
12    },
13    FuelClient,
14};
15use fuel_core_types::{
16    fuel_asm::{
17        op,
18        GTFArgs,
19        RegId,
20    },
21    fuel_crypto::PublicKey,
22    fuel_tx::{
23        ConsensusParameters,
24        Contract,
25        ContractId,
26        Finalizable,
27        Input,
28        Output,
29        Transaction,
30        TransactionBuilder,
31        TxId,
32        UniqueIdentifier,
33        UtxoId,
34    },
35    fuel_types::{
36        canonical::Serialize,
37        Address,
38        AssetId,
39        Salt,
40    },
41    fuel_vm::SecretKey,
42};
43use itertools::Itertools;
44
45use crate::config::{
46    ClientConfig,
47    SuiteConfig,
48};
49
50// The base amount needed to cover the cost of a simple transaction
51pub const BASE_AMOUNT: u64 = 100_000_000;
52
53pub struct TestContext {
54    pub alice: Wallet,
55    pub bob: Wallet,
56    pub config: SuiteConfig,
57}
58
59impl TestContext {
60    pub async fn new(config: SuiteConfig) -> Self {
61        let alice_client = Self::new_client(config.endpoint.clone(), &config.wallet_a);
62        let bob_client = Self::new_client(config.endpoint.clone(), &config.wallet_b);
63        Self {
64            alice: Wallet::new(config.wallet_a.secret, alice_client).await,
65            bob: Wallet::new(config.wallet_b.secret, bob_client).await,
66            config,
67        }
68    }
69
70    fn new_client(default_endpoint: String, wallet: &ClientConfig) -> FuelClient {
71        FuelClient::new(wallet.endpoint.clone().unwrap_or(default_endpoint)).unwrap()
72    }
73}
74
75#[derive(Debug, Clone)]
76pub struct Wallet {
77    pub secret: SecretKey,
78    pub address: Address,
79    pub client: FuelClient,
80    pub consensus_params: ConsensusParameters,
81}
82
83impl Wallet {
84    pub async fn new(secret: SecretKey, client: FuelClient) -> Self {
85        let public_key: PublicKey = (&secret).into();
86        let address = Input::owner(&public_key);
87        // get consensus params
88        let consensus_params = client
89            .chain_info()
90            .await
91            .expect("failed to get chain info")
92            .consensus_parameters;
93        Self {
94            secret,
95            address,
96            client,
97            consensus_params,
98        }
99    }
100
101    /// returns the balance associated with a wallet
102    pub async fn balance(&self, asset_id: Option<AssetId>) -> anyhow::Result<u64> {
103        self.client
104            .balance(&self.address, Some(&asset_id.unwrap_or_default()))
105            .await
106            .context("failed to retrieve balance")
107    }
108
109    /// Checks if wallet has a coin (regardless of spent status)
110    pub async fn owns_coin(&self, utxo_id: UtxoId) -> anyhow::Result<bool> {
111        let coin = self.client.coin(&utxo_id).await?;
112
113        Ok(coin.is_some())
114    }
115
116    /// Creates the transfer transaction.
117    pub async fn transfer_tx(
118        &self,
119        destination: Address,
120        transfer_amount: u64,
121        asset_id: Option<AssetId>,
122    ) -> anyhow::Result<Transaction> {
123        let asset_id = asset_id.unwrap_or(*self.consensus_params.base_asset_id());
124        let total_amount = transfer_amount + BASE_AMOUNT;
125        // select coins
126        let coins = &self
127            .client
128            .coins_to_spend(&self.address, vec![(asset_id, total_amount, None)], None)
129            .await?[0];
130
131        // build transaction
132        let mut tx = TransactionBuilder::script(Default::default(), Default::default());
133        tx.max_fee_limit(BASE_AMOUNT);
134        tx.script_gas_limit(0);
135
136        for coin in coins {
137            if let CoinType::Coin(coin) = coin {
138                tx.add_unsigned_coin_input(
139                    self.secret,
140                    coin.utxo_id,
141                    coin.amount,
142                    coin.asset_id,
143                    Default::default(),
144                );
145            }
146        }
147        tx.add_output(Output::Coin {
148            to: destination,
149            amount: transfer_amount,
150            asset_id,
151        });
152        tx.add_output(Output::Change {
153            to: self.address,
154            amount: 0,
155            asset_id,
156        });
157        tx.with_params(self.consensus_params.clone());
158
159        Ok(tx.finalize_as_transaction())
160    }
161
162    /// Creates the script transaction that collects fee.
163    pub async fn collect_fee_tx(
164        &self,
165        coinbase_contract: ContractId,
166        asset_id: AssetId,
167    ) -> anyhow::Result<Transaction> {
168        // select coins
169        let coins = &self
170            .client
171            .coins_to_spend(
172                &self.address,
173                vec![(*self.consensus_params.base_asset_id(), BASE_AMOUNT, None)],
174                None,
175            )
176            .await?[0];
177
178        let output_index = 2u64;
179        let call_struct_register = 0x10;
180        // Now call the fee collection contract to withdraw the fees
181        let script = vec![
182            // Point to the call structure
183            op::gtf_args(call_struct_register, 0x00, GTFArgs::ScriptData),
184            op::addi(
185                call_struct_register,
186                call_struct_register,
187                (asset_id.size() + output_index.size()) as u16,
188            ),
189            op::call(call_struct_register, RegId::ZERO, RegId::ZERO, RegId::CGAS),
190            op::ret(RegId::ONE),
191        ];
192
193        // build transaction
194        let mut tx_builder = TransactionBuilder::script(
195            script.into_iter().collect(),
196            asset_id
197                .to_bytes()
198                .into_iter()
199                .chain(output_index.to_bytes().into_iter())
200                .chain(coinbase_contract.to_bytes().into_iter())
201                .chain(0u64.to_bytes().into_iter())
202                .chain(0u64.to_bytes().into_iter())
203                .collect(),
204        );
205        tx_builder.max_fee_limit(BASE_AMOUNT);
206        tx_builder
207            .script_gas_limit(self.consensus_params.tx_params().max_gas_per_tx() / 10);
208
209        tx_builder.add_input(Input::contract(
210            Default::default(),
211            Default::default(),
212            Default::default(),
213            Default::default(),
214            coinbase_contract,
215        ));
216        for coin in coins {
217            if let CoinType::Coin(coin) = coin {
218                tx_builder.add_unsigned_coin_input(
219                    self.secret,
220                    coin.utxo_id,
221                    coin.amount,
222                    coin.asset_id,
223                    Default::default(),
224                );
225            }
226        }
227        tx_builder.add_output(Output::contract(
228            0,
229            Default::default(),
230            Default::default(),
231        ));
232        tx_builder.add_output(Output::Change {
233            to: self.address,
234            amount: 0,
235            asset_id,
236        });
237        tx_builder.add_output(Output::Variable {
238            to: Default::default(),
239            amount: Default::default(),
240            asset_id: Default::default(),
241        });
242        tx_builder.with_params(self.consensus_params.clone());
243
244        Ok(tx_builder.finalize_as_transaction())
245    }
246
247    /// Transfers coins from this wallet to another
248    pub async fn transfer(
249        &self,
250        destination: Address,
251        transfer_amount: u64,
252        asset_id: Option<AssetId>,
253    ) -> anyhow::Result<TransferResult> {
254        let tx = self
255            .transfer_tx(destination, transfer_amount, asset_id)
256            .await?;
257        let tx_id = tx.id(&self.consensus_params.chain_id());
258        println!("submitting tx... {:?}", tx_id);
259        let status = self.client.submit_and_await_commit(&tx).await?;
260
261        // we know the transferred coin should be output 0 from above
262        let transferred_utxo = UtxoId::new(tx_id, 0);
263
264        // get status and return the utxo id of transferred coin
265        Ok(TransferResult {
266            tx_id,
267            transferred_utxo,
268            success: matches!(status, TransactionStatus::Success { .. }),
269            status,
270        })
271    }
272
273    pub async fn deploy_contract(
274        &self,
275        config: ContractConfig,
276        salt: Salt,
277    ) -> anyhow::Result<()> {
278        let asset_id = *self.consensus_params.base_asset_id();
279        let total_amount = BASE_AMOUNT;
280        // select coins
281        let coins = &self
282            .client
283            .coins_to_spend(&self.address, vec![(asset_id, total_amount, None)], None)
284            .await?[0];
285
286        let ContractConfig {
287            contract_id,
288            code: bytes,
289            states,
290            ..
291        } = config;
292
293        let state: Vec<_> = states
294            .into_iter()
295            .map(|entry| entry.try_into())
296            .try_collect()?;
297
298        let state_root = Contract::initial_state_root(state.iter());
299        let mut tx = TransactionBuilder::create(bytes.into(), salt, state);
300
301        for coin in coins {
302            if let CoinType::Coin(coin) = coin {
303                tx.add_unsigned_coin_input(
304                    self.secret,
305                    coin.utxo_id,
306                    coin.amount,
307                    coin.asset_id,
308                    Default::default(),
309                );
310            }
311        }
312
313        tx.add_output(Output::ContractCreated {
314            contract_id,
315            state_root,
316        });
317        tx.add_output(Output::Change {
318            to: self.address,
319            amount: 0,
320            asset_id,
321        });
322        tx.max_fee_limit(BASE_AMOUNT);
323
324        let tx = tx.finalize();
325        println!("The size of the transaction is {}", tx.size());
326
327        let status = self
328            .client
329            .submit_and_await_commit(&tx.clone().into())
330            .await?;
331
332        // check status of contract deployment
333        if let TransactionStatus::Failure { .. } | TransactionStatus::SqueezedOut { .. } =
334            &status
335        {
336            return Err(anyhow!(format!("unexpected transaction status {status:?}")));
337        }
338
339        Ok(())
340    }
341}
342
343pub struct TransferResult {
344    pub tx_id: TxId,
345    pub transferred_utxo: UtxoId,
346    pub success: bool,
347    pub status: TransactionStatus,
348}