Browse Source

Wrote deployment and stuff

master
Lev 2 years ago
parent
commit
74539dc653
  1. 32
      build/cli.js
  2. 28
      build/jetton-wallet.deploy.ts
  3. 31
      build/main.deploy.ts
  4. 36
      build/minter.deploy.ts
  5. 104
      contracts/main.fc
  6. 16
      contracts/main.tlb
  7. 100
      contracts/main.ts
  8. 2
      contracts/minter.fc
  9. 31
      contracts/utils.ts
  10. 562
      package-lock.json
  11. 16
      package.json
  12. 30
      test/helpers.ts

32
build/cli.js

@ -0,0 +1,32 @@
const main = require('../contracts/main');
const subcommand = require('subcommand');
const {Address, SendMode, InternalMessage, CommonMessageInfo, StateInit, CellMessage} = require("ton");
const {getWallet} = require("../contracts/main");
const {sendInternalMessageWithWallet} = require("../contracts/utils");
const argv = process.argv.slice(2);
let commands = [
{
name: 'deposit',
options: [
{
name: 'minter',
help: 'The jetton address'
},
{
name: 'amount',
}
],
command: async function deposit(args) {
let [walletContract, walletKey] = await getWallet();
await sendInternalMessageWithWallet({
walletContract, secretKey: walletKey.secretKey, value: (parseFloat(args._[1]) + 0.2) * main.TON(), to: Address.parse(args._[0]),
body: main.depositMsg((parseFloat(args._[1]) * main.TON()))
})
}
}
]
let match = subcommand(commands);
let opts = match(argv);

28
build/jetton-wallet.deploy.ts

@ -1,31 +1,27 @@
import * as main from "../contracts/main"; import * as main from "../contracts/main";
import { Address, toNano, TupleSlice, WalletContract } from "ton"; import {Address, Cell, toNano, TupleSlice, WalletContract} from "ton";
import { hex as jettonWalletCodeCell } from "../build/jetton-wallet.compiled.json";
import { hex as minterCode } from "../build/minter.compiled.json";
import { sendInternalMessageWithWallet } from "../test/helpers"; import { sendInternalMessageWithWallet } from "../test/helpers";
import {minterParams} from "./minter.deploy";
// 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() {
return main.data({ let ownerAddress = Address.parse("EQD7zbEMaWC2yMgSJXmIF7HbLr1yuBo2GnZF_CJNkUiGSe32");
ownerAddress: Address.parseFriendly("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N").address, let minterAddress = main.getMinterAddress(minterParams(), Cell.fromBoc(minterCode)[0]);
counter: 10, return main.walletData({
balance: 0,
ownerAddress,
jettonMasterAddress: minterAddress,
jettonWalletCode: Cell.fromBoc(jettonWalletCodeCell)[0],
}); });
} }
// 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 null;
} }
// optional end-to-end sanity test for the actual on-chain contract to see it is actually working on-chain // 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) { 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()}`);
} }

31
build/main.deploy.ts

@ -1,31 +0,0 @@
import * as main from "../contracts/main";
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() {
return main.data({
ownerAddress: Address.parseFriendly("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N").address,
counter: 10,
});
}
// 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()}`);
}

36
build/minter.deploy.ts

@ -1,31 +1,23 @@
import * as main from "../contracts/main"; import * as main from "../contracts/main";
import { Address, toNano, TupleSlice, WalletContract } from "ton"; import {Address, beginCell, Cell, toNano, TupleSlice, WalletContract} from "ton";
import { sendInternalMessageWithWallet } from "../test/helpers"; import { sendInternalMessageWithWallet } from "../test/helpers";
import { hex as walletCode } from "../build/jetton-wallet.compiled.json";
import {encodeOffChainContent} from "../contracts/main";
// return the init Cell of the contract storage (according to load_data() contract method) export function minterParams() {
export function initData() { return {
return main.data({ adminAddress: Address.parse("EQD7zbEMaWC2yMgSJXmIF7HbLr1yuBo2GnZF_CJNkUiGSe32"),
ownerAddress: Address.parseFriendly("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N").address, supply: 0,
counter: 10, content: encodeOffChainContent(""),
}); wallet_code: Cell.fromBoc(walletCode)[0],
};
} }
// 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 null;
} }
// optional end-to-end sanity test for the actual on-chain contract to see it is actually working on-chain // return the init Cell of the contract storage (according to load_data() contract method)
export async function postDeployTest(walletContract: WalletContract, secretKey: Buffer, contractAddress: Address) { export function initData() {
const call = await walletContract.client.callGetMethod(contractAddress, "counter"); return main.minterData(minterParams());
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()}`);
} }

104
contracts/main.fc

@ -1,104 +0,0 @@
#pragma version >=0.2.0;
#include "imports/stdlib.fc";
#include "imports/constants.fc";
#include "imports/utils.fc";
;; =============== storage =============================
;; storage binary format is defined as TL-B in companion .tlb file
(slice, int) load_data() inline {
var ds = get_data().begin_parse();
return (
ds~load_msg_addr(), ;; owner_address
ds~load_uint(64) ;; counter
);
}
() save_data(slice owner_address, int counter) impure inline {
set_data(begin_cell()
.store_slice(owner_address)
.store_uint(counter, 64)
.end_cell());
}
;; =============== messages =============================
;; message binary format is defined as TL-B in companion .tlb file
() op_withdraw(int withdraw_amount, slice owner_address) impure;
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
;; parse incoming internal message
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
slice sender_address = cs~load_msg_addr();
;; handle bounced messages
if (flags & 1) {
return (); ;; ignore
}
;; load from contract storage
var (owner_address, counter) = load_data();
;; handle operations
int op = in_msg_body~load_uint(32);
int query_id = in_msg_body~load_uint(64);
if (op == op::increment) {
save_data(owner_address, counter + 1);
return ();
}
if (op == op::deposit) {
;; empty since ton received (msg_value) is added automatically to contract balance
;; ~dump msg_value; ;; an example of debug output, requires running contract in debug mode
return ();
}
if (op == op::withdraw) {
throw_unless(error::access_denied, equal_slices(sender_address, owner_address));
int withdraw_amount = in_msg_body~load_coins();
op_withdraw(withdraw_amount, owner_address);
return ();
}
if (op == op::transfer_ownership) {
throw_unless(error::access_denied, equal_slices(sender_address, owner_address));
slice new_owner_address = in_msg_body~load_msg_addr();
save_data(new_owner_address, counter);
return ();
}
throw(error::unknown_op);
}
() op_withdraw(int withdraw_amount, slice owner_address) impure {
var [balance, _] = get_balance();
throw_unless(error::insufficient_balance, balance >= withdraw_amount);
int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);
send_grams(owner_address, return_value);
}
;; =============== getters =============================
int meaning_of_life() method_id {
return 42;
}
slice owner_address() method_id {
var (owner_address, _) = load_data();
return owner_address;
}
int counter() method_id {
var (_, counter) = load_data();
return counter;
}
int balance() method_id {
var [balance, _] = get_balance();
return balance;
}

16
contracts/main.tlb

@ -1,16 +0,0 @@
// https://ton.org/docs/#/overviews/TL-B
// base types defined in https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb
// storage (according to save_data() contract method)
storage#_ owner_address:MsgAddress counter:uint64 = Storage
// ops
increment query_id:uint64 = InternalMsgBody
deposit query_id:uint64 = InternalMsgBody
withdraw query_id:uint64 withdraw_amount:Grams = InternalMsgBody
transfer_ownership query_id:uint64 new_owner_address:MsgAddress = InternalMsgBody

100
contracts/main.ts

@ -1,25 +1,101 @@
import BN from "bn.js"; import BN from "bn.js";
import { Cell, beginCell, Address } from "ton"; import {Address, beginCell, Cell, contractAddress, TonClient, WalletContract, WalletV3R2Source} from "ton";
import {mnemonicToWalletKey} from "ton-crypto";
// encode contract storage according to save_data() contract method // encode contract storage according to save_data() contract method
export function data(params: { ownerAddress: Address; counter: number }): Cell { // storage#_ total_supply:Coins admin_address:MsgAddress next_admin_address:MsgAddress content:^Cell jetton_wallet_code:^Cell = Storage;
return beginCell().storeAddress(params.ownerAddress).storeUint(params.counter, 64).endCell(); export function minterData(params: { adminAddress: Address; supply: number, nextAdminAddress?: Address, content: Cell, wallet_code: Cell }): Cell {
if (!params.nextAdminAddress) params.nextAdminAddress = params.adminAddress;
return beginCell()
.storeCoins(params.supply)
.storeAddress(params.adminAddress)
.storeAddress(params.nextAdminAddress)
.storeRef(params.content)
.storeRef(params.wallet_code)
.endCell();
} }
// message encoders for all ops (see contracts/imports/constants.fc for consts) function bufferToChunks(buff: Buffer, chunkSize: number) {
let chunks: Buffer[] = []
while (buff.byteLength > 0) {
chunks.push(buff.slice(0, chunkSize))
buff = buff.slice(chunkSize)
}
return chunks
}
export function makeSnakeCell(data: Buffer) {
let chunks = bufferToChunks(data, 127)
let rootCell = new Cell()
let curCell = rootCell
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i]
curCell.bits.writeBuffer(chunk)
export function increment(): Cell { if (chunks[i + 1]) {
return beginCell().storeUint(0x37491f2f, 32).storeUint(0, 64).endCell(); let nextCell = new Cell()
curCell.refs.push(nextCell)
curCell = nextCell
}
}
return rootCell
} }
export function deposit(): Cell { export function getMinterAddress(params: { adminAddress: Address; supply: number, nextAdminAddress?: Address, content: Cell, wallet_code: Cell }, initCodeCell: Cell): Address {
return beginCell().storeUint(0x47d54391, 32).storeUint(0, 64).endCell(); return contractAddress({workchain: 0, initialData: minterData(params), initialCode: initCodeCell});
} }
export function withdraw(params: { withdrawAmount: BN }): Cell { const OFF_CHAIN_CONTENT_PREFIX = 0x01
return beginCell().storeUint(0x41836980, 32).storeUint(0, 64).storeCoins(params.withdrawAmount).endCell();
export function encodeOffChainContent(content: string) {
let data = Buffer.from(content)
let offChainPrefix = Buffer.from([OFF_CHAIN_CONTENT_PREFIX])
data = Buffer.concat([offChainPrefix, data])
return makeSnakeCell(data)
} }
export function transferOwnership(params: { newOwnerAddress: Address }): Cell { // storage#_ balance:(VarUInteger 16) owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage;
return beginCell().storeUint(0x2da38aaf, 32).storeUint(0, 64).storeAddress(params.newOwnerAddress).endCell(); export function walletData(params: { balance: number, ownerAddress: Address, jettonMasterAddress: Address, jettonWalletCode: Cell }): Cell {
return beginCell()
.storeUint(params.balance, 15)
.storeAddress(params.ownerAddress)
.storeAddress(params.jettonMasterAddress)
.storeRef(params.jettonWalletCode)
.endCell();
}
export function TON(): number {
return 1000000000;
}
// message encoders for all ops (see contracts/imports/constants.fc for consts)
export function depositMsg(amount: number): Cell {
return beginCell().storeUint(0x77a33521, 32)
.storeUint(0, 64).storeCoins(amount).endCell();
}
export function withdrawMsg(params: { withdrawAmount: BN }): Cell {
return beginCell().storeUint(0x47d1895f, 32).storeUint(0, 64).storeCoins(params.withdrawAmount).endCell();
}
export async function getWallet(isTestnet: boolean = true) {
const client = new TonClient({
endpoint: `https://${isTestnet ? "testnet." : ""}toncenter.com/api/v2/jsonRPC`,
apiKey: isTestnet ? 'bed2b4589201ff3c575be653593f912a337c08eed416b60b02345763b9ee9c36' : 'a1e8a1055a387515158589dc7e9bad3035e7db2b9f9ea5cdad6b727f71e328db'
});
let deployerMnemonic = process.env.DEPLOYER_MNEMONIC;
if (!deployerMnemonic) throw new Error("DEPLOYER_MNEMONIC env var is not set");
// open the wallet and make sure it has enough TON
const walletKey = await mnemonicToWalletKey(deployerMnemonic.split(" "));
let workchain = 0;
return [WalletContract.create(client, WalletV3R2Source.create({
publicKey: walletKey.publicKey,
workchain
})), walletKey];
} }

2
contracts/minter.fc

@ -170,7 +170,7 @@ int deposit_gas_consumption() asm "35000000 PUSHINT"; ;; 0.035 TON
} }
if (op == op::provide_wallet_address) { if (op == op::provide_wallet_address) {
throw_unless(75, msg_value > fwd_fee + const::provide_address_gas_consumption); throw_unless(75, msg_value > fwd_fee + gas_consumption());
slice owner_address = in_msg_body~load_msg_addr(); slice owner_address = in_msg_body~load_msg_addr();
int include_address? = in_msg_body~load_uint(1); int include_address? = in_msg_body~load_uint(1);

31
contracts/utils.ts

@ -0,0 +1,31 @@
import { Address, Cell, CellMessage, InternalMessage, CommonMessageInfo, WalletContract, SendMode, Wallet } from "ton";
import BN from "bn.js";
// 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));
}

562
package-lock.json generated

File diff suppressed because it is too large Load Diff

16
package.json

@ -1,5 +1,5 @@
{ {
"name": "tonb", "name": "bton",
"description": "", "description": "",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
@ -10,10 +10,11 @@
"build": "ts-node ./build/_build.ts", "build": "ts-node ./build/_build.ts",
"deploy": "ts-node ./build/_deploy.ts", "deploy": "ts-node ./build/_deploy.ts",
"deploy:testnet": "ts-node ./build/_deploy.ts", "deploy:testnet": "ts-node ./build/_deploy.ts",
"postinstall": "ts-node ./build/_setup.ts" "postinstall": "ts-node ./build/_setup.ts",
"cli": "node --no-warnings --loader ts-node/esm build/cli.js "
}, },
"devDependencies": { "devDependencies": {
"@swc/core": "^1.2.177", "@swc/core": "^1.3.25",
"@types/bn.js": "^5.1.0", "@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0", "@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
@ -26,10 +27,10 @@
"mocha": "^9.1.3", "mocha": "^9.1.3",
"prando": "^6.0.1", "prando": "^6.0.1",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"ton": "^12.1.3", "ton": "^12.3.3",
"ton-contract-executor": "^0.4.8", "ton-contract-executor": "^0.4.8",
"ton-crypto": "^3.1.0", "ton-crypto": "^3.1.0",
"ts-node": "^10.4.0", "ts-node": "^10.9.1",
"typescript": "^4.5.4" "typescript": "^4.5.4"
}, },
"prettier": { "prettier": {
@ -46,6 +47,9 @@
"node": ">=16.15.0" "node": ">=16.15.0"
}, },
"dependencies": { "dependencies": {
"semver": "^7.3.7" "@tonconnect/protocol": "^2.0.1",
"semver": "^7.3.7",
"subcommand": "^2.1.1",
"ton-compiler": "^2.0.0"
} }
} }

30
test/helpers.ts

@ -2,6 +2,7 @@ import BN from "bn.js";
import { Address, Cell, CellMessage, InternalMessage, CommonMessageInfo, WalletContract, SendMode, Wallet } 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";
import { sendInternalMessageWithWallet} from "../contracts/utils";
export const zeroAddress = new Address(0, Buffer.alloc(32, 0)); export const zeroAddress = new Address(0, Buffer.alloc(32, 0));
@ -33,31 +34,4 @@ export function setBalance(contract: SmartContract, balance: BN) {
}); });
} }
// helper for end-to-end on-chain tests (normally post deploy) to allow sending InternalMessages to contracts using a wallet export { sendInternalMessageWithWallet };
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