如何实现DeFi平台的命令行执行工具——以Uniswap为例

0x00 背景

在以太坊链上有一个大名鼎鼎的DeFi平台------Uniswap,它是一个去中心化的虚拟币交易平台,相对于中心化的交易平台来说,它的虚拟资产掌握在用户的私钥里面,相对来说就更安全。因此也收到越来越多用户的青睐,今天就来来看看如何通过链上接口来实现一个可以用于查询行情和执行交易的功能。

Uniswap 作为DeFi领域里面自动做市商(AMM)的创新龙头,它的社区也提供了丰富相应文档和 API。但由于它过去随着市场变化也对其 AMM 的技术细节也进行了升级,目前有 v2、v3 以及 v4 版本。由于每个版本的接口和智能合约的实现都有比较大的差异,因此在使用代码实现命令行工具的时候就需要需要指定某个版本的 SDK 。本文的初衷是开发一个简单工具让用户可以使用代码或者命令行的方式进行下单,用户也不用考虑具体是哪个版本的 SDK ,只要用户传入 tokenIn、tokenOut 以及兑换的数量 amountIn 就可以了。因此一开始的想法是聚合这几个版本的接口,对外提供一个统一的查询和兑换的接口,这样就把 v2、v3等这些接口的细节给隐藏了。

最初的想法是使用代码来封装一个接口,至于是使用 v2 还是 v3 版本,在代码逻辑中将其隐藏,使用者无需关心具体是哪个版本的接口。直到发现了 Uniswap 社区就已经实现了一个smart-order-router智能合约。简单地说它做的事情就是我要做的,于是事情就变得简单起来了。只需要跟这个智能合约进行交互,它本身就可以自动"路由"到最佳的下单路径,比如 TokenA 要与 TokenD 进行兑换。如果 Uniswap 中已有 TokenA-TokenD 的交易对,那么这个 smart-order-router 会将 TokenA-TokenD 交易对的地址返回,如果没有这个交易兑,那么它会寻找 TokenA-TokenB-TokenC-TokenD 的交易对的链条,可以实现兑换,而且这里面它还隐藏 v2、v3 的版本信息,即使用者无需事先知道去哪个版本的交易对里面去查询。

0x01 实现

在以太坊区块链中与智能合约交互比较常见的语言可以使用 JavaScript,当然也可以使用其它语言如 PythonRust。本文使用的 JavaScript,虽然平时使用这个语言比较少,但是跟智能合约交互使用它还是比较方便的。不过需要注意的是很多项目里面会使用到各种依赖库,在使用 npm 或者 yarn 包管理工具安装相应的包,建议一定要严格按照对应项目的制定的版本号,不然很容易出现各种各样的问题T_T。

首先使用 AlphaRouter 查询到对应交易对的路径信息,这里用到的参数是有需要兑换的 tokenIn, 兑换的目标 tokenOut,以及兑换的数量 amountIn。 这个方法是一个异步 async 方法,返回的是一个 Promise 封装的 SwapRoute 对象。这里就能获取到我们需要的路径信息。

js 复制代码
export async function generateRoute(tokenIn: Token, tokenOut: Token, amountIn: number): Promise<SwapRoute | null> {
  const router = new AlphaRouter({
    chainId: AppChainId,
    provider: getProvider(),
  })

  const options: SwapOptionsSwapRouter02 = {
    recipient: getWalletAddress(),
    slippageTolerance: new Percent(50, 10_000),
    deadline: Math.floor(Date.now() / 1000 + 60*30),
    type: SwapType.SWAP_ROUTER_02,
  }

  const route = await router.route(
    CurrencyAmount.fromRawAmount(
      tokenIn,
      fromReadableAmount(
        amountIn,
        tokenIn.decimals
      ).toString()
    ),
    tokenOut,
    TradeType.EXACT_INPUT,
    options
  )

  return route
}

获取到路径信息之后,就可以查询到这个交易对相关的一些信息了,比如兑换的数量、油费等信息

js 复制代码
const route = await generateRoute(tokenIn, tokenOut, amountIn)

  if (route == null) {
    console.log(`Route is null, try another network.`)
    return
  }
  const tokenAmountOut = route.quote.toSignificant(tokenOut.decimals)
  const wei = fromReadableAmount(tokenAmountOut, tokenOut.decimals)
  console.log(`Quote Exact Token Out: ${tokenAmountOut} ${tokenOut.symbol}`)
  console.log(`Quote Exact Token Out(wei): ${wei}`)
  console.log(`Quote Gas Adjusted: ${route.quoteGasAdjusted.toSignificant(tokenOut.decimals)} `)
  console.log(`Estimated Gas Used USD: ${route.estimatedGasUsedUSD.toFixed(2)}`)
  console.log(`Estimated Gas: ${route.estimatedGasUsed}`)
  console.log(`Estimated Gas Price(wei): ${route.gasPriceWei}`)

这一步就是完成查询行情,看起来非常简单,是吧。

有了行情信息,下一步就可以执行交换了

执行交互其实就是发送交易信息,这一步其实就是使用区块链开发中常见的库ethers.js 比如这里代码逻辑是先获取到当前钱包和链接的节点 Provider 信息,然后做了一个授权的动作,即执行 getTokenTransferApproval 这个方法,它的目的是将用户的钱包中指定数量的 token 授权给合约,这样这个合约才有权限操作用户的 token。授权完成就执行了 sendTransaction 方法,这个方法其实就是执行钱包的发送交易的操作。执行交易之后,就会将交易信息发布到链上,矿工执行了这个交易之后,就会将这笔交易写到链上,并完成交易。

js 复制代码
export async function executeRoute(
  route: SwapRoute,
  tokenIn: Token,
): Promise<TransactionState> {
  const walletAddress = getWalletAddress()
  const provider = getProvider()

  if (!walletAddress || !provider) {
    throw new Error('Cannot execute a trade without a connected wallet')
  }

  const allowance = await checkAllowance(tokenIn.address,walletAddress,V3_SWAP_ROUTER_ADDRESS)
  
  if(!allowance) {
    console.log("Not Allowance, Start Approving")
    // 2^128 - 1
    const approveAmount = BigNumber.from(2).pow(128).sub(BigNumber.from(1))
    const tokenApproval = await getTokenTransferApproval(tokenIn, approveAmount.toString())
    .catch(err => {
      console.log(`Approval Error:\n ${err}`)
    })

    // Fail if transfer approvals do not go through
    if (tokenApproval !== TransactionState.Sent) {
      return TransactionState.Failed
    }
    console.log(`Approval is sent`)
  }
  console.log(`Start Sending Transaction`)
  const res = await sendTransaction({
    data: route.methodParameters?.calldata,
    to: V3_SWAP_ROUTER_ADDRESS,
    value: route?.methodParameters?.value,
    from: walletAddress,
    maxFeePerGas: MAX_FEE_PER_GAS,
    maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS,
  })

  return res
}

入口函数,这里是简单处理了命令行的参数

js 复制代码
async function main() {
  const args = process.argv.slice(2);
  if (args.length < 3) {
    console.log("need to specify tokenOutAddress.")
    return;
  }

  var tokenInAddress = args[0]
  var tokenOutAddress = args[1];
  var amountIn = parseFloat(args[2])
  var trade = args[3]

  await quoteAndTrade(tokenInAddress, tokenOutAddress, amountIn, trade)

}

用法是 ts-node src/route.ts tokenIn tokenOut amountIn

0x02 参考

相关推荐
caperxi13 分钟前
vue 踩坑记录
前端·javascript·vue.js
2301_7969821420 分钟前
location.protocol+‘//‘+location.hostname实现什么功能?
java·前端·javascript·python
天农学子4 小时前
elementui el-radio 垂直排列
前端·javascript·elementui
无名之逆4 小时前
B树和B+树
javascript·数据结构·b树·算法·编译原理·期末速成
乐~~~4 小时前
el-table表头添加搜索
javascript·vue.js·elementui
简单点了4 小时前
el-table解决过滤导致选中的丢失
前端·javascript·elementui
认识则是有缘5 小时前
Echarts 绘制地图省、市、区、县(以及点击显示下级,支持坐标定位)
前端·javascript·echarts
冰暮流星7 小时前
javascript如何利用for循环计算0-100的偶数和
javascript·笔记
kong79069289 小时前
jQuery入门(七)jQuery实现按钮分页
前端·javascript·jquery
Mac@分享吧10 小时前
若依ruoyi-vue部署在子域名下(做为子应用)
javascript·vue.js·ecmascript