如何实现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 参考

相关推荐
真滴book理喻26 分钟前
Vue(四)
前端·javascript·vue.js
程序员_三木1 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
开心工作室_kaic2 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育2 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博2 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js
温轻舟3 小时前
前端开发 之 12个鼠标交互特效上【附完整源码】
开发语言·前端·javascript·css·html·交互·温轻舟
web135085886353 小时前
2024-05-18 前端模块化开发——ESModule模块化
开发语言·前端·javascript
LCG元4 小时前
javascript页面设计案例【使用HTML、CSS和JavaScript创建一个基本的互动网页】
javascript
技术程序猿华锋4 小时前
Gemini 2.0 Flash 体验版实测:日常视觉识别的最佳选择,关键在于其API Key现在是免费调用
开发语言·javascript·ecmascript·googlecloud·gemini
TttHhhYy4 小时前
uniapp+vue开发app,蓝牙连接,蓝牙接收文件保存到手机特定文件夹,从手机特定目录(可自定义),读取文件内容,这篇首先说如何读取,手机目录如何寻找
开发语言·前端·javascript·vue.js·uni-app