Skip to content

Commit

Permalink
feat: add jetton tranfer
Browse files Browse the repository at this point in the history
  • Loading branch information
ya7on committed Sep 28, 2024
1 parent 34e93de commit dc57ec5
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 47 deletions.
10 changes: 6 additions & 4 deletions contracts/errors.tact
Original file line number Diff line number Diff line change
@@ -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;
const ERROR_JETTON_UNKNOWN_PARAMETER: Int = 6904;
// Jetton max supply exceeded
const ERROR_MAX_SUPPLY_EXCEEDED: Int = 6905;
29 changes: 29 additions & 0 deletions contracts/jetton/master.tact
Original file line number Diff line number Diff line change
Expand Up @@ -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<JettonTransferInternal>){
self.current_supply -= msg.amount;
}
}
8 changes: 6 additions & 2 deletions contracts/jetton/wallet.tact
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
5 changes: 5 additions & 0 deletions contracts/messages.tact
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
66 changes: 28 additions & 38 deletions contracts/teps/tep74.tact
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -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();
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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};
}
}
92 changes: 89 additions & 3 deletions tests/JettonMaster.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,16 +17,20 @@ const UPDATED_JETTON_MAX_SUPPLY = toNano("0");
describe('JettonMaster', () => {
let blockchain: Blockchain;
let deployer: SandboxContract<TreasuryContract>;
let other: SandboxContract<TreasuryContract>;
let jettonMaster: SandboxContract<JettonMaster>;
let jettonWallet: SandboxContract<JettonWallet>;
let otherJettonWallet: SandboxContract<JettonWallet>;

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(),
Expand All @@ -42,7 +46,6 @@ describe('JettonMaster', () => {
max_supply: JETTON_MAX_SUPPLY,
}
);

expect(deployResult.transactions).toHaveTransaction({
from: deployer.address,
to: jettonMaster.address,
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
});
});
});
Loading

0 comments on commit dc57ec5

Please sign in to comment.