diff --git a/contracts/errors.tact b/contracts/errors.tact index 215d686..25a0812 100644 --- a/contracts/errors.tact +++ b/contracts/errors.tact @@ -1,10 +1,12 @@ // Defaul Tact error code for invalid owner const ERROR_CODE_INVALID_OWNER: Int = 132; // Jetton wallet balance doesn't have enough tokens -const ERROR_CODE_NOT_ENOUGH_BALANCE: Int = 6911; +const ERROR_CODE_NOT_ENOUGH_BALANCE: Int = 6901; // Need more TONs in request for fee -const ERROR_CODE_NEED_FEE: Int = 6912; +const ERROR_CODE_NEED_FEE: Int = 6902; // Jetton already initialized -const ERROR_JETTON_INITIALIZED: Int = 6913; +const ERROR_JETTON_INITIALIZED: Int = 6903; // Jetton already initialized -const ERROR_JETTON_UNKNOWN_PARAMETER: Int = 6914; \ No newline at end of file +const ERROR_JETTON_UNKNOWN_PARAMETER: Int = 6904; +// Jetton max supply exceeded +const ERROR_MAX_SUPPLY_EXCEEDED: Int = 6905; \ No newline at end of file diff --git a/contracts/jetton/master.tact b/contracts/jetton/master.tact index f69d755..41e9a1f 100644 --- a/contracts/jetton/master.tact +++ b/contracts/jetton/master.tact @@ -65,4 +65,33 @@ contract JettonMaster with TEP74JettonMaster, Deployable, Ownable { } nativeThrowUnless(ERROR_JETTON_UNKNOWN_PARAMETER, updated); } + + 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 to = contractAddress(init); + send(SendParameters{ + to: to, + value: 0, + mode: SendRemainingValue, + code: init.code, + data: init.data, + bounce: true, + body: JettonTransferInternal{ + query_id: msg.query_id, + amount: msg.amount, + from: myAddress(), + response_destination: msg.destination, + forward_ton_amount: 0, + forward_payload: emptySlice() + }.toCell() + } + ); + self.current_supply += msg.amount; + } + + bounced(msg: bounced){ + self.current_supply -= msg.amount; + } } \ No newline at end of file diff --git a/contracts/jetton/wallet.tact b/contracts/jetton/wallet.tact index c7308db..ed5ed11 100644 --- a/contracts/jetton/wallet.tact +++ b/contracts/jetton/wallet.tact @@ -4,11 +4,15 @@ import "../teps/tep74.tact"; contract JettonWallet with TEP74JettonWallet { master: Address; owner: Address; - code: Cell; + jetton_wallet_code: Cell; + jetton_wallet_system: Cell; balance: Int = 0; init(master: Address, owner: Address){ self.master = master; self.owner = owner; - self.code = initOf JettonWallet(master, owner).code; + let init = initOf JettonWallet(master, master); + let data = init.data.beginParse(); + self.jetton_wallet_code = init.code; + self.jetton_wallet_system = data.loadRef(); } } \ No newline at end of file diff --git a/contracts/messages.tact b/contracts/messages.tact index 1246354..acb83aa 100644 --- a/contracts/messages.tact +++ b/contracts/messages.tact @@ -49,4 +49,9 @@ message(0x133702) JettonInitOk { message(0x133703) JettonSetParameter { key: String; value: Slice; +} +message(0x133704) JettonMint { + query_id: Int as uint64; + destination: Address; + amount: Int as coins; } \ No newline at end of file diff --git a/contracts/teps/tep74.tact b/contracts/teps/tep74.tact index 9f35818..7adbc9e 100644 --- a/contracts/teps/tep74.tact +++ b/contracts/teps/tep74.tact @@ -43,10 +43,14 @@ trait TEP74JettonMaster with TEP64Metadata { }; } - get fun get_wallet_address(owner: Address): Address { + fun generate_wallet_state_init(owner: Address): StateInit { let data = beginCell().storeRef(self.jetton_wallet_system).storeUint(0, 1).storeAddress(myAddress() ).storeAddress(owner).endCell(); - let init = StateInit{code: self.jetton_wallet_code, data: data}; + 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); return contractAddress(init); } } @@ -61,27 +65,10 @@ trait TEP74JettonWallet with Ownable { const MIN_BALANCE: Int = ton("0.01"); master: Address; owner: Address; - code: Cell; + jetton_wallet_code: Cell; + jetton_wallet_system: Cell; balance: Int; - fun packJettonWalletData(master: Address, owner: Address, code: Cell): Cell { - return beginCell().storeAddress(master).storeAddress(owner).storeRef(code).endCell(); - } - - fun calculateJettonWalletStateInit(master: Address, owner: Address, code: Cell): Cell { - return - beginCell().storeUint(0, 2).storeRef(code).storeRef(self.packJettonWalletData(master, owner, code) - ).storeUint(0, 1).endCell(); - } - - fun calculateJettonWalletAddress(stateInit: Cell): Slice { - return beginCell().storeUint(4, 3).storeInt(0, 8).storeUint(stateInit.hash(), 256).endCell().beginParse(); - } - - fun calculateUserJettonWalletAddress(master: Address, owner: Address, code: Cell): Slice { - return self.calculateJettonWalletAddress(self.calculateJettonWalletStateInit(master, owner, code)); - } - receive(msg: JettonTransfer){ let ctx = context(); self.requireOwner(); @@ -91,21 +78,15 @@ trait TEP74JettonWallet with Ownable { ((((ctx.readForwardFee() * 2) + (2 * self.GAS_CONSUMPTION)) + self.MIN_BALANCE) + msg.forward_ton_amount) < ctx.value ); - /* - master: Address; - owner: Address; - code: Cell; - balance: Int = 0; - */ - let data = self.packJettonWalletData(self.master, msg.destination, self.code); - let to = self.calculateUserJettonWalletAddress(self.master, msg.destination, self.code); + let init = self.generate_wallet_state_init(msg.destination); + let to = contractAddress(init); send(SendParameters{ - to: to.loadAddress(), + to: to, value: 0, mode: SendRemainingValue, - // bounce: true, - code: self.code, - data: data, + bounce: true, + code: init.code, + data: init.data, body: JettonTransferInternal{ query_id: msg.query_id, amount: msg.amount, @@ -121,10 +102,7 @@ trait TEP74JettonWallet with Ownable { receive(msg: JettonTransferInternal){ let ctx = context(); if (ctx.sender != self.master) { - let init = StateInit{ - code: self.code, - data: beginCell().storeAddress(self.master).storeAddress(msg.from).endCell() - }; + let init = self.generate_wallet_state_init(msg.from); nativeThrowUnless(ERROR_CODE_INVALID_OWNER, contractAddress(init) == ctx.sender); } self.balance = self.balance + msg.amount; @@ -192,6 +170,18 @@ trait TEP74JettonWallet with Ownable { } get fun get_wallet_data(): JettonWalletData { - return JettonWalletData{balance: self.balance, owner: self.owner, master: self.master, code: self.code}; + return + JettonWalletData{ + balance: self.balance, + owner: self.owner, + master: self.master, + 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/tests/JettonMaster.spec.ts b/tests/JettonMaster.spec.ts index 3ce37a1..80bac0e 100644 --- a/tests/JettonMaster.spec.ts +++ b/tests/JettonMaster.spec.ts @@ -1,5 +1,5 @@ import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; -import { Builder, Slice, toNano } from '@ton/core'; +import { Builder, toNano } from '@ton/core'; import { JettonWallet } from '../build/Jetton/tact_JettonWallet'; import { JettonMaster } from '../build/Jetton/tact_JettonMaster'; import '@ton/test-utils'; @@ -17,16 +17,20 @@ const UPDATED_JETTON_MAX_SUPPLY = toNano("0"); describe('JettonMaster', () => { let blockchain: Blockchain; let deployer: SandboxContract; + let other: SandboxContract; let jettonMaster: SandboxContract; let jettonWallet: SandboxContract; + let otherJettonWallet: SandboxContract; beforeEach(async () => { blockchain = await Blockchain.create(); deployer = await blockchain.treasury('deployer'); + other = await blockchain.treasury("other"); jettonMaster = blockchain.openContract(await JettonMaster.fromInit(deployer.address)); jettonWallet = blockchain.openContract(await JettonWallet.fromInit(jettonMaster.address, deployer.address)); + otherJettonWallet = blockchain.openContract(await JettonWallet.fromInit(jettonMaster.address, other.address)); const deployResult = await jettonMaster.send( deployer.getSender(), @@ -42,7 +46,6 @@ describe('JettonMaster', () => { max_supply: JETTON_MAX_SUPPLY, } ); - expect(deployResult.transactions).toHaveTransaction({ from: deployer.address, to: jettonMaster.address, @@ -54,6 +57,9 @@ describe('JettonMaster', () => { it('should correct build wallet address', async () => { let walletAddressData = await jettonMaster.getGetWalletAddress(deployer.address); expect(walletAddressData.toString()).toEqual(jettonWallet.address.toString()); + + let otherWalletAddressData = await jettonMaster.getGetWalletAddress(other.address); + expect(otherWalletAddressData.toString()).toEqual(otherJettonWallet.address.toString()); }); it('should return correct jetton metadata', async () => { @@ -89,6 +95,31 @@ describe('JettonMaster', () => { }); }); + it('should not double init', async () => { + const deployResult = await jettonMaster.send( + other.getSender(), + { + value: toNano("0.05"), + }, + { + $$type: 'JettonInit', + query_id: 0n, + jetton_name: JETTON_NAME, + jetton_description: JETTON_DESCRIPTION, + jetton_symbol: JETTON_SYMBOL, + max_supply: JETTON_MAX_SUPPLY, + } + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: other.address, + to: jettonMaster.address, + success: false, + deploy: false, + exitCode: 132, + }); + }); + it('should update jetton parameters', async () => { // Jetton name const nameUpdateResult = await jettonMaster.send( @@ -170,5 +201,60 @@ describe('JettonMaster', () => { let jettonMasterMetadata = await jettonMaster.getGetJettonData(); expect(jettonMasterMetadata.mintable).toEqual(false); // TODO metadata - }) + }); + + it('should mint tokens', async () => { + const mintResult = await jettonMaster.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + $$type: 'JettonMint', + query_id: 0n, + amount: toNano("1337"), + destination: deployer.address, + } + ); + expect(mintResult.transactions).toHaveTransaction({ + from: deployer.address, + to: jettonMaster.address, + success: true, + deploy: false, + }); + expect(mintResult.transactions).toHaveTransaction({ + from: jettonMaster.address, + to: jettonWallet.address, + success: true, + deploy: true, + }); + + let jettonMasterMetadata = await jettonMaster.getGetJettonData(); + expect(jettonMasterMetadata.total_supply).toEqual(toNano("1337")); + + let jettonWalletData = await jettonWallet.getGetWalletData(); + expect(jettonWalletData.balance).toEqual(toNano("1337")); + }); + + it('should not mint tokens not owner', async () => { + const mintResult = await jettonMaster.send( + other.getSender(), + { + value: toNano("0.05"), + }, + { + $$type: 'JettonMint', + query_id: 0n, + amount: toNano("1337"), + destination: other.address, + } + ); + expect(mintResult.transactions).toHaveTransaction({ + from: other.address, + to: jettonMaster.address, + success: false, + deploy: false, + exitCode: 132, + }); + }); }); diff --git a/tests/JettonWallet.spec.ts b/tests/JettonWallet.spec.ts new file mode 100644 index 0000000..8f350ce --- /dev/null +++ b/tests/JettonWallet.spec.ts @@ -0,0 +1,96 @@ +import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; +import { Builder, Slice, toNano } from '@ton/core'; +import { JettonWallet } from '../build/Jetton/tact_JettonWallet'; +import { JettonMaster } from '../build/Jetton/tact_JettonMaster'; +import '@ton/test-utils'; + +const JETTON_NAME = "Test jetton"; +const JETTON_DESCRIPTION = "Test jetton description. Test jetton description. Test jetton description"; +const JETTON_SYMBOL = "TSTJTN"; +const JETTON_MAX_SUPPLY = toNano("100500"); + +describe('JettonMaster', () => { + let blockchain: Blockchain; + let deployer: SandboxContract; + let other: SandboxContract; + let jettonMaster: SandboxContract; + let jettonWallet: SandboxContract; + let otherJettonWallet: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + + deployer = await blockchain.treasury('deployer'); + other = await blockchain.treasury("other"); + + jettonMaster = blockchain.openContract(await JettonMaster.fromInit(deployer.address)); + jettonWallet = blockchain.openContract(await JettonWallet.fromInit(jettonMaster.address, deployer.address)); + otherJettonWallet = blockchain.openContract(await JettonWallet.fromInit(jettonMaster.address, other.address)); + + await jettonMaster.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + $$type: 'JettonInit', + query_id: 0n, + jetton_name: JETTON_NAME, + jetton_description: JETTON_DESCRIPTION, + jetton_symbol: JETTON_SYMBOL, + max_supply: JETTON_MAX_SUPPLY, + } + ); + await jettonMaster.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + $$type: 'JettonMint', + query_id: 0n, + amount: toNano("1337"), + destination: deployer.address, + } + ); + }); + + it('should transfer tokens', async () => { + const transferResult = await jettonWallet.send( + deployer.getSender(), + { + value: toNano("0.05"), + }, + { + $$type: 'JettonTransfer', + query_id: 0n, + amount: toNano("228"), + destination: other.address, + custom_payload: null, + forward_payload: new Builder().asSlice(), + forward_ton_amount: 0n, + response_destination: other.address, + } + ); + expect(transferResult.transactions).toHaveTransaction({ + from: deployer.address, + to: jettonWallet.address, + deploy: false, + success: true, + op: 0x0f8a7ea5, + }); + expect(transferResult.transactions).toHaveTransaction({ + from: jettonWallet.address, + to: otherJettonWallet.address, + deploy: true, + success: true, + op: 0x178d4519, + }); + + let jettonWalletData = await jettonWallet.getGetWalletData(); + expect(jettonWalletData.balance).toEqual(toNano("1337") - toNano("228")); + + let otherJettonWalletData = await otherJettonWallet.getGetWalletData(); + expect(otherJettonWalletData.balance).toEqual(toNano("228")); + }); +});