1use 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
50pub 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 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 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 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 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 let coins = &self
127 .client
128 .coins_to_spend(&self.address, vec![(asset_id, total_amount, None)], None)
129 .await?[0];
130
131 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 pub async fn collect_fee_tx(
164 &self,
165 coinbase_contract: ContractId,
166 asset_id: AssetId,
167 ) -> anyhow::Result<Transaction> {
168 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 let script = vec![
182 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 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 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 let transferred_utxo = UtxoId::new(tx_id, 0);
263
264 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 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 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}