Skip to content

Echpochmak, is a framework based on TONOS Client Library for TON DApp development

License

Notifications You must be signed in to change notification settings

wintexpro/echpochmak

Repository files navigation

npm Contributors Forks Stargazers Issues MIT License


Logo

Echpochmak

⚠️ (Work In Progress)
Report Bug | Request Feature

About The Project

Echpochmak, is a framework based on TONOS Client Library for TON DApp development

It provides some high-level interfaces to interact with your TON smart-contracts, allowing you to write integration and e2e tests.

Content Table

Getting Started

  1. Install Echpochmak
npm install -g echpochmak

Test Examples

You can see examples of tests here

Usage

To run all tests:

echpochmak-cli test -p ./path/to/tests/folder

Synopsis

test [options]

Options

Option Description Default
-p, --path Path to test folder
-b, --bail Enable bail false
-v, --verbose Wite debug info false
-t, --timeout Timeout time for test 0
--version (boolean) Show version
--help (boolean) Show help information

If you need help, use

echpochmak-cli --help

Write test

Tests are written using Mocha

Restart tondev command

beforeEach(async () => {
  await restart(); //Recreate and start containers
});

Or set port (if you use don't default port in tondev-cli)

restart command does not set a port for the container, but only uses it for long polling

beforeEach(async () => {
  await restart(8080); //Default 80
});

The manager is in the global scope

const manager = new Manager();

Create new Manager object

beforeEach(async () => {
    manager = new Manager();

Create client

default ['net.ton.dev']

Signature

async createClient(servers: string[] = ['net.ton.dev'])

await manager.createClient(['http://localhost:8080/graphql']);

Generate keys

let keys = await manager.createKeysAndReturn();

Load contract

Signature

public async loadContract( contractPath: string, abiPath: string, options: loadOptions )

await manager.loadContract(
  './tests/contract/15_MessageReceiver.tvc', // tvc
  './tests/contract/15_MessageReceiver.abi.json' // abi
);
await manager.loadContract(
  './tests/contract/15_MessageReceiver.tvc', // tvc
  './tests/contract/15_MessageReceiver.abi.json' // abi
);
// Custom name
await manager.loadContract(
  './tests/contract/15_MessageSender.tvc',
  './tests/contract/15_MessageSender.abi.json',
  { contractName: 'sender ' } // Name
);
// Custom keys
await manager.loadContract(
  './tests/contract/15_MessageSender.tvc',
  './tests/contract/15_MessageSender.abi.json',
  { keys: anyKeys } // keys
);

Get future/current address

let address = manager.contracts['15_MessageReceiver'].address;

loadOption signature

export interface loadOptions {
  keys?: any;
  contractName?: string;
}

Referring to the contract

manager.contracts['Contract_name']; // Taken from the name of the tvc file without extension

Deploy contract

Signature

async deployContract(constructorParams = {}, giveGram = true, keys?)

await manager.contracts['15_MessageReceiver'].deployContract();
await manager.contracts['15_MessageSender'].deployContract();

Deploy with parameters

await manager.contracts['9_PiggyBank'].deployContract({
  own: manager.contracts['9_PiggyBank_Owner'].address,
  lim: 1000000,
});

"Complicated" deploy

This method allows you to provide: constructor parameters, constructor headers and init parameters of your deployable smart contract

await manager.contracts['someName'].complicatedDeploy(
  constructorParams,
  constructorHeader,
  initParams,
)
await manager.contracts['9_PiggyBank'].complicatedDeploy(
  {
    own: manager.contracts['9_PiggyBank_Owner'].address,
    lim: 1000000,
  },
  {
    pubkey: keypair.public,
    expire: new Date()
  },
  {
    randomNonce: '0x' + crypto.createHash('sha256').update(crypto.randomBytes(32)).digest('hex')
  }
);

### Or use custom keys

```js
await manager.contracts['9_PiggyBank'].deployContract({}, keys);

Add contract from address

Signature

async addContractFromAddress( address: string, abiPath: string, contractName: string, keyPair? )

await manager.addContractFromAddress(
  'address',
  'abiPath',
  'contractName',
  keyPair // HERE, the keys are indicated, if nothing is specified, those inside the contract object are used
);

Run contract

Signature

async runContract(functionName, input, keyPair?)

await manager.contracts['15_MessageSender'].runContract('sendMessage', {
  anotherContract: manager.contracts['15_MessageReceiver'].address,
});

Run contract(no sign)

await manager.contracts['15_MessageSender'].runContract(
  'sendMessage',
  {
    anotherContract: manager.contracts['15_MessageReceiver'].address,
  },
  null // HERE, the keys are indicated, if nothing is specified, those inside the contract object are used (see below);
);

RunLocal contract

Signature async runLocal(functionName, input, keyPair?)

await manager.contracts['15_MessageSender'].runLocal('getData', {});

Run contract with message

Signature

public async runWithMessage(functionName, input, keyPair?)

const msg = await manager.contracts['15_MessageSender'].runWithMessage(
  'sendMessage',
  {
    anotherContract: manager.contracts['15_MessageReceiver'].address,
  }
);

console.log(`Tokens were sent. Transaction id is ${msg.transaction.id}`);
console.log(`Run fees are  ${JSON.stringify(msg.fees, null, 2)}`);

Use Giver

Signature

async giveToAddress(address, amount?: number)

manager.giveToAddress(manager.contracts['15_MessageReceiver'].address); // give 1000000000000000 gram

Contract fields

manager.contracts['15_MessageSender'].address; // Contract address
manager.contracts['15_MessageSender'].contractPath; // Contract path
manager.contracts['15_MessageSender'].isDeployed; // boolean
manager.contracts['15_MessageSender'].contractPackage; //{ abi,imageBase64 }

Helpers

Use helpers from Manager object

manager.helpers.<helper>;

deployCheck

Signature public static async deployCheck(address, client)

await manager.helpers.deployCheck(contractAddress, manager.client);

getAccountBalance

Signature public static async getAccountBalance(address, client)

await manager.helpers.getAccountBalance(contractAddress, manager.client);

hasOnBounced

Signature public static async hasOnBounced(address, timestamp: number, client)

timestamp - The time when the transaction preceding OnBounced was made, guarantees the assistant will wait for the last message OnBounced

await manager.helpers.hasOnBounced(contractAddress, manager.client);

lastTransaction

Signature public static async lastMessage( address, client, fields = 'src, dst, bounce, bounced, value' )

let tx = await manager.helpers.lastTransaction(contractAddress, manager.client);

lastMessage

Signature public static async lastMessage( address, client, fields = 'src, dst, bounce, bounced, value' )

let msg = await manager.helpers.lastMessage(contractAddress, manager.client);

balanceHasChanged

Signature public static async balanceHasChanged( address, client, oldValue, type: hasChangedValue )

await manager.helpers.balanceHasChanged(
  contractAddress,
  manager.client,
  100000,
  hasChangedValue.big
);

hasChangedValue enum

export enum hasChangedValue {
  big,
  small,
}

getRunFees

Signature

static async getRunFees( contract: Contract, functionName, input, client, keyPair )

const fees = await manager.helpers.getRunFees(
  manager.contracts['15_MessageSender'],
  'send',
  manager.client,
  keys
);

getDeployFees

Signature

static async getDeployFees( contract: Contract, constructorParams, newAccount: boolean, client, keyPair )

const fees = await manager.helpers.getDeployFees(
  manager.contracts['15_MessageSender'],
  {},
  true,
  manager.client,
  keys
);

Asserts

assertError

Signature

assertError = async (asyncFn: any, code: number, message?)

Example

await assertError(
  async () => {
    await manager.contracts['9_PiggyBank'].runContract('getData', {});
  },
  3025,
  'getData'
);

A simple wallet contract, you may find it useful when testing fallback functions

Create Wallet object()

Signature

async createWallet(keys?)

let wallet = await manager.createWallet();

or set keys

let wallet = await manager.createWallet(keys);

Deploy wallet

await wallet.deploy();

SendTransaction

Signature

async sendTransaction(dest: string, value: number, bounce: boolean)

await wallet.sendTransaction(
  (dest: string),
  (value: number),
  (bounce: boolean)
);

Wallet fields

wallet.address; // wallet address
wallet.contractPath; // wallet path
wallet.isDeployed; // boolean
wallet.contractPackage; //{ abi,imageBase64 }

Custom Contract Object

Create class

CustomObject.example.js

const BaseContract = require('echpochmak').BaseContract; // Import BaseContract class

class BankContract extends BaseContract {
  async getData() {
    // new
    return await this.runLocal('getData', {}); // Using 'contract' object fields
  }
}

module.exports = BankContract;

Use in tests

Import contract object

const BankContract = require('./CustomObject.example');

Create object (Constructor used from BaseContract class)

const customBankObject = await new BankContract(
  './tests/contract/9_PiggyBank.tvc',
  './tests/contract/9_PiggyBank.abi.json',
  manager.client,
  await manager.createKeysAndReturn()
);

Add object in manager

manager.addCustomContract(customBankObject, 'customBank');

Use custom fields

const res = await manager.contracts['customBank'].getData();
Spoiler warning
/* eslint-disable @typescript-eslint/no-explicit-any */
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { deploy } from '../Deploy/Deploy';

export abstract class BaseContract {
public contractPackage: any;
public contractPath: string;
public isDeployed = false;
public address: any;
protected client: any;
protected keys: any;

constructor(
contractPath: string,
abiPath: string,
client,
keys: any,
noPath = false
) {
// read contract .tvc file
let imageBase64 = '';
if (!noPath) {
const buff = readFileSync(resolve(contractPath));
// convert tvc code into base64
imageBase64 = buff.toString('base64');
}

  // eslint-disable-next-line @typescript-eslint/no-var-requires
  const abi = require(resolve(abiPath));
  this.contractPackage = { abi, imageBase64 };
  this.client = client;
  this.keys = keys;
  this.isDeployed = false;

}

public async deployContract(constructorParams = {}, giveGram = true, keys?) {
this.address = await deploy(
this.client,
this.contractPackage,
keys || this.keys,
constructorParams,
giveGram
);
this.isDeployed = true;
}

public async runWithMessage(functionName, input, keyPair?) {
if (!this.isDeployed) {
throw new Error('Contract not deployed');
}
if (!keyPair) {
keyPair = this.keys;
}
const runMessage = await this.client.contracts.createRunMessage({
address: this.address,
abi: this.contractPackage.abi,
functionName,
input,
keyPair,
});
const messageProcessingState = await this.client.contracts.sendMessage(
runMessage.message
);
const result = await this.client.contracts.waitForRunTransaction(
runMessage,
messageProcessingState
);
return result;
}

public async runLocal(functionName, input, keyPair?) {
if (!this.isDeployed) {
throw new Error('Contract not deployed');
}
if (!keyPair) {
keyPair = this.keys;
}
const response = await this.client.contracts.runLocal({
address: this.address,
abi: this.contractPackage.abi,
functionName,
input,
keyPair,
});
return response;
}

public async futureAddress() {
const futureAddress = (
await this.client.contracts.getDeployData({
abi: this.contractPackage.abi,
imageBase64: this.contractPackage.imageBase64,
publicKeyHex: this.keys.public,
})
).address;
return futureAddress;
}
public async runContract(functionName, input, keyPair?) {
if (!this.isDeployed) {
throw new Error('Contract not deployed');
}
if (!keyPair) {
keyPair = this.keys;
}
const response = await this.client.contracts.run({
address: this.address,
abi: this.contractPackage.abi,
functionName: functionName,
input: input,
keyPair: keyPair, //there is no pubkey key check in the contract so we can leave it empty
});
return response.output;
}
}

Contributing

Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

License

Distributed under the Apache 2.0 License. See LICENSE for more information.

Contact