Skip to content

Commit

Permalink
feat: add optional minting to JettonInit (#30)
Browse files Browse the repository at this point in the history
### **PR Description**

#### **Title:**  
`feat: add optional minting to JettonInit`

---

### **Key Points**

- **Optional Minting:**  
- Added support for minting tokens during `JettonInit` if `mint_amount >
0`.
  - Skips minting if `mint_amount` is `0` or `null`.

- **Dynamic Minting Logic:**  
  - Updated `mint` function to accept an optional `value` parameter.  
- Uses `SendRemainingValue` or specified `value` with mode `0`
dynamically.

- **Test Cases Added:**  
- **Test 1:** Validates successful initialization with minting (`mint >
0`).
- **Test 2:** Ensures proper error handling for non-active wallets
(`mint = 0`).

---------

Co-authored-by: iamIcarus <5759704+iamIcarus@users.noreply.github.com>
  • Loading branch information
iamIcarus and iamIcarus authored Jan 15, 2025
1 parent a73b720 commit 11a1fb0
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 89 deletions.
110 changes: 77 additions & 33 deletions contracts/jetton/master.tact
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,86 @@ contract JettonMaster with TEP74JettonMaster, TEP89JettonDiscoverable, Deployabl
self.metadata.set("description", msg.jetton_description);
self.metadata.set("symbol", msg.jetton_symbol);
self.max_supply = msg.max_supply > 0 ? msg.max_supply : 0;
self.notify(JettonInitOk{query_id: msg.query_id}.toCell());
self.deployed = true;

// Mint tokens during initialization
if (msg.mint_amount != null && msg.mint_amount!! > 0) {
let ctx: Context = context();

// Calculate remaining balance for minting
let initialTransactionValue = ctx.value;
let forwardFee = ctx.readForwardFee();
let compute_gas: Int = ton("0.01");
let notify_gas: Int = ton("0.02");
let remainingValue = initialTransactionValue - notify_gas - compute_gas - forwardFee;

// Ensure remaining value is sufficient
nativeThrowUnless(ERROR_CODE_NEED_FEE,remainingValue > ton("0.05"));

//Excess ton will be handled by the transfer process
self.mint(msg.mint_amount!!, self.owner, msg.query_id,remainingValue);

send(SendParameters{
to: ctx.sender, // back to the sender
value: notify_gas, //reserved gas for this op
bounce: false,
mode: 0,
body: JettonInitOk{query_id: msg.query_id}.toCell()
});
}else{
// notify and send remaining ton back
self.notify(JettonInitOk{query_id: msg.query_id}.toCell());
}
}


receive(msg: JettonMint) {
nativeThrowIf(ERROR_CODE_MINTING_DISABLED, !self.mintable); // Reject mint if minting is disabled
nativeThrowUnless(ERROR_CODE_INVALID_AMOUNT, msg.amount > 0); // Reject mint with amount <= 0
self.requireOwner();

// The mint operation
self.mint(msg.amount, msg.destination, msg.query_id,null);
}

fun mint(amount: Int, destination: Address, query_id: Int, value: Int?) {
// Ensure the new supply does not exceed the max supply
if (self.max_supply > 0) {
nativeThrowIf(ERROR_MAX_SUPPLY_EXCEEDED, (self.current_supply + amount) > self.max_supply);
}

// Prepare wallet initialization
let init = self.discover_wallet_state_init(myAddress(), destination);
let to = contractAddress(init);

// Determine value and mode based on whether a value is provided
let sendValue = value != null ? value!! : 0;
let sendMode = value != null ? 0 : SendRemainingValue;

// Send minting transaction
send(SendParameters{
to: to,
value: sendValue,
mode: sendMode,
code: init.code,
data: init.data,
bounce: true,
body: JettonTransferInternal{
query_id: query_id,
amount: amount,
from: myAddress(),
response_destination: destination,
forward_ton_amount: 0,
forward_payload: emptySlice()
}.toCell()
});

// Update the current supply
self.current_supply += amount;
}



receive(msg: JettonSetParameter) {
self.requireOwner(); // Ensure only the current owner can update parameters

Expand All @@ -72,38 +148,6 @@ contract JettonMaster with TEP74JettonMaster, TEP89JettonDiscoverable, Deployabl
self.metadata.set(msg.key, msg.value); // Update metadata for other keys
}

receive(msg: JettonMint){
nativeThrowIf(ERROR_CODE_MINTING_DISABLED, !self.mintable); // Reject mint if minting is disabled
nativeThrowUnless(ERROR_CODE_INVALID_AMOUNT, msg.amount > 0); // Reject mint with amount <= 0
self.requireOwner();

// Ensure the new supply does not exceed the max supply
// Only for the case that max_supply is not set to unlimited (0)
if(self.max_supply > 0){
nativeThrowIf(ERROR_MAX_SUPPLY_EXCEEDED, (self.current_supply + msg.amount) > self.max_supply);
}

let init = self.discover_wallet_state_init(myAddress(), 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>){
nativeThrowUnless(ERROR_CODE_INVALID_AMOUNT, msg.amount > 0); // Is this needed? Keeping it for consistency
Expand Down
2 changes: 2 additions & 0 deletions contracts/messages.tact
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ message(0x133701) JettonInit {
jetton_description: Slice;
jetton_symbol: Slice;
max_supply: Int as coins;
mint_amount: Int? as coins;
}

message(0x133702) JettonInitOk {
query_id: Int as uint64;
}
Expand Down
1 change: 1 addition & 0 deletions scripts/deployJettonMaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export async function run(provider: NetworkProvider) {
jetton_description: beginCell().storeStringRefTail('Long' + ' long '.repeat(100) + 'description').asSlice(),
jetton_symbol: beginCell().storeStringRefTail('SMBL').asSlice(),
max_supply: toNano(1337),
mint_amount: null
}
);
await jettonMaster.send(
Expand Down
239 changes: 239 additions & 0 deletions tests/JettonInit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
import { beginCell, toNano } from '@ton/core';
import { JettonWallet } from '../build/Jetton/tact_JettonWallet';
import { JettonMaster } from '../build/Jetton/tact_JettonMaster';
import { OP_CODES , SYSTEM_CELL, ERROR_CODES} from './constants/constants';

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<TreasuryContract>;
let other: SandboxContract<TreasuryContract>;
let jettonMaster: SandboxContract<JettonMaster>;
let jettonWallet: SandboxContract<JettonWallet>;
let otherJettonWallet: SandboxContract<JettonWallet>;

// handle the init and optional minting
const initializeJettonMaster = async (mintAmount: bigint | null) => {
const initValue = mintAmount && mintAmount > 0n ? toNano("1") : toNano("0.05");

// Send the JettonInit message with the mintAmount
const deployResult = await jettonMaster.send(
deployer.getSender(),
{
value: initValue,
},
{
$$type: 'JettonInit',
query_id: 0n,
jetton_name: beginCell().storeStringTail(JETTON_NAME).asSlice(),
jetton_description: beginCell().storeStringTail(JETTON_DESCRIPTION).asSlice(),
jetton_symbol: beginCell().storeStringTail(JETTON_SYMBOL).asSlice(),
max_supply: JETTON_MAX_SUPPLY,
mint_amount: mintAmount, // Pass the mint amount as a parameter
}
);

// Verify the deployment transactions
expect(deployResult.transactions).toHaveTransaction({
from: deployer.address,
to: jettonMaster.address,
success: true,
deploy: true,
op: OP_CODES.JettonInit,
});

expect(deployResult.transactions).toHaveTransaction({
from: jettonMaster.address,
to: deployer.address,
success: true,
deploy: false,
op: OP_CODES.JettonInitOk,
});

// If mintAmount is specified and greater than 0, validate the minting logic
if (mintAmount && mintAmount > 0n) {
expect(deployResult.transactions).toHaveTransaction({
from: jettonMaster.address,
to: jettonWallet.address,
deploy: true,
success: true,
op: OP_CODES.JettonTransferInternal,
});
}
};

beforeEach(async () => {
blockchain = await Blockchain.create();

deployer = await blockchain.treasury('deployer');
other = await blockchain.treasury("other");

jettonMaster = blockchain.openContract(await JettonMaster.fromInit(deployer.address,0n));
jettonWallet = blockchain.openContract(await JettonWallet.fromInit(jettonMaster.address, deployer.address));
otherJettonWallet = blockchain.openContract(await JettonWallet.fromInit(jettonMaster.address, other.address));
});


it('should correct build wallet address', async () => {
await initializeJettonMaster(10n);

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 not double init', async () => {

await initializeJettonMaster(null);

const deployResult = await jettonMaster.send(
deployer.getSender(),
{
value: toNano("0.05"),
},
{
$$type: 'JettonInit',
query_id: 0n,
jetton_name: beginCell().storeStringTail(JETTON_NAME).asSlice(),
jetton_description: beginCell().storeStringTail(JETTON_DESCRIPTION).asSlice(),
jetton_symbol: beginCell().storeStringTail(JETTON_SYMBOL).asSlice(),
max_supply: JETTON_MAX_SUPPLY,
mint_amount: null
}
);

expect(deployResult.transactions).toHaveTransaction({
from: deployer.address,
to: jettonMaster.address,
success: false,
deploy: false,
op: OP_CODES.JettonInit,
exitCode: ERROR_CODES.JettonInitialized,
});
});


it('should mint tokens', async () => {
await initializeJettonMaster(null);

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,
op: OP_CODES.JettonMint,
});
expect(mintResult.transactions).toHaveTransaction({
from: jettonMaster.address,
to: jettonWallet.address,
success: true,
deploy: true,
op: OP_CODES.JettonTransferInternal,
});

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 init with mint tokens', async () => {
const mint = 120n
await initializeJettonMaster(mint);

// Fetch updated wallet data and validate balance
let jettonWalletData = await jettonWallet.getGetWalletData();
expect(jettonWalletData.balance).toEqual(mint);
});

it('should init with mint and continue minting outside init', async () => {
const mint = 120n
await initializeJettonMaster(mint);

// Fetch updated wallet data and validate balance
let jettonWalletDataBefore = await jettonWallet.getGetWalletData();
expect(jettonWalletDataBefore.balance).toEqual(mint);


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,
op: OP_CODES.JettonMint,
});
expect(mintResult.transactions).toHaveTransaction({
from: jettonMaster.address,
to: jettonWallet.address,
success: true,
deploy: false,
op: OP_CODES.JettonTransferInternal,
});

let jettonWalletDataAfter = await jettonWallet.getGetWalletData();
expect(jettonWalletDataAfter.balance).toEqual(mint + toNano("1337"));
});

it('should init with mint 0 new jettons (using non-null value)', async () => {
const mint = 0n;
await initializeJettonMaster(mint);

try {
// Attempt to fetch wallet data , since we didnt proceed with mint, account should be inactive
await jettonWallet.getGetWalletData();

// If no error is thrown, the test should fail
throw new Error('Expected an error when accessing a non-active wallet contract');
} catch (error) {
if (error instanceof Error) {
expect(error.message).toContain('Trying to run get method on non-active contract');
} else {
throw new Error(`Unexpected error type: ${typeof error}`);
}
}
});


it('should return system cell', async () => {
await initializeJettonMaster(null);

let systemCell = await jettonMaster.getTactSystemCell();
expect(systemCell).toEqualCell(SYSTEM_CELL);
});

});
Loading

0 comments on commit 11a1fb0

Please sign in to comment.