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 { 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 {minterParams} from "./minter.deploy"; |
||||
|
||||
// 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, |
||||
let ownerAddress = Address.parse("EQD7zbEMaWC2yMgSJXmIF7HbLr1yuBo2GnZF_CJNkUiGSe32"); |
||||
let minterAddress = main.getMinterAddress(minterParams(), Cell.fromBoc(minterCode)[0]); |
||||
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() { |
||||
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
|
||||
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 { Address, toNano, TupleSlice, WalletContract } from "ton"; |
||||
import {Address, beginCell, Cell, toNano, TupleSlice, WalletContract} from "ton"; |
||||
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 initData() { |
||||
return main.data({ |
||||
ownerAddress: Address.parseFriendly("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N").address, |
||||
counter: 10, |
||||
}); |
||||
export function minterParams() { |
||||
return { |
||||
adminAddress: Address.parse("EQD7zbEMaWC2yMgSJXmIF7HbLr1yuBo2GnZF_CJNkUiGSe32"), |
||||
supply: 0, |
||||
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() { |
||||
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
|
||||
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()}`); |
||||
} |
||||
// return the init Cell of the contract storage (according to load_data() contract method)
|
||||
export function initData() { |
||||
return main.minterData(minterParams()); |
||||
} |
@ -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 { 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
|
||||
export function data(params: { ownerAddress: Address; counter: number }): Cell { |
||||
return beginCell().storeAddress(params.ownerAddress).storeUint(params.counter, 64).endCell(); |
||||
// storage#_ total_supply:Coins admin_address:MsgAddress next_admin_address:MsgAddress content:^Cell jetton_wallet_code:^Cell = Storage;
|
||||
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 getMinterAddress(params: { adminAddress: Address; supply: number, nextAdminAddress?: Address, content: Cell, wallet_code: Cell }, initCodeCell: Cell): Address { |
||||
return contractAddress({workchain: 0, initialData: minterData(params), initialCode: initCodeCell}); |
||||
} |
||||
|
||||
const OFF_CHAIN_CONTENT_PREFIX = 0x01 |
||||
|
||||
export function increment(): Cell { |
||||
return beginCell().storeUint(0x37491f2f, 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) |
||||
} |
||||
|
||||
export function deposit(): Cell { |
||||
return beginCell().storeUint(0x47d54391, 32).storeUint(0, 64).endCell(); |
||||
// 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 { |
||||
return beginCell().storeUint(0x2da38aaf, 32).storeUint(0, 64).storeAddress(params.newOwnerAddress).endCell(); |
||||
// 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]; |
||||
} |
@ -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