soroban_rs/
operation.rs

1//! # Soroban Operation Creation
2//!
3//! This module provides functionality for creating Stellar operations for Soroban contracts.
4//! These operations represent the fundamental actions that can be performed with Soroban,
5//! such as uploading contract code, deploying contracts, and invoking contract functions.
6use stellar_xdr::curr::{
7    AccountId, Asset, ContractExecutable, ContractIdPreimage, CreateContractArgs,
8    CreateContractArgsV2, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Operation,
9    OperationBody, PaymentOp, ScAddress, ScSymbol, ScVal, SorobanAuthorizationEntry,
10    SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, VecM,
11};
12
13use crate::error::SorobanHelperError;
14
15/// Factory for creating Soroban operations.
16///
17/// This struct provides methods to create operations for common Soroban tasks,
18/// such as uploading contract WASM, deploying contracts, and invoking contract functions.
19/// These operations can be added to transactions and submitted to the Stellar network.
20pub struct Operations;
21
22impl Operations {
23    /// Creates an operation to upload contract WASM code to the Stellar network.
24    ///
25    /// # Parameters
26    ///
27    /// * `wasm_bytes` - The raw WASM bytecode to upload
28    ///
29    /// # Returns
30    ///
31    /// An operation that can be added to a transaction to upload the WASM
32    ///
33    /// # Errors
34    ///
35    /// Returns `SorobanHelperError::XdrEncodingFailed` if the WASM bytes
36    /// cannot be encoded into the XDR format
37    pub fn upload_wasm(wasm_bytes: Vec<u8>) -> Result<Operation, SorobanHelperError> {
38        Ok(Operation {
39            source_account: None,
40            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
41                host_function: HostFunction::UploadContractWasm(wasm_bytes.try_into().map_err(
42                    |e| {
43                        SorobanHelperError::XdrEncodingFailed(format!(
44                            "Failed to encode WASM bytes: {}",
45                            e
46                        ))
47                    },
48                )?),
49                auth: VecM::default(),
50            }),
51        })
52    }
53
54    /// Creates an operation to deploy a contract to the Stellar network.
55    ///
56    /// # Parameters
57    ///
58    /// * `contract_id_preimage` - The preimage used to derive the contract ID
59    /// * `wasm_hash` - The hash of the previously uploaded WASM code
60    /// * `constructor_args` - Optional arguments to pass to the contract constructor
61    ///
62    /// # Returns
63    ///
64    /// An operation that can be added to a transaction to deploy the contract
65    ///
66    /// # Errors
67    ///
68    /// Returns `SorobanHelperError::XdrEncodingFailed` if any of the arguments
69    /// cannot be encoded into the XDR format
70    pub fn create_contract(
71        contract_id_preimage: ContractIdPreimage,
72        wasm_hash: Hash,
73        constructor_args: Option<Vec<ScVal>>,
74    ) -> Result<Operation, SorobanHelperError> {
75        match constructor_args {
76            Some(args) => {
77                Self::create_contract_with_constructor(contract_id_preimage, wasm_hash, args)
78            }
79            None => Self::create_contract_without_constructor(contract_id_preimage, wasm_hash),
80        }
81    }
82
83    /// Creates an operation to deploy a contract with constructor arguments.
84    ///
85    /// # Parameters
86    ///
87    /// * `contract_id_preimage` - The preimage used to derive the contract ID
88    /// * `wasm_hash` - The hash of the previously uploaded WASM code
89    /// * `constructor_args` - Arguments to pass to the contract constructor
90    ///
91    /// # Returns
92    ///
93    /// An operation that can be added to a transaction to deploy the contract
94    ///
95    /// # Errors
96    ///
97    /// Returns `SorobanHelperError::XdrEncodingFailed` if any of the arguments
98    /// cannot be encoded into the XDR format
99    fn create_contract_with_constructor(
100        contract_id_preimage: ContractIdPreimage,
101        wasm_hash: Hash,
102        constructor_args: Vec<ScVal>,
103    ) -> Result<Operation, SorobanHelperError> {
104        let args: VecM<ScVal, { u32::MAX }> = constructor_args.try_into().map_err(|e| {
105            SorobanHelperError::XdrEncodingFailed(format!(
106                "Failed to encode constructor args: {}",
107                e
108            ))
109        })?;
110
111        let create_args = CreateContractArgsV2 {
112            contract_id_preimage,
113            executable: ContractExecutable::Wasm(wasm_hash),
114            constructor_args: args,
115        };
116
117        let auth_entry = SorobanAuthorizationEntry {
118            credentials: SorobanCredentials::SourceAccount,
119            root_invocation: SorobanAuthorizedInvocation {
120                function: SorobanAuthorizedFunction::CreateContractV2HostFn(create_args.clone()),
121                sub_invocations: VecM::default(),
122            },
123        };
124
125        Ok(Operation {
126            source_account: None,
127            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
128                auth: vec![auth_entry].try_into().map_err(|e| {
129                    SorobanHelperError::XdrEncodingFailed(format!(
130                        "Failed to encode auth entries: {}",
131                        e
132                    ))
133                })?,
134                host_function: HostFunction::CreateContractV2(create_args),
135            }),
136        })
137    }
138
139    /// Creates an operation to deploy a contract without constructor arguments.
140    ///
141    /// # Parameters
142    ///
143    /// * `contract_id_preimage` - The preimage used to derive the contract ID
144    /// * `wasm_hash` - The hash of the previously uploaded WASM code
145    ///
146    /// # Returns
147    ///
148    /// An operation that can be added to a transaction to deploy the contract
149    ///
150    /// # Errors
151    ///
152    /// Returns `SorobanHelperError::XdrEncodingFailed` if any of the arguments
153    /// cannot be encoded into the XDR format
154    fn create_contract_without_constructor(
155        contract_id_preimage: ContractIdPreimage,
156        wasm_hash: Hash,
157    ) -> Result<Operation, SorobanHelperError> {
158        let create_args = CreateContractArgs {
159            contract_id_preimage,
160            executable: ContractExecutable::Wasm(wasm_hash),
161        };
162
163        let auth_entry = SorobanAuthorizationEntry {
164            credentials: SorobanCredentials::SourceAccount,
165            root_invocation: SorobanAuthorizedInvocation {
166                function: SorobanAuthorizedFunction::CreateContractHostFn(create_args.clone()),
167                sub_invocations: VecM::default(),
168            },
169        };
170
171        Ok(Operation {
172            source_account: None,
173            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
174                auth: vec![auth_entry].try_into().map_err(|e| {
175                    SorobanHelperError::XdrEncodingFailed(format!(
176                        "Failed to encode auth entries: {}",
177                        e
178                    ))
179                })?,
180                host_function: HostFunction::CreateContract(create_args),
181            }),
182        })
183    }
184
185    /// Creates an operation to invoke a function on a deployed contract.
186    ///
187    /// # Parameters
188    ///
189    /// * `contract_id` - The ID of the deployed contract
190    /// * `function_name` - The name of the function to invoke
191    /// * `args` - Arguments to pass to the function
192    ///
193    /// # Returns
194    ///
195    /// An operation that can be added to a transaction to invoke the contract function
196    ///
197    /// # Errors
198    ///
199    /// Returns:
200    /// - `SorobanHelperError::InvalidArgument` if the function name is invalid
201    /// - `SorobanHelperError::XdrEncodingFailed` if the arguments cannot be encoded
202    pub fn invoke_contract(
203        contract_id: &stellar_strkey::Contract,
204        function_name: &str,
205        args: Vec<ScVal>,
206    ) -> Result<Operation, SorobanHelperError> {
207        let invoke_contract_args = InvokeContractArgs {
208            contract_address: ScAddress::Contract(Hash(contract_id.0)),
209            function_name: ScSymbol(function_name.try_into().map_err(|e| {
210                SorobanHelperError::InvalidArgument(format!("Invalid function name: {}", e))
211            })?),
212            args: args.try_into().map_err(|e| {
213                SorobanHelperError::XdrEncodingFailed(format!("Failed to encode arguments: {}", e))
214            })?,
215        };
216
217        Ok(Operation {
218            source_account: None,
219            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
220                host_function: HostFunction::InvokeContract(invoke_contract_args),
221                auth: VecM::default(),
222            }),
223        })
224    }
225
226    pub fn send_payment(
227        to: AccountId,
228        amount: i64,
229        asset: Asset,
230    ) -> Result<Operation, SorobanHelperError> {
231        Ok(Operation {
232            source_account: None,
233            body: OperationBody::Payment(PaymentOp {
234                amount,
235                destination: to.into(),
236                asset,
237            }),
238        })
239    }
240}
241
242#[cfg(test)]
243mod test {
244    use super::*;
245    use stellar_xdr::curr::{ContractIdPreimageFromAddress, PublicKey, ScVal};
246
247    #[test]
248    fn test_upload_wasm() {
249        let wasm_bytes = vec![0, 1, 2, 3, 4, 5];
250        let operation = Operations::upload_wasm(wasm_bytes.clone()).unwrap();
251
252        assert!(matches!(
253            operation.body,
254            OperationBody::InvokeHostFunction(_)
255        ));
256        if let OperationBody::InvokeHostFunction(op) = operation.body {
257            assert!(matches!(
258                op.host_function,
259                HostFunction::UploadContractWasm(_)
260            ));
261            assert_eq!(op.auth.len(), 0);
262        }
263    }
264
265    #[test]
266    fn test_create_contract_without_args() {
267        let account_id =
268            stellar_xdr::curr::AccountId(PublicKey::PublicKeyTypeEd25519([0; 32].into()));
269        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
270            address: ScAddress::Account(account_id),
271            salt: [1; 32].into(),
272        });
273
274        let wasm_hash = Hash([2; 32]);
275        let operation =
276            Operations::create_contract(contract_id_preimage.clone(), wasm_hash.clone(), None)
277                .unwrap();
278
279        assert!(matches!(
280            operation.body,
281            OperationBody::InvokeHostFunction(_)
282        ));
283        if let OperationBody::InvokeHostFunction(op) = operation.body {
284            assert!(matches!(op.host_function, HostFunction::CreateContract(_)));
285            assert_eq!(op.auth.len(), 1);
286
287            if let HostFunction::CreateContract(args) = op.host_function {
288                assert_eq!(args.contract_id_preimage, contract_id_preimage);
289                assert!(matches!(args.executable, ContractExecutable::Wasm(h) if h == wasm_hash));
290            }
291        }
292    }
293
294    #[test]
295    fn test_create_contract_with_args() {
296        let account_id =
297            stellar_xdr::curr::AccountId(PublicKey::PublicKeyTypeEd25519([0; 32].into()));
298        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
299            address: ScAddress::Account(account_id),
300            salt: [1; 32].into(),
301        });
302
303        let wasm_hash = Hash([2; 32]);
304        let constructor_args = vec![ScVal::I32(42), ScVal::Bool(true)];
305        let operation = Operations::create_contract(
306            contract_id_preimage.clone(),
307            wasm_hash.clone(),
308            Some(constructor_args.clone()),
309        )
310        .unwrap();
311
312        assert!(matches!(
313            operation.body,
314            OperationBody::InvokeHostFunction(_)
315        ));
316        if let OperationBody::InvokeHostFunction(op) = operation.body {
317            assert!(matches!(
318                op.host_function,
319                HostFunction::CreateContractV2(_)
320            ));
321            assert_eq!(op.auth.len(), 1);
322
323            if let HostFunction::CreateContractV2(args) = op.host_function {
324                assert_eq!(args.contract_id_preimage, contract_id_preimage);
325                assert!(matches!(args.executable, ContractExecutable::Wasm(h) if h == wasm_hash));
326
327                assert_eq!(args.constructor_args.len(), 2);
328                assert!(matches!(args.constructor_args[0], ScVal::I32(42)));
329                assert!(matches!(args.constructor_args[1], ScVal::Bool(true)));
330            }
331        }
332    }
333
334    #[test]
335    fn test_invoke_contract() {
336        let contract_bytes = [3; 32];
337        let contract_id = stellar_strkey::Contract(contract_bytes);
338
339        let function_name = "test_function";
340        let args = vec![ScVal::I32(42), ScVal::Bool(true)];
341        let operation =
342            Operations::invoke_contract(&contract_id, function_name, args.clone()).unwrap();
343
344        assert!(matches!(
345            operation.body,
346            OperationBody::InvokeHostFunction(_)
347        ));
348        if let OperationBody::InvokeHostFunction(op) = operation.body {
349            assert!(matches!(op.host_function, HostFunction::InvokeContract(_)));
350            assert_eq!(op.auth.len(), 0);
351
352            if let HostFunction::InvokeContract(args) = op.host_function {
353                assert!(
354                    matches!(args.contract_address, ScAddress::Contract(hash) if hash.0 == contract_bytes)
355                );
356                assert_eq!(args.function_name.0.as_slice(), function_name.as_bytes());
357
358                assert_eq!(args.args.len(), 2);
359                assert!(matches!(args.args[0], ScVal::I32(42)));
360                assert!(matches!(args.args[1], ScVal::Bool(true)));
361            }
362        }
363    }
364
365    #[test]
366    fn test_invoke_contract_invalid_function_name() {
367        let contract_bytes = [3; 32];
368        let contract_id = stellar_strkey::Contract(contract_bytes);
369
370        let invalid_function_name = "a".repeat(33); // ScSymbol has a max length of 32
371        let args = vec![];
372
373        let result = Operations::invoke_contract(&contract_id, &invalid_function_name, args);
374
375        assert!(result.is_err());
376        assert!(matches!(
377            result,
378            Err(SorobanHelperError::InvalidArgument(_))
379        ));
380    }
381}