Exact input mode

In this example, we will conduct price query and swap with exact input amount in iZiSwap (Amount Mode) via UniversalQuoter and UniversalSwapRouter.

Here, quoter with exact input amount means pre-query amount of token acquired given exact input token amount. For example, if you want to swap 1 ETH to N USDC, the step is used to determine N.

swap with exact input amount means to invoke the real swap with the amount.

Quoter and swap are called throw 2 different contracts.

Suppose we want to swap test USDT token to test BNB token, where the test USDT is standard ERC-20 token deployed by us on BSC testnet and the test BNB is native token on BSC testnet.

The full example codes can be found here.

1. Some imports

These are some basic modules used in most examples.

1import {BaseChain, ChainId, initialChainTable, TokenInfoFormatted} from 'iziswap-sdk/lib/base/types'
2import {privateKey} from '../../.secret'
3import Web3 from 'web3';
4import { amount2Decimal } from 'iziswap-sdk/lib/base/token/token';
5import { BigNumber } from 'bignumber.js'

And these are some imports for UniversalQuoter and UniversalSwapRouter.

1import { getSwapExactInputCall, getUniversalQuoterContract, getUniversalSwapRouterContract, quoteExactInput } from 'iziswap-sdk/lib/universalRouter'
2import { SwapExactInputParams } from 'iziswap-sdk/lib/universalRouter/types';

Here quoteExactInput will return amount of token acquired (N). getSwapExactInputCall will return calling for swapping with exact input.

2. Initialization

Following codes define some basic object like rpc, web3 and account which we may need. The network in this example is bsc testnet.

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

Following codes define our UniversalQuoter contract object.

1const quoterAddress = '0xA55D57C62A1B998E4bDD956c31096b783b7ce1cF'
2const quoterContract = getUniversalQuoterContract(quoterAddress, web3)
3
4console.log('quoter address: ', quoterAddress)

Following codes define some test tokens deployed by us on bsc testnet.

 1const USDC = {
 2    chainId: chain.id,
 3    symbol: 'USDC',
 4    address: '0x876508837C162aCedcc5dd7721015E83cbb4e339',
 5    decimal: 6
 6} as TokenInfoFormatted;
 7
 8const USDT = {
 9    chainId: chain.id,
10    symbol: 'USDT',
11    address: '0x6AECfe44225A50895e9EC7ca46377B9397D1Bb5b',
12    decimal: 6
13} as TokenInfoFormatted;
14
15const BNB = {
16    chainId: chain.id,
17    symbol: 'BNB',
18    address: '0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd',
19    decimal: 18,
20} as TokenInfoFormatted;
21
22const WBNB = {
23    chainId: chain.id,
24    symbol: 'WBNB',
25    address: '0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd',
26    decimal: 18,
27} as TokenInfoFormatted;

And following codes define params for universal quoter.

 1const inputAmountDecimal = 20;
 2const inputAmount = new BigNumber(inputAmountDecimal).times(10 ** USDT.decimal).toFixed(0)
 3
 4const quoterParams = {
 5    // note:
 6    //     if you want to pay via native BNB/WBNB,
 7    //         just fill first token in tokenChain
 8    //         with BNB/WBNB
 9    //         like [BNB, ... other tokens] or [WBNB, ... other tokens]
10    //     and if you want to buy BNB/WBNB,
11    //         just fill the last token in tokenChain
12    //         with BNB/WBNB
13    //         like [... other tokens, BNB] or [... other tokens, WBNB]
14    //     Both of BNB and WBNB are defined in code above
15    // tokenChain: [WBNB, USDC, USDT],
16    tokenChain: [USDT, USDC, BNB],
17
18    // fee percent of pool(tokenChain[i], tokenChain[i+1])
19    // 0.3 means fee tier of 0.3%
20    //     only need for V3Pool
21    //     for V2Pool, you can fill arbitrary value
22    feeTier: [0.04, 0],
23    // isV2[i] == true, means pool(tokenChain[i], tokenChain[i+1]) is a V2Pool
24    // otherwise, the corresponding pool is V3Pool
25    //     same length as feeTier
26    isV2: [false, true],
27
28    inputAmount,
29    // "minOutputAmount" is not used in quoter
30    //     and you can fill arbitrary value in quoter
31    minOutputAmount: '0',
32    // outChargeFeeTier% of trader's acquired token (outToken)
33    // will be additionally charged by universalRouter
34    // if outChargeFeeTier is 0.2, 0.2% of outToken will be additionally charged
35    // if outChargeFeeTier is 0, no outToken will be additionally charged
36    // outChargeFeeTier should not be greater than 5 (etc, 5%)
37    outChargeFeeTier: 0.2,
38} as SwapExactInputParams;

In the above code, we ready to pay 20 test USDT to buy some test BNB. And then, we can see 3 arrays, tokenChain, feeTier, and isV2. These 3 lists together define the path we want to do price inquiry. We can see that there are 2 pools in our path. The first pool on the path is a V3-pool with pair of (USDT, USDC, 0.04%), here 0.04% is the fee rate of this pool. The second pool on the path is a V2-pool with pair of (USDC, BNB).

The fields of SwapExactInputParams is explained in the following code.

 1export interface SwapExactInputParams {
 2    // input: tokenChain.first()
 3    // output: tokenChain.last()
 4    tokenChain: TokenInfoFormatted[];
 5    // fee percent of pool(tokenChain[i], tokenChain[i+1]) 0.3 means 0.3%
 6    //     only need for V3Pool
 7    //     for V2Pool, you can fill arbitrary value
 8    feeTier: number[];
 9    // isV2[i] == true, means pool(tokenChain[i], tokenChain[i+1]) is a V2Pool
10    // otherwise, the corresponding pool is V3Pool
11    //     same length as feeTier
12    isV2: boolean[];
13    // 10-decimal format integer number, like 100, 150000, ...
14    // or hex format number start with '0x'
15    // decimal amount = inputAmount / (10 ** inputToken.decimal)
16    inputAmount: string;
17    minOutputAmount: string;
18    // outChargeFeeTier% of trader's acquired token (outToken)
19    // will be additionally charged by universalRouter
20    // if outChargeFeeTier is 0.2, 0.2% of outToken will be additionally charged
21    // if outChargeFeeTier is 0, no outToken will be additionally charged
22    // outChargeFeeTier should not be greater than 5 (etc, 5%)
23    outChargeFeeTier: number;
24    recipient?: string;
25    deadline?: string;
26}

iZiSwap’s UniversalQuoter and UniversalSwap contracts support swap chain with multi V2 and V3 swap pools. For example, if you have some token0, and wants to get token3 through the path token0 -> (token0, token1, 0.05%) -> token1 -> (token1, token2, 0.3%) -> token2 -> (token2, token3) -> token3 (here, suppose the last pair (token2, token3) is a V2-pool), you should fill the tokenChain, feeTier and isV2 fields with following code

1// here, token0..3 are TokenInfoFormatted
2params.tokenChain = [token0, token1, token2, token3]
3params.feeChain = [0.05, 0.3, 0]
4params.isV2 = [false, false, true]

In general, the supported fee rates for V3-pools on the mainnet are 500 (0.05%), 3000 (0.3%), and 10000 (1%); and for the testnet are 400 (0.04%), 2000 (0.2%) and 10000 (1%). One needs to check if the choosen pool exists and has enough liquidity. The liquidity condition can be checked on the analytics page here .

3. Wrapped Native or Native token

In sdk-interfaces of UniversalQuoter and UniversalSwapRouter, if you want to pay or buy wrapped native or native token, just simply set tokenChain.first() or tokenChain.last() as wrapped native or native token. And we can also found that the only difference between wrapped native and native token is symbol field in TokenInfoFormatted

If you want to pay test BNB to buy test USDT, you can use following code. And in the following code, params is an instance of SwapExactInputParams

1// objects in tokenChain are all TokenInfoFormatted
2// BNB and USDT are defined above in section 2.
3params.tokenChain = [BNB, ... /* other mid tokens*/, USDT]

If you want to pay test USDT to buy test WBNB, you can use following code.

1// objects in tokenChain are all TokenInfoFormatted
2// WBNB and USDT are defined above in section 2.
3params.tokenChain = [USDT, ... /* other mid tokens*/, WBNB]

more detail can be viewed in the code comment in section 2.

4. Out Token FeeTier

In the code in section 2, we can notice that the object quoterParams has a field named outChargeFeeTier.

This field specify the fee tier to charge from out token, in this example, the out token is BNB.

If you specify outChargeFeeTier as 0.2, 0.2% of out token will be charged before sending to trader.

If you want 0.3% of out token is charged, you can use following code.

1params.outChargeFeeTier = 0.3

5. Use UniversalQuoter to pre-query amount of BNB acquired

1// whether limit maximum point range for each V3Pool in quoter
2const limit = true;
3const {outputAmount} = await quoteExactInput(quoterContract, quoterParams, limit);
4
5const outputAmountDecimal = amount2Decimal(new BigNumber(outputAmount), USDT)
6
7console.log(' input amount decimal: ', inputAmountDecimal)
8console.log(' output amount decimal: ', outputAmountDecimal)

In the above code, we are ready to pay 20.0 USDT (decimal amount, and defined in section 2). We simply call function quoteExactInput to get the acquired amount of token BNB. The function quoteExactInput need 3 params:

    • quoterContract: obtained through getUniversalQuoterContract in section 2

    • a quoterParams instance: obtained in section 2

    • limit: a boolean, true if we want to limit point range (no more than 10000) walked through in V3 pools during quoting, and false if we donot limit it.

Now we have finished the Quoter part.

1. Use UniversalSwap to actually pay test token USDT to get test BNB

First, we use getSwapContract to get the Swap contract

1const swapAddress = '0x8684E397A84D718dD65da5938B6985BA60C957c5' // Swap contract on BSC testnet
2const swapContract = getUniversalSwapRouterContract(swapAddress, web3)

Second, use getSwapExactInputCall to get calling (transaction handler) of swap:

 1const swapParams = {
 2    ...quoterParams,
 3    // slippery is 1.5%
 4    minOutputAmount: new BigNumber(outputAmount).times(0.985).toFixed(0)
 5} as SwapExactInputParams
 6
 7const gasPrice = '5000000000'
 8
 9const {calling: swapCalling, options} = getSwapExactInputCall(
10    swapContract,
11    account.address,
12    chain,
13    swapParams,
14    gasPrice
15)

In the above code, we ready to pay 20 test USDT (decimal amount). We simply call function getSwapExactInputCall to get test BNB. The params needed by function getSwapExactInputCall can be viewed in the following code:

 1/**
 2* @param universalSwapRouter, universal swap router contract, can be obtained through getUniversalSwapRouterContract(...)
 3* @param account, address of user
 4* @param chain, object of BaseChain, describe which chain we are using
 5* @param params, some settings of this swap, including swapchain, input amount, min required output amount
 6* @param gasPrice, gas price of this swap transaction
 7* @return calling, calling of this swap transaction
 8* @return options, options of this swap transaction, used in sending transaction
 9*/
10export const getSwapExactInputCall = (
11    universalSwapRouter: Contract<ContractAbi>,
12    account: string,
13    chain: BaseChain,
14    params: SwapExactInputParams,
15    gasPrice: number | string
16):{calling: any, options: any}

SwapExactInputParams has been explained in section 2

We usually keep outChargeFeeTier, tokenChain, feeTier, isV2 unchanged from “quoterParams”, expect minOutputAmount. And usually, we can fill SwapExactInputParams through following code.

1const swapParams = {
2    ...quoterParams,
3    // slippery is 1.5%
4    minOutputAmount: new BigNumber(outputAmount).times(0.985).toFixed(0)
5} as SwapExactInputParams

Notice that in this example, input token (test USDT) is ERC20 token and output token (test BNB) is a native token. However, if you want to pay or receive wrapped native (ERC20) or native token, you can refer to section 3

7. Approve (skip if you pay native token directly)

Before sending transaction or estimating gas, you need to approve contract Swap to have authority to spend your token. Since the contract need to transfer some input token to the pool.

If the allowance is enough or the input token is chain gas token, just skip this step.

 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 input token is not chain token (BNB on BSC or ETH on Ethereum...), we need transfer input token to pool
27// otherwise we can skip following codes
28{
29    const usdtContract = new web3.eth.Contract(erc20ABI, USDT.address);
30    // you could approve a very large amount (much more greater than amount to transfer),
31    // and don't worry about that because swapContract 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 = usdtContract.methods.approve(
34        swapAddress,
35        "0xffffffffffffffffffffffffffffffff"
36    );
37    // estimate gas
38    const gasLimit = await approveCalling.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 swapContract before
43    // if you want to enlarge approve amount, you should refer to interface of erc20 token
44    await approveCalling.send({gas: Number(gasLimit)})
45}

8. Estimate gas (optional)

Before actually send the transaction, this is double check (or user experience enhancement measures) to check whether the gas spending is normal.

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

9. Send transaction!

Now, we can then send the transaction.

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

1// it is suggested to fill the gas with a number a little greater than estimated "gasLimit".
2await swapCalling.send({...options, gas: Number(gasLimit)})

Otherwise, you could use following code

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

After sending transaction, we will successfully finish swapping with exact amount of input token (if no revert occurred).