Skip to main content

o2_tools/
contract_ext.rs

1use crate::{
2    call_handler_ext::{
3        TransactionConfig,
4        maybe_return_coins,
5    },
6    utxo_manager::SharedUtxoManager,
7    wallet_ext::{
8        BuilderData,
9        WalletExt,
10    },
11};
12use fuel_core_client::client::FuelClient;
13use fuel_core_types::{
14    fuel_tx::{
15        Create,
16        Output,
17        TxPointer,
18        UniqueIdentifier,
19        Witness,
20    },
21    fuel_types::canonical::Serialize,
22};
23use fuels::{
24    accounts::ViewOnlyAccount,
25    prelude::{
26        Contract,
27        Error as FuelsError,
28        Result as FuelsResult,
29        Wallet,
30    },
31    programs::contract::{
32        DeployResponse,
33        Regular,
34    },
35};
36use std::future::Future;
37
38pub(crate) trait ContractExt {
39    fn almost_sync_deploy(
40        self,
41        account: &Wallet,
42        builder_data: &BuilderData,
43        utxo_manager: &SharedUtxoManager,
44        tx_config: &Option<TransactionConfig>,
45        submit_clients: &[FuelClient],
46    ) -> impl Future<Output = FuelsResult<DeployResponse>>;
47}
48
49impl ContractExt for Contract<Regular> {
50    /// Deploys a compiled contract to a running node.
51    /// To deploy a contract, you need an account with enough assets to pay for deployment.
52    /// This account will also receive the change.
53    async fn almost_sync_deploy(
54        self,
55        account: &Wallet,
56        builder_data: &BuilderData,
57        utxo_manager: &SharedUtxoManager,
58        tx_config: &Option<TransactionConfig>,
59        submit_clients: &[FuelClient],
60    ) -> FuelsResult<DeployResponse> {
61        let consensus_parameters = &builder_data.consensus_parameters;
62        let max_fee = builder_data.max_fee();
63        let base_asset_id = *consensus_parameters.base_asset_id();
64        let chain_id = consensus_parameters.chain_id();
65        let secret_key = account.signer().secret_key();
66        let owner = account.address();
67
68        let contract_id = self.contract_id();
69        let state_root = self.state_root();
70        let salt = self.salt();
71        let storage_slots = self.storage_slots();
72        let code = self.code();
73        let bytecode = Witness::from(code);
74
75        let witness_limit = bytecode.size() + crate::wallet_ext::SIGNATURE_MARGIN;
76
77        let mut builder = fuel_core_types::fuel_tx::TransactionBuilder::<Create>::create(
78            bytecode,
79            salt,
80            storage_slots.to_vec(),
81        );
82
83        builder
84            .with_chain_id(consensus_parameters.chain_id())
85            .max_fee_limit(max_fee)
86            .witness_limit(witness_limit as u64);
87
88        let input_coins = {
89            utxo_manager
90                .lock()
91                .await
92                .guaranteed_extract_coins(owner, base_asset_id, max_fee as u128)
93                .map_err(|e| FuelsError::Other(e.to_string()))
94        }?;
95
96        for coin in input_coins.iter() {
97            builder.add_unsigned_coin_input(
98                secret_key,
99                coin.utxo_id,
100                coin.amount,
101                coin.asset_id,
102                TxPointer::default(),
103            );
104        }
105
106        builder.add_output(Output::Change {
107            to: owner,
108            amount: 0,
109            asset_id: base_asset_id,
110        });
111
112        builder.add_output(Output::ContractCreated {
113            contract_id,
114            state_root,
115        });
116
117        let tx = builder.finalize_as_transaction();
118        let tx_id = tx.id(&chain_id);
119
120        let result = match account
121            .send_transaction(chain_id, &tx, submit_clients)
122            .await
123        {
124            Ok(result) => result,
125            Err(e) => {
126                let err_msg = e.to_string();
127                if crate::wallet_ext::is_coin_invalid_error(&err_msg) {
128                    tracing::warn!(
129                        %tx_id,
130                        "Deploy rejected due to invalid coins, \
131                         not returning coins to UtxoManager: {err_msg}",
132                    );
133                } else {
134                    tracing::warn!(
135                        %tx_id,
136                        "Deploy failed, returning coins to UtxoManager: {err_msg}",
137                    );
138                    let mut utxo_manager = utxo_manager.lock().await;
139                    utxo_manager.load_from_coins_vec(input_coins);
140                }
141                return Err(FuelsError::Other(e.to_string()));
142            }
143        };
144
145        // Transaction accepted — spawn background expiration safety net.
146        maybe_return_coins(
147            account,
148            &tx,
149            tx_id,
150            tx_config.and_then(|c| c.expiration_height),
151            utxo_manager,
152        );
153
154        let tx_id = Some(result.tx_id);
155
156        {
157            let mut utxo_manager = utxo_manager.lock().await;
158            utxo_manager.load_from_coins_vec(result.known_coins);
159            utxo_manager.load_from_coins_vec(result.dynamic_coins);
160        }
161
162        Ok(DeployResponse {
163            tx_status: Some(result.tx_status.take_success_checked(None)?),
164            tx_id,
165            contract_id,
166        })
167    }
168}