Git Product home page Git Product logo

ton-contract-executor's Introduction

TON Contract Executor GitHub license npm version

This library allows you to run TON Virtual Machine locally and execute contract. That allows you to write & debug & fully test your contracts before launching them to the network.

Features

TON Contract executor allows you to:

  • execute smart contracts from existing code and data Cells
  • get TVM execution logs
  • debug your contracts via debug primitives
  • seamlessly handle internal state changes of contract data and code
  • call so-called get methods of smart contracts
  • send and debug internal and external messages
  • debug messages sent by smart contract
  • manipulate the C7 register of the smart contract (including time, random seed, network config, etc.)
  • make some gas optimizations

Basically you can develop, debug, and fully cover your contract with unit-tests fully locally without deploying it to the network

Installation

yarn add ton-contract-executor

How it works

This package internally uses original TVM which runs on actual validator nodes to execute smart contracts. TVM is built to WASM so this library could be used on any platform. We also added some layer of abstraction on top of original TVM to allow it to run contracts via JSON configuration (those changes could be found here)

Usage

Usage is pretty straightforward: first of all, you should create an instance of SmartContract. You could think of SmartContract as an existing deployed smart contract with which you can communicate.

Creating SmartContract from FunC source code (here the @ton-community/func-js package is used for compilation):

import { compileFunc } from "@ton-community/func-js";
import { SmartContract } from "ton-contract-executor";
import { Cell } from "@ton/core";

async function main() {
    const source = `
    () main() {
        ;; noop
    }

    int sum(int a, int b) method_id {
        return a + b;
    }
`

    const compileResult = await compileFunc({
        sources: {
            'contract.fc': source,
        },
        entryPoints: ['contract.fc'],
    })

    if (compileResult.status === 'error') throw new Error('compilation failed')

    const contract = await SmartContract.fromCell(
        Cell.fromBoc(Buffer.from(compileResult.codeBoc, 'base64'))[0],
        new Cell(),
    )
}

In some cases it's useful to create SmartContract from existing precompiled code Cell & data Cell. For example if you need to debug some existing contract from network.

Here is an example of creating a local copy of existing wallet smart contract from the network deployed at EQD4FPq-PRDieyQKkizFTRtSDyucUIqrj0v_zXJmqaDp6_0t address and getting its seq:

import {Address, Cell, TonClient} from "@ton/core";
import {SmartContract} from "ton-contract-executor";

const contractAddress = Address.parse('EQD4FPq-PRDieyQKkizFTRtSDyucUIqrj0v_zXJmqaDp6_0t')

let client = new TonClient({
    endpoint: 'https://toncenter.com/api/v2/jsonRPC'
})

async function main() {
    let state = await client.getContractState(contractAddress)

    let code = Cell.fromBoc(state.code!)[0]
    let data = Cell.fromBoc(state.data!)[0]

    let wallet = await SmartContract.fromCell(code, data)

    let res = await wallet.invokeGetMethod('seqno', [])
    console.log('Wallet seq is: ', res.result[0])
}

Interacting with contract

Once you have created instance of SmartContract you can start interacting with it.

Invoking get methods

You can invoke any get method on contract using invokeGetMethod function:

import { SmartContract, stackInt } from "ton-contract-executor";
import { Cell } from "@ton/core";

async function main() {
    const source = `
    () main() {
        ;; noop
    }

    int sum(int a, int b) method_id {
        return a + b;
    }
`

    const compileResult = await compileFunc({
        sources: {
            'contract.fc': source,
        },
        entryPoints: ['contract.fc'],
    })

    if (compileResult.status === 'error') throw new Error('compilation failed')

    const contract = await SmartContract.fromCell(
        Cell.fromBoc(Buffer.from(compileResult.codeBoc, 'base64'))[0],
        new Cell(),
    )
    
    const res = await contract.invokeGetMethod('sum', [
        // argument a
        stackInt(1),
        // argument b
        stackInt(2),
    ])
    
    console.log('1 + 2 = ', res.result[0])
}

You can create arguments of other types for get methods using exported functions stackInt, stackCell, stackSlice, stackTuple and stackNull.

Sending messages to contract

You can send both external and internal messages to your contract by calling sendMessage:

import { SmartContract, internal } from "ton-contract-executor";
import { Cell } from "@ton/core";

async function main() {
    const contract = await SmartContract.fromCell(
        Cell.fromBoc(Buffer.from(compileResult.codeBoc, 'base64'))[0],
        new Cell(),
    )
    
    const msgBody = new Cell()
    
    const res = await this.contract.sendInternalMessage(internal({
        dest: contractAddress,
        value: 1n, // 1 nanoton
        bounce: false,
        body: msgBody,
    }))
}

ton-contract-executor exports two helpers, internal and externalIn to help you create the necessary Message objects.

There are two aliases for sendMessage - sendInternalMessage and sendExternalMessage, but they only check that the type of the provided Message is internal or external-in respectively, otherwise their behavior is the same as sendMessage.

Setting gas limits

invokeGetMethod, sendMessage, sendInternalMessage, sendExternalMessage all support last optional opts?: { gasLimits?: GasLimits; } argument for setting gas limits. As an example, the following code

import { compileFunc } from "@ton-community/func-js";
import { SmartContract, stackInt } from "ton-contract-executor";
import { Cell } from "@ton/core";

async function main() {
    const source = `
    () main() {
        ;; noop
    }

    int sum(int a, int b) method_id {
        return a + b;
    }
`

    const compileResult = await compileFunc({
        sources: {
            'contract.fc': source,
        },
        entryPoints: ['contract.fc'],
    })

    if (compileResult.status === 'error') throw new Error('compilation failed')

    let contract = await SmartContract.fromCell(
        Cell.fromBoc(Buffer.from(compileResult.codeBoc, 'base64'))[0],
        new Cell(),
    )

    console.log(await contract.invokeGetMethod('sum', [
        stackInt(1),
        stackInt(2),
    ], {
        gasLimits: {
            limit: 308,
        },
    }))
}

will output a failed execution result to console, because such a call requires 309 gas.

Execution result

As the result of calling sendMessage, sendInternalMessage, sendExternalMessage or invokeGetMethod, an ExecutionResult object is returned.

ExecutionResult could be either successful or failed:

declare type FailedExecutionResult = {
    type: 'failed';
    exit_code: number;
    gas_consumed: number;
    result: NormalizedStackEntry[];
    actionList: OutAction[];
    action_list_cell?: Cell;
    logs: string;
};
declare type SuccessfulExecutionResult = {
    type: 'success';
    exit_code: number;
    gas_consumed: number;
    result: NormalizedStackEntry[];
    actionList: OutAction[];
    action_list_cell?: Cell;
    logs: string;
};
declare type ExecutionResult = FailedExecutionResult | SuccessfulExecutionResult;

What is what:

  • exit_code: exit code of TVM
  • result: resulting stack (basically the result of function in case of get methods)
  • gas_consumed: consumed gas amount
  • actionList (list of output actions of smart contract, such as messages )
  • action_list_cell: raw cell with serialized action list
  • logs: logs of TVM

Configuration of SmartContract

You also can configure some parameters of your smart contract:

fromCell accepts configuration object as third parameter:

type SmartContractConfig = {
    getMethodsMutate: boolean;  // this allows you to use set_code in get methods (useful for debugging)
    debug: boolean;             // enables or disables TVM logs (it's useful to disable logs if you rely on performance)
    runner: TvmRunner;
};

TvmRunner allows you to select TVM executor for specific contract, by default all contracts use TvmRunnerAsynchronous which runs thread pool of WASM TVM (it uses worker_threads on node and web workers when bundled for web).

Contract time

By default, for each call to TVM current unixtime is set to C7 register, but you can change it by calling setUnixTime on SmartContract instance.

C7 register

C7 register is used to access some external information in contract:

export declare type C7Config = {
    unixtime?: number;
    balance?: bigint;
    myself?: Address;
    randSeed?: bigint;
    actions?: number;
    messagesSent?: number;
    blockLt?: number;
    transLt?: number;
    globalConfig?: Cell;
};

We prefill it by default, but you can change it by calling setC7Config or setC7.

Termination of worker threads

In order for your tests to terminate successfully, you need to terminate the spawned worker threads, which can be done as follows:

import {TvmRunnerAsynchronous} from "ton-contract-executor";

await TvmRunnerAsynchronous.getShared().cleanup()

Shipping to web

ton-contract-executor can be bundled using webpack, but a polyfill for Buffer is required.

This can be done by installing the buffer package and adding the following to your webpack configuration:

  resolve: {
    fallback: {
      "buffer": require.resolve("buffer/")
    }
  }

However, if you are using @ton-community/func-js for compilation, you also need polyfills for crypto and stream (crypto-browserify and stream-browserify respectively), and add the following to your webpack configuration:

  resolve: {
    fallback: {
      "fs": false,
      "path": false,
      "crypto": require.resolve("crypto-browserify"),
      "stream": require.resolve("stream-browserify"),
      "buffer": require.resolve("buffer/")
    }
  }

Building the WASM part

If you need to build the WASM part of this package, you can use this repo

License

MIT

ton-contract-executor's People

Contributors

doronaviguy avatar dvlkv avatar krigga avatar naltox avatar stels-cs avatar tsivarev avatar xssnick avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ton-contract-executor's Issues

Compatibility with browser JS

In-browser FunC compilation would be a great thing for dApps.

Currently attempt to load the library (through https://unpkg.com/[email protected]/dist/index.js) gives the error

Uncaught ReferenceError: exports is not defined

Incompatible with latest ton releases

After ton 13.0.0 release, the message structure has been largely refactored. Some basic types: InternalMessage, CommonMessageInfo, and, CellMessage seems to no longer exist.

Even using their internal function for the internal message, it will fail:

     should run:
     TypeError: message.writeTo is not a function
      at SmartContract.sendInternalMessage (node_modules/ton-contract-executor/dist/smartContract/SmartContract.js:119:17)
      at /Users/poanlin/Audit/TON/hack-challenge-1/1. mutual fund/test.ts:86:33
      at step (1. mutual fund/test.ts:60:23)
      at Object.next (1. mutual fund/test.ts:41:53)
      at /Users/poanlin/Audit/TON/hack-challenge-1/1. mutual fund/test.ts:35:71
      at new Promise (<anonymous>)
      at __awaiter (1. mutual fund/test.ts:31:12)
      at Context.<anonymous> (1. mutual fund/test.ts:72:20)

It might need some refactor to support the new version of ton, or bring the struct to this library itself to not depend on another package.

BitString from "ton" is not assignable to type BitString from ton-contract-executor

How can I fix it?

Argument of type 'import(".../hello-world/node_modules/ton/dist/boc/Cell").Cell' is not assignable to parameter of type 'import(".../hello-world/node_modules/ton-contract-executor/node_modules/ton/dist/boc/Cell").Cell'.
  Types of property 'bits' are incompatible.
    Type 'import(".../hello-world/node_modules/ton/dist/boc/BitString").BitString' is not assignable to type 'import(".../hello-world/node_modules/ton-contract-executor/node_modules/ton/dist/boc/BitString").BitString'.
      Property '#private' in type 'BitString' refers to a different member that cannot be accessed from within type 'BitString'.

contract = await SmartContract.fromCell(initCodeCell, initDataCell);

Error on Node.js 18

(node:15366) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

node:internal/event_target:908
  process.nextTick(() => { throw err; });
                           ^
TypeError [Error]: Failed to parse URL from /home/noel/Desktop/conex/node_modules/.pnpm/[email protected][email protected]/node_modules/ton-contract-executor/dist/vm-exec/vm-exec.wasm
    at new Request (node:internal/deps/undici/undici:4813:19)
    at Agent.fetch2 (node:internal/deps/undici/undici:5539:29)
    ... 4 lines matching cause stack trace ...
    at /home/noel/Desktop/conex/node_modules/.pnpm/[email protected][email protected]/node_modules/ton-contract-executor/dist/vm-exec/vm-exec.js:9:38689
    at getInstance (/home/noel/Desktop/conex/node_modules/.pnpm/[email protected][email protected]/node_modules/ton-contract-executor/dist/vm-exec/vmExec.js:16:22)
    at vm_exec (/home/noel/Desktop/conex/node_modules/.pnpm/[email protected][email protected]/node_modules/ton-contract-executor/dist/vm-exec/vmExec.js:22:28)
    at runTVM (/home/noel/Desktop/conex/node_modules/.pnpm/[email protected][email protected]/node_modules/ton-contract-executor/dist/executor/executor.js:58:39)
Emitted 'error' event on Worker instance at:
    at [kOnErrorMessage] (node:internal/worker:289:10)
    at [kOnMessage] (node:internal/worker:300:37)
    at MessagePort.<anonymous> (node:internal/worker:201:57)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:639:20)
    at exports.emitMessage (node:internal/per_context/messageport:23:28) {
  [cause]: TypeError [ERR_INVALID_URL]: Invalid URL
      at new NodeError (node:internal/errors:377:5)
      at URL.onParseError (node:internal/url:563:9)
      at new URL (node:internal/url:643:5)
      at new Request (node:internal/deps/undici/undici:4811:25)
      at Agent.fetch2 (node:internal/deps/undici/undici:5539:29)
      at Object.fetch (node:internal/deps/undici/undici:6370:20)
      at fetch (node:internal/bootstrap/pre_execution:196:25)
      at instantiateAsync (/home/noel/Desktop/conex/node_modules/.pnpm/[email protected][email protected]/node_modules/ton-contract-executor/dist/vm-exec/vm-exec.js:9:10542)
      at createWasm (/home/noel/Desktop/conex/node_modules/.pnpm/[email protected][email protected]/node_modules/ton-contract-executor/dist/vm-exec/vm-exec.js:9:11152)
      at /home/noel/Desktop/conex/node_modules/.pnpm/[email protected][email protected]/node_modules/ton-contract-executor/dist/vm-exec/vm-exec.js:9:38689
}

Node.js v18.2.0

I ran the examples and this is the result. My system is arch linux.

Maintaining contract balance between calls (or returning c7 contents)

Contract balance between calls

Users would expect the following new test in SmartContract.spec.ts to pass:

    it('should maintain balance between calls', async () => {
        const source = `      
            () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) {
                return ();
            }

            int get_bal() method_id {
                var [bal, _] = get_balance();
                return bal;
            }
        `
        let contract = await SmartContract.fromFuncSource(source, new Cell())
        await contract.sendInternalMessage(new InternalMessage({
            to: Address.parse('EQD4FPq-PRDieyQKkizFTRtSDyucUIqrj0v_zXJmqaDp6_0t'),
            value: toNano(42),
            bounce: false,
            body: new CommonMessageInfo({
                body: new CellMessage(new Cell())
            })
        }))
        let res = await contract.invokeGetMethod('get_bal', [])

        expect(res.result[0]).toBeInstanceOf(BN)
        expect(res.result[0]).toEqual(toNano(42))
    })

Yes it fails:

    - Expected  - 1
    + Received  + 1

    - "9c7652400"
    + "03e8"

Exporting the value of c7 register

Thank you for allowing to configure c7 register! This indeed allows setting the contract balance easily with a command like:

contract.setC7Config({
    balance: balance.toNumber(),
});

But ideally we would want to configure this behavior to be automatic - so things like balance would be maintained automatically between calls. If users want to disable the automatic maintenance of c7, we can add a flag to SmartContractConfig.

I propose to add the exported contents of the c7 register to the result of every successful execution in TVMExecutionResultOk. This would require exporting the c7 contents as JSON from the c code at the end of vm_exec (and rebuilding the wasm). After doing that, we will have easy control of what to do with it from TypeScript.

We'll be happy to help!

We love this project. We're a team of several TON developers working on an AMM (DEX). Your project is the only decent way to test contracts in the TON ecosystem. We understand that you're probably busy, but we'll be happy to help you maintain it. We have a good understanding of your code and if you want the help, we can add features like c7 export by ourselves as a PR.

@talkol @doronaviguy

how to increase contract balance?

this test case run failed:

    const balanceDataBefore = await contract.invokeGetMethod("my_balance", []);
    const balanceBefore = balanceDataBefore .result[0] as bigint;
contract.sendInternalMessage(
      internalMessage({ from: account1, value: BigInt(100000) })
    );
const balanceData= await contract.invokeGetMethod("my_balance", []);
    const balance = balanceData.result[0] as bigint;
    expect(balance - balanceBefore ).to.be.eq(BigInt(100000));

contract code here:

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
}

int my_balance() method_id {
  [int res, cell a] = get_balance();
  return res;
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.