soroban_sdk/
testutils.rs

1#![cfg(any(test, feature = "testutils"))]
2#![cfg_attr(feature = "docs", doc(cfg(feature = "testutils")))]
3
4//! Utilities intended for use when testing.
5
6pub mod arbitrary;
7
8mod sign;
9use std::rc::Rc;
10
11pub use sign::ed25519;
12
13mod mock_auth;
14pub use mock_auth::{
15    AuthorizedFunction, AuthorizedInvocation, MockAuth, MockAuthContract, MockAuthInvoke,
16};
17use soroban_env_host::TryIntoVal;
18
19pub mod storage;
20
21pub mod cost_estimate;
22
23use crate::{xdr, ConstructorArgs, Env, Val, Vec};
24use soroban_ledger_snapshot::LedgerSnapshot;
25
26pub use crate::env::EnvTestConfig;
27
28pub trait Register {
29    fn register<'i, I, A>(self, env: &Env, id: I, args: A) -> crate::Address
30    where
31        I: Into<Option<&'i crate::Address>>,
32        A: ConstructorArgs;
33}
34
35impl<C> Register for C
36where
37    C: ContractFunctionSet + 'static,
38{
39    fn register<'i, I, A>(self, env: &Env, id: I, args: A) -> crate::Address
40    where
41        I: Into<Option<&'i crate::Address>>,
42        A: ConstructorArgs,
43    {
44        env.register_contract_with_constructor(id, self, args)
45    }
46}
47
48impl<'w> Register for &'w [u8] {
49    fn register<'i, I, A>(self, env: &Env, id: I, args: A) -> crate::Address
50    where
51        I: Into<Option<&'i crate::Address>>,
52        A: ConstructorArgs,
53    {
54        env.register_contract_wasm_with_constructor(id, self, args)
55    }
56}
57
58#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub struct Snapshot {
61    pub generators: Generators,
62    pub auth: AuthSnapshot,
63    pub ledger: LedgerSnapshot,
64    pub events: EventsSnapshot,
65}
66
67impl Snapshot {
68    // Read in a [`Snapshot`] from a reader.
69    pub fn read(r: impl std::io::Read) -> Result<Snapshot, std::io::Error> {
70        Ok(serde_json::from_reader::<_, Snapshot>(r)?)
71    }
72
73    // Read in a [`Snapshot`] from a file.
74    pub fn read_file(p: impl AsRef<std::path::Path>) -> Result<Snapshot, std::io::Error> {
75        Self::read(std::fs::File::open(p)?)
76    }
77
78    // Write a [`Snapshot`] to a writer.
79    pub fn write(&self, w: impl std::io::Write) -> Result<(), std::io::Error> {
80        Ok(serde_json::to_writer_pretty(w, self)?)
81    }
82
83    // Write a [`Snapshot`] to file.
84    pub fn write_file(&self, p: impl AsRef<std::path::Path>) -> Result<(), std::io::Error> {
85        let p = p.as_ref();
86        if let Some(dir) = p.parent() {
87            if !dir.exists() {
88                std::fs::create_dir_all(dir)?;
89            }
90        }
91        self.write(std::fs::File::create(p)?)
92    }
93}
94
95#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
96#[serde(rename_all = "snake_case")]
97pub struct EventsSnapshot(pub std::vec::Vec<EventSnapshot>);
98
99impl EventsSnapshot {
100    // Read in a [`EventsSnapshot`] from a reader.
101    pub fn read(r: impl std::io::Read) -> Result<EventsSnapshot, std::io::Error> {
102        Ok(serde_json::from_reader::<_, EventsSnapshot>(r)?)
103    }
104
105    // Read in a [`EventsSnapshot`] from a file.
106    pub fn read_file(p: impl AsRef<std::path::Path>) -> Result<EventsSnapshot, std::io::Error> {
107        Self::read(std::fs::File::open(p)?)
108    }
109
110    // Write a [`EventsSnapshot`] to a writer.
111    pub fn write(&self, w: impl std::io::Write) -> Result<(), std::io::Error> {
112        Ok(serde_json::to_writer_pretty(w, self)?)
113    }
114
115    // Write a [`EventsSnapshot`] to file.
116    pub fn write_file(&self, p: impl AsRef<std::path::Path>) -> Result<(), std::io::Error> {
117        let p = p.as_ref();
118        if let Some(dir) = p.parent() {
119            if !dir.exists() {
120                std::fs::create_dir_all(dir)?;
121            }
122        }
123        self.write(std::fs::File::create(p)?)
124    }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub struct EventSnapshot {
130    pub event: xdr::ContractEvent,
131    pub failed_call: bool,
132}
133
134impl From<crate::env::internal::events::HostEvent> for EventSnapshot {
135    fn from(v: crate::env::internal::events::HostEvent) -> Self {
136        Self {
137            event: v.event,
138            failed_call: v.failed_call,
139        }
140    }
141}
142
143#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
144#[serde(rename_all = "snake_case")]
145pub struct AuthSnapshot(
146    pub std::vec::Vec<std::vec::Vec<(xdr::ScAddress, xdr::SorobanAuthorizedInvocation)>>,
147);
148
149impl AuthSnapshot {
150    // Read in a [`AuthSnapshot`] from a reader.
151    pub fn read(r: impl std::io::Read) -> Result<AuthSnapshot, std::io::Error> {
152        Ok(serde_json::from_reader::<_, AuthSnapshot>(r)?)
153    }
154
155    // Read in a [`AuthSnapshot`] from a file.
156    pub fn read_file(p: impl AsRef<std::path::Path>) -> Result<AuthSnapshot, std::io::Error> {
157        Self::read(std::fs::File::open(p)?)
158    }
159
160    // Write a [`AuthSnapshot`] to a writer.
161    pub fn write(&self, w: impl std::io::Write) -> Result<(), std::io::Error> {
162        Ok(serde_json::to_writer_pretty(w, self)?)
163    }
164
165    // Write a [`AuthSnapshot`] to file.
166    pub fn write_file(&self, p: impl AsRef<std::path::Path>) -> Result<(), std::io::Error> {
167        let p = p.as_ref();
168        if let Some(dir) = p.parent() {
169            if !dir.exists() {
170                std::fs::create_dir_all(dir)?;
171            }
172        }
173        self.write(std::fs::File::create(p)?)
174    }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
178#[serde(rename_all = "snake_case")]
179pub struct Generators {
180    address: u64,
181    nonce: i64,
182    mux_id: u64,
183}
184
185impl Default for Generators {
186    fn default() -> Generators {
187        Generators {
188            address: 0,
189            nonce: 0,
190            mux_id: 0,
191        }
192    }
193}
194
195impl Generators {
196    // Read in a [`Generators`] from a reader.
197    pub fn read(r: impl std::io::Read) -> Result<Generators, std::io::Error> {
198        Ok(serde_json::from_reader::<_, Generators>(r)?)
199    }
200
201    // Read in a [`Generators`] from a file.
202    pub fn read_file(p: impl AsRef<std::path::Path>) -> Result<Generators, std::io::Error> {
203        Self::read(std::fs::File::open(p)?)
204    }
205
206    // Write a [`Generators`] to a writer.
207    pub fn write(&self, w: impl std::io::Write) -> Result<(), std::io::Error> {
208        Ok(serde_json::to_writer_pretty(w, self)?)
209    }
210
211    // Write a [`Generators`] to file.
212    pub fn write_file(&self, p: impl AsRef<std::path::Path>) -> Result<(), std::io::Error> {
213        let p = p.as_ref();
214        if let Some(dir) = p.parent() {
215            if !dir.exists() {
216                std::fs::create_dir_all(dir)?;
217            }
218        }
219        self.write(std::fs::File::create(p)?)
220    }
221}
222
223impl Generators {
224    pub fn address(&mut self) -> [u8; 32] {
225        self.address = self.address.checked_add(1).unwrap();
226        let b: [u8; 8] = self.address.to_be_bytes();
227        [
228            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, b[0], b[1],
229            b[2], b[3], b[4], b[5], b[6], b[7],
230        ]
231    }
232
233    pub fn nonce(&mut self) -> i64 {
234        self.nonce = self.nonce.checked_add(1).unwrap();
235        self.nonce
236    }
237
238    pub fn mux_id(&mut self) -> u64 {
239        self.mux_id = self.mux_id.checked_add(1).unwrap();
240        self.mux_id
241    }
242}
243
244#[doc(hidden)]
245pub type ContractFunctionF = dyn Send + Sync + Fn(Env, &[Val]) -> Val;
246#[doc(hidden)]
247pub trait ContractFunctionRegister {
248    fn register(name: &'static str, func: &'static ContractFunctionF);
249}
250#[doc(hidden)]
251pub trait ContractFunctionSet {
252    fn call(&self, func: &str, env: Env, args: &[Val]) -> Option<Val>;
253}
254
255#[doc(inline)]
256pub use crate::env::internal::LedgerInfo;
257
258/// Returns a default `LedgerInfo` suitable for testing.
259pub(crate) fn default_ledger_info() -> LedgerInfo {
260    LedgerInfo {
261        protocol_version: 23,
262        sequence_number: 0,
263        timestamp: 0,
264        network_id: [0; 32],
265        base_reserve: 0,
266        min_persistent_entry_ttl: 4096,
267        min_temp_entry_ttl: 16,
268        max_entry_ttl: 6_312_000,
269    }
270}
271
272/// Test utilities for [`Ledger`][crate::ledger::Ledger].
273pub trait Ledger {
274    /// Set ledger info.
275    fn set(&self, l: LedgerInfo);
276
277    /// Sets the protocol version.
278    fn set_protocol_version(&self, protocol_version: u32);
279
280    /// Sets the sequence number.
281    fn set_sequence_number(&self, sequence_number: u32);
282
283    /// Sets the timestamp.
284    fn set_timestamp(&self, timestamp: u64);
285
286    /// Sets the network ID.
287    fn set_network_id(&self, network_id: [u8; 32]);
288
289    /// Sets the base reserve.
290    fn set_base_reserve(&self, base_reserve: u32);
291
292    /// Sets the minimum temporary entry time-to-live.
293    fn set_min_temp_entry_ttl(&self, min_temp_entry_ttl: u32);
294
295    /// Sets the minimum persistent entry time-to-live.
296    fn set_min_persistent_entry_ttl(&self, min_persistent_entry_ttl: u32);
297
298    /// Sets the maximum entry time-to-live.
299    fn set_max_entry_ttl(&self, max_entry_ttl: u32);
300
301    /// Get ledger info.
302    fn get(&self) -> LedgerInfo;
303
304    /// Modify the ledger info.
305    fn with_mut<F>(&self, f: F)
306    where
307        F: FnMut(&mut LedgerInfo);
308}
309
310pub mod budget {
311    use core::fmt::{Debug, Display};
312
313    #[doc(inline)]
314    use crate::env::internal::budget::CostTracker;
315    #[doc(inline)]
316    pub use crate::xdr::ContractCostType;
317
318    /// Budget that tracks the resources consumed for the environment.
319    ///
320    /// The budget consistents of two cost dimensions:
321    ///  - CPU instructions
322    ///  - Memory
323    ///
324    /// Inputs feed into those cost dimensions.
325    ///
326    /// Note that all cost dimensions – CPU instructions, memory – and the VM
327    /// cost type inputs are likely to be underestimated when running Rust code
328    /// compared to running the WASM equivalent.
329    ///
330    /// ### Examples
331    ///
332    /// ```
333    /// use soroban_sdk::{Env, Symbol};
334    ///
335    /// # #[cfg(feature = "testutils")]
336    /// # fn main() {
337    /// #     let env = Env::default();
338    /// env.cost_estimate().budget().reset_default();
339    /// // ...
340    /// println!("{}", env.cost_estimate().budget());
341    /// # }
342    /// # #[cfg(not(feature = "testutils"))]
343    /// # fn main() { }
344    /// ```
345    pub struct Budget(pub(crate) crate::env::internal::budget::Budget);
346
347    impl Display for Budget {
348        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
349            writeln!(f, "{}", self.0)
350        }
351    }
352
353    impl Debug for Budget {
354        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
355            writeln!(f, "{:?}", self.0)
356        }
357    }
358
359    impl Budget {
360        pub(crate) fn new(b: crate::env::internal::budget::Budget) -> Self {
361            Self(b)
362        }
363
364        /// Reset the budget.
365        pub fn reset_default(&mut self) {
366            self.0.reset_default().unwrap();
367        }
368
369        pub fn reset_unlimited(&mut self) {
370            self.0.reset_unlimited().unwrap();
371        }
372
373        pub fn reset_limits(&mut self, cpu: u64, mem: u64) {
374            self.0.reset_limits(cpu, mem).unwrap();
375        }
376
377        pub fn reset_tracker(&mut self) {
378            self.0.reset_tracker().unwrap();
379        }
380
381        /// Returns the CPU instruction cost.
382        ///
383        /// Note that CPU instructions are likely to be underestimated when
384        /// running Rust code compared to running the WASM equivalent.
385        pub fn cpu_instruction_cost(&self) -> u64 {
386            self.0.get_cpu_insns_consumed().unwrap()
387        }
388
389        /// Returns the memory cost.
390        ///
391        /// Note that memory is likely to be underestimated when running Rust
392        /// code compared to running the WASM equivalent.
393        pub fn memory_bytes_cost(&self) -> u64 {
394            self.0.get_mem_bytes_consumed().unwrap()
395        }
396
397        /// Get the cost tracker associated with the cost type. The tracker
398        /// tracks the cumulative iterations and inputs and derived cpu and
399        /// memory. If the underlying model is a constant model, then inputs is
400        /// `None` and only iterations matter.
401        ///
402        /// Note that VM cost types are likely to be underestimated when running
403        /// natively as Rust code inside tests code compared to running the WASM
404        /// equivalent.
405        pub fn tracker(&self, cost_type: ContractCostType) -> CostTracker {
406            self.0.get_tracker(cost_type).unwrap()
407        }
408
409        /// Print the budget costs and inputs to stdout.
410        pub fn print(&self) {
411            println!("{}", self.0);
412        }
413    }
414}
415
416/// Test utilities for [`Events`][crate::events::Events].
417pub trait Events {
418    /// Returns all events that have been published by contracts.
419    ///
420    /// Returns a [`Vec`] of three element tuples containing:
421    /// - Contract ID
422    /// - Event Topics as a [`Vec<Val>`]
423    /// - Event Data as a [`Val`]
424    fn all(&self) -> Vec<(crate::Address, Vec<Val>, Val)>;
425}
426
427/// Test utilities for [`Logs`][crate::logs::Logs].
428pub trait Logs {
429    /// Returns all diagnostic events that have been logged.
430    fn all(&self) -> std::vec::Vec<String>;
431    /// Prints all diagnostic events to stdout.
432    fn print(&self);
433}
434
435/// Test utilities for [`BytesN`][crate::BytesN].
436pub trait BytesN<const N: usize> {
437    // Generate a BytesN filled with random bytes.
438    //
439    // The value filled is not cryptographically secure.
440    fn random(env: &Env) -> crate::BytesN<N>;
441}
442
443/// Generates an array of N random bytes.
444///
445/// The value returned is not cryptographically secure.
446pub(crate) fn random<const N: usize>() -> [u8; N] {
447    use rand::RngCore;
448    let mut arr = [0u8; N];
449    rand::thread_rng().fill_bytes(&mut arr);
450    arr
451}
452
453pub trait Address {
454    /// Generate a new Address.
455    ///
456    /// Implementation note: this always builds the contract addresses now. This
457    /// shouldn't normally matter though, as contracts should be agnostic to
458    /// the underlying Address value.
459    fn generate(env: &Env) -> crate::Address;
460}
461
462pub trait MuxedAddress {
463    /// Create a new MuxedAddress with arbitrary `Address` and id parts.
464    ///
465    /// Note, that since currently only accounts can be multiplexed, the
466    /// underlying `Address` will be an account (not contract) address.
467    fn generate(env: &Env) -> crate::MuxedAddress;
468
469    /// Returns a new `MuxedAddress` that has the same `Address` part as the
470    /// provided `address` and the provided multiplexing id.
471    ///
472    /// `address` can be either an `Address` or `MuxedAddress` and it has to
473    /// be an account (non-contract) address.
474    ///
475    /// Note on usage: the simplest way to test `MuxedAddress` is to generate
476    /// an arbitrary valid address with `MuxedAddress::generate`, then
477    /// `MuxedAddress::new` can be used to alter only the multiplexing id part
478    /// of that address.
479    fn new<T: Into<crate::MuxedAddress>>(address: T, id: u64) -> crate::MuxedAddress;
480}
481
482pub trait Deployer {
483    /// Gets the TTL of the given contract's instance.
484    ///
485    /// TTL is the number of ledgers left until the instance entry is considered
486    /// expired, excluding the current ledger.
487    ///
488    /// Panics if there is no instance corresponding to the provided address,
489    /// or if the instance has expired.
490    fn get_contract_instance_ttl(&self, contract: &crate::Address) -> u32;
491
492    /// Gets the TTL of the given contract's Wasm code entry.
493    ///
494    /// TTL is the number of ledgers left until the contract code entry
495    /// is considered expired, excluding the current ledger.
496    ///
497    /// Panics if there is no contract instance/code corresponding to
498    /// the provided address, or if the instance/code has expired.
499    fn get_contract_code_ttl(&self, contract: &crate::Address) -> u32;
500}
501
502pub use xdr::AccountFlags as IssuerFlags;
503
504#[derive(Clone)]
505pub struct StellarAssetIssuer {
506    env: Env,
507    account_id: xdr::AccountId,
508}
509
510impl StellarAssetIssuer {
511    pub(crate) fn new(env: Env, account_id: xdr::AccountId) -> Self {
512        Self { env, account_id }
513    }
514
515    /// Returns the flags for the issuer.
516    pub fn flags(&self) -> u32 {
517        let k = Rc::new(xdr::LedgerKey::Account(xdr::LedgerKeyAccount {
518            account_id: self.account_id.clone(),
519        }));
520
521        let (entry, _) = self.env.host().get_ledger_entry(&k).unwrap().unwrap();
522
523        match &entry.data {
524            xdr::LedgerEntryData::Account(e) => e.flags,
525            _ => panic!("expected account entry but got {:?}", entry.data),
526        }
527    }
528
529    /// Adds the flag specified to the existing issuer flags
530    pub fn set_flag(&self, flag: IssuerFlags) {
531        self.overwrite_issuer_flags(self.flags() | (flag as u32))
532    }
533
534    /// Clears the flag specified from the existing issuer flags
535    pub fn clear_flag(&self, flag: IssuerFlags) {
536        self.overwrite_issuer_flags(self.flags() & (!(flag as u32)))
537    }
538
539    pub fn address(&self) -> crate::Address {
540        xdr::ScAddress::Account(self.account_id.clone())
541            .try_into_val(&self.env.clone())
542            .unwrap()
543    }
544
545    /// Sets the issuer flags field.
546    /// Each flag is a bit with values corresponding to [xdr::AccountFlags]
547    ///
548    /// Use this to test interactions between trustlines/balances and the issuer flags.
549    fn overwrite_issuer_flags(&self, flags: u32) {
550        if u64::from(flags) > xdr::MASK_ACCOUNT_FLAGS_V17 {
551            panic!(
552                "issuer flags value must be at most {}",
553                xdr::MASK_ACCOUNT_FLAGS_V17
554            );
555        }
556
557        let k = Rc::new(xdr::LedgerKey::Account(xdr::LedgerKeyAccount {
558            account_id: self.account_id.clone(),
559        }));
560
561        let (entry, _) = self.env.host().get_ledger_entry(&k).unwrap().unwrap();
562        let mut entry = entry.as_ref().clone();
563
564        match entry.data {
565            xdr::LedgerEntryData::Account(ref mut e) => e.flags = flags,
566            _ => panic!("expected account entry but got {:?}", entry.data),
567        }
568
569        self.env
570            .host()
571            .add_ledger_entry(&k, &Rc::new(entry), None)
572            .unwrap();
573    }
574}
575
576pub struct StellarAssetContract {
577    address: crate::Address,
578    issuer: StellarAssetIssuer,
579    asset: xdr::Asset,
580}
581
582impl StellarAssetContract {
583    pub(crate) fn new(
584        address: crate::Address,
585        issuer: StellarAssetIssuer,
586        asset: xdr::Asset,
587    ) -> Self {
588        Self {
589            address,
590            issuer,
591            asset,
592        }
593    }
594
595    pub fn address(&self) -> crate::Address {
596        self.address.clone()
597    }
598
599    pub fn issuer(&self) -> StellarAssetIssuer {
600        self.issuer.clone()
601    }
602
603    #[doc(hidden)]
604    pub fn asset(&self) -> xdr::Asset {
605        self.asset.clone()
606    }
607}