TON Sandbox

repository·master·Indexed 42 days ago

https://github.com/ton-org/sandbox

A high-fidelity TON smart contract emulation environment for realistic testing of compute, fees, and balances. Version 0.45.0 provides tools for implementing contract wrappers, simulating network interactions via RemoteBlockchainStorage, testing cross-contract interactions, and performing gas benchmarking and code coverage analysis.

Tokens
23.6K
Snippets
75
Records
94
Agent score
53%

What's inside Sandbox

  1. Understand the UI Protocol message structure

    master

    The UI Protocol uses a specific message format to transmit test results and blockchain state. The primary message type is test-data, which aggregates information about the current test, the transactions that occurred, and the state of the contracts involved.

    {
      "type": "test-data",
      "testInfo": TestInfo | undefined,
      "transactions": RawTransactionsInfo,
      "contracts": RawContractData[]
    }
  2. Emulate real network interactions with RemoteBlockchainStorage

    master
    Use RemoteBlockchainStorage to emulate interactions with contracts that are located on real TON networks (Mainnet or Testnet) within your Sandbox environment. This allows you to test how your local sandbox setup interacts with existing, live contracts without needing to deploy them locally.
  3. How contract wrappers and the 'open' pattern work

    master

    The recommended way to interact with contracts in Sandbox is to implement the Contract interface from @ton/core and then use blockchain.openContract to create a proxy wrapper.

    The Proxy Wrapper Behavior

    When you "open" a contract instance using blockchain.openContract(contractInstance):

    1. Automatic Provider Injection: All methods starting with get or send will automatically receive a ContractProvider as their first argument. You do not need to pass it manually when calling methods on the opened instance.
    2. Promisified send methods: Methods starting with send are promisified. They return an object containing the original method's result in a result field, along with the results of running the blockchain message queue.
    3. Direct get methods: Methods starting with get return the original result as-is to the caller.

    Implementation Requirements

    • Method Signatures: Every get or send method in your contract class MUST accept provider: ContractProvider as the first argument, even if the method body does not use it. This is required for the proxy wrapper to function.
    • Method Constraints:
      • Inside a send method, ideally make at most one call to provider.internal or provider.external.
      • Never make calls to provider.external or provider.internal inside a get method; doing so will cause incorrect results in subsequent send calls.
    // Example of a contract implementation compatible with Sandbox wrappers
    import { Address, beginCell, Cell, Contract, ContractProvider, Sender, toNano, Builder } from "@ton/core";
    
    class NftItem implements Contract {
        // Must accept provider as first argument
        async sendTransfer(provider: ContractProvider, via: Sender, params: { 
            value?: bigint, 
            to: Address 
        }) {
            await provider.internal(via, {
                value: params.value ?? toNano('0.05'),
                body: beginCell().storeUint(0x5fcc3d14, 32).endCell()
            });
        }
    
        // Must accept provider as first argument
        async getData(provider: ContractProvider) {
            const { stack } = await provider.get('get_nft_data', []);
            return stack.readBoolean();
        }
    }
    
    // Usage in a test
    const nftItem = await blockchain.openContract(new NftItem(address, init));
    await nftItem.sendTransfer(treasury.getSender(), { to: recipient }); // provider is injected automatically
  4. Test cross-contract interactions

    master

    Sandbox emulates cross-contract interactions. When a message is sent, the result contains information about all resulting transactions. You can verify complex flows (like a minting operation that creates a new wallet and returns excess funds) by chaining multiple toHaveTransaction expectations on the same result object.

    const mintResult = await jettonMinter.sendMint(deployer.getSender(), deployer.address, initialJettonBalance, toNano('0.05'), toNano('1'));
    
    // Test deployment of the jetton wallet
    expect(mintResult.transactions).toHaveTransaction({
        from: jettonMinter.address,
        to: deployerJettonWallet.address,
        deploy: true,
    });
    
    // Test excess funds returned to minter
    expect(mintResult.transactions).toHaveTransaction({
        from: deployerJettonWallet.address,
        to: jettonMinter.address
    });
  5. Use Blockchain snapshots to save and restore state

    master

    You can capture the entire state of a Blockchain instance (including all contracts, network config, transaction lt, unix timestamp, verbosity, and libraries) and restore it later. This is useful for resetting tests to a known configuration point.

    Basic usage:

    const snapshot = blockchain.snapshot();
    // ... later ...
    await blockchain.loadFrom(snapshot);

    Serializing snapshots for disk/network storage: Since snapshots contain Cells and Buffers which are not JSON-safe, use snapshotToSerializable and snapshotFromSerializable to convert them to plain JSON objects.

    import { snapshotToSerializable, snapshotFromSerializable } from './BlockchainSnapshot';
    
    // Save to disk
    const serializable = snapshotToSerializable(blockchain.snapshot());
    fs.writeFileSync('snapshot.json', JSON.stringify(serializable));
    
    // Restore from disk
    const snapshotJson = JSON.parse(fs.readFileSync('snapshot.json', 'utf8'));
    await blockchain.loadFrom(snapshotFromSerializable(snapshotJson));
    import { snapshotToSerializable, snapshotFromSerializable } from './BlockchainSnapshot';
    
    // Capture and serialize
    const serializable = snapshotToSerializable(blockchain.snapshot());
    
    // Restore from serialized object
    const restored = snapshotFromSerializable(serializable);
    await blockchain.loadFrom(restored);
  6. Enable code coverage collection in Sandbox

    master

    To collect coverage data during your smart contract tests, you must call blockchain.enableCoverage() on your Blockchain instance. This should be done within a beforeEach hook after creating the blockchain instance but before deploying your contracts.

    You can also enable coverage conditionally using an environment variable:

    blockchain.enableCoverage(process.env["COVERAGE"] === "true");
    import {Blockchain} from '@ton/sandbox';
    
    describe('Contract Tests', () => {
        let blockchain: Blockchain;
        let contract: SandboxContract<MyContract>;
    
        beforeEach(async () => {
            blockchain = await Blockchain.create();
    
            blockchain.enableCoverage();
    
            // Deploy your contract
            contract = blockchain.openContract(MyContract.fromInit());
        });
    });
  7. Install @ton/test-utils for enhanced testing

    master

    To add advanced testing helpers like .toHaveTransaction (for Jest) or .transaction / .to.have.transaction (for Chai), install the @ton/test-utils package. Remember to import the package in your unit test files to enable the matchers.

    Using npm:

    npm i --save-dev @ton/test-utils

    Using yarn:

    yarn add @ton/test-utils -D
    npm i --save-dev @ton/test-utils
  8. Configure Sandbox randomness and deterministic behavior

    master

    In the Sandbox, randomness is deterministic. The same randomSeed will always produce the same sequence of random numbers.

    To provide a specific randomSeed when calling a Get Method:

    const res = await blockchain.runGetMethod(example.address,
            'get_method',
            [],
            { randomSeed: randomBytes(32) }
    );

    To set a global randomSeed for all opened contracts in the blockchain instance, use the blockchain.random setter:

    blockchain.random = randomBytes(32);
    const res = await blockchain.runGetMethod(example.address,
            'get_method',
            [],
            { randomSeed: randomBytes(32) }
    );
    
    // Or globally:
    blockchain.random = randomBytes(32);
  9. Assert on specific transaction fee components

    master

    To perform granular assertions on fees, access the description property of a transaction. You can extract computePhase gas fees and actionPhase total action fees to ensure your contract operates within specific budget constraints.

    const tx1 = launchResult.transactions[1];
    
    // Access compute fees (requires checking if type is 'vm')
    const computeFee = tx1.description.computePhase.type === 'vm' 
        ? tx1.description.computePhase.gasFees 
        : undefined;
    
    // Access action fees
    const actionFee = tx1.description.actionPhase?.totalActionFees;
    
    expect(computeFee + actionFee).toBeLessThan(toNano('1'));
  10. Perform tests on contracts from a real network

    master

    You can use Sandbox to test against contracts deployed on a real network (e.g., mainnet) by using RemoteBlockchainStorage. When the Blockchain instance encounters an unknown contract, it will pull its state from the remote network.

    Note: This only applies to unknown contracts. If a contract's state has already been set locally (via creation, manual setting, or previous retrieval), it will not be re-read from the network.

    import { TonClient4 } from '@ton/ton'
    import { Blockchain, RemoteBlockchainStorage, wrapTonClient4ForRemote } from '@ton/sandbox'
    import { getHttpV4Endpoint } from '@orbs-network/ton-access'
    
    const blockchain = await Blockchain.create({
        storage: new RemoteBlockchainStorage(wrapTonClient4ForRemote(new TonClient4({
            endpoint: await getHttpV4Endpoint({
                network: 'mainnet'
            })
        })))
    })

    To pull state from a specific block number instead of the latest, pass blockSeqno as the second argument to the RemoteBlockchainStorage constructor.

  11. Implement smart contract wrappers

    master
    The examples/contracts directory provides reference implementations for smart contract wrappers. Developers can use these examples as a template or inspiration to write their own custom wrappers for TON smart contracts, ensuring they integrate correctly with the Sandbox testing environment.
  12. Generate coverage reports (HTML and Text)

    master

    After your tests have run, you can retrieve the coverage data for a specific contract using blockchain.coverage(contract). Once you have the coverage object, you can generate different types of reports using the .report(type) method.

    Supported report types:

    • "html": Generates an interactive HTML report with line-by-line details.
    • "text": Generates a console-friendly text report.

    Always check if the coverage object exists before attempting to generate a report.

    import {writeFileSync} from 'fs';
    
    afterAll(() => {
        const coverage = blockchain.coverage(contract);
        if (!coverage) return;
        
        // Generate HTML report for detailed analysis
        const htmlReport = coverage.report("html");
        writeFileSync("coverage.html", htmlReport);
    
        // Print text report to console
        const textReport = coverage.report("text");
        console.log(textReport);
    });