Skip to content
This repository was archived by the owner on Jan 31, 2023. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

strategy:
matrix:
node-version: [16.x, 17.x]
node-version: [16.x, 18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand All @@ -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
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