diff --git a/README.md b/README.md index 7098c58..2d7e321 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ To setup your machine for development, please make sure you have the following: * Deploy * Make sure all contracts are built and your setup is ready to deploy: * 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` (it will be created if not found) + * The deployment wallet is configured in `build/deploy.config.json` (file will be created if not found) * To deploy to mainnet (production), run in terminal `npm run deploy` * To deploy to testnet instead (where TON is free), run `npm run deploy:testnet` * Follow the on-screen instructions of the deploy script diff --git a/build/deploy.ts b/build/deploy.ts index 7389c21..a687010 100644 --- a/build/deploy.ts +++ b/build/deploy.ts @@ -11,27 +11,35 @@ axiosThrottle.use(axios, { requestsPerSecond: 0.5 }); // required since toncente import fs from "fs"; import path from "path"; import glob from "fast-glob"; -import { Cell, CellMessage, CommonMessageInfo, contractAddress, fromNano, InternalMessage, SendMode, StateInit, toNano, TonClient, WalletContract, WalletV3R2Source } from "ton"; +import { Address, Cell, CellMessage, CommonMessageInfo, fromNano, InternalMessage, StateInit, toNano } from "ton"; +import { TonClient, WalletContract, WalletV3R2Source, contractAddress, SendMode } from "ton"; import { mnemonicNew, mnemonicToWalletKey } from "ton-crypto"; +import { postDeployTest } from "./main.deploy"; async function main() { console.log(`=================================================================`); console.log(`Deploy script running, let's find some contracts to deploy..`); - // check some global settings + // check input arguments (given through environment variables) if (process.env.TESTNET) { - console.log(`\n* We are deploying to 'testnet' (https://t.me/testgiver_ton_bot will give you test TON)`); + console.log(`\n* We are working with 'testnet' (https://t.me/testgiver_ton_bot will give you test TON)`); } else { - console.log(`\n* We are deploying to 'mainnet'`); + console.log(`\n* We are working with 'mainnet'`); } + // initialize globals + const client = new TonClient({ endpoint: `https://${process.env.TESTNET ? "testnet." : ""}toncenter.com/api/v2/jsonRPC` }); + const deployerWalletType = "org.ton.wallets.v3.r2"; // 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 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) 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 }; + const deployWalletJsonContent = { created: new Date().toISOString(), deployerWalletType, deployerMnemonic }; fs.writeFileSync(deployConfigJson, JSON.stringify(deployWalletJsonContent, null, 2)); console.log(` - Created new wallet in '${deployConfigJson}' - keep this file secret!`); } else { @@ -45,9 +53,8 @@ async function main() { } // open the wallet and make sure it has enough TON - const client = new TonClient({ endpoint: `https://${process.env.TESTNET ? "testnet." : ""}toncenter.com/api/v2/jsonRPC` }); const walletKey = await mnemonicToWalletKey(deployerMnemonic.split(" ")); - const walletContract = WalletContract.create(client, WalletV3R2Source.create({ publicKey: walletKey.publicKey, workchain: 0 })); + 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))) { @@ -65,19 +72,19 @@ async function main() { const contractName = path.parse(path.parse(rootContract).name).name; // prepare the init data cell - const deployInit = require(__dirname + "/../" + rootContract); - if (typeof deployInit.initData !== "function") { + const deployInitScript = require(__dirname + "/../" + rootContract); + if (typeof deployInitScript.initData !== "function") { console.log(` - ERROR: '${rootContract}' does not have 'initData()' function`); process.exit(1); } - const initDataCell = deployInit.initData() as Cell; + const initDataCell = deployInitScript.initData() as Cell; // prepare the init message - if (typeof deployInit.initMessage !== "function") { + if (typeof deployInitScript.initMessage !== "function") { console.log(` - ERROR: '${rootContract}' does not have 'initMessage()' function`); process.exit(1); } - const initMessageCell = deployInit.initMessage() as Cell | null; + const initMessageCell = deployInitScript.initMessage() as Cell | null; // prepare the init code cell const cellArtifact = `build/${contractName}.cell`; @@ -88,10 +95,11 @@ async function main() { const initCodeCell = Cell.fromBoc(fs.readFileSync(cellArtifact))[0]; // make sure the contract was not already deployed - const newContractAddress = contractAddress({ workchain: 0, initialData: initDataCell, initialCode: initCodeCell }); + const newContractAddress = contractAddress({ workchain, initialData: initDataCell, initialCode: initCodeCell }); console.log(` - Based on your init code+data, your new contract address is: ${newContractAddress.toFriendly()}`); if (await client.isContractDeployed(newContractAddress)) { - console.log(` - Looks like the contract is already deployed in this address, skipping`); + console.log(` - Looks like the contract is already deployed in this address, skipping deployment`); + await performPostDeploymentTest(rootContract, deployInitScript, walletContract, walletKey.secretKey, newContractAddress); continue; } @@ -104,7 +112,7 @@ async function main() { sendMode: SendMode.PAY_GAS_SEPARATLY + SendMode.IGNORE_ERRORS, order: new InternalMessage({ to: newContractAddress, - value: toNano(0.02), // this will almost in full be the balance of the new contract and allow it to pay rent + value: newContractFunding, bounce: false, body: new CommonMessageInfo({ stateInit: new StateInit({ data: initDataCell, code: initCodeCell }), @@ -116,16 +124,21 @@ async function main() { console.log(` - Deploy transaction sent successfully`); // make sure that the contract was deployed - console.log(` - Waiting 10 seconds to check if the contract was actually deployed..`); - await sleep(10000); + console.log(` - Block explorer link: https://${process.env.TESTNET ? "test." : ""}tonwhales.com/explorer/address/${newContractAddress.toFriendly()}`); + 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(); + if (seqnoAfter > seqno) break; + } if (await client.isContractDeployed(newContractAddress)) { console.log(` - SUCCESS! Contract deployed successfully to address: ${newContractAddress.toFriendly()}`); 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); } else { console.log(` - FAILURE! Contract address still looks uninitialized: ${newContractAddress.toFriendly()}`); } - console.log(` - Block explorer link: https://${process.env.TESTNET ? "test." : ""}tonwhales.com/explorer/address/${newContractAddress.toFriendly()}`); } console.log(``); @@ -138,3 +151,12 @@ main(); function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } + +async function performPostDeploymentTest(rootContract: string, deployInitScript: any, walletContract: WalletContract, secretKey: Buffer, newContractAddress: Address) { + 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 postDeployTest(walletContract, secretKey, newContractAddress); +} diff --git a/build/main.deploy.ts b/build/main.deploy.ts index 05a6497..ec05f82 100644 --- a/build/main.deploy.ts +++ b/build/main.deploy.ts @@ -1,5 +1,6 @@ import * as main from "../contracts/main"; -import { Address } from "ton"; +import { Address, toNano, TupleSlice, WalletContract } from "ton"; +import { sendInternalMessageWithWallet } from "../test/helpers"; // return the init Cell of the contract storage (according to load_data() contract method) export function initData() { @@ -9,7 +10,22 @@ export function initData() { }); } -// return the op that should be sent to the contract on deployment, can be "null" so send an empty message +// return the op that should be sent to the contract on deployment, can be "null" to send an empty message export function initMessage() { return main.increment(); } + +// 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(); + console.log(` # Getter 'counter' = ${counter.toString()}`); + + const message = main.increment(); + await sendInternalMessageWithWallet({ walletContract, secretKey, to: contractAddress, value: toNano(0.02), body: message }); + console.log(` # Sent 'increment' op message`); + + const call2 = await walletContract.client.callGetMethod(contractAddress, "counter"); + const counter2 = new TupleSlice(call2.stack).readBigNumber(); + console.log(` # Getter 'counter' = ${counter2.toString()}`); +} diff --git a/test/helpers.ts b/test/helpers.ts index d57397f..7874944 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,5 +1,5 @@ import BN from "bn.js"; -import { Address, Cell, CellMessage, InternalMessage, CommonMessageInfo } from "ton"; +import { Address, Cell, CellMessage, InternalMessage, CommonMessageInfo, WalletContract, SendMode, Wallet } from "ton"; import { SmartContract } from "ton-contract-executor"; import Prando from "prando"; @@ -14,6 +14,7 @@ export function randomAddress(seed: string, workchain?: number) { return new Address(workchain ?? 0, hash); } +// used with ton-contract-executor (unit tests) to sendInternalMessage easily export function internalMessage(params: { from?: Address; to?: Address; value?: BN; bounce?: boolean; body?: Cell }) { const message = params.body ? new CellMessage(params.body) : undefined; return new InternalMessage({ @@ -25,9 +26,38 @@ export function internalMessage(params: { from?: Address; to?: Address; value?: }); } -// temp fix until ton-contract-executor remembers c7 value between calls +// temp fix until ton-contract-executor (unit tests) remembers c7 value between calls export function setBalance(contract: SmartContract, balance: BN) { contract.setC7Config({ balance: balance.toNumber(), }); } + +// helper for end-to-end on-chain tests (normally post deploy) to allow sending InternalMessages to contracts using a wallet +export async function sendInternalMessageWithWallet(params: { walletContract: WalletContract; secretKey: Buffer; to: Address; value: BN; bounce?: boolean; body?: Cell }) { + const message = params.body ? new CellMessage(params.body) : undefined; + const seqno = await params.walletContract.getSeqNo(); + const transfer = params.walletContract.createTransfer({ + secretKey: params.secretKey, + seqno: seqno, + sendMode: SendMode.PAY_GAS_SEPARATLY + SendMode.IGNORE_ERRORS, + order: new InternalMessage({ + to: params.to, + value: params.value, + bounce: params.bounce ?? false, + body: new CommonMessageInfo({ + body: message, + }), + }), + }); + await params.walletContract.client.sendExternalMessage(params.walletContract, transfer); + for (let attempt = 0; attempt < 10; attempt++) { + await sleep(2000); + const seqnoAfter = await params.walletContract.getSeqNo(); + if (seqnoAfter > seqno) return; + } +} + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +}