new limit order

here, we provide a simple example for creating a new limit order

The full example code of this chapter can be spotted here.

1. some imports

 1import {BaseChain, ChainId, initialChainTable, PriceRoundingType } from 'iziswap-sdk/lib/base/types'
 2import {privateKey} from '../../.secret'
 3import Web3 from 'web3';
 4import { decimal2Amount, fetchToken, getSwapTokenAddress } from 'iziswap-sdk/lib/base/token/token'
 5import { getDeactiveSlot, getLimitOrderManagerContract, getPoolAddress } from 'iziswap-sdk/lib/limitOrder/view';
 6import { getNewLimOrderCall } from 'iziswap-sdk/lib/limitOrder/limitOrder';
 7import { AddLimOrderParam } from 'iziswap-sdk/lib/limitOrder/types'
 8import { pointDeltaRoundingDown, pointDeltaRoundingUp, priceDecimal2Point } from 'iziswap-sdk/lib/base/price'
 9import { BigNumber } from 'bignumber.js'
10import { getPointDelta, getPoolContract } from 'iziswap-sdk/lib/pool/funcs';

the detail of these imports can be viewed in following content

2. specify which chain, rpc url, web3, and account

1const chain:BaseChain = initialChainTable[ChainId.BSC]
2const rpc = 'https://bsc-dataseed2.defibit.io/'
3const web3 = new Web3(new Web3.providers.HttpProvider(rpc))
4const account =  web3.eth.accounts.privateKeyToAccount(privateKey)
5const accountAddress = account.address

here

BaseChain is a data structure to describe a chain, in this example we use bsc chain.

ChainId is an enum to describe chain id, value of the enum is equal to value of chain id

initialChainTable is a mapping from some most used ChainId to BaseChain, of course you can fill fields of BaseChain by yourself

privateKey is a string, which is your private key, and should be configured by your self

web3 package is a public package to interact with block chain

rpc is the rpc url on the chain you specified

3. get web3.eth.Contract object of limitOrderManager

1const limitOrderAddress = '0x9Bf8399c9f5b777cbA2052F83E213ff59e51612B'
2const limitOrderManager = getLimitOrderManagerContract(limitOrderAddress, web3)

here, getLiquidityManagerContract is an api provided by our sdk, which returns a web3.eth.Contract object of LiquidityManager

4. fetch 2 erc20-tokens’ infomations and specify selltoken,earntoken and fee

1const testAAddress = '0xCFD8A067e1fa03474e79Be646c5f6b6A27847399'
2const testBAddress = '0xAD1F11FBB288Cd13819cCB9397E59FAAB4Cdc16F'
3
4const testA = await fetchToken(testAAddress, chain, web3)
5const testB = await fetchToken(testBAddress, chain, web3)
6const fee = 2000 // 2000 means 0.2%
7
8const sellToken = testA
9const earnToken = testB

fetchToken() returns a TokenInfoFormatted obj of that token, which containing following fields,

of course you can fill TokenInfoFormatted by yourself if you have known each field correctly of the erc20-token the TokenInfoFormatted fields used in sdk currently are only symbol, address, and decimal.

 1export interface TokenInfoFormatted {
 2    // chain id of chain
 3    chainId: number;
 4    // name of token
 5    name: string;
 6    // symbol of token
 7    symbol: string;
 8    // img url, not necessary for sdk, you can fill any string or undefined
 9    icon: string;
10    // address of token
11    address: string;
12    // decimal value of token, acquired by calling 'decimals()'
13    decimal: number;
14    // not necessary for sdk, you can fill any date or undefined
15    addTime?: Date;
16    // not necessary for sdk, you can fill either true/false/undefined
17    custom: boolean;
18    // this field usually undefined.
19    // wrap token address of this token if this token has transfer fee.
20    // this field only has meaning when you want to use sdk of box to deal with problem of transfer fee
21    wrapTokenAddress?: string;
22}

notice that, usually we set TokenInfoFormatted.wrapTokenAddress as undefined.

5. compute sellPoint (price) and sell amount

first set decimal price, and transform the decimal price to point on the pool

1const sellPriceDecimalAByB = 0.25
2const sellPoint = priceDecimal2Point(sellToken, earnToken, sellPriceDecimalAByB, PriceRoundingType.PRICE_ROUNDING_UP)

secondly, query pool contract to get pointDelta, sell point of limit order must be times of pointDelta.

1const poolAddress = await getPoolAddress(limitOrderManager, testA, testB, fee)
2const pool = getPoolContract(poolAddress, web3)
3const pointDelta = await getPointDelta(pool)

thirdly, compute sellPoint rounding to times of pointDelta.

1const state = await getPoolState(pool)
2let sellPointRoundingPointDelta = sellPoint
3if (getSwapTokenAddress(sellToken).toLowerCase() < getSwapTokenAddress(earnToken).toLowerCase()) {
4    sellPointRoundingPointDelta = pointDeltaRoundingDown(sellPointRoundingPointDelta, pointDelta)
5} else {
6    sellPointRoundingPointDelta = pointDeltaRoundingUp(sellPointRoundingPointDelta, pointDelta)
7}
8const sellAmountDecimal = 1000
9const sellAmount = decimal2Amount(sellAmountDecimal, testA).toFixed(0)

We should notice that, if sellToken is tokenX (etc. sellToken < earnToken), the sell point should be greater than or equal to current point. otherwise, sell point should be less than or equal to current point.

6. get newLimitOrder calling

when we send a transaction calling limit order manager to add a new limit order, we should specify an empty slot idx.

which is obtained by calling getDeactiveSlot before sending this transaction, like following code.

1const slotIdx = await getDeactiveSlot(limitOrderManager, accountAddress)

then, we can fill AddLimOrderParam obj, which will be the parameter to interface of creating limit order

const params : AddLimOrderParam = {
    idx: slotIdx,
    sellToken,
    earnToken,
    fee,
    point: sellPointRoundingPointDelta,
    sellAmount
}

the field of AddLimOrderParam is displayed in following code

export interface AddLimOrderParam {
    // slotIdx, to specify an empty slot on contract to store your limit order
    idx: string,
    // which token to sell
    sellToken: TokenInfoFormatted,
    // which token to earn
    earnToken: TokenInfoFormatted,
    // fee of token pair (swap pool)
    fee: number,
    // sell point computed
    point: number,
    // undecimal amount of sell token you want to sell
    sellAmount: string,
    deadline?: string,
    // only sellToken is WBNB/WETH or other wrapped chain token (erc20 form), this field has meaning
    // if strictERC20Token is true, you will provide sellToken from your existing wrapped chain token (erc20 form)
    // and msg.value can be 0.
    // if this field is false, msg.value should not be smaller than sellAmount, and the LimitOrderManager contract
    // will transform your provided bnb/eth or other chain token to wrapped chain token form (erc20 form).
    strictERC20Token?: boolean
}

thirdly, call getNewLimOrderCall to get calling and options obj

1const {newLimOrderCalling, options} = getNewLimOrderCall(
2    limitOrderManager,
3    accountAddress,
4    chain,
5    params,
6    gasPrice
7)

we should notice that, if tokenX or tokenY is chain token (like ETH on ethereum or BNB on bsc), we should specify one or some fields in params to indicate sdk paying in form of Chain Token or paying in form of Wrapped Chain Token (like WETH on ethereum or WBNB on bsc).

In the sdk version 1.1.* or before, one should specify a field named strictERC20Token to indicate that. true for paying token in form of Wrapped Chain Token, false for paying in form of Chain Token. In the sdk version 1.2.* or later, you have two ways to indicate sdk.

The first way is as before, specifing strictERC20Token field. The second way is specifing strictERC20Token as undefined and specifying the corresponding token in this param as WETH or ETH.

7. approve (skip if you pay chain token directly)

before send transaction or estimate gas, you need to approve contract limitOrderManager to have authority to spend your token, because you need transfer some sellToken to pool.

 1// the approve interface abi of erc20 token
 2const erc20ABI = [{
 3  "inputs": [
 4    {
 5      "internalType": "address",
 6      "name": "spender",
 7      "type": "address"
 8    },
 9    {
10      "internalType": "uint256",
11      "name": "amount",
12      "type": "uint256"
13    }
14  ],
15  "name": "approve",
16  "outputs": [
17    {
18      "internalType": "bool",
19      "name": "",
20      "type": "bool"
21    }
22  ],
23  "stateMutability": "nonpayable",
24  "type": "function"
25}];
26// if sellToken is not chain token (BNB on bsc chain or ETH on eth chain...), we need transfer tokenA to pool
27// otherwise we can skip following codes
28{
29    const sellTokenContract = new web3.eth.Contract(erc20ABI, sellToken.address);
30    // you could approve a very large amount (much more greater than amount to transfer),
31    // and don't worry about that because limitOrderManager only transfer your token to pool with amount you specified and your token is safe
32    // then you do not need to approve next time for this user's address
33    const approveCalling = sellTokenContract.methods.approve(
34        limitOrderAddress,
35        "0xffffffffffffffffffffffffffffffff"
36    );
37    // estimate gas
38    const gasLimit = await mintCalling.estimateGas({from: account})
39    // then send transaction to approve
40    // you could simply use followiing line if you use metamask in your frontend code
41    // otherwise, you should use the function "web3.eth.accounts.signTransaction"
42    // notice that, sending transaction for approve may fail if you have approved the token to limitOrderManager before
43    // if you want to enlarge approve amount, you should refer to interface of erc20 token
44    await approveCalling.send({gas: gasLimit})
45}

8. estimate gas (optional)

of course you can skip this step if you don’t want to limit gas. before estimate gas and send transaction, make sure you have approve limitOrderAddress of sellToken

1// before estimate gas and send transaction,
2// make sure you have approve limitOrderAddress of sellToken
3const gasLimit = await newLimOrderCalling.estimateGas(options)

9. finally, send transaction!

for metamask or other explorer’s wallet provider, you can easily write

1await newLimOrderCalling.send({...options, gas: gasLimit})

otherwise, if you are runing codes in console, you could use following code

 1const gasPrice = '5000000000'
 2const signedTx = await web3.eth.accounts.signTransaction(
 3    {
 4        ...options,
 5        to: limitOrderAddress,
 6        data: newLimOrderCalling.encodeABI(),
 7        gas: new BigNumber(gasLimit * 1.1).toFixed(0, 2),
 8    },
 9    privateKey
10)
11// nonce += 1;
12const tx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

after this step, we have successfully minted the liquidity (if no revert occurred).