Skip to main content
TEP-74 standard specifies that Jetton wallets must support transfer operation.
Funds at riskEach jetton stores a decimals parameter in its metadata. Transferring without accounting for decimals can result in sending 1000 times the intended amount—irreversible on mainnet.Mitigation: Always retrieve and apply the correct decimals value. Test on testnet first. Read decimals parameter for details.
To attach a comment, the message has to encode it in forward_payload field, and forward_ton_amount is some amount of toncoin attached to let the receiving wallet process the message. Format of forward_payload for comments and other kinds of attached data can be found in the API section. If forward_ton_amount is 0, forward_payload doesn’t have to comply with the schema. A single manual transfer can be done with a web service (for example, Minter). A programmatic transfer is usually done with an SDK (for example, assets-sdk) that handles low-level message serialization details. The provided example uses TON Center API that might require a key. Also you’ll need a mnemonic of a wallet that will pay for the transfer.
Funds at riskBeware that API keys and mnemonic must not be committed or shared publicly.A better approach is to use a .env file that is excluded from repository with .gitignore. For Github CI purposes, consult their documentation.
import { Address, toNano, WalletContractV5R1, TonClient } from "@ton/ton";
import { mnemonicToPrivateKey } from "@ton/crypto";
import { AssetsSDK, createApi } from "@ton-community/assets-sdk";

const network = "testnet";
// a list of 24 space-separated words
const mnemonic = "foo bar baz";
const apiKey = "<API_KEY>";
const jettonMasterAddress = Address.parse("<JETTON_MASTER_ADDR>");
const destinationRegularWalletAddress = Address.parse("<DESTINATION_WALLET_ADDR>");

async function main() {
    // create an RPC client that will send network requests
    const client = new TonClient({
        endpoint: "https://toncenter.com/api/v2/jsonRPC",
        apiKey,
    });

    // extract private and public keys from the mnemonic
    const keyPair = await mnemonicToPrivateKey(mnemonic.split(" "));

    // create a client for TON wallet
    const wallet = WalletContractV5R1.create({
        workchain: 0,
        // public key is required to deploy a new wallet
        // if it wasn't deployed yet
        publicKey: keyPair.publicKey,
    });

    const provider = client.provider(wallet.address);

    // sender is an object used by assets-sdk to send messages
    // private key is used to sign messages sent to a wallet
    const sender = wallet.sender(provider, keyPair.secretKey);

    // create an assets-sdk client 
    const api = await createApi(network);
    const sdk = AssetsSDK.create({ api, sender });

    // create a client for interacting with jettons of a
    // certain type
    const jetton = await sdk.openJetton(jettonMasterAddress);

    // create a client for the sender's Jetton wallet
    const jettonWallet = await jetton.getWallet(sdk.sender!.address!);

    // tell sender's Jetton wallet to transfer Jettons
    await jettonWallet.send(sender, destinationRegularWalletAddress, toNano(10));
}

void main();
For reference, here’s a low-level example of the process, where message serialization is done manually.
import { Address, beginCell, internal, SendMode, toNano } from "@ton/core";
import { TonClient, WalletContractV5R1, TupleItemSlice } from "@ton/ton";
import { mnemonicToPrivateKey } from "@ton/crypto";

// a list of 24 space-separated words
const mnemonic = "foo bar baz";
const apiKey = "<API key>";
const jettonMasterAddress = Address.parse(
    "<Jetton master address>",
);
const destinationRegularWalletAddress = Address.parse(
    "<destination wallet address>",
);

async function main() {
    // connect to your regular walletV5
    const client = new TonClient({
        endpoint: "https://toncenter.com/api/v2/jsonRPC",
        apiKey,
    });

    const keyPair = await mnemonicToPrivateKey(mnemonic.split(" "));
    const walletContract = WalletContractV5R1.create({
        workchain: 0,
        publicKey: keyPair.publicKey,
    });

    const provider = client.provider(walletContract.address);


    // Find your Jetton wallet Address
    const walletAddressCell = beginCell()
        .storeAddress(walletContract.address)
        .endCell();
    const el: TupleItemSlice = {
        type: "slice",
        cell: walletAddressCell,
    };
    const data = await client.runMethod(
        jettonMasterAddress,
        "get_wallet_address",
        [el],
    );
    const jettonWalletAddress = data.stack.readAddress();

    // form the transfer message
    const forwardPayload = beginCell()
        .storeUint(0, 32) // 0 opcode means we have a comment
        .storeStringTail("for coffee")
        .endCell();

    const messageBody = beginCell()
        // opcode for jetton transfer
        .storeUint(0x0f8a7ea5, 32)
        // query id
        .storeUint(0, 64)
        // jetton amount, amount * 10^9
        .storeCoins(toNano(5))
        // the address of the new jetton owner
        .storeAddress(destinationRegularWalletAddress)
        // response destination (in this case, the destination wallet)
        .storeAddress(destinationRegularWalletAddress)
        // no custom payload
        .storeBit(0)
        // forward amount - if >0, will send notification message
        .storeCoins(toNano("0.02"))
        // store forwardPayload as a reference
        .storeBit(1)
        .storeRef(forwardPayload)
        .endCell();

    const transferMessage = internal({
        to: jettonWalletAddress,
        value: toNano("0.1"),
        bounce: true,
        body: messageBody,
    });

    // send the transfer message through your wallet
    const seqno = await walletContract.getSeqno(provider);
    await walletContract.sendTransfer(provider, {
        seqno: seqno,
        secretKey: keyPair.secretKey,
        messages: [transferMessage],
        sendMode: SendMode.PAY_GAS_SEPARATELY,
    });
}

void main();