stellar-baselib 0.6.0

A low level Rust library that offers a comprehensive set of functions for reading, writing, hashing, and signing primitive XDR constructs utilized in the Stellar network
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
use rand_core::{OsRng, RngCore as _, TryRngCore};

use crate::address::{Address, AddressTrait};
use crate::asset::{Asset, AssetBehavior};
use crate::keypair::{Keypair, KeypairBehavior};
use crate::operation;
use crate::operation::Operation;
use crate::utils::decode_encode_muxed_account::encode_muxed_account_to_address;
use crate::xdr;
use std::str::FromStr;

impl Operation {
    /// Invoke a stellar host function
    ///
    /// This is the low level function that requires a `HostFunction`. Helpers functions can
    /// be better suited to your needs:
    /// - [create_contract](Self::create_contract)
    /// - [wrap_asset](Self::wrap_asset)
    /// - [upload_wasm](Self::upload_wasm)
    /// - [invoke_contract](Self::invoke_contract)
    /// - [Contracts::call](crate::contract::ContractBehavior::call)
    pub fn invoke_host_function(
        &self,
        func: xdr::HostFunction,
        auth: Option<Vec<xdr::SorobanAuthorizationEntry>>,
    ) -> Result<xdr::Operation, operation::Error> {
        let auth_arr = auth.unwrap_or_default().try_into().unwrap_or_default();

        let invoke_host_function_op = xdr::InvokeHostFunctionOp {
            host_function: func,
            auth: auth_arr,
        };

        let op_body = xdr::OperationBody::InvokeHostFunction(invoke_host_function_op);

        Ok(xdr::Operation {
            source_account: self.source.clone(),
            body: op_body,
        })
    }

    /// Invokes the contract `method` with its `args`
    pub fn invoke_contract(
        &self,
        contract_id: &str,
        method: &str,
        args: Vec<xdr::ScVal>,
        auth: Option<Vec<xdr::SorobanAuthorizationEntry>>,
    ) -> Result<xdr::Operation, operation::Error> {
        let contract_address = Address::from_string(contract_id)
            .map_err(|_| operation::Error::InvalidField("contract_id".into()))?
            .to_sc_address()
            .map_err(|_| operation::Error::InvalidField("contract_id".into()))?;

        let function_name = xdr::ScSymbol(
            method
                .try_into()
                .map_err(|_| operation::Error::InvalidField("method".into()))?,
        );

        let args = args
            .try_into()
            .map_err(|_| operation::Error::InvalidField("args".into()))?;

        let func = xdr::HostFunction::InvokeContract(xdr::InvokeContractArgs {
            contract_address,
            function_name,
            args,
        });

        self.invoke_host_function(func, auth)
    }

    /// Create a new contract for the `wasm_hash`.
    ///
    /// The `salt` and `deployer` are used to computed the contract_id pre-image of the newly
    /// created contract.
    ///
    /// If the contract has a `__constructor` methods, you can provide the `constructor_args`,
    /// this constructor will be invoked during the contract creation.
    pub fn create_contract(
        &self,
        deployer: &str,
        wasm_hash: [u8; 32],
        salt: Option<[u8; 32]>,
        auth: Option<Vec<xdr::SorobanAuthorizationEntry>>,
        constructor_args: Vec<xdr::ScVal>,
    ) -> Result<xdr::Operation, operation::Error> {
        let salt = match salt {
            Some(s) => xdr::Uint256(s),
            _ => xdr::Uint256(Self::get_salty()),
        };

        let address = Address::from_string(deployer)
            .map_err(|_| operation::Error::InvalidField("deployer".into()))?
            .to_sc_address()
            .map_err(|_| operation::Error::InvalidField("deployer".into()))?;

        let constructor_args: xdr::VecM<xdr::ScVal> = constructor_args
            .try_into()
            .map_err(|_| operation::Error::InvalidField("constructor_args".into()))?;

        let func = xdr::HostFunction::CreateContractV2(xdr::CreateContractArgsV2 {
            contract_id_preimage: xdr::ContractIdPreimage::Address(
                xdr::ContractIdPreimageFromAddress { address, salt },
            ),
            executable: xdr::ContractExecutable::Wasm(xdr::Hash(wasm_hash)),
            constructor_args,
        });

        self.invoke_host_function(func, auth)
    }

    /// Create a new contract from a CAP-85 external contract executable reference.
    ///
    /// Instead of a wasm hash, the new contract instance points to an executable owned by
    /// another contract: `executable_owner` (a `C...` contract address) publishes the wasm
    /// hash in its persistent storage under the `tag` entry. The `tag` is binary data
    /// identifying the code being deployed; it is passed through undecoded and does not
    /// need to be valid UTF-8.
    ///
    /// The `salt` and `deployer` are used to compute the contract_id pre-image of the newly
    /// created contract, exactly as in [create_contract](Self::create_contract).
    ///
    /// If the contract has a `__constructor` method, you can provide the `constructor_args`,
    /// this constructor will be invoked during the contract creation.
    ///
    /// Requires Protocol 28.
    pub fn create_contract_from_external_ref(
        &self,
        deployer: &str,
        executable_owner: &str,
        tag: &[u8],
        salt: Option<[u8; 32]>,
        auth: Option<Vec<xdr::SorobanAuthorizationEntry>>,
        constructor_args: Vec<xdr::ScVal>,
    ) -> Result<xdr::Operation, operation::Error> {
        let salt = match salt {
            Some(s) => xdr::Uint256(s),
            _ => xdr::Uint256(Self::get_salty()),
        };

        let address = Address::from_string(deployer)
            .map_err(|_| operation::Error::InvalidField("deployer".into()))?
            .to_sc_address()
            .map_err(|_| operation::Error::InvalidField("deployer".into()))?;

        // Only a contract can hold the tag entry that names the wasm, reject
        // any other kind of address early.
        let owner = Address::from_string(executable_owner)
            .map_err(|_| operation::Error::InvalidField("executable_owner".into()))?
            .to_sc_address()
            .map_err(|_| operation::Error::InvalidField("executable_owner".into()))?;
        if !matches!(owner, xdr::ScAddress::Contract(_)) {
            return Err(operation::Error::InvalidField("executable_owner".into()));
        }

        let tag = xdr::ScString(
            tag.to_vec()
                .try_into()
                .map_err(|_| operation::Error::InvalidField("tag".into()))?,
        );

        let constructor_args: xdr::VecM<xdr::ScVal> = constructor_args
            .try_into()
            .map_err(|_| operation::Error::InvalidField("constructor_args".into()))?;

        let func = xdr::HostFunction::CreateContractV2(xdr::CreateContractArgsV2 {
            contract_id_preimage: xdr::ContractIdPreimage::Address(
                xdr::ContractIdPreimageFromAddress { address, salt },
            ),
            executable: xdr::ContractExecutable::ExternalRef(xdr::ContractExecutableExternalRef {
                executable_owner: owner,
                tag,
            }),
            constructor_args,
        });

        self.invoke_host_function(func, auth)
    }

    /// Create a Stellar Asset Contract for the [Asset], this wraps a classic Stellar asset in
    /// Soroban.
    pub fn wrap_asset(
        &self,
        asset: &Asset,
        auth: Option<Vec<xdr::SorobanAuthorizationEntry>>,
    ) -> Result<xdr::Operation, operation::Error> {
        let func = xdr::HostFunction::CreateContract(xdr::CreateContractArgs {
            contract_id_preimage: xdr::ContractIdPreimage::Asset(asset.to_xdr_object()),
            executable: xdr::ContractExecutable::StellarAsset,
        });

        self.invoke_host_function(func, auth)
    }

    /// Upload the `wasm` executable.
    ///
    /// The executable can be used to deploy a new contract using
    /// [create_contract](Self::create_contract).
    pub fn upload_wasm(
        &self,
        wasm: &[u8],
        auth: Option<Vec<xdr::SorobanAuthorizationEntry>>,
    ) -> Result<xdr::Operation, operation::Error> {
        let bytes = wasm
            .to_vec()
            .try_into()
            .map_err(|_| operation::Error::InvalidField("wasm".into()))?;
        let func = xdr::HostFunction::UploadContractWasm(bytes);
        self.invoke_host_function(func, auth)
    }

    fn get_salty() -> [u8; 32] {
        let mut salt = [0u8; 32];
        let mut rng = OsRng;
        rng.try_fill_bytes(&mut salt);
        salt
    }
}

#[cfg(test)]
mod tests {
    use sha2::digest::crypto_common::Key;
    use stellar_strkey::Strkey;

    use crate::contract::ContractBehavior;
    use crate::contract::Contracts;
    use crate::xdr::WriteXdr;

    use super::*;

    #[test]
    fn test_invoke_host_function() {
        let contract_id = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE";
        let id = if let Strkey::Contract(stellar_strkey::Contract(id)) =
            Strkey::from_str(contract_id).unwrap()
        {
            id
        } else {
            panic!("Fail")
        };

        let func = xdr::HostFunction::InvokeContract(xdr::InvokeContractArgs {
            contract_address: xdr::ScAddress::Contract(xdr::ContractId(xdr::Hash::from(id))),
            function_name: xdr::ScSymbol::from(xdr::StringM::from_str("hello").unwrap()),
            args: vec![xdr::ScVal::String(xdr::ScString::from(
                xdr::StringM::from_str("world").unwrap(),
            ))]
            .try_into()
            .unwrap(),
        });

        let op = Operation::new()
            .invoke_host_function(func.clone(), None)
            .unwrap();

        if let xdr::OperationBody::InvokeHostFunction(f) = op.body {
            assert_eq!(f.host_function, func);
            if let xdr::HostFunction::InvokeContract(xdr::InvokeContractArgs {
                contract_address,
                function_name,
                args,
            }) = f.host_function
            {
                if let xdr::ScAddress::Contract(xdr::ContractId(xdr::Hash(cid))) = contract_address
                {
                    assert_eq!(cid, id);
                } else {
                    panic!("Fail")
                }
            }
        }
    }

    #[test]
    fn test_invoke_contract() {
        let contract_id = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE";

        let contract = Contracts::new(contract_id).unwrap();

        let op = Operation::new()
            .invoke_contract(contract_id, "call_me", [].into(), None)
            .unwrap();

        let cop = contract.call("call_me", None);
        assert_eq!(op, cop);

        if let xdr::OperationBody::InvokeHostFunction(xdr::InvokeHostFunctionOp {
            host_function:
                xdr::HostFunction::InvokeContract(xdr::InvokeContractArgs {
                    contract_address,
                    function_name,
                    args,
                }),
            auth,
        }) = op.body
        {
            let exp_contract_address = xdr::ScAddress::from_str(contract_id).unwrap();
            assert_eq!(contract_address, exp_contract_address);

            let exp_fname = xdr::ScSymbol("call_me".try_into().unwrap());
            assert_eq!(function_name, exp_fname);

            return;
        }
        panic!("Fail")
    }

    #[test]
    fn test_invoke_contract_bad_contract_id() {
        let contract_id = "GA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE";

        let op = Operation::new().invoke_contract(contract_id, "call_me", [].into(), None);

        assert_eq!(
            op.err(),
            Some(operation::Error::InvalidField("contract_id".into()))
        );
    }
    #[test]
    fn test_invoke_contract_bad_method() {
        let contract_id = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE";

        let op = Operation::new().invoke_contract(
            contract_id,
            "call_me_but_this_is_a_too_long_method",
            [].into(),
            None,
        );

        assert_eq!(
            op.err(),
            Some(operation::Error::InvalidField("method".into()))
        );
    }

    #[test]
    fn test_create_contract() {
        let deployer = Keypair::random().unwrap().public_key();
        let wasm_hash = [0; 32];
        let salt = Keypair::random().unwrap().raw_pubkey();
        let op = Operation::new()
            .create_contract(&deployer, wasm_hash, Some(salt), None, [].into())
            .unwrap();

        if let xdr::OperationBody::InvokeHostFunction(xdr::InvokeHostFunctionOp {
            host_function:
                xdr::HostFunction::CreateContractV2(xdr::CreateContractArgsV2 {
                    contract_id_preimage:
                        xdr::ContractIdPreimage::Address(xdr::ContractIdPreimageFromAddress {
                            address,
                            salt: actual_salt,
                        }),
                    executable,
                    constructor_args,
                }),
            auth,
        }) = op.body
        {
            assert_eq!(address, xdr::ScAddress::from_str(&deployer).unwrap());
            assert_eq!(actual_salt, xdr::Uint256(salt));
            assert_eq!(
                executable,
                xdr::ContractExecutable::Wasm(xdr::Hash(wasm_hash))
            );
            //
            return;
        }
        panic!("Fail")
    }
    #[test]
    fn test_create_contract_default_salt() {
        let deployer = Keypair::random().unwrap().public_key();
        let wasm_hash = [0; 32];
        let op = Operation::new()
            .create_contract(&deployer, wasm_hash, None, None, [].into())
            .unwrap();

        if let xdr::OperationBody::InvokeHostFunction(xdr::InvokeHostFunctionOp {
            host_function:
                xdr::HostFunction::CreateContractV2(xdr::CreateContractArgsV2 {
                    contract_id_preimage:
                        xdr::ContractIdPreimage::Address(xdr::ContractIdPreimageFromAddress {
                            address,
                            salt: actual_salt,
                        }),
                    executable,
                    constructor_args,
                }),
            auth,
        }) = op.body
        {
            assert_eq!(address, xdr::ScAddress::from_str(&deployer).unwrap());
            assert_ne!(actual_salt, xdr::Uint256([0; 32]));
            assert_eq!(
                executable,
                xdr::ContractExecutable::Wasm(xdr::Hash(wasm_hash))
            );
            //
            return;
        }
        panic!("Fail")
    }

    #[test]
    fn test_create_contract_from_external_ref() {
        let deployer = Keypair::random().unwrap().public_key();
        let owner = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE";
        // A binary, non-UTF-8 tag must pass through undecoded.
        let tag = [0x00u8, 0xffu8, 0x80u8, 0x01u8];
        let salt = Keypair::random().unwrap().raw_pubkey();

        let op = Operation::new()
            .create_contract_from_external_ref(&deployer, owner, &tag, Some(salt), None, [].into())
            .unwrap();

        if let xdr::OperationBody::InvokeHostFunction(xdr::InvokeHostFunctionOp {
            host_function:
                xdr::HostFunction::CreateContractV2(xdr::CreateContractArgsV2 {
                    contract_id_preimage:
                        xdr::ContractIdPreimage::Address(xdr::ContractIdPreimageFromAddress {
                            address,
                            salt: actual_salt,
                        }),
                    executable: xdr::ContractExecutable::ExternalRef(ext_ref),
                    ..
                }),
            ..
        }) = op.body
        {
            assert_eq!(address, xdr::ScAddress::from_str(&deployer).unwrap());
            assert_eq!(actual_salt, xdr::Uint256(salt));
            assert_eq!(ext_ref.executable_owner, xdr::ScAddress::from_str(owner).unwrap());
            assert_eq!(ext_ref.tag.0.as_slice(), &tag);
            return;
        }
        panic!("Fail")
    }

    #[test]
    fn test_create_contract_from_external_ref_rejects_account_owner() {
        let deployer = Keypair::random().unwrap().public_key();
        // An account (G...) cannot own an external executable.
        let owner = Keypair::random().unwrap().public_key();

        let op = Operation::new().create_contract_from_external_ref(
            &deployer,
            &owner,
            b"tag",
            None,
            None,
            [].into(),
        );

        assert_eq!(
            op.err(),
            Some(operation::Error::InvalidField("executable_owner".into()))
        );
    }

    #[test]
    fn test_create_contract_bad_deployer() {
        let deployer = Keypair::random().unwrap().public_key().replace("G", "M");
        let wasm_hash = Keypair::random().unwrap().raw_pubkey();
        let op = Operation::new().create_contract(&deployer, wasm_hash, None, None, [].into());

        assert_eq!(
            op.err(),
            Some(operation::Error::InvalidField("deployer".into()))
        );
    }

    #[test]
    fn test_wrap_asset() {
        let native = Asset::native();

        let op = Operation::new().wrap_asset(&native, None).unwrap();
        if let xdr::OperationBody::InvokeHostFunction(xdr::InvokeHostFunctionOp {
            host_function:
                xdr::HostFunction::CreateContract(xdr::CreateContractArgs {
                    contract_id_preimage: xdr::ContractIdPreimage::Asset(asset),
                    executable: xdr::ContractExecutable::StellarAsset,
                }),
            auth,
        }) = op.body
        {
            assert_eq!(native.to_xdr_object(), asset);
            //
            return;
        }
        panic!("Fail")
    }

    #[test]
    fn test_upload_wasm() {
        let wasm = [0; 420];
        let op = Operation::new().upload_wasm(&wasm, None).unwrap();

        if let xdr::OperationBody::InvokeHostFunction(xdr::InvokeHostFunctionOp {
            host_function: xdr::HostFunction::UploadContractWasm(bytes),
            auth,
        }) = op.body
        {
            assert_eq!(bytes.as_slice(), &wasm);
            //
            return;
        }
        panic!("Fail")
    }
}