Skip to content
This repository has been archived by the owner on Jan 31, 2023. It is now read-only.

Modernize - func-js, ton@13 #16

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: mkdir bin && wget https://github.com/ton-defi-org/ton-binaries/releases/download/ubuntu-18/fift -P ./bin && chmod +x ./bin/fift && wget https://github.com/ton-defi-org/ton-binaries/releases/download/ubuntu-18/func -P ./bin && chmod +x ./bin/func && wget https://github.com/ton-defi-org/ton-binaries/releases/download/fiftlib/fiftlib.zip -P ./bin && unzip ./bin/fiftlib.zip -d ./bin/fiftlib
- run: mkdir bin && wget https://github.com/ton-defi-org/ton-binaries/releases/download/ubuntu-18-0.3.0/fift -P ./bin && chmod +x ./bin/fift && wget https://github.com/ton-defi-org/ton-binaries/releases/download/ubuntu-18-0.3.0/func -P ./bin && chmod +x ./bin/func && wget https://github.com/ton-defi-org/ton-binaries/releases/download/fiftlib/fiftlib.zip -P ./bin && unzip ./bin/fiftlib.zip -d ./bin/fiftlib
- run: npm ci
- run: npm run build
- run: npm test
11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This project is part of a set of 3 typical repositories needed for a blockchain

* `contracts/*.fc` - Smart contracts for TON blockchain written in [FunC](https://ton.org/docs/#/func) language
* `test/*.spec.ts` - Test suite for the contracts in TypeScript running on [Mocha](https://mochajs.org/) test runner
* `build/_build.ts` - Build script to compile the FunC code to [Fift](https://ton-blockchain.github.io/docs/fiftbase.pdf) and [TVM](https://ton-blockchain.github.io/docs/tvm.pdf) opcodes
* `build/_build.ts` - Build script to compile the FunC code to [TVM](https://ton-blockchain.github.io/docs/tvm.pdf) opcodes
* `build/_deploy.ts` - Deploy script to deploy the compiled code to TON mainnet (or testnet)
* `build/_setup.ts` - Setup script to install build dependencies (used primarily for Glitch.com support)

Expand All @@ -36,14 +36,7 @@ To setup your local machine for development, please make sure you have the follo

* A modern version of Node.js (version 16.15.0 or later)
* Installation instructions can be found [here](https://nodejs.org/)
* Run in terminal `node -v` to verify your installation, the project was tested on `v17.3.0`
* The `func` CLI tool (FunC compiler)
* Installation instructions can be found [here](https://github.com/ton-defi-org/ton-binaries)
* Run in terminal `func -V` to verify your installation
* The `fift` CLI tool
* Installation instructions can be found [here](https://github.com/ton-defi-org/ton-binaries)
* Don't forget to set the `FIFTPATH` env variable as part of the installation above
* Run in terminal `fift -V` and `fift` to verify your installation
* Run in terminal `node -v` to verify your installation, the project was tested on `v16.15.0`
* A decent IDE with FunC and TypeScript support
* We recommend using [Visual Studio Code](https://code.visualstudio.com/) with the [FunC plugin](https://marketplace.visualstudio.com/items?itemName=tonwhales.func-vscode) installed

Expand Down
107 changes: 11 additions & 96 deletions build/_build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,72 +9,21 @@
import fs from "fs";
import path from "path";
import process from "process";
import child_process from "child_process";
import glob from "fast-glob";
import { Cell } from "ton";
import semver from "semver";
import { Cell } from "ton-core";
import { compileFunc } from "@ton-community/func-js";

async function main() {
console.log("=================================================================");
console.log("Build script running, let's find some FunC contracts to compile..");

// if we have an explicit bin directory, use the executables there (needed for glitch.com)
if (fs.existsSync("bin")) {
process.env.PATH = path.join(__dirname, "..", "bin") + path.delimiter + process.env.PATH;
process.env.FIFTPATH = path.join(__dirname, "..", "bin", "fiftlib");
}

// make sure func compiler is available
const minSupportFunc = "0.2.0";
try {
const funcVersion = child_process
.execSync("func -V")
.toString()
.match(/semantic version: v([0-9.]+)/)?.[1];
if (!semver.gte(semver.coerce(funcVersion) ?? "", minSupportFunc)) throw new Error("Nonexistent version or outdated");
} catch (e) {
console.log(`\nFATAL ERROR: 'func' with version >= ${minSupportFunc} executable is not found, is it installed and in path?`);
process.exit(1);
}

// make sure fift cli is available
let fiftVersion = "";
try {
fiftVersion = child_process.execSync("fift -V").toString();
} catch (e) {}
if (!fiftVersion.includes("Fift build information")) {
console.log("\nFATAL ERROR: 'fift' executable is not found, is it installed and in path?");
process.exit(1);
}

// go over all the root contracts in the contracts directory
const rootContracts = glob.sync(["contracts/*.fc", "contracts/*.func"]);
for (const rootContract of rootContracts) {
// compile a new root contract
console.log(`\n* Found root contract '${rootContract}' - let's compile it:`);
const contractName = path.parse(rootContract).name;

// delete existing build artifacts
const fiftArtifact = `build/${contractName}.fif`;
if (fs.existsSync(fiftArtifact)) {
console.log(` - Deleting old build artifact '${fiftArtifact}'`);
fs.unlinkSync(fiftArtifact);
}
const mergedFuncArtifact = `build/${contractName}.merged.fc`;
if (fs.existsSync(mergedFuncArtifact)) {
console.log(` - Deleting old build artifact '${mergedFuncArtifact}'`);
fs.unlinkSync(mergedFuncArtifact);
}
const fiftCellArtifact = `build/${contractName}.cell.fif`;
if (fs.existsSync(fiftCellArtifact)) {
console.log(` - Deleting old build artifact '${fiftCellArtifact}'`);
fs.unlinkSync(fiftCellArtifact);
}
const cellArtifact = `build/${contractName}.cell`;
if (fs.existsSync(cellArtifact)) {
console.log(` - Deleting old build artifact '${cellArtifact}'`);
fs.unlinkSync(cellArtifact);
}
const hexArtifact = `build/${contractName}.compiled.json`;
if (fs.existsSync(hexArtifact)) {
console.log(` - Deleting old build artifact '${hexArtifact}'`);
Expand All @@ -99,61 +48,27 @@ async function main() {

// run the func compiler to create a fif file
console.log(` - Trying to compile '${rootContract}' with 'func' compiler..`);
let buildErrors: string;
try {
buildErrors = child_process.execSync(`func -APS -o build/${contractName}.fif ${rootContract} 2>&1 1>node_modules/.tmpfunc`).toString();
} catch (e) {
buildErrors = e.stdout.toString();
}
if (buildErrors.length > 0) {
console.log(" - OH NO! Compilation Errors! The compiler output was:");
console.log(`\n${buildErrors}`);
process.exit(1);
} else {
console.log(" - Compilation successful!");
}

// make sure fif build artifact was created
if (!fs.existsSync(fiftArtifact)) {
console.log(` - For some reason '${fiftArtifact}' was not created!`);
process.exit(1);
} else {
console.log(` - Build artifact created '${fiftArtifact}'`);
}

// create a temp cell.fif that will generate the cell
let fiftCellSource = '"Asm.fif" include\n';
fiftCellSource += `${fs.readFileSync(fiftArtifact).toString()}\n`;
fiftCellSource += `boc>B "${cellArtifact}" B>file`;
fs.writeFileSync(fiftCellArtifact, fiftCellSource);
const compileResult = await compileFunc({
targets: [rootContract],
sources: (x) => fs.readFileSync(x).toString("utf8"),
});

// run fift cli to create the cell
try {
child_process.execSync(`fift ${fiftCellArtifact}`);
} catch (e) {
console.log("FATAL ERROR: 'fift' executable failed, is FIFTPATH env variable defined?");
if (compileResult.status === "error") {
console.log(" - OH NO! Compilation Errors! The compiler output was:");
console.log(`\n${compileResult.message}`);
process.exit(1);
}

// Remove intermediary
fs.unlinkSync(fiftCellArtifact);

// make sure cell build artifact was created
if (!fs.existsSync(cellArtifact)) {
console.log(` - For some reason, intermediary file '${cellArtifact}' was not created!`);
process.exit(1);
}
console.log(" - Compilation successful!");

fs.writeFileSync(
hexArtifact,
JSON.stringify({
hex: Cell.fromBoc(fs.readFileSync(cellArtifact))[0].toBoc().toString("hex"),
hex: Cell.fromBoc(Buffer.from(compileResult.codeBoc, "base64"))[0].toBoc().toString("hex"),
})
);

// Remove intermediary
fs.unlinkSync(cellArtifact);

// make sure hex artifact was created
if (!fs.existsSync(hexArtifact)) {
console.log(` - For some reason '${hexArtifact}' was not created!`);
Expand Down
71 changes: 38 additions & 33 deletions build/_deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@
// ./build/ - directory for build artifacts (mycontract.compiled.json) and deploy init data scripts (mycontract.deploy.ts)
// ./.env - config file with DEPLOYER_MNEMONIC - secret mnemonic of deploying wallet (will be created if not found)

import axios from "axios";
import axiosThrottle from "axios-request-throttle";
axiosThrottle.use(axios, { requestsPerSecond: 0.5 }); // required since toncenter jsonRPC limits to 1 req/sec without API key
import { getHttpEndpoint } from "@orbs-network/ton-access";

import dotenv from "dotenv";
dotenv.config();

import fs from "fs";
import path from "path";
import glob from "fast-glob";
import { Address, Cell, CellMessage, CommonMessageInfo, fromNano, InternalMessage, StateInit, toNano } from "ton";
import { TonClient, WalletContract, WalletV3R2Source, contractAddress, SendMode } from "ton";
import { Address, Cell, fromNano, toNano, contractAddress, internal } from "ton-core";
import { TonClient, SendMode, WalletContractV3R2 } from "ton";
import { mnemonicNew, mnemonicToWalletKey } from "ton-crypto";

async function main() {
Expand All @@ -32,9 +30,10 @@ async function main() {
}

// initialize globals
const client = new TonClient({ endpoint: `https://${isTestnet ? "testnet." : ""}toncenter.com/api/v2/jsonRPC` });
const endpoint = await getHttpEndpoint({ network: isTestnet ? "testnet" : "mainnet" });
const client = new TonClient({ endpoint });
const deployerWalletType = "org.ton.wallets.v3.r2"; // also see WalletV3R2Source class used below
const newContractFunding = toNano(0.02); // this will be (almost in full) the balance of a new deployed contract and allow it to pay rent
const newContractFunding = toNano("0.02"); // this will be (almost in full) the balance of a new deployed contract and allow it to pay rent
const workchain = 0; // normally 0, only special contracts should be deployed to masterchain (-1)

// make sure we have a wallet mnemonic to deploy from (or create one if not found)
Expand All @@ -53,10 +52,16 @@ async function main() {

// open the wallet and make sure it has enough TON
const walletKey = await mnemonicToWalletKey(deployerMnemonic.split(" "));
const walletContract = WalletContract.create(client, WalletV3R2Source.create({ publicKey: walletKey.publicKey, workchain }));
console.log(` - Wallet address used to deploy from is: ${walletContract.address.toFriendly()}`);
const walletBalance = await client.getBalance(walletContract.address);
if (walletBalance.lt(toNano(0.2))) {
const wallet = client.open(
WalletContractV3R2.create({
publicKey: walletKey.publicKey,
workchain: 0,
})
);

console.log(` - Wallet address used to deploy from is: ${wallet.address.toString()}`);
const walletBalance = await client.getBalance(wallet.address);
if (walletBalance < toNano("0.2")) {
console.log(` - ERROR: Wallet has less than 0.2 TON for gas (${fromNano(walletBalance)} TON), please send some TON for gas first`);
process.exit(1);
} else {
Expand Down Expand Up @@ -91,52 +96,52 @@ async function main() {
console.log(` - ERROR: '${hexArtifact}' not found, did you build?`);
process.exit(1);
}
const initCodeCell = Cell.fromBoc(JSON.parse(fs.readFileSync(hexArtifact).toString()).hex)[0];
const initCodeCell = Cell.fromBoc(Buffer.from(JSON.parse(fs.readFileSync(hexArtifact).toString()).hex, "hex"))[0];

// make sure the contract was not already deployed
const newContractAddress = contractAddress({ workchain, initialData: initDataCell, initialCode: initCodeCell });
console.log(` - Based on your init code+data, your new contract address is: ${newContractAddress.toFriendly()}`);
const newContractAddress = contractAddress(0, { code: initCodeCell, data: initDataCell });
console.log(` - Based on your init code+data, your new contract address is: ${newContractAddress.toString()}`);
if (await client.isContractDeployed(newContractAddress)) {
console.log(` - Looks like the contract is already deployed in this address, skipping deployment`);
await performPostDeploymentTest(rootContract, deployInitScript, walletContract, walletKey.secretKey, newContractAddress);
await performPostDeploymentTest(rootContract, deployInitScript, wallet, client, newContractAddress, walletKey.secretKey);
continue;
}

// deploy by sending an internal message to the deploying wallet
console.log(` - Let's deploy the contract on-chain..`);
const seqno = await walletContract.getSeqNo();
const transfer = walletContract.createTransfer({
const seqno = await wallet.getSeqno();
const transfer = wallet.createTransfer({
secretKey: walletKey.secretKey,
seqno: seqno,
sendMode: SendMode.PAY_GAS_SEPARATLY + SendMode.IGNORE_ERRORS,
order: new InternalMessage({
to: newContractAddress,
value: newContractFunding,
bounce: false,
body: new CommonMessageInfo({
stateInit: new StateInit({ data: initDataCell, code: initCodeCell }),
body: initMessageCell !== null ? new CellMessage(initMessageCell) : null,
messages: [
internal({
to: newContractAddress,
value: newContractFunding,
bounce: false,
init: { data: initDataCell, code: initCodeCell },
body: initMessageCell,
}),
}),
],
});
await client.sendExternalMessage(walletContract, transfer);
await client.sendExternalMessage(wallet, transfer);
console.log(` - Deploy transaction sent successfully`);

// make sure that the contract was deployed
console.log(` - Block explorer link: https://${process.env.TESTNET ? "test." : ""}tonwhales.com/explorer/address/${newContractAddress.toFriendly()}`);
console.log(` - Block explorer link: https://${process.env.TESTNET ? "testnet." : ""}tonscan.org/address/${newContractAddress.toString()}`);
console.log(` - Waiting up to 20 seconds to check if the contract was actually deployed..`);
for (let attempt = 0; attempt < 10; attempt++) {
await sleep(2000);
const seqnoAfter = await walletContract.getSeqNo();
const seqnoAfter = await wallet.getSeqno();
if (seqnoAfter > seqno) break;
}
if (await client.isContractDeployed(newContractAddress)) {
console.log(` - SUCCESS! Contract deployed successfully to address: ${newContractAddress.toFriendly()}`);
console.log(` - SUCCESS! Contract deployed successfully to address: ${newContractAddress.toString()}`);
const contractBalance = await client.getBalance(newContractAddress);
console.log(` - New contract balance is now ${fromNano(contractBalance)} TON, make sure it has enough to pay rent`);
await performPostDeploymentTest(rootContract, deployInitScript, walletContract, walletKey.secretKey, newContractAddress);
await performPostDeploymentTest(rootContract, deployInitScript, wallet, client, newContractAddress, walletKey.secretKey);
} else {
console.log(` - FAILURE! Contract address still looks uninitialized: ${newContractAddress.toFriendly()}`);
console.log(` - FAILURE! Contract address still looks uninitialized: ${newContractAddress.toString()}`);
}
}

Expand All @@ -151,11 +156,11 @@ function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function performPostDeploymentTest(rootContract: string, deployInitScript: any, walletContract: WalletContract, secretKey: Buffer, newContractAddress: Address) {
async function performPostDeploymentTest(rootContract: string, deployInitScript: any, wallet: any, client: TonClient, newContractAddress: Address, secretKey: Buffer) {
if (typeof deployInitScript.postDeployTest !== "function") {
console.log(` - Not running a post deployment test, '${rootContract}' does not have 'postDeployTest()' function`);
return;
}
console.log(` - Running a post deployment test:`);
await deployInitScript.postDeployTest(walletContract, secretKey, newContractAddress);
await deployInitScript.postDeployTest(wallet, client, newContractAddress, secretKey);
}
16 changes: 9 additions & 7 deletions build/main.deploy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as main from "../contracts/main";
import { Address, toNano, TupleSlice, WalletContract } from "ton";
import { Address, toNano, TupleReader } from "ton-core";
import { WalletContractV3R2, TonClient } from "ton";
import { sendInternalMessageWithWallet } from "../test/helpers";

// return the init Cell of the contract storage (according to load_data() contract method)
Expand All @@ -16,16 +17,17 @@ export function initMessage() {
}

// optional end-to-end sanity test for the actual on-chain contract to see it is actually working on-chain
export async function postDeployTest(walletContract: WalletContract, secretKey: Buffer, contractAddress: Address) {
const call = await walletContract.client.callGetMethod(contractAddress, "counter");
const counter = new TupleSlice(call.stack).readBigNumber();
export async function postDeployTest(wallet: any, client: TonClient, contractAddress: Address, secretKey: Buffer) {
const call = await client.callGetMethod(contractAddress, "counter");

const counter = call.stack.readBigNumber();
console.log(` # Getter 'counter' = ${counter.toString()}`);

const message = main.increment();
await sendInternalMessageWithWallet({ walletContract, secretKey, to: contractAddress, value: toNano(0.02), body: message });
await sendInternalMessageWithWallet({ wallet, client, to: contractAddress, value: toNano("0.02"), body: message, secretKey });
console.log(` # Sent 'increment' op message`);

const call2 = await walletContract.client.callGetMethod(contractAddress, "counter");
const counter2 = new TupleSlice(call2.stack).readBigNumber();
const call2 = await client.callGetMethod(contractAddress, "counter");
const counter2 = call2.stack.readBigNumber();
console.log(` # Getter 'counter' = ${counter2.toString()}`);
}
5 changes: 2 additions & 3 deletions contracts/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import BN from "bn.js";
import { Cell, beginCell, Address } from "ton";
import { Cell, beginCell, Address } from "ton-core";

// encode contract storage according to save_data() contract method
export function data(params: { ownerAddress: Address; counter: number }): Cell {
Expand All @@ -16,7 +15,7 @@ export function deposit(): Cell {
return beginCell().storeUint(0x47d54391, 32).storeUint(0, 64).endCell();
}

export function withdraw(params: { withdrawAmount: BN }): Cell {
export function withdraw(params: { withdrawAmount: bigint }): Cell {
return beginCell().storeUint(0x41836980, 32).storeUint(0, 64).storeCoins(params.withdrawAmount).endCell();
}

Expand Down
Loading