Lev
2 years ago
12 changed files with 486 additions and 504 deletions
@ -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); |
@ -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()}`); |
|
||||||
} |
} |
||||||
|
@ -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()}`); |
|
||||||
} |
|
@ -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()}`); |
|
||||||
} |
} |
@ -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; |
|
||||||
} |
|
@ -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 |
|
@ -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) |
||||||
|
|
||||||
|
if (chunks[i + 1]) { |
||||||
|
let nextCell = new Cell() |
||||||
|
curCell.refs.push(nextCell) |
||||||
|
curCell = nextCell |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return rootCell |
||||||
|
} |
||||||
|
|
||||||
export function increment(): Cell { |
export function getMinterAddress(params: { adminAddress: Address; supply: number, nextAdminAddress?: Address, content: Cell, wallet_code: Cell }, initCodeCell: Cell): Address { |
||||||
return beginCell().storeUint(0x37491f2f, 32).storeUint(0, 64).endCell(); |
return contractAddress({workchain: 0, initialData: minterData(params), initialCode: initCodeCell}); |
||||||
} |
} |
||||||
|
|
||||||
export function deposit(): Cell { |
const OFF_CHAIN_CONTENT_PREFIX = 0x01 |
||||||
return beginCell().storeUint(0x47d54391, 32).storeUint(0, 64).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) |
||||||
|
} |
||||||
|
|
||||||
|
// storage#_ balance:(VarUInteger 16) owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage;
|
||||||
|
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 withdraw(params: { withdrawAmount: BN }): Cell { |
|
||||||
return beginCell().storeUint(0x41836980, 32).storeUint(0, 64).storeCoins(params.withdrawAmount).endCell(); |
export function TON(): number { |
||||||
|
return 1000000000; |
||||||
} |
} |
||||||
|
|
||||||
export function transferOwnership(params: { newOwnerAddress: Address }): Cell { |
// message encoders for all ops (see contracts/imports/constants.fc for consts)
|
||||||
return beginCell().storeUint(0x2da38aaf, 32).storeUint(0, 64).storeAddress(params.newOwnerAddress).endCell(); |
|
||||||
|
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]; |
||||||
} |
} |
@ -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)); |
||||||
|
} |
Loading…
Reference in new issue