Browse Source

Added post deploy e2e test

master
Tal Kol 2 years ago
parent
commit
447b4c7d56
  1. 2
      README.md
  2. 58
      build/deploy.ts
  3. 20
      build/main.deploy.ts
  4. 34
      test/helpers.ts

2
README.md

@ -77,7 +77,7 @@ To setup your machine for development, please make sure you have the following:
* Deploy * Deploy
* Make sure all contracts are built and your setup is ready to 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 * 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 mainnet (production), run in terminal `npm run deploy`
* To deploy to testnet instead (where TON is free), run `npm run deploy:testnet` * To deploy to testnet instead (where TON is free), run `npm run deploy:testnet`
* Follow the on-screen instructions of the deploy script * Follow the on-screen instructions of the deploy script

58
build/deploy.ts

@ -11,27 +11,35 @@ axiosThrottle.use(axios, { requestsPerSecond: 0.5 }); // required since toncente
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import glob from "fast-glob"; 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 { mnemonicNew, mnemonicToWalletKey } from "ton-crypto";
import { postDeployTest } from "./main.deploy";
async function main() { async function main() {
console.log(`=================================================================`); console.log(`=================================================================`);
console.log(`Deploy script running, let's find some contracts to deploy..`); 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) { 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 { } 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) // make sure we have a wallet mnemonic to deploy from (or create one if not found)
const deployConfigJson = `build/deploy.config.json`; const deployConfigJson = `build/deploy.config.json`;
let deployerMnemonic; let deployerMnemonic;
if (!fs.existsSync(deployConfigJson)) { if (!fs.existsSync(deployConfigJson)) {
console.log(`\n* Config file '${deployConfigJson}' not found, creating a new wallet for deploy..`); console.log(`\n* Config file '${deployConfigJson}' not found, creating a new wallet for deploy..`);
deployerMnemonic = (await mnemonicNew(24)).join(" "); 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)); fs.writeFileSync(deployConfigJson, JSON.stringify(deployWalletJsonContent, null, 2));
console.log(` - Created new wallet in '${deployConfigJson}' - keep this file secret!`); console.log(` - Created new wallet in '${deployConfigJson}' - keep this file secret!`);
} else { } else {
@ -45,9 +53,8 @@ async function main() {
} }
// open the wallet and make sure it has enough TON // 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 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()}`); console.log(` - Wallet address used to deploy from is: ${walletContract.address.toFriendly()}`);
const walletBalance = await client.getBalance(walletContract.address); const walletBalance = await client.getBalance(walletContract.address);
if (walletBalance.lt(toNano(0.2))) { if (walletBalance.lt(toNano(0.2))) {
@ -65,19 +72,19 @@ async function main() {
const contractName = path.parse(path.parse(rootContract).name).name; const contractName = path.parse(path.parse(rootContract).name).name;
// prepare the init data cell // prepare the init data cell
const deployInit = require(__dirname + "/../" + rootContract); const deployInitScript = require(__dirname + "/../" + rootContract);
if (typeof deployInit.initData !== "function") { if (typeof deployInitScript.initData !== "function") {
console.log(` - ERROR: '${rootContract}' does not have 'initData()' function`); console.log(` - ERROR: '${rootContract}' does not have 'initData()' function`);
process.exit(1); process.exit(1);
} }
const initDataCell = deployInit.initData() as Cell; const initDataCell = deployInitScript.initData() as Cell;
// prepare the init message // prepare the init message
if (typeof deployInit.initMessage !== "function") { if (typeof deployInitScript.initMessage !== "function") {
console.log(` - ERROR: '${rootContract}' does not have 'initMessage()' function`); console.log(` - ERROR: '${rootContract}' does not have 'initMessage()' function`);
process.exit(1); process.exit(1);
} }
const initMessageCell = deployInit.initMessage() as Cell | null; const initMessageCell = deployInitScript.initMessage() as Cell | null;
// prepare the init code cell // prepare the init code cell
const cellArtifact = `build/${contractName}.cell`; const cellArtifact = `build/${contractName}.cell`;
@ -88,10 +95,11 @@ async function main() {
const initCodeCell = Cell.fromBoc(fs.readFileSync(cellArtifact))[0]; const initCodeCell = Cell.fromBoc(fs.readFileSync(cellArtifact))[0];
// make sure the contract was not already deployed // 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()}`); console.log(` - Based on your init code+data, your new contract address is: ${newContractAddress.toFriendly()}`);
if (await client.isContractDeployed(newContractAddress)) { 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; continue;
} }
@ -104,7 +112,7 @@ async function main() {
sendMode: SendMode.PAY_GAS_SEPARATLY + SendMode.IGNORE_ERRORS, sendMode: SendMode.PAY_GAS_SEPARATLY + SendMode.IGNORE_ERRORS,
order: new InternalMessage({ order: new InternalMessage({
to: newContractAddress, 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, bounce: false,
body: new CommonMessageInfo({ body: new CommonMessageInfo({
stateInit: new StateInit({ data: initDataCell, code: initCodeCell }), stateInit: new StateInit({ data: initDataCell, code: initCodeCell }),
@ -116,16 +124,21 @@ async function main() {
console.log(` - Deploy transaction sent successfully`); console.log(` - Deploy transaction sent successfully`);
// make sure that the contract was deployed // make sure that the contract was deployed
console.log(` - Waiting 10 seconds to check if the contract was actually deployed..`); console.log(` - Block explorer link: https://${process.env.TESTNET ? "test." : ""}tonwhales.com/explorer/address/${newContractAddress.toFriendly()}`);
await sleep(10000); 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)) { if (await client.isContractDeployed(newContractAddress)) {
console.log(` - SUCCESS! Contract deployed successfully to address: ${newContractAddress.toFriendly()}`); console.log(` - SUCCESS! Contract deployed successfully to address: ${newContractAddress.toFriendly()}`);
const contractBalance = await client.getBalance(newContractAddress); const contractBalance = await client.getBalance(newContractAddress);
console.log(` - New contract balance is now ${fromNano(contractBalance)} TON, make sure it has enough to pay rent`); 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 { } else {
console.log(` - FAILURE! Contract address still looks uninitialized: ${newContractAddress.toFriendly()}`); 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(``); console.log(``);
@ -138,3 +151,12 @@ main();
function sleep(ms: number) { function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)); 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);
}

20
build/main.deploy.ts

@ -1,5 +1,6 @@
import * as main from "../contracts/main"; 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) // return the init Cell of the contract storage (according to load_data() contract method)
export function initData() { 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() { export function initMessage() {
return main.increment(); 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()}`);
}

34
test/helpers.ts

@ -1,5 +1,5 @@
import BN from "bn.js"; 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 { SmartContract } from "ton-contract-executor";
import Prando from "prando"; import Prando from "prando";
@ -14,6 +14,7 @@ export function randomAddress(seed: string, workchain?: number) {
return new Address(workchain ?? 0, hash); 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 }) { export function internalMessage(params: { from?: Address; to?: Address; value?: BN; bounce?: boolean; body?: Cell }) {
const message = params.body ? new CellMessage(params.body) : undefined; const message = params.body ? new CellMessage(params.body) : undefined;
return new InternalMessage({ 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) { export function setBalance(contract: SmartContract, balance: BN) {
contract.setC7Config({ contract.setC7Config({
balance: balance.toNumber(), 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));
}

Loading…
Cancel
Save