Uniswap V2 SDK 学习笔记

功能简介

Uniswap V2 的合约代码分两部分:Core 实现某个交易的 Pair 的管理逻辑,Periphery 提供了与 Uniswap V2 进行交互的外围合约实现路由,即一个或者多个交易对的兑换逻辑。

Core 主要有以下合约

  • UniswapV2Pair:配对合约,管理着流动性资金池,不同币对有着不同的配对合约实例,比如 USDT-WETH 这一个币对,就对应一个配对合约实例,DAI-WETH 又对应另一个配对合约实例。配对合约继承了 UniswapV2ERC20 合约,即配对合约同时也是 LP Token 合约。

Periphery 主要有以下合约

路由合约是与用户进行交互的入口,主要提供了添加流动性、移除流动性和兑换的系列接口,并提供了几个查询接口。

SDK 的作用:

这篇笔记主要记录在服务端上使用 Uniswap SDK,通过 ethers.js 调用路由合约函数实现与 Uniswap 交互。

环境配置

 

$ mkdir univ2 && cd univ2 $ npm init -y $ npm i @uniswap/sdk ethers dotenv # 在 .env 文件里填好节点信息和私钥

合约函数调用:Swap

实现 swap 功能的接口有 9 个:

  • swapExactTokensForTokens:用 ERC20 兑换 ERC20,但支付的数量是指定的,而兑换回的数量则是未确定的
  • swapTokensForExactTokens:也是用 ERC20 兑换 ERC20,与上一个函数不同,指定的是兑换回的数量
  • swapExactETHForTokens:指定 ETH 数量兑换 ERC20
  • swapTokensForExactETH:用 ERC20 兑换成指定数量的 ETH
  • swapExactTokensForETH:用指定数量的 ERC20 兑换 ETH
  • swapETHForExactTokens:用 ETH 兑换指定数量的 ERC20
  • swapExactTokensForTokensSupportingFeeOnTransferTokens:指定数量的 ERC20 兑换 ERC20,支持转账时扣费
  • swapExactETHForTokensSupportingFeeOnTransferTokens:指定数量的 ETH 兑换 ERC20,支持转账时扣费
  • swapExactTokensForETHSupportingFeeOnTransferTokens:指定数量的 ERC20 兑换 ETH,支持转账时扣费

下面从最简单的不需 Approve 操作的 swapExactETHForTokens 讲起:

swapExactETHForTokens

实际上,Uniswap V2 的 pair 在内部都使用 WETH,但路由合约可以帮我们解决 ETH → WETH 包装的问题,所以我们调用函数时可以直接发送 ETH。

目标:在 Rinkeby 测试网上将 0.003 ETH 兑换为尽可能多的 LINK.

参数列表

function swapExactETHForTokens(
    uint amountOutMin, // 交易获得代币最小值
    address[] calldata path, // 交易路径列表
    address to, // 交易获得的 token 发送到的地址
    uint deadline // 过期时间
) external virtual override payable ensure(deadline) returns (
    uint[] memory amounts // 交易期望数量列表
){
    ...
}

path, to, deadline 都很好解决,问题就是如何求出 amountOutMin.

SDK 提供了方便的类和函数来帮助我们计算。

创建 Provider 实例

import { ethers } from 'ethers' 
import 'dotenv/config' 
const rpcurl = `https://rinkeby.infura.io/v3/${process.env.INFURA_PROJECT_ID}`; 

const provider = new ethers.providers.JsonRpcProvider(rpcurl); 

const signer = new ethers.Wallet(process.env.PRIVATE_KEY); 

const account = signer.connect(provider);

创建合约对象

Router02 在主网和测试网上的合约地址都是一样的。

// import { ethers } from 'ethers'
const uniV2ABI = ['function swapExactETHForTokens(uint amountOutMin, address[] calldata path, \
  address to, uint deadline) external payable returns (uint[] memory amounts)'];
const uniswapContract = new ethers.Contract('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', uniV2ABI, account);

提供 Token 信息

import { ChainId, Token } from '@uniswap/sdk'
const WETH = new Token(ChainId.RINKEBY, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18);
const LINK = new Token(ChainId.RINKEBY, '0x01BE23585060835E02B77ef475b0Cc51aA1e0709', 18);

获取 Pair 信息

Fetcher.fetchPairData(tokenA: Token, tokenB: Token, provider?: ethers.providers.BaseProvider): Promise<Pair>

import { Fetcher } from '@uniswap/sdk'
const pair = await Fetcher.fetchPairData(LINK, WETH, provider);

创建路径 route

// import { Route } from '@uniswap/sdk'
const route = new Route([pair], WETH);

本例的路径(Pair 数组)就是最简单的一个单独的 WETH → LINK Pair.

创建交易 trade

new Trade(route: Route, amount: CurrencyAmount, tradeType: TradeType): Trade

new TokenAmount(token: Token, amount: BigintIsh): TokenAmount

TokenAmount 继承了 CurrencyAmount 类。

因为我们要将 Exact ETH 换成其他 Token, 所以 tradeType 使用 TradeType.EXACT_INPUT

 
// import { TokenAmount, Trade, TradeType } from '@uniswap/sdk' 
// import { ethers } from 'ethers' 
const trade = new Trade(route, new TokenAmount(WETH,ethers.utils.parseEther('0.003')), TradeType.EXACT_INPUT);

计算参数

就像点击 Uniswap 前端界面“齿轮”图标后看到的那样,要为交易设置滑点容差。

 import { Percent , Trade} from '@uniswap/sdk'

 const slippageTolerance = new Percent('50', '10000'); // 50 / 10000 = 0.50%

 //计算出兑换币数量
 const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw

然后调用 trade.minimumAmountOut()方法就得到了amountOutMin.

 
const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw; 

// 官方文档注释:needs to be converted to e.g. hex

发送交易

参考 Uniswap Tutorial for Developers (Solidity & Javascript),但视频里的代码有点问题

如文档所说,raw 是一个 JSBI 对象,要转换后才可以塞进调用swap函数时填的参数里,实测并不一定必须转成16进制字符串表示,普通字符串即可。

完整代码

包含了一些调试语句。

 

// swapExactETHForTokens import { ChainId, Token, Fetcher, Pair, TokenAmount, Route, Trade, TradeType, Percent } from '@uniswap/sdk' import { ethers } from 'ethers' import 'dotenv/config' const rpcurl = `https://rinkeby.infura.io/v3/${process.env.INFURA_PROJECT_ID}`; const provider = new ethers.providers.JsonRpcProvider(rpcurl); const signer = new ethers.Wallet(process.env.PRIVATE_KEY); const account = signer.connect(provider); const WETH = new Token(ChainId.RINKEBY, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18); const LINK = new Token(ChainId.RINKEBY, '0x01BE23585060835E02B77ef475b0Cc51aA1e0709', 18); const uniV2ABI = ['function swapExactETHForTokens(uint amountOutMin, address[] calldata path, \ address to, uint deadline) external payable returns (uint[] memory amounts)']; const uniswapContract = new ethers.Contract('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', uniV2ABI, account); const run = async () => { const pair = await Fetcher.fetchPairData(LINK, WETH, provider); // input: WETH const route = new Route([pair], WETH); // 1 WETH = ??? LINK // toSignificant(6) 保留6位有效数字 console.log(route.midPrice.numerator.toString()); console.log(route.midPrice.denominator.toString()); console.log('WETH-LINK', route.midPrice.toSignificant(6)); // 1 LINK = ??? WETH console.log(route.midPrice.invert().numerator.toString()); console.log(route.midPrice.invert().denominator.toString()); console.log('LINK-WETH', route.midPrice.invert().toSignificant(6)); const trade = new Trade(route, new TokenAmount(WETH, ethers.utils.parseEther('0.003')), TradeType.EXACT_INPUT); console.log(trade.executionPrice.toSignificant(6)); const slippageTolerance = new Percent('50', '10000'); const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw; // console.log(amountOutMin.toString()) const path = [WETH.address, LINK.address]; const to = '0x...' // PRIVATE_KEY's Address, 或者随便一个地址用来接收 const deadline = Math.floor(Date.now() / 1000) + 60 * 20 // 20 minutes from the current Unix time const value = trade.inputAmount.raw; console.log(value.toString()) const tx = await uniswapContract.swapExactETHForTokens(amountOutMin.toString(), path, to, deadline, { value: value.toString(), // maxFeePerGas: ethers.utils.parseUnits('2','gwei'), // maxPriorityFeePerGas: ethers.utils.parseUnits('2','gwei'), }); console.log(`Transaction hash: ${tx.hash}`); const receipt = await tx.wait(); console.log(receipt); } run();

swapExactTokensForETH

接下来看如何将 Exact 数量的 Token 换成 ETH.

目标:将 10 LINK 兑换为尽可能多的 ETH.

参数列表

 

function swapExactTokensForETH( uint amountIn,// 交易支付代币数量 uint amountOutMin, // 交易获得代币最小值 address[] calldata path, // 交易路径列表 address to, // 交易获得的 token 发送到的地址 uint deadline // 过期时间 ) external virtual override ensure(deadline) returns ( uint[] memory amounts // 交易期望数量列表 ){ ... }

照葫芦画瓢

 

const uniV2ABI = ['function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, \ address to, uint deadline) external returns (uint[] memory amounts)']; const uniswapContract = new ethers.Contract('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', uniV2ABI, account); ... const pair = await Fetcher.fetchPairData(LINK, WETH, provider); const route = new Route([pair], LINK); const trade = new Trade(route, new TokenAmount(LINK, ethers.utils.parseEther('10')), TradeType.EXACT_INPUT); const slippageTolerance = new Percent('50', '10000'); const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw; const path = [LINK.address, WETH.address]; const to = '0x...'; const deadline = Math.floor(Date.now() / 1000) + 60 * 20 // 20 minutes from the current Unix time const amountIn = trade.inputAmount.raw; const swapTx = await uniswapContract.swapExactTokensForETH(amountIn.toString(), amountOutMin.toString(), path, to, deadline);

根据经验,应该先授权 (approve)允许 Uniswap 调用您的 LINK,再兑换,授权成功前兑换按钮是灰的。

但是在命令行这么自由的地方,如果直接发送交易会怎么样?

(node:11444) UnhandledPromiseRejectionWarning: Error: cannot estimate gas; transaction may fail or may require manual gas limit [ See: Error Codes ] (reason="execution reverted: TransferHelper: TRANSFER_FROM_FAILED", method="estimateGas", transaction=…)

连 TxHash 都没有,提前就给毙掉了。如果我还不死心,手动指定 gas limit 呢?

 

const swapTx = await uniswapContract.swapExactTokensForETH(amountIn.toString(), amountOutMin.toString(), path, to, deadline, { gasLimit: ethers.utils.parseUnits('200000', 'wei'), });

结果:

(node:15476) Error: transaction failed [ See: Error Codes ] (transactionHash=…)

提交上链了,但显然还是寄。

似乎会一直用到提供的上限然后才失败。我的记录:

Gas Limit & Usage by Txn: 200,000 | 197,643 (98.82%)

闹完了来学习正确做法。

授权

回想在小狐狸钱包 MetaMask 的操作过程:

notion image

根据 ERC20 规范:

 

function approve(address _spender, uint256 _value) public returns (bool success)

授权 _spender 可以从我们账户最多转移代币的数量 _value,可以多次转移,总量不超过 _value 。这个函数可以再次调用,以覆盖授权额度 _value 。

所以需要做的就是创建 LINK 合约对象,调用 approve 方法,把路由合约地址填进去。

 

const linkABI = ['function approve(address spender, uint256 value) returns (bool)']; const linkContract = new ethers.Contract('0x01BE23585060835E02B77ef475b0Cc51aA1e0709', linkABI, account); ... // 最多允许使用 11 LINK const approveTx = await linkContract.approve(uniswapContract.address, ethers.utils.parseEther('11')); console.log(`Transaction hash: ${approveTx.hash}`); const approveReceipt = await approveTx.wait(); console.log(approveReceipt);

如果需要取消授权,只需将第二个参数改为0.

https://etherscan.io/tokenapprovalcheckerRevoke Your Token Approvals on Over 100 Networks | Revoke.cash 帮忙做的就是这件事。

完整代码

 

// swapExactTokensForETH import { ChainId, Token, Fetcher, TokenAmount, Route, Trade, TradeType, Percent} from '@uniswap/sdk' import { ethers } from 'ethers' import 'dotenv/config' const rpcurl = `https://rinkeby.infura.io/v3/${process.env.INFURA_PROJECT_ID}`; const provider = new ethers.providers.JsonRpcProvider(rpcurl); const signer = new ethers.Wallet(process.env.PRIVATE_KEY); const account = signer.connect(provider); const WETH = new Token(ChainId.RINKEBY, '0xc778417E063141139Fce010982780140Aa0cD5Ab', 18); const LINK = new Token(ChainId.RINKEBY, '0x01BE23585060835E02B77ef475b0Cc51aA1e0709', 18); const uniV2ABI = ['function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, \ address to, uint deadline) external returns (uint[] memory amounts)']; const linkABI = ['function approve(address spender, uint256 value) returns (bool)']; const uniswapContract = new ethers.Contract('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', uniV2ABI, account); const linkContract = new ethers.Contract('0x01BE23585060835E02B77ef475b0Cc51aA1e0709', linkABI, account); const run = async () => { const pair = await Fetcher.fetchPairData(LINK, WETH, provider); const route = new Route([pair], LINK); const trade = new Trade(route, new TokenAmount(LINK, ethers.utils.parseEther('10')), TradeType.EXACT_INPUT); const slippageTolerance = new Percent('50', '10000'); const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw; const path = [LINK.address, WETH.address]; const to = '0x...' // PRIVATE_KEY_1's Address const deadline = Math.floor(Date.now() / 1000) + 60 * 20 // 20 minutes from the current Unix time const amountIn = trade.inputAmount.raw; const approveTx = await linkContract.approve(uniswapContract.address, ethers.utils.parseEther('11')); console.log(`Transaction hash: ${approveTx.hash}`); const approveReceipt = await approveTx.wait(); console.log(approveReceipt); const swapTx = await uniswapContract.swapExactTokensForETH(amountIn.toString(), amountOutMin.toString(), path, to, deadline); console.log(`Transaction hash: ${swapTx.hash}`); const swapReceipt = await swapTx.wait(); console.log(swapReceipt); } run();

补充:查询额度

模仿授权操作的写法。

 

// check allowance const linkABI = ['function approve(address spender, uint256 value) returns (bool)', // 添加这一段 'function allowance(address _owner, address _spender) public view returns (uint256 remaining)']; const run = async () => { const remaining = await linkContract.allowance(account.address, uniswapContract.address); console.log(ethers.utils.formatUnits(remaining, 'ether')); } run();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值