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
* 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

58
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);
}

20
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()}`);
}

34
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));
}

Loading…
Cancel
Save