All code examples are written in TypeScript using Blueprint.
If you need more implementation details, refer to the DEX source code: it contains
OOP implementations of all contracts, so this page is likely temporary until a full-fledged SDK is created based on that code.
This document is for reference only and may contain errors. If you come across any, please consider contacting us
via Telegram @swap.coffee DEV Chat.
(De)serialization of assets
import { Address, beginCell, Builder, Cell, Slice } from '@ton/core';
export abstract class CellSerializable {
public abstract write(b: Builder): void;
public toCell(): Cell {
const b = beginCell();
this.write(b);
return b.endCell();
}
}
export abstract class Asset extends CellSerializable {
static fromSlice(slice: Slice): Asset {
const type = slice.loadUint(2);
if (type === 0) {
return AssetNative.INSTANCE;
} else if (type === 1) {
return new AssetJetton(slice.loadUintBig(8), slice.loadUintBig(256));
} else if (type === 2) {
return new AssetExtra(slice.loadUintBig(32));
} else {
throw new Error('unexpected asset type');
}
}
static fromAny(value: any): Asset {
if (value == null) {
return AssetNative.INSTANCE;
} else if (value instanceof Address) {
return AssetJetton.fromAddress(value);
} else if (typeof value === 'bigint') {
return new AssetExtra(value);
} else {
throw new Error('unexpected asset type');
}
}
}
export class AssetNative extends Asset {
static INSTANCE = new AssetNative();
private constructor() {
super();
}
public write(b: Builder): void {
b.storeUint(0, 2);
}
}
export class AssetJetton extends Asset {
constructor(
public chain: bigint,
public hash: bigint,
) {
super();
}
static fromAddress(address: Address): AssetJetton {
return new AssetJetton(
BigInt(address.workChain),
beginCell().storeBuffer(address.hash).endCell().beginParse().loadUintBig(256),
);
}
public write(b: Builder): void {
b.storeUint(1, 2).storeUint(this.chain, 8).storeUint(this.hash, 256);
}
public getAddress(): Address {
return beginCell()
.storeUint(4, 3)
.storeUint(this.chain, 8)
.storeUint(this.hash, 256)
.endCell()
.beginParse()
.loadAddress()
}
}
export class AssetExtra extends Asset {
constructor(public id: bigint) {
super();
}
public write(b: Builder): void {
b.storeUint(2, 2).storeUint(this.id, 32);
}
}
Vaults
Checking for existence and activity
To check whether asset’s Vault exists and is active, you firstly need to fetch
its address from the factory, then invoke a get-method is_active.
import { NetworkProvider } from '@ton/blueprint';
import { Address } from '@ton/core';
export async function run(provider: NetworkProvider) {
const asset: Asset = AssetNative.INSTANCE // or AssetJetton or AssetExtra
const factoryAddress = Address.parse('EQAsf2sDPfoo-0IjnRA7l_gJBB9jyo4zqfCG_1IFCCI_Qbef')
let resp = await provider.provider(factoryAddress).get('get_vault_address', [{ type: 'slice', cell: asset.toCell() }])
const vaultAddress = resp.stack.readAddress()
const vault = provider.provider(vaultAddress)
const vaultState = await vault.getState()
if (vaultState.state.type !== 'active') {
throw new Error('Vault does not exist')
}
resp = await vault.get('is_active', [])
const isActive = resp.stack.readBigNumber() !== 0n
if (!isActive) {
throw new Error('Vault exists, but is not active')
}
}
Creating a new one
To create a new Vault, you need to send a create_vault internal
message to the factory.
If you are creating a Vault for jetton, make sure it goes through activation process
as described here.
If it was unable to automatically activate itself, please contact us via Telegram @swap.coffee DEV Chat.
import { NetworkProvider } from '@ton/blueprint';
import { beginCell, toNano, Address } from '@ton/core';
export async function run(provider: NetworkProvider) {
const asset: Asset = AssetNative.INSTANCE // or AssetJetton or AssetExtra
const factoryAddress = Address.parse('EQAsf2sDPfoo-0IjnRA7l_gJBB9jyo4zqfCG_1IFCCI_Qbef')
const builder = beginCell()
.storeUint(0xc0ffee06, 32)
.storeUint(0, 64)
asset.write(builder)
await provider.sender().send({
to: factoryAddress,
value: toNano(.05),
body: builder.endCell()
})
}
Pools
Creating a new one
- Check that Vaults for both assets exist and are active (or create them instead).
- Send create_pool messages to both Vaults. Type of each message depends on the type of asset of associated Vault:
import { NetworkProvider } from '@ton/blueprint';
import { beginCell, toNano, Address, Builder } from '@ton/core';
export async function run(provider: NetworkProvider) {
const asset1: Asset = AssetNative.INSTANCE // or AssetJetton or AssetExtra
const asset1Amount = 100n
const asset2: Asset = new AssetExtra(1n) // or AssetNative or AssetJetton
const asset2Amount = 100n
const factoryAddress = Address.parse('EQAsf2sDPfoo-0IjnRA7l_gJBB9jyo4zqfCG_1IFCCI_Qbef')
const factory = provider.provider(factoryAddress)
let resp = await factory.get('get_vault_address', [{ type: 'slice', cell: asset1.toCell() }])
const vault1Address = resp.stack.readAddress()
resp = await factory.get('get_vault_address', [{ type: 'slice', cell: asset2.toCell() }])
const vault2Address = resp.stack.readAddress()
if (!provider.sender().address) {
throw new Error('Sender address must be present')
}
const poolParams = beginCell()
asset1.write(poolParams)
asset2.write(poolParams)
poolParams.storeUint(0, 3) // constant_product AMM
poolParams.storeMaybeRef(null) // no AMM settings
const poolCreationParams = beginCell()
.storeAddress(provider.sender().address) // recipient of LP tokens
.storeBit(false) // send funds to the address specified above instead of sender in case of failures
.storeMaybeRef(null) // no notifications
.storeUint(1, 1) // is_active = true
.storeMaybeRef(null) // no extra settings
await sendCreatePool(provider, poolParams, poolCreationParams, vault1Address, asset1, asset1Amount)
await sendCreatePool(provider, poolParams, poolCreationParams, vault2Address, asset2, asset2Amount)
}
async function sendCreatePool(
provider: NetworkProvider,
poolParams: Builder,
poolCreationParams: Builder,
vaultAddress: Address,
asset: Asset,
amount: bigint
) {
const sender = provider.sender()
if (asset instanceof AssetNative) {
await sender.send({
to: vaultAddress,
value: amount + toNano(.1),
body: beginCell()
.storeUint(0xc0ffee02, 32) // create_pool_native opcode
.storeUint(0, 64)
.storeCoins(amount)
.storeBuilder(poolParams)
.storeBuilder(poolCreationParams)
.endCell()
})
} else if (asset instanceof AssetJetton) {
const jettonMaster = provider.provider(asset.getAddress())
let resp = await jettonMaster.get('get_wallet_address', [{
type: 'slice',
cell: beginCell().storeAddress(sender.address).endCell()
}])
const jettonWalletAddress = resp.stack.readAddress()
await sender.send({
to: jettonWalletAddress,
value: toNano(.15),
body: beginCell()
.storeUint(0xf8a7ea5, 32) // jetton_transfer opcode
.storeUint(0, 64)
.storeAddress(vaultAddress)
.storeAddress(sender.address)
.storeMaybeRef(null) // no custom_payload
.storeCoins(toNano(.1)) // fwd_gas
.storeMaybeRef(
beginCell()
.storeUint(0xc0ffee11, 32) // create_pool_jetton opcode
.storeBuilder(poolParams)
.storeBuilder(poolCreationParams)
.endCell()
).endCell()
})
} else if (asset instanceof AssetExtra) {
await sender.send({
to: vaultAddress,
value: toNano(.1),
extracurrency: {
[Number(asset.id)]: amount
},
body: beginCell()
.storeUint(0xc0ffee03, 32) // create_pool_extra opcode
.storeUint(0, 64)
.storeBuilder(poolParams)
.storeBuilder(poolCreationParams)
.endCell()
})
}
}
Providing liquidity to existing one
All you need is to send deposit_liquidity messages to both Vaults. Type of each message depends on the type of asset of associated Vault:
The code itself is very similar to the one used in pools creation:
import { NetworkProvider } from '@ton/blueprint';
import { beginCell, toNano, Address, Cell } from '@ton/core';
export async function run(provider: NetworkProvider) {
const asset1: Asset = AssetNative.INSTANCE // or AssetJetton or AssetExtra
const asset1Amount = 100n
const asset2: Asset = new AssetExtra(1n) // or AssetNative or AssetJetton
const asset2Amount = 100n
const factoryAddress = Address.parse('EQAsf2sDPfoo-0IjnRA7l_gJBB9jyo4zqfCG_1IFCCI_Qbef')
const factory = provider.provider(factoryAddress)
let resp = await factory.get('get_vault_address', [{ type: 'slice', cell: asset1.toCell() }])
const vault1Address = resp.stack.readAddress()
resp = await factory.get('get_vault_address', [{ type: 'slice', cell: asset2.toCell() }])
const vault2Address = resp.stack.readAddress()
if (!provider.sender().address) {
throw new Error('Sender address must be present')
}
const poolParams = beginCell()
asset1.write(poolParams)
asset2.write(poolParams)
poolParams.storeUint(0, 3) // constant_product AMM
poolParams.storeMaybeRef(null) // no AMM settings
const poolParamsCell = poolParams.endCell()
const depositLiquidityParamsCell = beginCell()
.storeAddress(provider.sender().address) // recipient of LP tokens
.storeBit(false) // send funds to the address specified above instead of sender in case of failures
.storeAddress(null) // no referral
.storeUint(Math.floor(Date.now() / 1000) + 900, 32) // deadline
.storeUint(0, 2) // using no condition
.storeMaybeRef(null) // no extra settings
.storeMaybeRef(null) // no notifications
.endCell()
await sendCreatePool(provider, poolParamsCell, depositLiquidityParamsCell, vault1Address, asset1, asset1Amount)
await sendCreatePool(provider, poolParamsCell, depositLiquidityParamsCell, vault2Address, asset2, asset2Amount)
}
async function sendCreatePool(
provider: NetworkProvider,
poolParams: Cell,
depositLiquidityParams: Cell,
vaultAddress: Address,
asset: Asset,
amount: bigint
) {
const sender = provider.sender()
if (asset instanceof AssetNative) {
await sender.send({
to: vaultAddress,
value: amount + toNano(.1),
body: beginCell()
.storeUint(0xc0ffee04, 32) // deposit_liquidity_native opcode
.storeUint(0, 64)
.storeCoins(amount)
.storeRef(depositLiquidityParams)
.storeRef(poolParams)
.endCell()
})
} else if (asset instanceof AssetJetton) {
const jettonMaster = provider.provider(asset.getAddress())
let resp = await jettonMaster.get('get_wallet_address', [{
type: 'slice',
cell: beginCell().storeAddress(sender.address).endCell()
}])
const jettonWalletAddress = resp.stack.readAddress()
await sender.send({
to: jettonWalletAddress,
value: toNano(.15),
body: beginCell()
.storeUint(0xf8a7ea5, 32) // jetton_transfer opcode
.storeUint(0, 64)
.storeAddress(vaultAddress)
.storeAddress(sender.address)
.storeMaybeRef(null) // no custom_payload
.storeCoins(toNano(.1)) // fwd_gas
.storeMaybeRef(
beginCell()
.storeUint(0xc0ffee12, 32) // deposit_liquidity_jetton opcode
.storeRef(depositLiquidityParams)
.storeRef(poolParams)
.endCell()
).endCell()
})
} else if (asset instanceof AssetExtra) {
await sender.send({
to: vaultAddress,
value: toNano(.1),
extracurrency: {
[Number(asset.id)]: amount
},
body: beginCell()
.storeUint(0xc0ffee05, 32) // deposit_liquidity_extra opcode
.storeUint(0, 64)
.storeRef(depositLiquidityParams)
.storeRef(poolParams)
.endCell()
})
}
}
Rolling back liquidity provisioning
Both pool creation and liquidity provisioning operations require 2 transactions with corresponding assets to be sent.
If only one of them appeared in the blockchain, you may want to rollback the whole process, therefore receiving sent
funds back. In order to do so, please follow the given instructions:
- Inspect the one transaction that appeared in the blockchain, and find out
pool_creator
or liquidity_depository
contract address. It is the last contract in the transaction execution chain.
- Send a withdraw_deposit message to that contract.
import { NetworkProvider } from '@ton/blueprint';
import { beginCell, toNano, Address } from '@ton/core';
export async function run(provider: NetworkProvider) {
const contractAddress = Address.parse('INSERT ADDRESS HERE') // address of either pool_creator or liquidity_depository
await provider.sender().send({
to: contractAddress,
value: toNano(.05),
body: beginCell()
.storeUint(0xc0ffee07, 32)
.storeUint(0, 64)
.endCell()
})
}
- Fetch Vault’s address from factory for the input asset.
- Fetch Pools addresses from factory (or another source of truth) for every pool throughout your way.
- Send swap message to the Vault of input asset. Type of the message depends on the type of asset:
import { NetworkProvider } from '@ton/blueprint';
import { beginCell, toNano, Address, Builder } from '@ton/core';
export async function run(provider: NetworkProvider) {
const inputAsset: Asset = AssetNative.INSTANCE // or AssetJetton or AssetExtra
const poolTonUsdtAddress = Address.parse('EQDETPC2Trne37AQx7JKUIFA2G9uB3ifk4O7myFB6pwI6u8M')
const poolUsdtTonnelAddress = Address.parse('EQBjj5LzN3L1PvFvH5mzbTc8zp3Sp_vSWmSI3nWKM-Fr3W_g')
const inputAssetAmount = toNano(1)
const factoryAddress = Address.parse('EQAsf2sDPfoo-0IjnRA7l_gJBB9jyo4zqfCG_1IFCCI_Qbef')
const factory = provider.provider(factoryAddress)
let resp = await factory.get('get_vault_address', [{ type: 'slice', cell: inputAsset.toCell() }])
const vaultAddress = resp.stack.readAddress()
const sender = provider.sender()
const swapParamsCell = beginCell()
.storeUint(Math.floor(Date.now() / 1000) + 900, 32) // deadline
.storeAddress(sender.address) // recipient of output asset
.storeAddress(null) // no referral
.storeMaybeRef(null) // no notifications
.endCell()
let swapStepParams: Builder
if (noMultihop) { // example for TON -> USDT
swapStepParams = beginCell()
.storeUint(getHashFromAddress(poolTonUsdtAddress), 256)
.storeCoins(0n) // min USDT to receive after swap
.storeMaybeRef(null) // no next step
} else { // example for TON -> USDT -> TONNEL
swapStepParams = beginCell()
.storeUint(getHashFromAddress(poolTonUsdtAddress), 256)
.storeCoins(0n) // min USDT to receive after 1st swap
.storeMaybeRef(
beginCell()
.storeUint(getHashFromAddress(poolUsdtTonnelAddress), 256)
.storeCoins(0n) // min TONNEL to receive after 2nd swap
.storeMaybeRef(null) // no next step
.endCell()
)
}
if (inputAsset instanceof AssetNative) {
await sender.send({
to: vaultAddress,
value: toNano(.05) + inputAssetAmount,
body: beginCell()
.storeUint(0xc0ffee00, 32) // swap_native opcode
.storeUint(0, 64)
.storeCoins(inputAssetAmount)
.storeBuilder(swapStepParams)
.storeRef(swapParamsCell)
.endCell()
})
} else if (inputAsset instanceof AssetJetton) {
const jettonMaster = provider.provider(inputAsset.getAddress())
let resp = await jettonMaster.get('get_wallet_address', [{
type: 'slice',
cell: beginCell().storeAddress(sender.address).endCell()
}])
const jettonWalletAddress = resp.stack.readAddress()
await sender.send({
to: jettonWalletAddress,
value: toNano(.1),
body: beginCell()
.storeUint(0xf8a7ea5, 32) // jetton_transfer opcode
.storeUint(0, 64)
.storeAddress(vaultAddress)
.storeAddress(sender.address)
.storeMaybeRef(null) // no custom_payload
.storeCoins(toNano(.05)) // fwd_gas
.storeMaybeRef(
beginCell()
.storeUint(0xc0ffee10, 32) // swap_jetton opcode
.storeBuilder(swapStepParams)
.storeRef(swapParamsCell)
.endCell()
).endCell()
})
} else if (inputAsset instanceof AssetExtra) {
await sender.send({
to: vaultAddress,
value: toNano(.05),
extracurrency: {
[Number(inputAsset.id)]: inputAssetAmount
},
body: beginCell()
.storeUint(0xc0ffee01, 32) // swap_extra opcode
.storeUint(0, 64)
.storeBuilder(swapStepParams)
.storeRef(swapParamsCell)
.endCell()
})
}
}
function getHashFromAddress(address: Address): bigint {
return beginCell().storeBuffer(address.hash).endCell().beginParse().loadUintBig(256)
}
Notifications
Notifications are a system that allows sending a custom_payload
upon the successful or failed execution of
operations on the DEX.
They can be used during pool creation, liquidity provisioning or withdrawal, as well as performing swaps. In all these
cases, the approach to using notifications is the same: they are optional, and you can specify a notification for only
the success case, only the failure case, or define separate notifications for each outcome. Each notification can carry
a unique custom_payload
and have a distinct recipient.
Notifications are passed as an additional parameter when constructing a transaction to be sent by the user. However,
depending on the operation, there are some differences in how they are ultimately processed. More on that below.
Keep in mind that depending on the situation, a notification may be sent either as an internal message with its own
opcode, or as a forward_payload
within a jetton_transfer
. In both cases, the user-defined custom_payload
is not
sent directly: instead, it is encapsulated within a separate entity.
In other words, the recipient must either be capable of handling notification’s opcode, or a proxy contract must be used
to unwrap the encapsulated message and forward it further.
The fwd_gas
for notification must be explicitly specified in cases where the notification recipient differs from the
funds recipient, or when the notification is sent as a forward_payload
within a jetton_transfer
.
On pool creation or liquidity provisioning
- In case of failure, the notification will be sent twice - once from each Vault, along with the refund of the
assets that were intended for liquidity provision.
- In case of success, the notification will be sent once, but the delivery method may vary:
- If the notification recipient is the same as the LP tokens recipient, the notification will be delivered as a
forward_payload
inside the jetton_transfer
that sends the LP tokens.
- If recipients differ, the notification will be sent as an internal message with its own opcode.
On liquidity withdrawal
For liquidity withdrawals only on-success notification may be provided. If operation succeeds, the notification will
be sent twice - once from each Vault, along with the refund of the assets that were received as a result of a
liquidity withdrawal.
On swap
In all cases the notification will be sent once throughout the Vault. The method of delivery may vary:
- If the recipient of the funds is different from the notification recipient, the notification will be sent as an
internal message with its own opcode.
- If both the funds and the notification are to be sent to the same address, the behavior depends on the asset type:
- If the payout is in TON or Extra Currency, it will be an internal message with the notification opcode, and the corresponding TON or Extra Currency tokens will be attached to it.
- If the payout is in Jetton, the notification - still encapsulating the user’s
custom_payload
- will be sent a forward_payload
within the jetton_transfer
.