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(stellar_xdr::curr::ContractId(Hash(
209                contract_id.0,
210            ))),
211            function_name: ScSymbol(function_name.try_into().map_err(|e| {
212                SorobanHelperError::InvalidArgument(format!("Invalid function name: {}", e))
213            })?),
214            args: args.try_into().map_err(|e| {
215                SorobanHelperError::XdrEncodingFailed(format!("Failed to encode arguments: {}", e))
216            })?,
217        };
218
219        Ok(Operation {
220            source_account: None,
221            body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
222                host_function: HostFunction::InvokeContract(invoke_contract_args),
223                auth: VecM::default(),
224            }),
225        })
226    }
227
228    pub fn send_payment(
229        to: AccountId,
230        amount: i64,
231        asset: Asset,
232    ) -> Result<Operation, SorobanHelperError> {
233        Ok(Operation {
234            source_account: None,
235            body: OperationBody::Payment(PaymentOp {
236                amount,
237                destination: to.into(),
238                asset,
239            }),
240        })
241    }
242}
243
244#[cfg(test)]
245mod test {
246    use super::*;
247    use stellar_xdr::curr::{ContractIdPreimageFromAddress, PublicKey, ScVal};
248
249    #[test]
250    fn test_upload_wasm() {
251        let wasm_bytes = vec![0, 1, 2, 3, 4, 5];
252        let operation = Operations::upload_wasm(wasm_bytes.clone()).unwrap();
253
254        assert!(matches!(
255            operation.body,
256            OperationBody::InvokeHostFunction(_)
257        ));
258        if let OperationBody::InvokeHostFunction(op) = operation.body {
259            assert!(matches!(
260                op.host_function,
261                HostFunction::UploadContractWasm(_)
262            ));
263            assert_eq!(op.auth.len(), 0);
264        }
265    }
266
267    #[test]
268    fn test_create_contract_without_args() {
269        let account_id =
270            stellar_xdr::curr::AccountId(PublicKey::PublicKeyTypeEd25519([0; 32].into()));
271        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
272            address: ScAddress::Account(account_id),
273            salt: [1; 32].into(),
274        });
275
276        let wasm_hash = Hash([2; 32]);
277        let operation =
278            Operations::create_contract(contract_id_preimage.clone(), wasm_hash.clone(), None)
279                .unwrap();
280
281        assert!(matches!(
282            operation.body,
283            OperationBody::InvokeHostFunction(_)
284        ));
285        if let OperationBody::InvokeHostFunction(op) = operation.body {
286            assert!(matches!(op.host_function, HostFunction::CreateContract(_)));
287            assert_eq!(op.auth.len(), 1);
288
289            if let HostFunction::CreateContract(args) = op.host_function {
290                assert_eq!(args.contract_id_preimage, contract_id_preimage);
291                assert!(matches!(args.executable, ContractExecutable::Wasm(h) if h == wasm_hash));
292            }
293        }
294    }
295
296    #[test]
297    fn test_create_contract_with_args() {
298        let account_id =
299            stellar_xdr::curr::AccountId(PublicKey::PublicKeyTypeEd25519([0; 32].into()));
300        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
301            address: ScAddress::Account(account_id),
302            salt: [1; 32].into(),
303        });
304
305        let wasm_hash = Hash([2; 32]);
306        let constructor_args = vec![ScVal::I32(42), ScVal::Bool(true)];
307        let operation = Operations::create_contract(
308            contract_id_preimage.clone(),
309            wasm_hash.clone(),
310            Some(constructor_args.clone()),
311        )
312        .unwrap();
313
314        assert!(matches!(
315            operation.body,
316            OperationBody::InvokeHostFunction(_)
317        ));
318        if let OperationBody::InvokeHostFunction(op) = operation.body {
319            assert!(matches!(
320                op.host_function,
321                HostFunction::CreateContractV2(_)
322            ));
323            assert_eq!(op.auth.len(), 1);
324
325            if let HostFunction::CreateContractV2(args) = op.host_function {
326                assert_eq!(args.contract_id_preimage, contract_id_preimage);
327                assert!(matches!(args.executable, ContractExecutable::Wasm(h) if h == wasm_hash));
328
329                assert_eq!(args.constructor_args.len(), 2);
330                assert!(matches!(args.constructor_args[0], ScVal::I32(42)));
331                assert!(matches!(args.constructor_args[1], ScVal::Bool(true)));
332            }
333        }
334    }
335
336    #[test]
337    fn test_invoke_contract() {
338        let contract_bytes = [3; 32];
339        let contract_id = stellar_strkey::Contract(contract_bytes);
340
341        let function_name = "test_function";
342        let args = vec![ScVal::I32(42), ScVal::Bool(true)];
343        let operation =
344            Operations::invoke_contract(&contract_id, function_name, args.clone()).unwrap();
345
346        assert!(matches!(
347            operation.body,
348            OperationBody::InvokeHostFunction(_)
349        ));
350        if let OperationBody::InvokeHostFunction(op) = operation.body {
351            assert!(matches!(op.host_function, HostFunction::InvokeContract(_)));
352            assert_eq!(op.auth.len(), 0);
353
354            if let HostFunction::InvokeContract(args) = op.host_function {
355                assert!(
356                    matches!(args.contract_address, ScAddress::Contract(stellar_xdr::curr::ContractId(hash)) if hash.0 == contract_bytes)
357                );
358                assert_eq!(args.function_name.0.as_slice(), function_name.as_bytes());
359
360                assert_eq!(args.args.len(), 2);
361                assert!(matches!(args.args[0], ScVal::I32(42)));
362                assert!(matches!(args.args[1], ScVal::Bool(true)));
363            }
364        }
365    }
366
367    #[test]
368    fn test_invoke_contract_invalid_function_name() {
369        let contract_bytes = [3; 32];
370        let contract_id = stellar_strkey::Contract(contract_bytes);
371
372        let invalid_function_name = "a".repeat(33); // ScSymbol has a max length of 32
373        let args = vec![];
374
375        let result = Operations::invoke_contract(&contract_id, &invalid_function_name, args);
376
377        assert!(result.is_err());
378        assert!(matches!(
379            result,
380            Err(SorobanHelperError::InvalidArgument(_))
381        ));
382    }
383}