From 8049f8f6bfa5fb785141f8d163ef6d7561067161 Mon Sep 17 00:00:00 2001 From: Suprun Date: Sun, 29 Sep 2024 13:25:04 +0300 Subject: [PATCH] feat: add jetton wallet discoverable (#6) --- contracts/jetton/master.tact | 5 ++- contracts/messages.tact | 10 +++++ contracts/teps/tep74.tact | 25 ++++--------- contracts/teps/tep89.tact | 45 ++++++++++++++++++++++ tests/JettonMaster.spec.ts | 72 +++++++++++++++++++++++++++++++++++- 5 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 contracts/teps/tep89.tact diff --git a/contracts/jetton/master.tact b/contracts/jetton/master.tact index 41e9a1f..fbcc40c 100644 --- a/contracts/jetton/master.tact +++ b/contracts/jetton/master.tact @@ -2,11 +2,12 @@ import "@stdlib/deploy"; import "@stdlib/ownable"; import "./wallet.tact"; import "../teps/tep74.tact"; +import "../teps/tep89.tact"; import "../messages.tact"; import "../errors.tact"; @interface("org.ton.jetton.master") -contract JettonMaster with TEP74JettonMaster, Deployable, Ownable { +contract JettonMaster with TEP74JettonMaster, TEP89JettonDiscoverable, Deployable, Ownable { // Maximum tokens can be minted max_supply: Int = ton("21000000"); // Current tokens minted @@ -69,7 +70,7 @@ contract JettonMaster with TEP74JettonMaster, Deployable, Ownable { receive(msg: JettonMint){ self.requireOwner(); nativeThrowIf(ERROR_MAX_SUPPLY_EXCEEDED, (self.current_supply + msg.amount) > self.max_supply); - let init = self.generate_wallet_state_init(msg.destination); + let init = self.discover_wallet_state_init(myAddress(), msg.destination); let to = contractAddress(init); send(SendParameters{ to: to, diff --git a/contracts/messages.tact b/contracts/messages.tact index acb83aa..27eec63 100644 --- a/contracts/messages.tact +++ b/contracts/messages.tact @@ -36,6 +36,16 @@ message(0x7bdd97de) JettonBurnInternal { message(0xd53276db) Excesses { query_id: Int as uint64; } +message(0x2c76b973) ProvideWalletAddress { + query_id: Int as uint64; + owner_address: Address; + include_address: Bool; +} +message(0xd1735400) TakeWalletAddress { + query_id: Int as uint64; + wallet_address: Address; + owner_address: Address?; +} message(0x133701) JettonInit { query_id: Int as uint64; jetton_name: String; diff --git a/contracts/teps/tep74.tact b/contracts/teps/tep74.tact index f750b53..a515e78 100644 --- a/contracts/teps/tep74.tact +++ b/contracts/teps/tep74.tact @@ -2,6 +2,7 @@ import "@stdlib/ownable"; import "./tep64.tact"; +import "./tep89.tact"; import "../messages.tact"; import "../errors.tact"; import "../consts.tact"; @@ -13,7 +14,7 @@ struct JettonMasterData { jetton_wallet_code: Cell; } // TON jetton standard. Trait for jetton master -trait TEP74JettonMaster with TEP64Metadata { +trait TEP74JettonMaster with TEP64Metadata, DiscoverWalletAddress { // Maximum tokens can be minted max_supply: Int; // Current tokens minted @@ -33,7 +34,7 @@ trait TEP74JettonMaster with TEP64Metadata { receive(msg: JettonBurnInternal){ let ctx = context(); - let init = self.generate_wallet_state_init(msg.sender); + let init = self.discover_wallet_state_init(myAddress(), msg.sender); let wallet_address = contractAddress(init); nativeThrowUnless(ERROR_CODE_INVALID_OWNER, ctx.sender == wallet_address); send(SendParameters{ @@ -58,14 +59,8 @@ trait TEP74JettonMaster with TEP64Metadata { }; } - fun generate_wallet_state_init(owner: Address): StateInit { - let data = beginCell().storeRef(self.jetton_wallet_system).storeUint(0, 1).storeAddress(myAddress() - ).storeAddress(owner).endCell(); - return StateInit{code: self.jetton_wallet_code, data: data}; - } - get fun get_wallet_address(owner: Address): Address { - let init = self.generate_wallet_state_init(owner); + let init = self.discover_wallet_state_init(myAddress(), owner); return contractAddress(init); } } @@ -75,7 +70,7 @@ struct JettonWalletData { master: Address; code: Cell; } -trait TEP74JettonWallet with Ownable { +trait TEP74JettonWallet with Ownable, DiscoverWalletAddress { const GAS_CONSUMPTION: Int = ton("0.01"); const MIN_BALANCE: Int = ton("0.01"); master: Address; @@ -93,7 +88,7 @@ trait TEP74JettonWallet with Ownable { ((((ctx.readForwardFee() * 2) + (2 * self.GAS_CONSUMPTION)) + self.MIN_BALANCE) + msg.forward_ton_amount) < ctx.value ); - let init = self.generate_wallet_state_init(msg.destination); + let init = self.discover_wallet_state_init(self.master, msg.destination); let to = contractAddress(init); send(SendParameters{ to: to, @@ -117,7 +112,7 @@ trait TEP74JettonWallet with Ownable { receive(msg: JettonTransferInternal){ let ctx = context(); if (ctx.sender != self.master) { - let init = self.generate_wallet_state_init(msg.from); + let init = self.discover_wallet_state_init(self.master, msg.from); nativeThrowUnless(ERROR_CODE_INVALID_OWNER, contractAddress(init) == ctx.sender); } self.balance = self.balance + msg.amount; @@ -193,10 +188,4 @@ trait TEP74JettonWallet with Ownable { code: self.jetton_wallet_code }; } - - fun generate_wallet_state_init(owner: Address): StateInit { - let data = beginCell().storeRef(self.jetton_wallet_system).storeUint(0, 1).storeAddress(self.master - ).storeAddress(owner).endCell(); - return StateInit{code: self.jetton_wallet_code, data: data}; - } } \ No newline at end of file diff --git a/contracts/teps/tep89.tact b/contracts/teps/tep89.tact new file mode 100644 index 0000000..344e0bc --- /dev/null +++ b/contracts/teps/tep89.tact @@ -0,0 +1,45 @@ +import "../messages.tact"; +import "../errors.tact"; +// Trait for generating jetton wallet state init +trait DiscoverWalletAddress { + // Initial code of jetton wallet + jetton_wallet_code: Cell; + // System cell of jetton wallet contract (Tact feature) + jetton_wallet_system: Cell; + + fun discover_wallet_state_init(master: Address, owner: Address): StateInit { + let data = beginCell().storeRef(self.jetton_wallet_system).storeUint(0, 1).storeAddress(master + ).storeAddress(owner).endCell(); + return StateInit{code: self.jetton_wallet_code, data: data}; + } +} +// Trait for discover jetton wallet address by owner +// https://github.com/ton-blockchain/TEPs/blob/master/text/0089-jetton-wallet-discovery.md +trait TEP89JettonDiscoverable with DiscoverWalletAddress { + // Initial code of jetton wallet + jetton_wallet_code: Cell; + // System cell of jetton wallet contract (Tact feature) + jetton_wallet_system: Cell; + + receive(msg: ProvideWalletAddress){ + let ctx = context(); + nativeThrowUnless(ERROR_CODE_NEED_FEE, ctx.value <= ton("0.05")); // TODO + let init = self.discover_wallet_state_init(myAddress(), msg.owner_address); + let address = contractAddress(init); + let owner_address: Address? = null; + if (msg.include_address) { + owner_address = msg.owner_address; + } + send(SendParameters{ + to: ctx.sender, + value: 0, + mode: SendRemainingValue, + body: TakeWalletAddress{ + query_id: msg.query_id, + wallet_address: address, + owner_address: owner_address + }.toCell() + } + ); + } +} \ No newline at end of file diff --git a/tests/JettonMaster.spec.ts b/tests/JettonMaster.spec.ts index 45356c3..23a998e 100644 --- a/tests/JettonMaster.spec.ts +++ b/tests/JettonMaster.spec.ts @@ -1,5 +1,5 @@ import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; -import { Builder, toNano } from '@ton/core'; +import { beginCell, Builder, toNano } from '@ton/core'; import { JettonWallet } from '../build/Jetton/tact_JettonWallet'; import { JettonMaster } from '../build/Jetton/tact_JettonMaster'; import '@ton/test-utils'; @@ -275,4 +275,74 @@ describe('JettonMaster', () => { exitCode: 132, }); }); + + it('should discover address', async () => { + let discoverResult = await jettonMaster.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + $$type: 'ProvideWalletAddress', + query_id: 0n, + owner_address: other.address, + include_address: false, + } + ); + expect(discoverResult.transactions).toHaveTransaction({ + from: deployer.address, + to: jettonMaster.address, + success: true, + deploy: false, + op: 0x2c76b973, + }); + expect(discoverResult.transactions).toHaveTransaction({ + from: jettonMaster.address, + to: deployer.address, + success: true, + deploy: false, + op: 0xd1735400, + body: beginCell() + .storeUint(0xd1735400, 32) + .storeUint(0, 64) + .storeAddress(otherJettonWallet.address) + .storeAddress(null) + .endCell() + }); + }); + + it('should discover include address', async () => { + let discoverResult = await jettonMaster.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + $$type: 'ProvideWalletAddress', + query_id: 0n, + owner_address: other.address, + include_address: true, + } + ); + expect(discoverResult.transactions).toHaveTransaction({ + from: deployer.address, + to: jettonMaster.address, + success: true, + deploy: false, + op: 0x2c76b973, + }); + expect(discoverResult.transactions).toHaveTransaction({ + from: jettonMaster.address, + to: deployer.address, + success: true, + deploy: false, + op: 0xd1735400, + body: beginCell() + .storeUint(0xd1735400, 32) + .storeUint(0, 64) + .storeAddress(otherJettonWallet.address) + .storeAddress(other.address) + .endCell() + }); + }); });