From f6ca83c40d4a239e79f8c8322d53ac6cb8896a11 Mon Sep 17 00:00:00 2001 From: Tal Kol Date: Mon, 2 May 2022 22:44:21 +0100 Subject: [PATCH] Added deploy script --- README.md | 12 ++- build/.gitignore | 3 +- build/build.ts | 227 ++++++++++++++++++++++--------------------- build/deploy.ts | 110 +++++++++++++++++++++ build/main.deploy.ts | 10 ++ package-lock.json | 37 +++++-- package.json | 4 +- 7 files changed, 281 insertions(+), 122 deletions(-) create mode 100644 build/deploy.ts create mode 100644 build/main.deploy.ts diff --git a/README.md b/README.md index 9fab391..1139a54 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ 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 * `build/build.ts` - Build script to compile the FunC code to [Fift](https://ton-blockchain.github.io/docs/fiftbase.pdf) -* `build/*.fif` - Output Fift files for every contract that was compiled, not uploaded to git * `build/deploy.ts` - Deploy script to deploy the compiled code to TON mainnet * `test/*.spec.ts` - Test suite for the contracts running on [Mocha](https://mochajs.org/) test runner @@ -65,11 +64,18 @@ To setup your machine for development, please make sure you have the following: * Build * In the root repo dir, run in terminal `npm run build` * Compilation errors will appear on screen + * Resulting build artifacts include: + * `mycontract.merged.fc` - merged and flattened FunC source code with all imports + * `mycontract.fif` - Fift file result of compilation (not very useful by itself) + * `mycontract.cell` - the binary code cell of the compiled contract (for deployment) * Test * In the root repo dir, run in terminal `npm run test` - * Make sure to build before running tests + * Don't forget to build (or rebuild) before running tests + * Tests are running inside Node.js by running TVM in web-assembly using `ton-contract-executor` * Deploy * In the root repo dir, run in terminal `npm run deploy` - * Follow the on-screen instructions to deploy to mainnet \ No newline at end of file + * Follow the on-screen instructions to deploy to mainnet + * Each contract to deploy should have a script `build/mycontract.deploy.ts` to return its init data cell + * The deployment wallet is configured in `build/deploy.config.json` (file will be created if not found) \ No newline at end of file diff --git a/build/.gitignore b/build/.gitignore index 22e613e..3872f92 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -1,3 +1,4 @@ *.fif *.fc -*.cell \ No newline at end of file +*.cell +deploy.config.json \ No newline at end of file diff --git a/build/build.ts b/build/build.ts index d342fd8..edad113 100644 --- a/build/build.ts +++ b/build/build.ts @@ -1,5 +1,5 @@ // This is a simple generic build script in TypeScript that should work for most projects without modification -// It assumes that it is running from the repo root, and the directories are organized this way: +// The script assumes that it is running from the repo root, and the directories are organized this way: // ./build/ - directory for build artifacts exists // ./contracts/*.fc - root contracts that are deployed separately are here // ./contracts/imports/*.fc - shared utility code that should be imported as compilation dependency is here @@ -12,129 +12,134 @@ import process from "process"; import child_process from "child_process"; import glob from "fast-glob"; -console.log(`=================================================================`); -console.log(`Build script running, let's find some FunC contracts to compile..`); - -// make sure func compiler is available -let funcVersion = ""; -try { - funcVersion = child_process.execSync("func -V").toString(); -} catch (e) {} -if (!funcVersion.includes(`Func build information`)) { - console.log(`\nFATAL ERROR: 'func' 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); -} +async function main() { + console.log(`=================================================================`); + console.log(`Build script running, let's find some FunC contracts to compile..`); -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); + // make sure func compiler is available + let funcVersion = ""; + try { + funcVersion = child_process.execSync("func -V").toString(); + } catch (e) {} + if (!funcVersion.includes(`Func build information`)) { + console.log(`\nFATAL ERROR: 'func' executable is not found, is it installed and in path?`); + process.exit(1); } - const cellArtifact = `build/${contractName}.cell`; - if (fs.existsSync(cellArtifact)) { - console.log(` - Deleting old build artifact '${cellArtifact}'`); - fs.unlinkSync(cellArtifact); + + // 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); } - // check if we have a tlb file - const tlbFile = `contracts/${contractName}.tlb`; - if (fs.existsSync(tlbFile)) { - console.log(` - TL-B file '${tlbFile}' found, calculating crc32 on all ops..`); - const tlbContent = fs.readFileSync(tlbFile).toString(); - const tlbOpMessages = tlbContent.match(/^(\w+).*=\s*InternalMsgBody$/gm) ?? []; - for (const tlbOpMessage of tlbOpMessages) { - const crc = crc32(tlbOpMessage); - const asQuery = `0x${(crc & 0x7fffffff).toString(16)}`; - const asResponse = `0x${((crc | 0x80000000) >>> 0).toString(16)}`; - console.log(` op '${tlbOpMessage.split(" ")[0]}': '${asQuery}' as query (&0x7fffffff), '${asResponse}' as response (|0x80000000)`); + // 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); } - } else { - console.log(` - Warning: TL-B file for contract '${tlbFile}' not found, are your op consts according to standard?`); - } - // create a merged fc file with source code from all dependencies - let sourceToCompile = ""; - const importFiles = glob.sync([`contracts/imports/*.fc`, `contracts/imports/*.func`, `contracts/imports/${contractName}/*.fc`, `contracts/imports/${contractName}/*.func`]); - for (const importFile of importFiles) { - console.log(` - Adding import '${importFile}'`); - sourceToCompile += `${fs.readFileSync(importFile).toString()}\n`; - } - console.log(` - Adding the contract itself '${rootContract}'`); - sourceToCompile += `${fs.readFileSync(rootContract).toString()}\n`; - fs.writeFileSync(mergedFuncArtifact, sourceToCompile); - console.log(` - Build artifact created '${mergedFuncArtifact}'`); - - // run the func compiler to create a fif file - console.log(` - Trying to compile '${mergedFuncArtifact}' with 'func' compiler..`); - const buildErrors = child_process.execSync(`func -APS -o build/${contractName}.fif ${mergedFuncArtifact} 2>&1 >&-`).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!`); - } + // check if we have a tlb file + const tlbFile = `contracts/${contractName}.tlb`; + if (fs.existsSync(tlbFile)) { + console.log(` - TL-B file '${tlbFile}' found, calculating crc32 on all ops..`); + const tlbContent = fs.readFileSync(tlbFile).toString(); + const tlbOpMessages = tlbContent.match(/^(\w+).*=\s*InternalMsgBody$/gm) ?? []; + for (const tlbOpMessage of tlbOpMessages) { + const crc = crc32(tlbOpMessage); + const asQuery = `0x${(crc & 0x7fffffff).toString(16)}`; + const asResponse = `0x${((crc | 0x80000000) >>> 0).toString(16)}`; + console.log(` op '${tlbOpMessage.split(" ")[0]}': '${asQuery}' as query (&0x7fffffff), '${asResponse}' as response (|0x80000000)`); + } + } else { + console.log(` - Warning: TL-B file for contract '${tlbFile}' not found, are your op consts according to standard?`); + } - // 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 merged fc file with source code from all dependencies + let sourceToCompile = ""; + const importFiles = glob.sync([`contracts/imports/*.fc`, `contracts/imports/*.func`, `contracts/imports/${contractName}/*.fc`, `contracts/imports/${contractName}/*.func`]); + for (const importFile of importFiles) { + console.log(` - Adding import '${importFile}'`); + sourceToCompile += `${fs.readFileSync(importFile).toString()}\n`; + } + console.log(` - Adding the contract itself '${rootContract}'`); + sourceToCompile += `${fs.readFileSync(rootContract).toString()}\n`; + fs.writeFileSync(mergedFuncArtifact, sourceToCompile); + console.log(` - Build artifact created '${mergedFuncArtifact}'`); + + // run the func compiler to create a fif file + console.log(` - Trying to compile '${mergedFuncArtifact}' with 'func' compiler..`); + const buildErrors = child_process.execSync(`func -APS -o build/${contractName}.fif ${mergedFuncArtifact} 2>&1 >&-`).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!`); + } - // 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); + // 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}'`); + } - // 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?`); - process.exit(1); - } + // 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); + + // 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?`); + process.exit(1); + } - // make sure cell build artifact was created - if (!fs.existsSync(cellArtifact)) { - console.log(` - For some reason '${cellArtifact}' was not created!`); - process.exit(1); - } else { - console.log(` - Build artifact created '${cellArtifact}'`); - fs.unlinkSync(fiftCellArtifact); + // make sure cell build artifact was created + if (!fs.existsSync(cellArtifact)) { + console.log(` - For some reason '${cellArtifact}' was not created!`); + process.exit(1); + } else { + console.log(` - Build artifact created '${cellArtifact}'`); + fs.unlinkSync(fiftCellArtifact); + } } + + console.log(``); } -console.log(``); +main(); // helpers diff --git a/build/deploy.ts b/build/deploy.ts new file mode 100644 index 0000000..cfcf5b8 --- /dev/null +++ b/build/deploy.ts @@ -0,0 +1,110 @@ +// This is a simple generic deploy script in TypeScript that should work for most projects without modification +// Every contract you want to deploy should have a mycontract.deploy.ts script that returns its init data +// The script assumes that it is running from the repo root, and the directories are organized this way: +// ./build/ - directory for build artifacts (mycontract.cell) and deploy init data scripts (mycontract.deploy.ts) +// ./build/deploy.config.json - JSON config file with secret mnemonic of deploying wallet (will be created if not found) + +import fs from "fs"; +import path from "path"; +import glob from "fast-glob"; +import { Address, Cell, CommonMessageInfo, contractAddress, fromNano, InternalMessage, SendMode, StateInit, toNano, TonClient, WalletContract, WalletV3R2Source } from "ton"; +import { mnemonicNew, mnemonicToWalletKey } from "ton-crypto"; +import { BN } from "bn.js"; + +async function main() { + console.log(`=================================================================`); + console.log(`Deploy script running, let's find some contracts to deploy..`); + + // make sure we have a wallet mnemonic to deploy from (or create one if not found) + const deployConfigJson = `build/deploy.config.json`; + let deployerMnemonic; + if (!fs.existsSync(deployConfigJson)) { + console.log(`\n* Config file '${deployConfigJson}' not found, creating a new wallet for deploy..`); + deployerMnemonic = (await mnemonicNew(24)).join(" "); + const deployWalletJsonContent = { created: new Date().toISOString(), deployerMnemonic }; + fs.writeFileSync(deployConfigJson, JSON.stringify(deployWalletJsonContent, null, 2)); + console.log(` - Created new wallet in '${deployConfigJson}' - keep this file secret!`); + } else { + console.log(`\n* Config file '${deployConfigJson}' found and will be used for deployment!`); + const deployConfigJsonContent = require(__dirname + "/../" + deployConfigJson); + if (!deployConfigJsonContent.deployerMnemonic) { + console.log(` - ERROR: '${deployConfigJson}' does not have the key 'deployerMnemonic'`); + process.exit(1); + } + deployerMnemonic = deployConfigJsonContent.deployerMnemonic; + } + + // open the wallet and make sure it has enough TON + const client = new TonClient({ endpoint: "https://toncenter.com/api/v2/jsonRPC" }); + const walletKey = await mnemonicToWalletKey(deployerMnemonic.split(" ")); + const walletContract = WalletContract.create(client, WalletV3R2Source.create({ publicKey: walletKey.publicKey, workchain: 0 })); + console.log(` - Wallet address used for deployment is: ${walletContract.address.toFriendly()}`); + const walletBalance = await client.getBalance(walletContract.address); + if (walletBalance.lt(toNano(1))) { + console.log(` - ERROR: Wallet has less than 1 TON for gas (${fromNano(walletBalance)} TON), please send some TON for gas first`); + process.exit(1); + } + + // go over all the contracts we have deploy scripts for + const rootContracts = glob.sync(["build/*.deploy.ts"]); + for (const rootContract of rootContracts) { + // deploy a new root contract + console.log(`\n* Found root contract to deploy '${rootContract}':`); + const contractName = path.parse(path.parse(rootContract).name).name; + + // prepare the init data cell + const deployInit = require(__dirname + "/../" + rootContract); + if (typeof deployInit.initData !== "function") { + console.log(` - ERROR: '${rootContract}' does not have 'initData()' function`); + process.exit(1); + } + const initDataCell = deployInit.initData() as Cell; + + // prepare the init code cell + const cellArtifact = `build/${contractName}.cell`; + if (!fs.existsSync(cellArtifact)) { + console.log(` - ERROR: '${cellArtifact}' not found, did you build?`); + process.exit(1); + } + const initCodeCell = Cell.fromBoc(fs.readFileSync(cellArtifact))[0]; + + // deploy by sending an internal message to the deploying wallet + sleep(1000); // to make sure we don't fail due to throttling + const newContractAddress = contractAddress({ workchain: 0, initialData: initDataCell, initialCode: initCodeCell }); + console.log(` - About to deploy contract to new address: ${newContractAddress.toFriendly()}`); + const seqno = await getSeqNo(client, walletContract.address); + sleep(1000); // to make sure we don't fail due to throttling + const transfer = await walletContract.createTransfer({ + secretKey: walletKey.secretKey, + seqno: seqno, + sendMode: SendMode.PAY_GAS_SEPARATLY + SendMode.IGNORE_ERRORS, + order: new InternalMessage({ + to: newContractAddress, + value: new BN(0.5), + bounce: false, + body: new CommonMessageInfo({ stateInit: new StateInit({ data: initDataCell, code: initCodeCell }) }), + }), + }); + await client.sendExternalMessage(walletContract, transfer); + console.log(` - Contract deployed successfully!`); + } + + console.log(``); +} + +main(); + +// helpers + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function getSeqNo(client: TonClient, walletAddress: Address) { + if (await client.isContractDeployed(walletAddress)) { + let res = await client.callGetMethod(walletAddress, "seqno"); + return parseInt(res.stack[0][1], 16); + } else { + return 0; + } +} diff --git a/build/main.deploy.ts b/build/main.deploy.ts new file mode 100644 index 0000000..79aa49e --- /dev/null +++ b/build/main.deploy.ts @@ -0,0 +1,10 @@ +import * as main from "../contracts/main"; +import { Address } from "ton"; + +// return the init Cell of the contract storage (according to load_data() contract method) +export function initData() { + return main.data({ + ownerAddress: Address.parseFriendly("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N").address, + counter: 0, + }); +} diff --git a/package-lock.json b/package-lock.json index 90c3b3f..c120fa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "prettier": "^2.6.2", "ton": "^9.6.3", "ton-contract-executor": "^0.4.8", + "ton-crypto": "^3.1.0", "ts-node": "^10.4.0", "typescript": "^4.5.4" } @@ -1438,9 +1439,9 @@ } }, "node_modules/ton-crypto": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-2.1.0.tgz", - "integrity": "sha512-PZnmCOShfgq9tCRM8E7hG8nCkpkOyZvDLPXmZN92ZEBrfTT0NKKf0imndkxG5DkgWMjc6IKfgpnEaJDH9qN6ZQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-3.1.0.tgz", + "integrity": "sha512-OgUuGoT8UKvm5jvRd/fiCo46MCrlOMt9Nr7nPQC3vkLjKmbDk+qJ4gQsO14IZwrOm5xkhDMlF5ZTVH/kN9y0gg==", "dev": true, "dependencies": { "jssha": "3.2.0", @@ -1457,6 +1458,17 @@ "jssha": "3.2.0" } }, + "node_modules/ton/node_modules/ton-crypto": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-2.1.0.tgz", + "integrity": "sha512-PZnmCOShfgq9tCRM8E7hG8nCkpkOyZvDLPXmZN92ZEBrfTT0NKKf0imndkxG5DkgWMjc6IKfgpnEaJDH9qN6ZQ==", + "dev": true, + "dependencies": { + "jssha": "3.2.0", + "ton-crypto-primitives": "2.0.0", + "tweetnacl": "1.0.3" + } + }, "node_modules/ts-node": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", @@ -2660,6 +2672,19 @@ "teslabot": "^1.3.0", "ton-crypto": "2.1.0", "tweetnacl": "1.0.3" + }, + "dependencies": { + "ton-crypto": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-2.1.0.tgz", + "integrity": "sha512-PZnmCOShfgq9tCRM8E7hG8nCkpkOyZvDLPXmZN92ZEBrfTT0NKKf0imndkxG5DkgWMjc6IKfgpnEaJDH9qN6ZQ==", + "dev": true, + "requires": { + "jssha": "3.2.0", + "ton-crypto-primitives": "2.0.0", + "tweetnacl": "1.0.3" + } + } } }, "ton-compiler": { @@ -2692,9 +2717,9 @@ } }, "ton-crypto": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-2.1.0.tgz", - "integrity": "sha512-PZnmCOShfgq9tCRM8E7hG8nCkpkOyZvDLPXmZN92ZEBrfTT0NKKf0imndkxG5DkgWMjc6IKfgpnEaJDH9qN6ZQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-3.1.0.tgz", + "integrity": "sha512-OgUuGoT8UKvm5jvRd/fiCo46MCrlOMt9Nr7nPQC3vkLjKmbDk+qJ4gQsO14IZwrOm5xkhDMlF5ZTVH/kN9y0gg==", "dev": true, "requires": { "jssha": "3.2.0", diff --git a/package.json b/package.json index fb9ede4..edfbc22 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,9 @@ "author": "", "scripts": { "prettier": "npx prettier --write '{test,contracts,build}/**/*.{ts,js,json}'", + "test": "mocha --exit test/**/*.spec.ts", "build": "ts-node ./build/build.ts", - "test": "mocha --exit test/**/*.spec.ts" + "deploy": "ts-node ./build/deploy.ts" }, "devDependencies": { "@types/bn.js": "^5.1.0", @@ -21,6 +22,7 @@ "prettier": "^2.6.2", "ton": "^9.6.3", "ton-contract-executor": "^0.4.8", + "ton-crypto": "^3.1.0", "ts-node": "^10.4.0", "typescript": "^4.5.4" },