From a19790a8decd4aae55e2fd388d15b70ccaa635b5 Mon Sep 17 00:00:00 2001 From: ennucore Date: Wed, 1 Feb 2023 16:11:14 +0100 Subject: [PATCH] Started the contract, wrote deploy and interactions, tests (do not pass yet) --- sources/jetton.deploy.ts | 54 ++---------- sources/jetton.spec.ts | 27 ------ sources/jetton.tact | 20 ++++- sources/output/jetton_JettonDefaultWallet.ts | 4 +- .../tests/__snapshots__/jetton.spec.ts.snap | 29 +++++++ sources/tests/jetton.spec.ts | 36 ++++++++ sources/tests/scenario.ts | 10 +++ sources/utils/config.ts | 42 ++++++++++ .../utils/{jetton-helpers.ts => helpers.ts} | 19 ++++- sources/utils/interactions.ts | 83 +++++++++++++++++++ sources/utils/rmlogs.sh | 3 + sources/wallet.tact | 2 +- yarn.lock | 2 +- 13 files changed, 250 insertions(+), 81 deletions(-) delete mode 100644 sources/jetton.spec.ts create mode 100644 sources/tests/__snapshots__/jetton.spec.ts.snap create mode 100644 sources/tests/jetton.spec.ts create mode 100644 sources/tests/scenario.ts create mode 100644 sources/utils/config.ts rename sources/utils/{jetton-helpers.ts => helpers.ts} (80%) create mode 100644 sources/utils/interactions.ts create mode 100644 sources/utils/rmlogs.sh diff --git a/sources/jetton.deploy.ts b/sources/jetton.deploy.ts index 645f664..056d92d 100644 --- a/sources/jetton.deploy.ts +++ b/sources/jetton.deploy.ts @@ -1,57 +1,22 @@ import { beginCell, contractAddress, toNano, TonClient, TonClient4, Address, WalletContractV4, internal, fromNano, Cell} from "ton"; import {mnemonicToPrivateKey} from "ton-crypto"; -import {buildOnchainMetadata} from "./utils/jetton-helpers"; +import {buildOnchainMetadata} from "./utils/helpers"; import {TONB} from "./output/jetton_TONB"; +import {client, wallet_data, workchain, owner, jettonParams, default_content} from "./utils/config"; (async () => { //need changes for jetton - //create client for testnet Toncenter API - const client = new TonClient({ - endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC', - apiKey: 'bb38df0c2756c66e2ab49f064e2484ec444b01244d2bd49793bd5b58f61ae3d2' - }) - //create client for testnet sandboxv4 API - alternative endpoint const client4 = new TonClient4({ endpoint: "https://sandbox-v4.tonhubapi.com" }) - - // Insert your test wallet's 24 words, make sure you have some test Toncoins on its balance. Every deployment spent 0.5 test toncoin. - let mnemonics = "basic security merge opera inject core melody polar become force cool glance history order warfare consider company slim twice balcony scare shoot winner rude"; - // read more about wallet apps https://ton.org/docs/participate/wallets/apps#tonhub-test-environment - - let keyPair = await mnemonicToPrivateKey(mnemonics.split(" ")); - let secretKey = keyPair.secretKey; - //workchain = 1 - masterchain (expensive operation cost, validator's election contract works here) - //workchain = 0 - basechain (normal operation cost, user's contracts works here) - let workchain = 0; //we are working in basechain. - - //Create deployment wallet contract - let wallet = WalletContractV4.create({ workchain, publicKey: keyPair.publicKey}); - let contract = client.open(wallet); + let {my_wallet, secretKey} = await wallet_data(); // Get deployment wallet balance - let balance: bigint = await contract.getBalance(); - - // This is example data - Modify these params for your own jetton - // - Data is stored on-chain (except for the image data itself) - - const jettonParams = { - name: "TactJet", - description: "This is description of Test tact jetton", - image: "https://ipfs.io/ipfs/QmbPZjC1tuP6ickCCBtoTCQ9gc3RpkbKx7C1LMYQdcLwti" // Image url - }; - - // Owner should usually be the deploying wallet's address. - let owner = Address.parse('EQAuAiFGgkxoQvBWXjXQcLYb8BW4fO6UJkt6_uCONJ2y5VUk'); - - - // Create content Cell - let content = buildOnchainMetadata(jettonParams); - + let balance: bigint = await my_wallet.getBalance(); // Compute init data for deployment - let init = await TONB.init(owner, content); + let init = await TONB.init(owner, default_content); let destination_address = contractAddress(workchain, init); @@ -61,15 +26,13 @@ import {TONB} from "./output/jetton_TONB"; // send a message on new address contract to deploy it - let seqno: number = await contract.getSeqno(); + let seqno: number = await my_wallet.getSeqno(); - //TL-B mint#01fb345b amount:int257 = Mint - let msg = beginCell().storeBuffer(Buffer.from("01fb345b", "hex")).storeInt(amount, 257).endCell(); console.log('🛠️Preparing new outgoing massage from deployment wallet. Seqno = ', seqno); console.log('Current deployment wallet balance = ', fromNano(balance).toString(), '💎TON'); console.log('Totally supply for deployed Token = ', supply, ', amount = ', amount.toString()); - await contract.sendTransfer({ + await my_wallet.sendTransfer({ seqno, secretKey, messages: [internal({ @@ -78,8 +41,7 @@ import {TONB} from "./output/jetton_TONB"; init: { code : init.code, data : init.data - }, - body: msg + } })] }); console.log('======deployment message sent to ', destination_address, ' ======'); diff --git a/sources/jetton.spec.ts b/sources/jetton.spec.ts deleted file mode 100644 index 7057e14..0000000 --- a/sources/jetton.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { toNano, beginCell } from "ton"; -import { ContractSystem } from "ton-emulator"; -import {TONB} from './output/jetton_TONB'; - -describe('jetton', () => { - it('should deploy', async () => { - - // Create jetton - let system = await ContractSystem.create(); - let owner = system.treasure('owner'); - - let contract = system.open(await TONB.fromInit(owner.address, null)); - let tracker = system.track(contract.address); - - // Mint - await contract.send(owner, { value: toNano(1) }, { $$type: 'Mint', amount: toNano(1000000) }); - await system.run(); - expect(tracker.events()).toMatchSnapshot(); - - // Check owner - expect((await contract.getOwner()).toString()).toEqual(owner.address.toString()); - - // Data - let data = await contract.getGetJettonData(); - // console.warn(data); - }); -}); \ No newline at end of file diff --git a/sources/jetton.tact b/sources/jetton.tact index 9f386ee..e8eb80a 100644 --- a/sources/jetton.tact +++ b/sources/jetton.tact @@ -4,10 +4,20 @@ import "./wallet"; import "./linker"; import "./jetton_trait"; -message Mint { +message Deposit { amount: Int; } +message Withdraw { + amount: Int; +} + +struct WithdrawalRequests { + addresses: map[Int]Address; + amounts: map[Int]Int; + n_requests: Int; +} + const gas_consumption: Int = ton("0.01"); const withdraw_gas_consumption: Int = ton("0.05"); @@ -32,9 +42,15 @@ contract TONB with Jetton { self.content = content; } - receive(msg: Mint) { + receive(msg: Deposit) { let ctx: Context = context(); require(ctx.value >= deposit_gas_consumption, "not enough money for deposit"); self.mint(ctx.sender, msg.amount, ctx.sender); } + + receive(msg: Withdraw) { + let ctx: Context = context(); + require(ctx.value >= withdraw_gas_consumption, "not enough money for withdraw"); + self.burn(ctx.sender, msg.amount, ctx.sender); + } } \ No newline at end of file diff --git a/sources/output/jetton_JettonDefaultWallet.ts b/sources/output/jetton_JettonDefaultWallet.ts index 2157d9e..92d89c0 100644 --- a/sources/output/jetton_JettonDefaultWallet.ts +++ b/sources/output/jetton_JettonDefaultWallet.ts @@ -768,9 +768,9 @@ async function JettonDefaultWallet_init(master: Address, owner: Address) { if (!res.success) { throw Error(res.error); } if (res.exitCode !== 0 && res.exitCode !== 1) { if (JettonDefaultWallet_errors[res.exitCode]) { - throw new ComputeError(JettonDefaultWallet_errors[res.exitCode].message, res.exitCode, { logs: res.vmLogs }); + throw new ComputeError(JettonDefaultWallet_errors[res.exitCode].message, res.exitCode); } else { - throw new ComputeError('Exit code: ' + res.exitCode, res.exitCode, { logs: res.vmLogs }); + throw new ComputeError('Exit code: ' + res.exitCode, res.exitCode); } } diff --git a/sources/tests/__snapshots__/jetton.spec.ts.snap b/sources/tests/__snapshots__/jetton.spec.ts.snap new file mode 100644 index 0000000..779a3ec --- /dev/null +++ b/sources/tests/__snapshots__/jetton.spec.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jetton should deploy 1`] = ` +[ + { + "type": "deploy", + }, + { + "message": { + "body": { + "cell": "x{8999B1640000000000000000000000000000000000000000000000000001C6BF526340004_}", + "type": "cell", + }, + "bounce": true, + "from": "kQAI-3FJVc_ywSuY4vq0bYrzR7S4Och4y7bTU_i5yLOB3A6P", + "to": "kQCrEWck7tGRuwnumilB-HRzp2v-IxRzWG4Lw0mP_VYl72fg", + "type": "internal", + "value": 1000000000n, + }, + "type": "received", + }, + { + "gasUsed": 22165n, + "type": "processed", + }, +] +`; + +exports[`jetton should deploy 2`] = `[]`; diff --git a/sources/tests/jetton.spec.ts b/sources/tests/jetton.spec.ts new file mode 100644 index 0000000..20124ec --- /dev/null +++ b/sources/tests/jetton.spec.ts @@ -0,0 +1,36 @@ +import { toNano, beginCell } from "ton"; +import { ContractSystem } from "ton-emulator"; +import {TONB} from '../output/jetton_TONB'; +import { default_content } from '../utils/config'; +import { TONBWallet } from '../output/jetton_TONBWallet'; + +describe('jetton', () => { + it('should deploy', async () => { + + // Create jetton + let system = await ContractSystem.create(); + let owner = system.treasure('owner'); + + let contract = system.open(await TONB.fromInit(owner.address, default_content)); + let tracker = system.track(contract.address); + let wallet_contract = system.open(await TONBWallet.fromInit(contract.address, owner.address)); + let wallet_tracker = system.track(wallet_contract.address); + + // Mint + let res1 = await contract.send(owner, { value: toNano(1) }, { $$type: 'Deposit', amount: toNano(1000000) }); + let res = await system.run(); + console.log(res1) + console.log(res) + expect(tracker.events()).toMatchSnapshot(); + expect(wallet_tracker.events()).toMatchSnapshot(); + console.log(await wallet_contract.getGetWalletData()); + + // Check owner + expect((await contract.getOwner()).toString()).toEqual(owner.address.toString()); + + // Data + let data = await contract.getGetJettonData(); + // console.warn(data); + console.log(tracker.events()) + }); +}); \ No newline at end of file diff --git a/sources/tests/scenario.ts b/sources/tests/scenario.ts new file mode 100644 index 0000000..61cf494 --- /dev/null +++ b/sources/tests/scenario.ts @@ -0,0 +1,10 @@ +import { deployTONB, deposit, withdraw } from "../utils/interactions"; +import { wallet_data } from '../utils/config'; +import { randomAddress, TON } from '../utils/helpers'; + +(async () => { + let {my_wallet, secretKey} = await wallet_data(); + let tonb_addr = await deployTONB(); + await deposit(my_wallet, secretKey, 1 * TON(), tonb_addr); + await withdraw(my_wallet, secretKey, 0.5 * TON(), tonb_addr); +})(); diff --git a/sources/utils/config.ts b/sources/utils/config.ts new file mode 100644 index 0000000..8ae172d --- /dev/null +++ b/sources/utils/config.ts @@ -0,0 +1,42 @@ + +import { TonClient, Address, WalletContractV3R2 } from "ton"; +import { mnemonicToPrivateKey } from "ton-crypto"; +import { buildOnchainMetadata } from "./helpers"; + +export const client = new TonClient({ + endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC', + apiKey: 'bb38df0c2756c66e2ab49f064e2484ec444b01244d2bd49793bd5b58f61ae3d2' +}) + +let mnemonics = "basic security merge opera inject core melody polar become force cool glance history order warfare consider company slim twice balcony scare shoot winner rude"; +// read more about wallet apps https://ton.org/docs/participate/wallets/apps#tonhub-test-environment + +export async function wallet_data() { + let keyPair = await mnemonicToPrivateKey(mnemonics.split(" ")); + let secretKey = keyPair.secretKey; + //Create deployment wallet contract + let wallet_contract = WalletContractV3R2.create({ workchain, publicKey: keyPair.publicKey }); + let my_wallet = client.open(wallet_contract); + return { my_wallet, secretKey, keyPair }; +} +//workchain = 1 - masterchain (expensive operation cost, validator's election contract works here) +//workchain = 0 - basechain (normal operation cost, user's contracts works here) +export let workchain = 0; //we are working in basechain. + + +// This is example data - Modify these params for your own jetton +// - Data is stored on-chain (except for the image data itself) + +export const jettonParams = { + name: "TactJet", + description: "This is description of Test tact jetton", + image: "https://ipfs.io/ipfs/QmbPZjC1tuP6ickCCBtoTCQ9gc3RpkbKx7C1LMYQdcLwti" // Image url +}; + +// Owner should usually be the deploying wallet's address. +export let owner = Address.parse('EQAuAiFGgkxoQvBWXjXQcLYb8BW4fO6UJkt6_uCONJ2y5VUk'); + + +// Create content Cell +export let default_content = buildOnchainMetadata(jettonParams); + diff --git a/sources/utils/jetton-helpers.ts b/sources/utils/helpers.ts similarity index 80% rename from sources/utils/jetton-helpers.ts rename to sources/utils/helpers.ts index 990d4e8..4101d9d 100644 --- a/sources/utils/jetton-helpers.ts +++ b/sources/utils/helpers.ts @@ -1,6 +1,8 @@ import { Sha256 } from "@aws-crypto/sha256-js"; -import { beginCell, Cell } from "ton"; +import { beginCell, Cell, Address } from "ton"; import { Dictionary } from "ton-core"; +import Prando from "prando"; + const ONCHAIN_CONTENT_PREFIX = 0x00; const SNAKE_PREFIX = 0x00; @@ -59,4 +61,17 @@ export function buildOnchainMetadata(data: { .storeInt(ONCHAIN_CONTENT_PREFIX, 8) .storeDict(dict) .endCell(); -} \ No newline at end of file +} + +export function TON(): number { + return 1000000000; +} + +export function randomAddress(seed: string, workchain?: number) { + const random = new Prando(seed); + const hash = Buffer.alloc(32); + for (let i = 0; i < hash.length; i++) { + hash[i] = random.nextInt(0, 255); + } + return new Address(workchain ?? 0, hash); +} diff --git a/sources/utils/interactions.ts b/sources/utils/interactions.ts new file mode 100644 index 0000000..c397c80 --- /dev/null +++ b/sources/utils/interactions.ts @@ -0,0 +1,83 @@ +import { Cell, Address, internal, beginCell, contractAddress, toNano, fromNano } from "ton"; +import { storeDeposit, storeWithdraw } from "../output/jetton_TONB"; +import { TON } from "./helpers"; +import { wallet_data, owner, default_content, workchain } from './config'; +import {TONB} from "../output/jetton_TONB"; +import { TONBWallet } from '../output/jetton_TONBWallet'; + +export async function sendMessage(wallet: any, secretKey: Buffer, msg: {value: string | bigint, to: Address, bounce?: boolean, init?: {code?: Cell, data?: Cell}, body?: any}) { + let seqno: number = await wallet.getSeqno(); + return await wallet.sendTransfer({ + seqno, + secretKey, + messages: [internal(msg)] + }); +} + +export async function deposit(wallet: any, secretKey: Buffer, value_: bigint | number, tonb: Address) { + console.log('📤Sending deposit message to ', tonb, ' with value ', value_, '💎TON'); + let value = BigInt(value_); + let msg_body_b = beginCell(); + storeDeposit({amount: value as bigint, $$type: 'Deposit'})(msg_body_b); + let msg_body = msg_body_b.endCell(); + let msg_value = value as bigint + BigInt(0.1 * TON()); + let res = await sendMessage(wallet, secretKey, {value: msg_value, to: tonb, body: msg_body}); + console.log(res); + console.log('======deposit message sent ======'); +} + +export async function withdraw(wallet: any, secretKey: Buffer, value_: bigint | number, tonb: Address) { + let value = BigInt(value_); + let msg_body_b = beginCell(); + storeWithdraw({amount: value as bigint, $$type: 'Withdraw'})(msg_body_b); + let msg_body = msg_body_b.endCell(); + let msg_value = BigInt(0.1 * TON()); + await sendMessage(wallet, secretKey, {value: msg_value, to: tonb, body: msg_body}); +} + +export async function deployTONB(tonb_content?: any) { + if (!tonb_content) { + tonb_content = default_content; + } + let {my_wallet, secretKey, keyPair} = await wallet_data(); + // Get deployment wallet balance + let balance: bigint = await my_wallet.getBalance(); + // Compute init data for deployment + let init = await TONB.init(owner, tonb_content); + let destination_address = contractAddress(workchain, init); + + + let deployAmount = toNano('0.5'); + let supply = 500; + let amount = BigInt(supply * Math.pow(10, 9)); + + + // send a message on new address contract to deploy it + let seqno: number = await my_wallet.getSeqno(); + + + console.log('🛠️Preparing new outgoing massage from deployment wallet. Seqno = ', seqno, ', wallet = ', my_wallet.address); + console.log('Current deployment wallet balance = ', fromNano(balance).toString(), '💎TON'); + // console.log('Totally supply for deployed Token = ', supply, ', amount = ', amount.toString()); + let res = await my_wallet.sendTransfer({ + seqno, + secretKey, + messages: [internal({ + value: deployAmount, + to: destination_address, + init: { + code : init.code, + data : init.data + } + })] + }); + console.log('======deployment message sent to ', destination_address, ' ======'); + console.log(res); + return destination_address; +} + +export async function getAddress(master_: string | Address, owner_: string | Address) { + let master = typeof master_ === 'string' ? Address.parse(master_) : master_; + let owner = typeof owner_ === 'string' ? Address.parse(owner_) : owner_; + return contractAddress(workchain, await TONBWallet.init(master, owner)); +} \ No newline at end of file diff --git a/sources/utils/rmlogs.sh b/sources/utils/rmlogs.sh new file mode 100644 index 0000000..e96e50c --- /dev/null +++ b/sources/utils/rmlogs.sh @@ -0,0 +1,3 @@ +#!/bin/hash +for file in $(find sources/output -type f); do sed -i 's/,\ {\ logs:\ res.vmLogs\ }//g' $file; done +for file in $(find sources/output -type f); do sed -i 's/stringtoreplace//g' $file; done \ No newline at end of file diff --git a/sources/wallet.tact b/sources/wallet.tact index 396e15b..35f5f02 100644 --- a/sources/wallet.tact +++ b/sources/wallet.tact @@ -109,7 +109,7 @@ contract TONBWallet { // Check sender let ctx: Context = context(); - require(ctx.sender == self.owner, "Invalid sender"); + require(ctx.sender == self.owner || ctx.sender == self.master, "Invalid sender"); // Update balance self.balance = self.balance - msg.amount; diff --git a/yarn.lock b/yarn.lock index a29d3b1..a44b4de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2618,7 +2618,7 @@ pkg-dir@^4.2.0: prando@^6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/prando/-/prando-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/prando/-/prando-6.0.1.tgz#ffa8de84c2adc4975dd9df37ae4ada0458face53" integrity sha512-ghUWxQ1T9IJmPu6eshc3VU0OwveUtXQ33ZLXYUcz1Oc5ppKLDXKp0TBDj6b0epwhEctzcQSNGR2iHyvQSn4W5A== pretty-format@^29.0.0, pretty-format@^29.3.1: