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 max_fee_coins = tx_config
89            .and_then(|config| config.max_fee_coins)
90            .unwrap_or(usize::MAX);
91
92        let input_coins = {
93            utxo_manager
94                .lock()
95                .await
96                .guaranteed_extract_coins(
97                    owner,
98                    base_asset_id,
99                    max_fee as u128,
100                    max_fee_coins,
101                )
102                .map_err(|e| FuelsError::Other(e.to_string()))
103        }?;
104
105        for coin in input_coins.iter() {
106            builder.add_unsigned_coin_input(
107                secret_key,
108                coin.utxo_id,
109                coin.amount,
110                coin.asset_id,
111                TxPointer::default(),
112            );
113        }
114
115        builder.add_output(Output::Change {
116            to: owner,
117            amount: 0,
118            asset_id: base_asset_id,
119        });
120
121        builder.add_output(Output::ContractCreated {
122            contract_id,
123            state_root,
124        });
125
126        let tx = builder.finalize_as_transaction();
127        let tx_id = tx.id(&chain_id);
128
129        let result = match account
130            .send_transaction(chain_id, &tx, submit_clients)
131            .await
132        {
133            Ok(result) => result,
134            Err(e) => {
135                let err_msg = e.to_string();
136                if crate::wallet_ext::is_coin_invalid_error(&err_msg) {
137                    tracing::warn!(
138                        %tx_id,
139                        "Deploy rejected due to invalid coins, \
140                         not returning coins to UtxoManager: {err_msg}",
141                    );
142                } else {
143                    tracing::warn!(
144                        %tx_id,
145                        "Deploy failed, returning coins to UtxoManager: {err_msg}",
146                    );
147                    let mut utxo_manager = utxo_manager.lock().await;
148                    utxo_manager.load_from_coins_vec(input_coins);
149                }
150                return Err(FuelsError::Other(e.to_string()));
151            }
152        };
153
154        // Transaction accepted — spawn background expiration safety net.
155        maybe_return_coins(
156            account,
157            &tx,
158            tx_id,
159            tx_config.and_then(|c| c.expiration_height),
160            utxo_manager,
161        );
162
163        let tx_id = Some(result.tx_id);
164
165        {
166            let mut utxo_manager = utxo_manager.lock().await;
167            utxo_manager.load_from_coins_vec(result.known_coins);
168            utxo_manager.load_from_coins_vec(result.dynamic_coins);
169        }
170
171        Ok(DeployResponse {
172            tx_status: Some(result.tx_status.take_success_checked(None)?),
173            tx_id,
174            contract_id,
175        })
176    }
177}