From f0bd98e90558534cb98232aec300c53f8a142418 Mon Sep 17 00:00:00 2001 From: B4ckSl4sh <91282981+Shvandre@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:00:04 +0300 Subject: [PATCH] refactor: improved code style, removed unnecessary comments, updated to match Tact v1.6 (#48) --- package.json | 3 +- sources/contract.spec.ts | 218 ++++++++---------------- sources/jetton_minter_discoverable.tact | 200 +++++++++------------- sources/jetton_wallet.tact | 166 ++++++++---------- sources/messages.tact | 90 +++++----- tact.config.json | 3 +- yarn.lock | 48 +++++- 7 files changed, 314 insertions(+), 414 deletions(-) diff --git a/package.json b/package.json index 86a42ed..68d3f18 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,6 @@ "ts-jest": "^29.0.3", "ts-node": "^10.9.1", "typescript": "^4.9.4" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/sources/contract.spec.ts b/sources/contract.spec.ts index 218985e..ff8adae 100644 --- a/sources/contract.spec.ts +++ b/sources/contract.spec.ts @@ -11,10 +11,10 @@ import { ChangeOwner, JettonMinter, Mint, - TokenUpdateContent, - TokenBurn, ProvideWalletAddress, storeTokenTransfer, storeTokenBurn, storeMint, CustomChangeOwner + JettonUpdateContent, + JettonBurn, ProvideWalletAddress, storeJettonTransfer, storeJettonBurn, storeMint, JettonTransferInternal } from "./output/Jetton_JettonMinter"; -import { JettonWallet, TokenTransfer } from "./output/Jetton_JettonWallet"; +import { JettonWallet, JettonTransfer } from "./output/Jetton_JettonWallet"; import "@ton/test-utils"; import { getRandomInt, randomAddress } from "./utils/utils"; @@ -35,7 +35,8 @@ JettonMinter.prototype.getWalletAddress = async function (this: JettonMinter, pr }; JettonMinter.prototype.getAdminAddress = async function (this: JettonMinter, provider: ContractProvider) { - return this.getOwner(provider); + let res = await this.getGetJettonData(provider); + return res.adminAddress; }; JettonMinter.prototype.getContent = async function (this: JettonMinter, provider: ContractProvider) { @@ -57,9 +58,18 @@ JettonMinter.prototype.sendMint = async function ( } const msg: Mint = { $$type: "Mint", - query_id: 0n, - amount: jetton_amount, + queryId: 0n, receiver: to, + tonAmount: total_ton_amount, + mintMessage: { + $$type: "JettonTransferInternal", + queryId: 0n, + amount: jetton_amount, + sender: this.address, + responseDestination: this.address, + forwardTonAmount: forward_ton_amount, + forwardPayload: beginCell().storeUint(0, 1).asSlice(), + } }; return this.send(provider, via, { value: total_ton_amount + toNano("0.015") }, msg); }; @@ -70,8 +80,8 @@ JettonMinter.prototype.sendChangeAdmin = async function ( via: Sender, newOwner: Address ) { - const msg: CustomChangeOwner = { - $$type: "CustomChangeOwner", + const msg: ChangeOwner = { + $$type: "ChangeOwner", queryId: 0n, newOwner: newOwner, }; @@ -84,8 +94,9 @@ JettonMinter.prototype.sendChangeContent = async function ( via: Sender, content: Cell ) { - const msg: TokenUpdateContent = { - $$type: "TokenUpdateContent", + const msg: JettonUpdateContent = { + $$type: "JettonUpdateContent", + queryId: 0n, content: content, }; return this.send(provider, via, { value: toNano("0.05") }, msg); @@ -101,9 +112,9 @@ JettonMinter.prototype.sendDiscovery = async function ( ) { const msg: ProvideWalletAddress = { $$type: "ProvideWalletAddress", - query_id: 0n, - owner_address: address, - include_address: includeAddress, + queryId: 0n, + ownerAddress: address, + includeAddress: includeAddress, }; return this.send(provider, via, { value: value }, msg); }; @@ -160,13 +171,14 @@ describe("JettonMinter", () => { notDeployer = await blockchain.treasury('notDeployer'); defaultContent = beginCell().endCell(); - let msg: TokenUpdateContent = { - $$type: "TokenUpdateContent", + let msg: JettonUpdateContent = { + $$type: "JettonUpdateContent", + queryId: 0n, content: defaultContent, } - jettonMinter = blockchain.openContract(await JettonMinter.fromInit(deployer.address, defaultContent)); + jettonMinter = blockchain.openContract(await JettonMinter.fromInit(0n, deployer.address, defaultContent)); //We send Update content to deploy the contract, because it is not automatically deployed after blockchain.openContract //And to deploy it we should send any message. But update content message with same content does not affect anything. That is why I chose it. @@ -181,7 +193,7 @@ describe("JettonMinter", () => { minter_code = jettonMinter.init?.code!!; //const playerWallet = await jettonMinter.getGetWalletAddress(deployer.address); - jettonWallet = blockchain.openContract(await JettonWallet.fromInit(deployer.address, jettonMinter.address)); + jettonWallet = blockchain.openContract(await JettonWallet.fromInit(0n, deployer.address, jettonMinter.address)); jwallet_code = jettonWallet.init?.code!!; userWallet = async (address: Address)=> { @@ -214,15 +226,15 @@ describe("JettonMinter", () => { forwardPayload: Cell | null ) => { const parsedForwardPayload = forwardPayload != null ? forwardPayload.beginParse() : new Builder().storeUint(0, 1).endCell().beginParse(); //Either bit equals 0 - let msg: TokenTransfer = { - $$type: "TokenTransfer", - query_id: 0n, + let msg: JettonTransfer = { + $$type: "JettonTransfer", + queryId: 0n, amount: jetton_amount, destination: to, - response_destination: responseAddress, - custom_payload: customPayload, - forward_ton_amount: forward_ton_amount, - forward_payload: parsedForwardPayload, + responseDestination: responseAddress, + customPayload: customPayload, + forwardTonAmount: forward_ton_amount, + forwardPayload: parsedForwardPayload, }; return await newUserWallet.send(via, { value }, msg); @@ -235,12 +247,12 @@ describe("JettonMinter", () => { responseAddress: Address, customPayload: Cell | null ) => { - let msg: TokenBurn = { - $$type: "TokenBurn", - query_id: 0n, + let msg: JettonBurn = { + $$type: "JettonBurn", + queryId: 0n, amount: jetton_amount, - response_destination: responseAddress, - custom_payload: customPayload, + responseDestination: responseAddress, + customPayload: customPayload, }; return await newUserWallet.send(via, { value }, msg); @@ -564,10 +576,23 @@ describe("JettonMinter", () => { // The behavior is implementation-defined. // I'm still not sure if the code handling these bounces is really necessary, // but I could be wrong. Refer to this issue for details: https://github.com/tact-lang/jetton/issues/10 + // This check are 100% nessessary if we add an option not to deploy jetton wallet of destination address. it('minter should restore supply on internal_transfer bounce', async () => { const deployerJettonWallet = await userWallet(deployer.address); const mintAmount = BigInt(getRandomInt(1000, 2000)); - const mintMsg = beginCell().store(storeMint({$$type: "Mint", query_id: 0n, amount: mintAmount, receiver: deployer.address})).endCell(); + const mintMsg = beginCell().store(storeMint({$$type: "Mint", + mintMessage: {$$type: "JettonTransferInternal", + amount: mintAmount, + sender: deployer.address, + responseDestination: deployer.address, + queryId: 0n, + forwardTonAmount: 0n, + forwardPayload: beginCell().storeUint(0, 1).asSlice() + }, + queryId: 0n, + receiver: deployer.address, + tonAmount: mintAmount + })).endCell(); const supplyBefore = await jettonMinter.getTotalSupply(); const minterSmc = await blockchain.getContract(jettonMinter.address); @@ -609,14 +634,14 @@ describe("JettonMinter", () => { const notDeployerJettonWallet = await userWallet(notDeployer.address); const balanceBefore = await deployerJettonWallet.getJettonBalance(); const txAmount = BigInt(getRandomInt(100, 200)); - const transferMsg = beginCell().store(storeTokenTransfer({$$type: "TokenTransfer", - query_id: 0n, + const transferMsg = beginCell().store(storeJettonTransfer({$$type: "JettonTransfer", + queryId: 0n, amount: txAmount, - response_destination: deployer.address, + responseDestination: deployer.address, destination: notDeployer.address, - custom_payload: null, - forward_ton_amount: 0n, - forward_payload: beginCell().endCell().beginParse() + customPayload: null, + forwardTonAmount: 0n, + forwardPayload: beginCell().storeUint(0, 1).asSlice() })).endCell() const walletSmc = await blockchain.getContract(deployerJettonWallet.address); @@ -652,7 +677,12 @@ describe("JettonMinter", () => { const balanceBefore = await deployerJettonWallet.getJettonBalance(); const burnAmount = BigInt(getRandomInt(100, 200)); - const burnMsg = beginCell().store(storeTokenBurn({amount: burnAmount, $$type: "TokenBurn", query_id: 0n, response_destination: deployer.address, custom_payload: null})).endCell() + const burnMsg = beginCell().store(storeJettonBurn({$$type: "JettonBurn", + queryId: 0n, + amount: burnAmount, + responseDestination: deployer.address, + customPayload: null + })).endCell() const walletSmc = await blockchain.getContract(deployerJettonWallet.address); @@ -682,65 +712,6 @@ describe("JettonMinter", () => { expect(await deployerJettonWallet.getJettonBalance()).toEqual(balanceBefore); }); }); - // implementation detail - it('works with minimal ton amount', async () => { - const deployerJettonWallet = await userWallet(deployer.address); - let initialJettonBalance = await deployerJettonWallet.getJettonBalance(); - const someAddress = Address.parse("EQD__________________________________________0vo"); - const someJettonWallet = await userWallet(someAddress); - let initialJettonBalance2 = await someJettonWallet.getJettonBalance(); - await deployer.send({value:toNano('1'), bounce:false, to: deployerJettonWallet.address}); - let forwardAmount = toNano('0.3'); - /* - forward_ton_amount + - fwd_count * fwd_fee + - (2 * gas_consumption + min_tons_for_storage)); - */ - let minimalFee = 2n* fwd_fee + 2n*gas_consumption + min_tons_for_storage; - let sentAmount = forwardAmount + minimalFee; // not enough, need > - - let forwardPayload = null; - (await blockchain.getContract(deployerJettonWallet.address)).balance; - (await blockchain.getContract(someJettonWallet.address)).balance; - let sendResult = await deployerJettonWallet.sendTransfer(deployer.getSender(), sentAmount, - sentAmount, someAddress, - deployer.address, null, forwardAmount, forwardPayload); - - expect(sendResult.transactions).toHaveTransaction({ - from: deployer.address, - to: deployerJettonWallet.address, - aborted: true, - exitCode: Errors.not_enough_ton, - }); - sentAmount += 1n; // now enough - sendResult = await deployerJettonWallet.sendTransfer(deployer.getSender(), sentAmount, - sentAmount, someAddress, - deployer.address, null, forwardAmount, forwardPayload); - expect(sendResult.transactions).not.toHaveTransaction({ //no excesses - from: someJettonWallet.address, - to: deployer.address, - }); - /* - transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) - sender:MsgAddress forward_payload:(Either Cell ^Cell) - = InternalMsgBody; - */ - expect(sendResult.transactions).toHaveTransaction({ //notification - from: someJettonWallet.address, - to: someAddress, - value: forwardAmount, - body: beginCell().storeUint(Op.transfer_notification, 32).storeUint(0, 64) //default queryId - .storeCoins(sentAmount) - .storeAddress(deployer.address) - .storeUint(0, 1) - .endCell() - }); - expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance - sentAmount); - expect(await someJettonWallet.getJettonBalance()).toEqual(initialJettonBalance2 + sentAmount); - - (await blockchain.getContract(deployerJettonWallet.address)).balance; - expect((await blockchain.getContract(someJettonWallet.address)).balance).toBeGreaterThan(min_tons_for_storage); - }); // implementation detail it('wallet does not accept internal_transfer not from wallet', async () => { @@ -829,61 +800,6 @@ describe("JettonMinter", () => { expect(await jettonMinter.getTotalSupply()).toEqual(initialTotalSupply); }); - it('minimal burn message fee', async () => { - const deployerJettonWallet = await userWallet(deployer.address); - let initialJettonBalance = await deployerJettonWallet.getJettonBalance(); - let initialTotalSupply = await jettonMinter.getTotalSupply(); - let burnAmount = toNano('0.01'); - //let minimalFee = fwd_fee + 2n*gas_consumption + min_tons_for_storage; - //let minimalFee = toNano("0.006"); - let L = toNano(0.00000001); - let R = toNano(0.1); - //change false to true if you want to find minimal fee - //However, before doing it, remove gas-checks from the smart-contract code - //implementing binary search - while(R - L > 1 && false) { - let minimalFee = (L + R) / 2n; - try { - const sendLow = await deployerJettonWallet.sendBurn(deployer.getSender(), minimalFee, // ton amount - burnAmount, deployer.address, null); // amount, response address, custom payload - - expect(sendLow.transactions).toHaveTransaction({ - from: deployerJettonWallet.address, - to: jettonMinter.address, - exitCode: 0 - }); - R = minimalFee; - } - catch { - L = minimalFee; - } - } - console.log(L); - let minimalFee = 11408799n; - //It is the number you can get in console.log(L) if setting "false" to "true" in while loop above - - const sendLow = await deployerJettonWallet.sendBurn(deployer.getSender(), minimalFee, // ton amount - burnAmount, deployer.address, null); // amount, response address, custom payload - //Here was tests, that checks that there is enough ton to jetton wallet to send a message. - //However, I check that it is enough ton to process a message from jetton wallet to jetton minter - expect(sendLow.transactions).not.toHaveTransaction({ - from: deployerJettonWallet.address, - to: jettonMinter.address, - exitCode: 0, - }); - const sendEnough = await deployerJettonWallet.sendBurn(deployer.getSender(), minimalFee + 1n, - burnAmount, deployer.address, null); - - expect(sendEnough.transactions).toHaveTransaction({ - from: deployerJettonWallet.address, - to: jettonMinter.address, - exitCode: 0, - }); - expect(await deployerJettonWallet.getJettonBalance()).toEqual(initialJettonBalance - burnAmount); - expect(await jettonMinter.getTotalSupply()).toEqual(initialTotalSupply - burnAmount); - - }); - it('minter should only accept burn messages from jetton wallets', async () => { const deployerJettonWallet = await userWallet(deployer.address); const burnAmount = toNano('1'); diff --git a/sources/jetton_minter_discoverable.tact b/sources/jetton_minter_discoverable.tact index 8ba8f4d..8ffb5c4 100644 --- a/sources/jetton_minter_discoverable.tact +++ b/sources/jetton_minter_discoverable.tact @@ -1,10 +1,10 @@ -import "@stdlib/ownable"; -import "@stdlib/deploy"; +// https://github.com/ton-blockchain/TEPs/blob/master/text/0089-jetton-wallet-discovery.md + import "./jetton_wallet"; -import "./constants"; import "./messages"; -asm fun emptyAddress(): Address { b{00} PUSHSLICE } +const ProvideAddressGasConsumption: Int = ton("0.01"); +const Workchain: Int = 0; struct JettonMasterState { totalSupply: Int as coins; @@ -14,149 +14,115 @@ struct JettonMasterState { jettonWalletCode: Cell; } -//Actually this contract has OwnableTransferable functionality -//but this logic is implemented without OwnableTransferable trait -//to match refference implementation in terms of exit codes. -contract JettonMinter with MinterExitcodes, GasConstants { - totalSupply: Int as coins; - mintable: Bool; - owner: Address; - jettonContent: Cell; - - init(owner: Address, jettonContent: Cell) { - self.totalSupply = 0; - self.mintable = true; - self.owner = owner; - self.jettonContent = jettonContent; - } +contract JettonMinter( + totalSupply: Int as coins, + owner: Address, + jettonContent: Cell, +) { + receive(msg: JettonBurnNotification) { + let sender = parseStdAddress(sender().asSlice()); + let wallet = getJettonBasechainWalletByOwner(msg.sender); - receive(msg: TokenBurnNotification) { - //Check that the message is from msg.sender's jetton_wallet - nativeThrowUnless(self.UnauthorizedBurn, sender() == self.getJettonWalletByOwner(msg.sender)); + throwUnless(74, sender.workchain == Workchain && sender.address == wallet.hash!!); self.totalSupply -= msg.amount; - send(SendParameters{ - to: msg.response_destination, - value: 0, - bounce: false, - mode: SendRemainingValue | SendIgnoreErrors, //ignore errors, because supply already been updated - body: TokenExcesses{ - query_id: msg.query_id - }.toCell() - }); - } - receive(msg: CustomChangeOwner) { - // Check if the sender is the owner - nativeThrowUnless(self.IncorrectSender, sender() == self.owner); - // Update owner - self.owner = msg.newOwner; - } - - receive(msg: TokenUpdateContent) { - //Only owner may update content. - nativeThrowUnless(self.IncorrectSender, sender() == self.owner); - self.jettonContent = msg.content; // Update content + if (msg.responseDestination.isNotNone()) { + message(MessageParameters { + to: msg.responseDestination, + body: JettonExcesses{ queryId: msg.queryId }.toCell(), + value: 0, + bounce: false, + mode: SendRemainingValue | SendIgnoreErrors, // ignore errors, because supply has already been updated + }); + } } - // https://github.com/ton-blockchain/TEPs/blob/master/text/0089-jetton-wallet-discovery.md receive(msg: ProvideWalletAddress) { - nativeThrowUnless(self.InsufficientGasForDiscovery, context().value >= self.gasForDiscovery); - let includedAddress: Address? = null; - let workchain: Int = parseStdAddress(msg.owner_address.asSlice()).workchain; - //Note, that emptyAddress != null, it is different values. - //We do like that according to TEP above - let targetJettonWallet: Address = emptyAddress(); - - if(workchain == 0) { - //Only in this case (address is from basechain) we can calculate the address - targetJettonWallet = contractAddress(initOf JettonWallet(msg.owner_address, myAddress())); - } - if (msg.include_address) { - includedAddress = msg.owner_address; - } - send(SendParameters{ + // we use message fwdFee for estimation of forward_payload costs + let ctx = context(); + let fwdFee = ctx.readForwardFee(); + throwUnless(75, ctx.value > fwdFee + ProvideAddressGasConsumption); + + let ownerWorkchain: Int = parseStdAddress(msg.ownerAddress.asSlice()).workchain; + + let targetJettonWallet: BasechainAddress = + (ownerWorkchain == Workchain) ? + contractBasechainAddress(initOf JettonWallet(0, msg.ownerAddress, myAddress())) + : emptyBasechainAddress(); + + message(MessageParameters { + body: makeTakeWalletAddressMsg(targetJettonWallet, msg), to: sender(), value: 0, mode: SendRemainingValue, - body: self.takeWalletBody(targetJettonWallet, includedAddress, msg.query_id) }); } - receive(msg: Mint) { - nativeThrowUnless(self.IncorrectSender, sender() == self.owner); // Allow minting only by owner - - //We don't use nativeThrowUnless here as 'mintable' flag is implementation-defined - //And not present in token-contract https://github.com/ton-blockchain/token-contract/tree/main/ft - require(self.mintable, "Not mintable"); - //Maybe we should check that msg.value is enough to cover the gas fees - //But there is no such check in token-contract, - self.totalSupply += msg.amount; // Update total supply + receive(msg: JettonUpdateContent) { + throwUnless(73, sender() == self.owner); + self.jettonContent = msg.content; + } - let winit: StateInit = self.getJettonWalletInit(msg.receiver); + receive(msg: Mint) { + throwUnless(73, sender() == self.owner); + self.totalSupply += msg.mintMessage.amount; - send(SendParameters{ - to: contractAddress(winit), + deploy(DeployParameters{ value: 0, bounce: true, mode: SendRemainingValue, - body: TokenTransferInternal{ - query_id: msg.query_id, - amount: msg.amount, - from: myAddress(), - response_destination: self.owner, // Owner is minting, so send excess to owner - forward_ton_amount: 1, // 1 nanoton is enough to send a notification - forward_payload: emptySlice() - }.toCell(), - code: winit.code, - data: winit.data + body: msg.mintMessage.toCell(), + init: getJettonWalletInit(msg.receiver) }); } - bounced(msg: bounced){ - self.totalSupply -= msg.amount; - } - - //https://github.com/ton-blockchain/TEPs/blob/master/text/0089-jetton-wallet-discovery.md#scheme - //take_wallet_address#d1735400 query_id:uint64 wallet_address:MsgAddress owner_address:(Maybe ^MsgAddress) = InternalMsgBody; - inline fun takeWalletBody(targetJettonWallet: Address, includedAddress: Address?, query_id: Int): Cell { - let body: Builder = beginCell() - .storeUint(0xd1735400, 32) // takeWalletBody opcode - .storeUint(query_id, 64) - .storeSlice(targetJettonWallet.asSlice()); - if (includedAddress != null) { - let includedAddressCell: Cell = beginCell().storeSlice(includedAddress!!.asSlice()).endCell(); - body = body.storeBit(true); // Maybe bit - body = body.storeRef(includedAddressCell); - } else { - body = body.storeBit(false); // Maybe bit - } - return body.endCell(); - } - - inline fun getJettonWalletByOwner(jetton_wallet_owner: Address): Address { - let jwInit: StateInit = self.getJettonWalletInit(jetton_wallet_owner); - return contractAddress(jwInit); + receive(msg: ChangeOwner) { + throwUnless(73, sender() == self.owner); + self.owner = msg.newOwner; } - inline fun getJettonWalletInit(address: Address): StateInit { - return initOf JettonWallet(address, myAddress()); + bounced(msg: bounced) { + self.totalSupply -= msg.amount; } get fun get_jetton_data(): JettonMasterState { return JettonMasterState { totalSupply: self.totalSupply, - mintable: self.mintable, + mintable: true, adminAddress: self.owner, jettonContent: self.jettonContent, - //Owner may be any address, what matters here is the code, not data. - jettonWalletCode: initOf JettonWallet(myAddress(), myAddress()).code - } + jettonWalletCode: codeOf JettonWallet + }; } + get fun get_wallet_address(ownerAddress: Address): Address { - return self.getJettonWalletByOwner(ownerAddress); - } - get fun owner(): Address { - return self.owner; + return getJettonWalletByOwner(ownerAddress); } -} \ No newline at end of file +} + +asm fun emptyAddress(): Address { b{00} PUSHSLICE } + +inline fun makeTakeWalletAddressMsg(targetJettonWallet: BasechainAddress, msg: ProvideWalletAddress): Cell { + return + beginCell() + .storeUint(TakeWalletAddressOpcode, 32) + .storeUint(msg.queryId, 64) + .storeBasechainAddress(targetJettonWallet) + .storeMaybeRef(msg.includeAddress ? beginCell().storeAddress(msg.ownerAddress).endCell() : null) + .endCell(); +} + +inline fun getJettonWalletInit(address: Address): StateInit { + return initOf JettonWallet(0, address, myAddress()); +} + +inline fun getJettonWalletByOwner(jettonWalletOwner: Address): Address { + return contractAddress(getJettonWalletInit(jettonWalletOwner)); +} + +inline fun getJettonBasechainWalletByOwner(jettonWalletOwner: Address): BasechainAddress { + return contractBasechainAddress(getJettonWalletInit(jettonWalletOwner)); +} + +inline extends fun isNotNone(self: Address): Bool { return self.asSlice().preloadUint(2) != 0 } diff --git a/sources/jetton_wallet.tact b/sources/jetton_wallet.tact index 8cecffb..48b75c3 100644 --- a/sources/jetton_wallet.tact +++ b/sources/jetton_wallet.tact @@ -1,146 +1,118 @@ import "./messages"; -import "./constants"; - -asm fun myCode(): Cell { MYCODE } - -//This contract also has Ownable functionality, but it is implemented without Ownable trait -//to match refference implementation in terms of exit codes. -contract JettonWallet with WalletExitcodes, GasConstants { - balance: Int as coins; - owner: Address; - master: Address; - const minTonsForStorage: Int = ton("0.015"); // 0.01 TON in original funC implementation. Increased as we have approx. x2 code size - const gasConsumption: Int = ton("0.015"); // 0.015 TON in original funC implementation. - // According to tests, 23k gas is maximum gas consuption in transfer. 23k gas is 0.0092 TON - // More precisely, max gas I could get is 22725 - init(owner: Address, master: Address) { - self.balance = 0; - self.owner = owner; - self.master = master; - } - - - receive(msg: TokenTransfer) { - nativeThrowUnless(self.IncorrectSender, sender() == self.owner); - let totalFees: Int = (2 * context().readForwardFee() + 2 * self.gasConsumption) + self.minTonsForStorage + msg.forward_ton_amount; +contract JettonWallet( + balance: Int as coins, + owner: Address, + master: Address, +) { + const minTonsForStorage: Int = ton("0.01"); + const gasConsumption: Int = ton("0.015"); - //Context() returns Structure with info about incoming message - nativeThrowUnless(self.UnsufficientAmountOfTonAttached, context().value > totalFees); + receive(msg: JettonTransfer) { + throwUnless(333, parseStdAddress(msg.destination.asSlice()).workchain == 0); + throwUnless(705, sender() == self.owner); self.balance -= msg.amount; - - //coins type is unsigned type, so - //self.balance is unsigned when storing and loading (msg.amount is unsigned too), - //however when doing calculations it's value may be negative, so the check is correct - nativeThrowUnless(self.IncorrectBalanceAfrerSend, self.balance >= 0); - - - //We shouldn't send transfers to masterchain due to higher gas price - nativeThrowUnless(self.InvalidDestinationWorkchain, parseStdAddress(msg.destination.asSlice()).workchain == 0); - - let init: StateInit = initOf JettonWallet(msg.destination, self.master); - let wallet_address: Address = contractAddress(init); - - send(SendParameters{ - to: wallet_address, + throwUnless(706, self.balance >= 0); + throwUnless(708, msg.forwardPayload.bits() >= 1); + + let ctx = context(); + let fwdCount = 1 + msg.forwardTonAmount & 1; + throwUnless(709, ctx.value > + msg.forwardTonAmount + + fwdCount * ctx.readForwardFee() + + (2 * self.gasConsumption + self.minTonsForStorage) + ); + + deploy(DeployParameters{ value: 0, mode: SendRemainingValue, bounce: true, - body: TokenTransferInternal { - query_id: msg.query_id, + body: JettonTransferInternal{ + queryId: msg.queryId, amount: msg.amount, - from: self.owner, - response_destination: msg.response_destination, - forward_ton_amount: msg.forward_ton_amount, - forward_payload: msg.forward_payload + sender: self.owner, + responseDestination: msg.responseDestination, + forwardTonAmount: msg.forwardTonAmount, + forwardPayload: msg.forwardPayload }.toCell(), - code: init.code, - data: init.data + init: initOf JettonWallet(0, msg.destination, self.master), }); } - receive(msg: TokenTransferInternal) { - // This message should come only from master, or from other JettonWallet - if (sender() != self.master) { - let init: StateInit = initOf JettonWallet(msg.from, self.master); - nativeThrowUnless(self.IncorrectSenderInternal, contractAddress(init) == sender()); - } - // Update balance + receive(msg: JettonTransferInternal) { self.balance += msg.amount; - //Commented require() here because self.balance and msg.amount are coins, so they are unsigned - //require(self.balance >= 0, "Invalid balance"); - // Get value for gas + // This message should come only from master, or from other JettonWallet + let wallet: StateInit = initOf JettonWallet(0, msg.sender, self.master); + if (sender() != contractAddress(wallet)) { + throwUnless(707, self.master == sender()); + } - let ctx: Context = context(); //Context of current message + let ctx: Context = context(); let msgValue: Int = ctx.value; let tonBalanceBeforeMsg = myBalance() - msgValue; let storageFee = self.minTonsForStorage - min(tonBalanceBeforeMsg, self.minTonsForStorage); msgValue -= (storageFee + self.gasConsumption); - let fwd_fee: Int = ctx.readForwardFee(); - - - if (msg.forward_ton_amount > 0) { - msgValue = ((msgValue - msg.forward_ton_amount) - fwd_fee); - send(SendParameters{ + if (msg.forwardTonAmount > 0) { + let fwdFee: Int = ctx.readForwardFee(); + msgValue -= msg.forwardTonAmount + fwdFee; + message(MessageParameters{ to: self.owner, - value: msg.forward_ton_amount, + value: msg.forwardTonAmount, mode: SendPayGasSeparately, bounce: false, - body: TokenNotification{ // 0x7362d09c -- Remind the new Owner - query_id: msg.query_id, + body: JettonNotification{ // 0x7362d09c -- Remind the new Owner + queryId: msg.queryId, amount: msg.amount, - from: msg.from, - forward_payload: msg.forward_payload - }.toCell() + sender: msg.sender, + forwardPayload: msg.forwardPayload, + }.toCell(), }); } + // 0xd53276db -- Cashback to the original Sender - if (msg.response_destination != null && msgValue > 0) { - send(SendParameters{ - to: msg.response_destination!!, + if (msg.responseDestination != null && msgValue > 0) { + message(MessageParameters{ + to: msg.responseDestination!!, value: msgValue, - mode: SendIgnoreErrors, // Jetton transfer is already succeeded - //In official funC implementation it is SendIgnoreErrors + mode: SendIgnoreErrors, bounce: false, - body: TokenExcesses{ - query_id: msg.query_id - }.toCell() + body: JettonExcesses{ queryId: msg.queryId }.toCell(), }); } } - receive(msg: TokenBurn) { - nativeThrowUnless(self.IncorrectSender, sender() == self.owner); + receive(msg: JettonBurn) { + throwUnless(705, sender() == self.owner); - let ctx: Context = context(); - self.balance -= msg.amount; // Update balance - nativeThrowUnless(self.IncorrectBalanceAfrerSend, self.balance >= 0); + self.balance -= msg.amount; + throwUnless(706, self.balance >= 0); - // This is minimal possible amount of TONs for attached. - nativeThrowUnless(self.UnsufficientAmountOfTonForBurn, ctx.value > self.gasForBurn); - // Burn tokens - send(SendParameters{ + let ctx = context(); + let fwdFee: Int = ctx.readForwardFee(); + throwUnless(707, ctx.value > (fwdFee + 2 * self.gasConsumption)); + + message(MessageParameters{ to: self.master, value: 0, mode: SendRemainingValue, bounce: true, - body: TokenBurnNotification{ - query_id: msg.query_id, + body: JettonBurnNotification{ + queryId: msg.queryId, amount: msg.amount, sender: self.owner, - response_destination: msg.response_destination - }.toCell() + responseDestination: msg.responseDestination, + }.toCell(), }); } - bounced(msg: bounced){ + bounced(msg: bounced) { self.balance += msg.amount; } - bounced(msg: bounced){ + bounced(msg: bounced) { self.balance += msg.amount; } @@ -149,7 +121,7 @@ contract JettonWallet with WalletExitcodes, GasConstants { balance: self.balance, owner: self.owner, master: self.master, - code: myCode() //may be replaced with "initOf JettonDefaultWallet(self.owner, self.master).code" + code: myCode() }; } -} \ No newline at end of file +} diff --git a/sources/messages.tact b/sources/messages.tact index 7438f59..6031c95 100644 --- a/sources/messages.tact +++ b/sources/messages.tact @@ -1,9 +1,9 @@ struct JettonData { - total_supply: Int; + totalSupply: Int; mintable: Bool; owner: Address; content: Cell; - wallet_code: Cell; + jettonWalletCode: Cell; } struct JettonWalletData { @@ -17,74 +17,76 @@ struct MaybeAddress { address: Address?; } -message(3) CustomChangeOwner { // https://github.com/ton-blockchain/token-contract/blob/main/ft/jetton-minter-discoverable.fc#L126 +message(4) JettonUpdateContent { queryId: Int as uint64; - newOwner: Address; -} - -//https://github.com/ton-blockchain/token-contract/blob/main/ft/jetton-minter-discoverable.fc#L133 -message(4) TokenUpdateContent { content: Cell; } -message(0xf8a7ea5) TokenTransfer { - query_id: Int as uint64; +message(0xf8a7ea5) JettonTransfer { + queryId: Int as uint64; amount: Int as coins; destination: Address; - response_destination: Address?; - custom_payload: Cell?; - forward_ton_amount: Int as coins; - forward_payload: Slice as remaining; + responseDestination: Address?; + customPayload: Cell?; + forwardTonAmount: Int as coins; + forwardPayload: Slice as remaining; } -message(0x178d4519) TokenTransferInternal { - query_id: Int as uint64; +message(0x178d4519) JettonTransferInternal { + queryId: Int as uint64; amount: Int as coins; - from: Address; - response_destination: Address?; - forward_ton_amount: Int as coins; - forward_payload: Slice as remaining; + sender: Address; + responseDestination: Address?; + forwardTonAmount: Int as coins; + forwardPayload: Slice as remaining; } -message(0x7362d09c) TokenNotification { - query_id: Int as uint64; +message(0x7362d09c) JettonNotification { + queryId: Int as uint64; amount: Int as coins; - from: Address; - forward_payload: Slice as remaining; + sender: Address; + forwardPayload: Slice as remaining; } -message(0x595f07bc) TokenBurn { - query_id: Int as uint64; +message(0x595f07bc) JettonBurn { + queryId: Int as uint64; amount: Int as coins; - response_destination: Address; - custom_payload: Cell?; + responseDestination: Address; + customPayload: Cell?; } -message(0x7bdd97de) TokenBurnNotification { - query_id: Int as uint64; +message(0x7bdd97de) JettonBurnNotification { + queryId: Int as uint64; amount: Int as coins; sender: Address; - response_destination: Address; + responseDestination: Address; } -message(0xd53276db) TokenExcesses { - query_id: Int as uint64; +message(0xd53276db) JettonExcesses { + queryId: Int as uint64; } message(0x2c76b973) ProvideWalletAddress { - query_id: Int as uint64; - owner_address: Address; - include_address: Bool; + queryId: Int as uint64; + ownerAddress: Address; + includeAddress: Bool; } -message(0xd1735400) TakeWalletAddress { - query_id: Int as uint64; - wallet_address: Address; - owner_address: Cell?; //It is Maybe ^Address, just encoded it like this +const TakeWalletAddressOpcode: Int = 0xd1735400; +message(TakeWalletAddressOpcode) TakeWalletAddress { + queryId: Int as uint64; + walletAddress: Address; + ownerAddress: Cell?; //It is Maybe ^Address, just encoded it like this } -message(21) Mint { //We use opcode 21 to match token-contract. - query_id: Int as uint64; - amount: Int; +message(21) Mint { + queryId: Int as uint64; receiver: Address; -} \ No newline at end of file + tonAmount: Int as coins; + mintMessage: JettonTransferInternal; +} + +message(3) ChangeOwner { + queryId: Int as uint64; + newOwner: Address; +} diff --git a/tact.config.json b/tact.config.json index d207c59..530791e 100644 --- a/tact.config.json +++ b/tact.config.json @@ -6,8 +6,7 @@ "mode": "full", "options": { "external": false, - "debug": true, - "masterchain": true, + "debug": false, "ipfsAbiGetter": false, "interfacesGetter": false, "experimental": { diff --git a/yarn.lock b/yarn.lock index 699328c..32bc10e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -700,7 +700,24 @@ "@smithy/util-buffer-from" "^2.2.0" tslib "^2.6.2" -"@tact-lang/compiler@^1.5.0", "@tact-lang/compiler@~1.5.1": +"@tact-lang/compiler@^1.5.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@tact-lang/compiler/-/compiler-1.6.0.tgz#e5ec8fe606d4eca812f5f9042df76dac92ff086e" + integrity sha512-rJ6HcBLlk3onyYozX+5rlNMDurUD8TGFirSPeQjlDChPA4tWNolU6NpRNdlFx/Ac0/bCRjMF99UJ8+LRF7oQbQ== + dependencies: + "@tact-lang/opcode" "^0.3.0" + "@ton/core" "0.60.0" + "@ton/crypto" "^3.2.0" + "@tonstudio/parser-runtime" "^0.0.1" + blockstore-core "1.0.5" + change-case "^4.1.2" + ipfs-unixfs-importer "9.0.10" + json-bigint "^1.0.0" + ohm-js "^17.1.0" + path-normalize "^6.0.13" + zod "^3.22.4" + +"@tact-lang/compiler@~1.5.1": version "1.5.3" resolved "https://registry.yarnpkg.com/@tact-lang/compiler/-/compiler-1.5.3.tgz#cbf1bb35e6b9303f541dfbab444303b91775e1b2" integrity sha512-BxSCWfIQJUa0RC2ZltYfgTeN+3dRjMs4Hdxq61REfghpRiSU0IKdkLPGqJH/5zimzFFhjD4mhR0aFS71zUH9hA== @@ -734,6 +751,14 @@ resolved "https://registry.yarnpkg.com/@tact-lang/opcode/-/opcode-0.0.16.tgz#cc9e4117ce706d8bb6e054a520bab719f70adf96" integrity sha512-YJTUjoDOy+e+FHHppJiF+uWJ2IMjVknB9VQ5n78pknCE129DazCb/nFXnw0wVRDVcn8Tn59ky+pmjiQjQOjEbw== +"@tact-lang/opcode@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@tact-lang/opcode/-/opcode-0.3.0.tgz#c127b420a71bfeb383e0954b0de325f5e260c803" + integrity sha512-kSzdBR7r5B0I0gXqmRpqBwaYfkpGaSt4RT6oWs50yOB6OlB7bDZ45vumjGUFb0aEd0T6xeeM4yxMOgqiomtq8w== + dependencies: + "@ton/core" "^0.60.0" + "@ton/crypto" "^3.3.0" + "@tact-lang/ton-abi@^0.0.3": version "0.0.3" resolved "https://registry.yarnpkg.com/@tact-lang/ton-abi/-/ton-abi-0.0.3.tgz#68aa8ecf8e14d91f37252773395753e4c623798e" @@ -751,6 +776,20 @@ dependencies: symbol.inspect "1.0.1" +"@ton/core@0.60.0": + version "0.60.0" + resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.60.0.tgz#7968d6a53b63c81149b5b4c91725518bc88a2f87" + integrity sha512-vK0itrieVashNQ7geqpvlWcOyXsXKKtIo6h02HcVcMeNo+QxovBaDAvou3BUKDnf7ej6+rRSuXMSOIjBct/zIg== + dependencies: + symbol.inspect "1.0.1" + +"@ton/core@^0.60.0": + version "0.60.1" + resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.60.1.tgz#cc9a62fb308d7597b1217dc8e44c7e2dcc0aceaa" + integrity sha512-8FwybYbfkk57C3l9gvnlRhRBHbLYmeu0LbB1z9N+dhDz0Z+FJW8w0TJlks8CgHrAFxsT3FlR2LsqFnsauMp38w== + dependencies: + symbol.inspect "1.0.1" + "@ton/core@~0.56.3": version "0.56.3" resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.56.3.tgz#1162764573abb76032eba70f8497e5cb2ea532ee" @@ -772,7 +811,7 @@ dependencies: jssha "3.2.0" -"@ton/crypto@^3.2.0": +"@ton/crypto@^3.2.0", "@ton/crypto@^3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@ton/crypto/-/crypto-3.3.0.tgz#019103df6540fbc1d8102979b4587bc85ff9779e" integrity sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA== @@ -804,6 +843,11 @@ teslabot "^1.3.0" zod "^3.21.4" +"@tonstudio/parser-runtime@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@tonstudio/parser-runtime/-/parser-runtime-0.0.1.tgz#469955fb7ea354d4fadaa5964359b11fd17f926b" + integrity sha512-5s4fLkXWxa4SAd7QGGvJXe13GakEo0J3VF5dUI/i3A//bGZxMwCp1FcnbErpNs3y0LcAZoXE5FCUnDowDQptqw== + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"