Mint

In this section, we provide a simple example for creating a new liquidity position. Notice the liquidity position is a NFT.

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

1. Some imports

 1import Web3 from 'web3';
 2import { BigNumber } from 'bignumber.js'
 3import { privateKey } from '../../.secret'
 4import { BaseChain, ChainId, initialChainTable, PriceRoundingType } from 'iziswap-sdk/lib/base/types'
 5import { getPointDelta, getPoolContract, getPoolState } from 'iziswap-sdk/lib/pool/funcs';
 6import { getPoolAddress, getLiquidityManagerContract } from 'iziswap-sdk/lib/liquidityManager/view';
 7import { amount2Decimal, fetchToken } from 'iziswap-sdk/lib/base/token/token';
 8import { pointDeltaRoundingDown, pointDeltaRoundingUp, priceDecimal2Point } from 'iziswap-sdk/lib/base/price';
 9import { calciZiLiquidityAmountDesired } from 'iziswap-sdk/lib/liquidityManager/calc';
10import { getMintCall } from 'iziswap-sdk/lib/liquidityManager/liquidity';

Detail of these imports can be viewed in the following content.

2. Specify chain, rpc, web3, and account

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

where

    • 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. 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 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 liquidityManager

1const liquidityManagerAddress = '0xBF55ef05412f1528DbD96ED9E7181f87d8C9F453' // example BSC address
2const liquidityManagerContract = getLiquidityManagerContract(liquidityManagerAddress, web3)
3console.log('liquidity manager address: ', liquidityManagerAddress)

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

4. Fetch 2 ERC20 information

1const testAAddress = '0xCFD8A067e1fa03474e79Be646c5f6b6A27847399'
2const testBAddress = '0xAD1F11FBB288Cd13819cCB9397E59FAAB4Cdc16F'
3
4const testA = await fetchToken(testAAddress, chain, web3)
5const testB = await fetchToken(testBAddress, chain, web3)

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

You can fill TokenInfoFormatted by yourself, if you know each field correctly of the erc20-tokens related. 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}

We usually set TokenInfoFormatted.wrapTokenAddress as undefined.

5. Get state of the corresponding pool

First get the pool address of token pair (testA, testB, fee):

1const poolAddress = await getPoolAddress(liquidityManagerContract, testA, testB, fee)

The function getPoolAddress(…) queries liquidityManagerContract to get iZiSwap pool address of token pair (testA, testB, fee), where

    • liquidityManagerContract: liquidity manager contract, acquired in step 4.

    • testA: an erc20 token in type of TokenInfoFormatted, acquired in step 5.

    • testB: another erc20 token in type of TokenInfoFormatted, also acquired in step 5.

    • fee: an int number, fee/1e6 is fee rate of pool, etc, 2000 means 0.2% fee rate

When poolAddress is ready, you can call getPoolContract(…) to get the pool contract object.

1const pool = getPoolContract(poolAddress, web3)

Then we can get the state of the pool:

1const state = await getPoolState(pool)

where state is a State obj which extends from BaseState, with only fields in BaseState are used in this example.

 1export interface BaseState {
 2    // current point on the pool, see document in concepts(price/decimalPrice/undecimalPrice/point)
 3    // ranging from (-800000, 800000)
 4    currentPoint: number,
 5    // liquidity value on currentPoint, a decimal system format string
 6    liquidity: string,
 7    // value of liquidity of tokenX on currentPoint, a decimal system format string
 8    // liquidityY = liquidity - liquidityX
 9    liquidityX: string
10}

to compute undecimal-amount of token in minting, we will take use of state.currentPoint

6. Compute boundary point of liquidity on the pool

The boundary point is leftPoint and rightPoint of a liquidity position, according to Price of an ERC20 token , we know that point in the pool and decimal price can be transformed from each other.

We first determine the minimal and maximum decimal price of our liquidity ready to mint.

Assume the desired minimal decimal price of A_by_B is 0.099870 (this decimal price means 0.099870 testB to buy 1.0 testA, here, number 0.099870 and 1.0 are both decimal amount). and the max decimal price of A_by_B is 0.29881

We can get 2 point**s on the pool of min and max **decimal prices though following code:

1const point1 = priceDecimal2Point(testA, testB, 0.099870, PriceRoundingType.PRICE_ROUNDING_NEAREST)
2const point2 = priceDecimal2Point(testA, testB, 0.29881, PriceRoundingType.PRICE_ROUNDING_NEAREST)

where priceDecimal2Point(…) is a function to transform decimal price to the point on the pool, the function has following params:

/**
 * @param tokenA: TokenInfoFormatted, one erc20 token of pool
 * @param tokenB: TokenInfoFormatted, another erc20 token of pool
 * @param priceDecimalAByB: number,  decimal price of A_by_B (A_by_B means how much tokenB to buy 1 tokenA)
 * @param roundingType: PriceRoundingType, rounding type when transform price to point
 * @return point: number, point on the pool transformed from decimal price
 */
priceDecimal2Point(tokenA, tokenB, priceDecimalAByB, roundingType)

Since we do not ensure that tokenA’s address is smaller than tokenB, point1 may be larger than point2. We could not simply specify leftPoint as point1 and rightPoint as point2. Instead, we take min(point1, point2) as leftPoint and max(point1, point2) as rightPoint.

1let leftPoint = Math.min(point1, point2)
2let rightPoint = Math.max(point1, point2)

When we mint, the boundary point of liquidity must be times of pointDelta. Thus we should rounding leftPoint and rightPoint to times of pointDelta throw following codes:

1const pointDelta = await getPointDelta(pool)
2
3leftPoint = pointDeltaRoundingDown(leftPoint, pointDelta)
4rightPoint = pointDeltaRoundingUp(rightPoint, pointDelta)

where pointDelta is a number value queried from pool contract.

For fee rate of 0.2%, pointDelta usually equals to 40 (0.3% -> 60, 1% -> 200).

Besides, for leftPoint and rightPoint we must guarantee following inequality:

leftPoint >= pool.leftMostPt()
rightPoint <= pool.rightMostPt()
rightPoint - leftPoint < 400000

7. Specify or compute tokenA’s and tokenB’s max undecimal amount (optional)

Sometimes, a user wants to know the amount of tokenA when he fill amount of tokenB or vise versa.

Here we provide a function named calciZiLiquidityAmountDesired() in sdk to do this calculation.

Suppose we want to specify max decimal amount of tokenA ( token named testA) to be 100,

1const maxTestA = new BigNumber(100).times(10 ** testA.decimal)

then we can compute the corresponding undecimal amount of tokenB ( token named testB).

1const maxTestB = calciZiLiquidityAmountDesired(
2    leftPoint, rightPoint, state.currentPoint,
3    maxTestA, true, testA, testB
4)

Here, calciZiLiquidityAmountDesired(…) is a function provided by sdk, which is used for computing one erc20-token’s undecimal amount of a liquidity after given leftPoint rightPoint currentPoint and the other erc20-token’s undecimal amount.

The params are as follows:

/**
 * @param leftPoint: number, left point of the liquidity
 * @param rightPoint: number, right point of the liquidity
 * @param currentPoint: number, current point on the swap pool
 * @param amount: BigNumber, undecimal amount of one token
 * @param amountIsTokenA: boolean, true for amount is tokenA's undecimal amount, false for tokenB
 * @param tokenA: TokenInfoFormatted, tokenA information
 * @param tokenB: TokenInfoFormatted, tokenB information
 */
calciZiLiquidityAmountDesired(leftPoint, rightPoint, currentPoint, amount, amountIsTokenA, tokenA, tokenB):

After the call to calciZiLiquidityAmountDesired, we get a BigNumber stored in maxTestB, which is corresponding undecimal amount of tokenB ( token named testB).

8. Get the mint calling

First, construct necessary params and gasPrice for the mint calling.

 1const mintParams = {
 2    tokenA: testA,
 3    tokenB: testB,
 4    fee,
 5    leftPoint,
 6    rightPoint,
 7    maxAmountA: maxTestA.toFixed(0),
 8    maxAmountB: maxTestB.toFixed(0),
 9    minAmountA: maxTestA.times(0.985).toFixed(0),
10    minAmountB: maxTestB.times(0.985).toFixed(0),
11}
12
13const gasPrice = '5000000000'

Then, get mint calling by:

1const { mintCalling, options } = getMintCall(
2    liquidityManagerContract,
3    account.address,
4    chain,
5    mintParams,
6    gasPrice
7)

where mintParams of type MintParam, and maxAmountA, maxAmountB, minAmountA, minAmountB is required min-max undecimal amount of tokenA and tokenB deposited in this mint procedure. You can fill maxAmountA, maxAmountB, minAmountA, minAmountB to arbitrary value as you want.

The function getMintCall returns 2 object, mintCalling and options. When mintCalling and options are ready, we can estimate gas.

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 mintParams 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.

9. Approve (skip if you pay chain token directly)

Before estimate gas or send transaction, you need approve contract liquidityManager to have authority to spend your token, since you need transfer some tokenA and some tokenB 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
27// if tokenA is not chain token (BNB on bsc chain or ETH on eth chain...), we need transfer tokenA to pool
28// otherwise we can skip following codes
29if (maxTestA.gt(0)) {
30    const tokenAContract = new web3.eth.Contract(erc20ABI, testAAddress);
31    // you could approve a very large amount (much more greater than amount to transfer),
32    // and don't worry about that because liquidityManager only transfer your token to pool with amount you specified and your token is safe
33    // then you do not need to approve next time for this user's address
34    const approveCalling = tokenAContract.methods.approve(
35        liquidityManagerAddress,
36        "0xffffffffffffffffffffffffffffffff"
37    );
38    // estimate gas
39    const gasLimit = await mintCalling.estimateGas({from: account})
40    // then send transaction to approve
41    // you could simply use followiing line if you use metamask in your frontend code
42    // otherwise, you should use the function "web3.eth.accounts.signTransaction"
43    // notice that, sending transaction for approve may fail if you have approved the token to liquidityManager before
44    // if you want to enlarge approve amount, you should refer to interface of erc20 token
45    await approveCalling.send({gas: gasLimit})
46}
47
48// if tokenB is not chain token (BNB on bsc chain or ETH on eth chain...), we need transfer tokenA to pool
49// otherwise we can skip following codes
50if (mexTestB.gt(0)) {
51    const tokenBContract = new web3.eth.Contract(erc20ABI, testBAddress);
52    // you could approve a very large amount (much more greater than amount to transfer),
53    // and don't worry about that because liquidityManager only transfer your token to pool with amount you specified and your token is safe
54    // then you do not need to approve next time for this user's address
55    const approveCalling = tokenBContract.methods.approve(
56        liquidityManagerAddress,
57        "0xffffffffffffffffffffffffffffffff"
58    );
59    // estimate gas
60    const gasLimit = await mintCalling.estimateGas({from: account})
61    // then send transaction to approve
62    // you could simply use followiing line if you use metamask in your frontend code
63    // otherwise, you should use the function "web3.eth.accounts.signTransaction"
64    // notice that, sending transaction for approve may fail if you have approved the token to liquidityManager before
65    // if you want to enlarge approve amount, you should refer to interface of erc20 token
66    await approveCalling.send({gas: gasLimit})
67}

10. Estimate gas (optional)

You can skip this step if you do not want to limit gas.

1const gasLimit = await mintCalling.estimateGas(options)
2console.log('gas limit: ', gasLimit)

11. Finally, send transaction!

Now, we can send transaction to mint a new liquidity position.

For metamask or other injected wallet provider, you can easily write

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

Otherwise, if you are running codes in console, you could use the following code

 1// sign transaction
 2const signedTx = await web3.eth.accounts.signTransaction(
 3    {
 4        ...options,
 5        to: liquidityManagerAddress,
 6        data: mintCalling.encodeABI(),
 7        gas: new BigNumber(gasLimit * 1.1).toFixed(0, 2),
 8    },
 9    privateKey
10)
11// send transaction
12const tx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
13console.log('tx: ', tx)

Finally, we have successfully minted a liquidity position (if no revert occurred).