优雅简洁!在FMZ上用200行代码接入了Uniswap V3
近些年随着Defi概念的流行,在去中心化金融(DeFi)领域,Uniswap V3是最受关注的话题之一。作为领先的去中心化交换协议,Uniswap V3提供了更高效,更安全,更优秀的用户体验。现在,只需要200行代码,交易员和开发人员就可以在FMZ平台上轻松访问Uniswap V3。
FMZ是一个量化交易平台,支持数量交易策略的开发、回测和实盘部署。其易于使用的界面和强大的功能不难理解为为什么FMZ正在成为DeFi交易员和开发人员的首选。
将Uniswap V3整合到FMZ的过程简单易懂,只需要200行代码即可完成。这意味着,即使您是代码新手,也可以在FMZ上轻松连接Uniswap V3并立即开始交易。
FMZ已经封装了一系列的web3基础功能,除了Uniswap之外也可以用很少的代码封装其它的DEX交易所。接下来就让我带你一起零基础开始学习defi应用中的概念与技术,鉴于篇幅原因,以下描述尽量采用最简单易懂的方式,可能不是十分严谨,但是便于理解。
FMZ平台公开的「Uniswap V3 交易类库」
代码如下:
/* jshint esversion: 7 */
const ABI_Route = '[{"inputs":[{"internalType":"address","name":"_factoryV2","type":"address"},{"internalType":"address","name":"factoryV3","type":"address"},{"internalType":"address","name":"_positionManager","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveMax","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveMaxMinusOne","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveZeroThenMax","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveZeroThenMaxMinusOne","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes","name":"data","type":"bytes"}],"name":"callPositionManager","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"paths","type":"bytes[]"},{"internalType":"uint128[]","name":"amounts","type":"uint128[]"},{"internalType":"uint24","name":"maximumTickDivergence","type":"uint24"},{"internalType":"uint32","name":"secondsAgo","type":"uint32"}],"name":"checkOracleSlippage","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint24","name":"maximumTickDivergence","type":"uint24"},{"internalType":"uint32","name":"secondsAgo","type":"uint32"}],"name":"checkOracleSlippage","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct IV3SwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IV3SwapRouter.ExactInputSingleParams","name":"params","type":"tuple"}],"name":"exactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"}],"internalType":"struct IV3SwapRouter.ExactOutputParams","name":"params","type":"tuple"}],"name":"exactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IV3SwapRouter.ExactOutputSingleParams","name":"params","type":"tuple"}],"name":"exactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"factoryV2","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"getApprovalType","outputs":[{"internalType":"enum IApproveAndCall.ApprovalType","name":"","type":"uint8"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"amount0Min","type":"uint256"},{"internalType":"uint256","name":"amount1Min","type":"uint256"}],"internalType":"struct IApproveAndCall.IncreaseLiquidityParams","name":"params","type":"tuple"}],"name":"increaseLiquidity","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint256","name":"amount0Min","type":"uint256"},{"internalType":"uint256","name":"amount1Min","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"internalType":"struct IApproveAndCall.MintParams","name":"params","type":"tuple"}],"name":"mint","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"previousBlockhash","type":"bytes32"},{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"positionManager","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"pull","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"refundETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowedIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"}],"name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"}],"name":"swapTokensForExactTokens","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"value","type":"uint256"}],"name":"wrapETH","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]';
const ABI_Pool = '[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"}],\"name\":\"Burn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount0\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount1\",\"type\":\"uint128\"}],\"name\":\"Collect\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount0\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount1\",\"type\":\"uint128\"}],\"name\":\"CollectProtocol\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"paid0\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"paid1\",\"type\":\"uint256\"}],\"name\":\"Flash\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"observationCardinalityNextOld\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"observationCardinalityNextNew\",\"type\":\"uint16\"}],\"name\":\"IncreaseObservationCardinalityNext\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint160\",\"name\":\"sqrtPriceX96\",\"type\":\"uint160\"},{\"indexed\":false,\"internalType\":\"int24\",\"name\":\"tick\",\"type\":\"int24\"}],\"name\":\"Initialize\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"}],\"name\":\"Mint\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"feeProtocol0Old\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"feeProtocol1Old\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"feeProtocol0New\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"feeProtocol1New\",\"type\":\"uint8\"}],\"name\":\"SetFeeProtocol\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"int256\",\"name\":\"amount0\",\"type\":\"int256\"},{\"indexed\":false,\"internalType\":\"int256\",\"name\":\"amount1\",\"type\":\"int256\"},{\"indexed\":false,\"internalType\":\"uint160\",\"name\":\"sqrtPriceX96\",\"type\":\"uint160\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"liquidity\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"int24\",\"name\":\"tick\",\"type\":\"int24\"}],\"name\":\"Swap\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"}],\"name\":\"burn\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"internalType\":\"uint128\",\"name\":\"amount0Requested\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"amount1Requested\",\"type\":\"uint128\"}],\"name\":\"collect\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"amount0\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"amount1\",\"type\":\"uint128\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint128\",\"name\":\"amount0Requested\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"amount1Requested\",\"type\":\"uint128\"}],\"name\":\"collectProtocol\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"amount0\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"amount1\",\"type\":\"uint128\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"factory\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"fee\",\"outputs\":[{\"internalType\":\"uint24\",\"name\":\"\",\"type\":\"uint24\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feeGrowthGlobal0X128\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feeGrowthGlobal1X128\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"flash\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint16\",\"name\":\"observationCardinalityNext\",\"type\":\"uint16\"}],\"name\":\"increaseObservationCardinalityNext\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint160\",\"name\":\"sqrtPriceX96\",\"type\":\"uint160\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"liquidity\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"maxLiquidityPerTick\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"mint\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"observations\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"blockTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"int56\",\"name\":\"tickCumulative\",\"type\":\"int56\"},{\"internalType\":\"uint160\",\"name\":\"secondsPerLiquidityCumulativeX128\",\"type\":\"uint160\"},{\"internalType\":\"bool\",\"name\":\"initialized\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32[]\",\"name\":\"secondsAgos\",\"type\":\"uint32[]\"}],\"name\":\"observe\",\"outputs\":[{\"internalType\":\"int56[]\",\"name\":\"tickCumulatives\",\"type\":\"int56[]\"},{\"internalType\":\"uint160[]\",\"name\":\"secondsPerLiquidityCumulativeX128s\",\"type\":\"uint160[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"positions\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"liquidity\",\"type\":\"uint128\"},{\"internalType\":\"uint256\",\"name\":\"feeGrowthInside0LastX128\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"feeGrowthInside1LastX128\",\"type\":\"uint256\"},{\"internalType\":\"uint128\",\"name\":\"tokensOwed0\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"tokensOwed1\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolFees\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"token0\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"token1\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"feeProtocol0\",\"type\":\"uint8\"},{\"internalType\":\"uint8\",\"name\":\"feeProtocol1\",\"type\":\"uint8\"}],\"name\":\"setFeeProtocol\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"slot0\",\"outputs\":[{\"internalType\":\"uint160\",\"name\":\"sqrtPriceX96\",\"type\":\"uint160\"},{\"internalType\":\"int24\",\"name\":\"tick\",\"type\":\"int24\"},{\"internalType\":\"uint16\",\"name\":\"observationIndex\",\"type\":\"uint16\"},{\"internalType\":\"uint16\",\"name\":\"observationCardinality\",\"type\":\"uint16\"},{\"internalType\":\"uint16\",\"name\":\"observationCardinalityNext\",\"type\":\"uint16\"},{\"internalType\":\"uint8\",\"name\":\"feeProtocol\",\"type\":\"uint8\"},{\"internalType\":\"bool\",\"name\":\"unlocked\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"}],\"name\":\"snapshotCumulativesInside\",\"outputs\":[{\"internalType\":\"int56\",\"name\":\"tickCumulativeInside\",\"type\":\"int56\"},{\"internalType\":\"uint160\",\"name\":\"secondsPerLiquidityInsideX128\",\"type\":\"uint160\"},{\"internalType\":\"uint32\",\"name\":\"secondsInside\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"zeroForOne\",\"type\":\"bool\"},{\"internalType\":\"int256\",\"name\":\"amountSpecified\",\"type\":\"int256\"},{\"internalType\":\"uint160\",\"name\":\"sqrtPriceLimitX96\",\"type\":\"uint160\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"swap\",\"outputs\":[{\"internalType\":\"int256\",\"name\":\"amount0\",\"type\":\"int256\"},{\"internalType\":\"int256\",\"name\":\"amount1\",\"type\":\"int256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"int16\",\"name\":\"\",\"type\":\"int16\"}],\"name\":\"tickBitmap\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"tickSpacing\",\"outputs\":[{\"internalType\":\"int24\",\"name\":\"\",\"type\":\"int24\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"int24\",\"name\":\"\",\"type\":\"int24\"}],\"name\":\"ticks\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"liquidityGross\",\"type\":\"uint128\"},{\"internalType\":\"int128\",\"name\":\"liquidityNet\",\"type\":\"int128\"},{\"internalType\":\"uint256\",\"name\":\"feeGrowthOutside0X128\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"feeGrowthOutside1X128\",\"type\":\"uint256\"},{\"internalType\":\"int56\",\"name\":\"tickCumulativeOutside\",\"type\":\"int56\"},{\"internalType\":\"uint160\",\"name\":\"secondsPerLiquidityOutsideX128\",\"type\":\"uint160\"},{\"internalType\":\"uint32\",\"name\":\"secondsOutside\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"initialized\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"token0\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"token1\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]'
const ABI_Factory = '[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickSpacing\",\"type\":\"int24\"}],\"name\":\"FeeAmountEnabled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token0\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token1\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"},{\"indexed\":false,\"internalType\":\"int24\",\"name\":\"tickSpacing\",\"type\":\"int24\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"pool\",\"type\":\"address\"}],\"name\":\"PoolCreated\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"tokenA\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"tokenB\",\"type\":\"address\"},{\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"}],\"name\":\"createPool\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"pool\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"},{\"internalType\":\"int24\",\"name\":\"tickSpacing\",\"type\":\"int24\"}],\"name\":\"enableFeeAmount\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint24\",\"name\":\"\",\"type\":\"uint24\"}],\"name\":\"feeAmountTickSpacing\",\"outputs\":[{\"internalType\":\"int24\",\"name\":\"\",\"type\":\"int24\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"uint24\",\"name\":\"\",\"type\":\"uint24\"}],\"name\":\"getPool\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"parameters\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"factory\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token0\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token1\",\"type\":\"address\"},{\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"},{\"internalType\":\"int24\",\"name\":\"tickSpacing\",\"type\":\"int24\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]'
let ContractV3Factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984"
let ContractV3SwapRouterV2 = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
function computePoolPrice(decimals0, decimals1, sqrtPriceX96) {
[decimals0, decimals1, sqrtPriceX96] = [decimals0, decimals1, sqrtPriceX96].map(BigInt);
const TWO = BigInt(2);
const TEN = BigInt(10);
const SIX_TENTH = BigInt(1000000);
const Q192 = (TWO ** BigInt(96)) ** TWO;
return (
Number((sqrtPriceX96 ** TWO * TEN ** decimals0 * SIX_TENTH) / (Q192 * TEN ** decimals1)) /
Number(SIX_TENTH)
);
}
function toAmount(s, decimals) {
return Number((BigDecimal(BigInt(s))/BigDecimal(Math.pow(10, decimals))).toString())
}
function toInnerAmount(n, decimals) {
return (BigDecimal(n)*BigDecimal(Math.pow(10,decimals))).toFixed(0)
}
$.NewUniswapV3 = function(e) {
e = e || exchange
if (e.GetName() !== 'Web3') {
panic("only support Web3 exchange")
}
let self = {
tokenInfo: {},
walletAddress: e.IO("address"),
pool: {}
}
// register
e.IO("abi", ContractV3Factory, ABI_Factory)
e.IO("abi", ContractV3SwapRouterV2, ABI_Route)
self.addToken = function(name, address) {
let ret = e.IO("api", address, "decimals")
if (!ret) {
throw "get token decimals failed"
}
let decimals = Number(ret)
self.tokenInfo[name] = {
name: name,
decimals: decimals,
address: address
}
}
self.waitMined = function(tx) {
while (true) {
Sleep(1000)
let info = e.IO("api", "eth", "eth_getTransactionReceipt", tx)
if (info && info.gasUsed) {
return true
}
Log('Transaction not yet mined', tx)
}
}
self.swapToken = function(tokenIn, amountInDecimal, tokenOut, options) {
// options like {gasPrice: 11, gasLimit: 111, nonce: 111}
let tokenInInfo = self.tokenInfo[tokenIn]
let tokenOutInfo = self.tokenInfo[tokenOut]
if (!tokenInInfo) {
throw "not found token info " + tokenIn
}
if (!tokenOutInfo) {
throw "not found token info " + tokenOut
}
let amountIn = toInnerAmount(amountInDecimal, tokenInInfo.decimals)
let recipientAddress = self.walletAddress
if (tokenInInfo.name != 'ETH') {
let allowanceAmount = e.IO("api", tokenInInfo.address, "allowance", self.walletAddress, ContractV3SwapRouterV2);
let realAmount = toAmount(allowanceAmount, tokenInInfo.decimals)
if (realAmount < toAmount(amountIn, tokenInInfo.decimals)) {
Log("realAmount is", realAmount, "too small, try to approve large amount")
if (tokenInInfo.name == 'USDT') {
// As described in Tether code: To change the approve amount you first have to reduce the addresses allowance to 0 calling approve(spender, 0)
let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, 0)
if (!txApprove) {
throw "approve error"
}
Log("wait reduce approve", txApprove)
self.waitMined(txApprove)
}
let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
if (!txApprove) {
throw "approve error"
}
Log("wait approve", txApprove)
self.waitMined(txApprove)
Log("approve success amountIn", amountIn)
} else {
Log("allowance", realAmount, "no need to approve")
}
}
if (tokenOutInfo.name == 'ETH' || tokenOutInfo.address.toLowerCase() == '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') {
/*
ADDRESS_THIS https://degencode.substack.com/p/uniswapv3-multicall
https://etherscan.io/address/0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45#code
*/
recipientAddress = '0x0000000000000000000000000000000000000002'
}
let swapToken = e.IO("encode", ContractV3SwapRouterV2, "swapExactTokensForTokens", amountIn, 1, [tokenInInfo.address, tokenOutInfo.address], recipientAddress)
let data = [swapToken]
if (tokenOutInfo.name == 'ETH') {
data.push(e.IO("encode", ContractV3SwapRouterV2, "unwrapWETH9(uint256,address)", 1, self.walletAddress))
}
let tx = e.IO("api", ContractV3SwapRouterV2, "multicall(uint256,bytes[])", (tokenInInfo.name == 'ETH' ? amountIn : 0), (new Date().getTime() / 1000) + 3600, data, options || {})
if (tx) {
Log("tx: ", tx)
self.waitMined(tx)
Log("swap", tokenInInfo.name, "to", tokenOutInfo.name, "success")
return true
} else {
Log("trans error")
return false
}
}
self.getETHBalance = function(address) {
return toAmount(e.IO("api", "eth", "eth_getBalance", address || self.walletAddress, "latest"), 18)
}
self.balanceOf = function(token, address) {
let tokenInfo = self.tokenInfo[token]
if (!tokenInfo) {
throw "not found token info " + token
}
return toAmount(e.IO("api", tokenInfo.address, "balanceOf", address || self.walletAddress), tokenInfo.decimals)
}
self.sendETH = function(to, amount, options) {
return e.IO("api", "eth", "send", to, toInnerAmount(amount, 18), options || {})
}
self.getPrice = function(pair) {
let arr = pair.split('_')
let token0 = self.tokenInfo[arr[0]]
if (!token0) {
throw "token " + arr[0] + "not found"
}
let token1 = self.tokenInfo[arr[1]]
if (!token1) {
throw "token " + arr[1] + "not found"
}
let reverse = false
if (BigInt(token0.address) > BigInt(token1.address)) {
let tmp = token0
token0 = token1
token1 = tmp
reverse = true
}
let key = token0.address + '/' + token1.address
if (typeof(self.pool[key]) == 'undefined') {
let pool = e.IO("api", ContractV3Factory, "getPool", token0.address, token1.address, 3000)
if (pool) {
self.pool[key] = pool
// register pool address
e.IO("abi", pool, ABI_Pool)
}
}
if (typeof(self.pool[key]) == 'undefined') {
throw "pool " + pair + " not found"
}
let slot0 = e.IO("api", self.pool[key], "slot0")
if (!slot0) {
return null
}
let price = computePoolPrice(token0.decimals, token1.decimals, slot0.sqrtPriceX96)
if (reverse) {
price = 1 / price
}
return price
}
return self
}
$.testUniswap = function() {
let ex = $.NewUniswapV3()
Log("walletAddress: ", ex.walletAddress)
let tokenAddressMap = {
"ETH": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH
"USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"1INCH": "0x111111111117dC0aa78b770fA6A738034120C302",
}
for (let name in tokenAddressMap) {
ex.addToken(name, tokenAddressMap[name])
}
Log(ex.getPrice('ETH_USDT'))
Log(ex.getPrice('1INCH_USDT'))
// swap 0.01 ETH to USDT
Log(ex.swapToken('ETH', 0.01, 'USDT'))
let usdtBalance = ex.balanceOf('USDT')
Log("balance of USDT", usdtBalance)
// swap reverse
Log(ex.swapToken('USDT', usdtBalance, 'ETH'))
Log("balance of ETH", ex.getETHBalance())
// Log(ex.sendETH('0x11111', 0.02))
}
以太坊网络
以太坊网络可以理解为一个软件基础设施,在这个网络上可以部署运行各种智能合约,智能合约有各种各样的功能、应用场景。运行以太坊客户端的设备就构成了以太坊网络中的节点。
Uniswap V3中的一些概念
不熟悉Uniswap V3协议的同学,这里需要先简单了解几个概念。Uniswap V3也是在以太坊上部署运行的智能合约。
1、路由(Route):路由也是一个智能合约,用于管理token兑换。
2、池(Pool):池也是一个智能合约,用于存储两种以太坊代币,并在这两种代币之间进行交换。
3、工厂合约(Factory):工厂合约是用来创建池的智能合约。
4、ABI:(应用程序二进制接口)是一种规范,用于描述智能合约如何与外部世界互相通信。它指定了智能合约的函数名称,参数类型和返回值类型,以及如何编码和解码数据,并确定了智能合约的外部接口。可以理解为要调用某个接口,就必须按照接口约定的标准调用,而ABI中记录的就是约定的一系列标准。
一旦智能合约在以太坊上部署,就有一个地址。
剖析Uniswap V3 交易类库的代码
Uniswap V3 交易类库代码主要分为4个部分,我们来逐一讲解。
Part 1:与Uniswap V3交互时用到的常量
const ABI_Route = '[{"inputs":[{"internalType":"address...
const ABI_Pool = '[{\"inputs\":[],\"stateMutability\":\"nonpayable...
const ABI_Factory = '[{\"inputs\":[],\"stateMutability\":\"...
let ContractV3Factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984"
let ContractV3SwapRouterV2 = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
通过上面的基础概念预热,这里就很容易理解啦。
ABI_Route这个常量储存的字符串就是路由智能合约的ABI。
ABI_Pool储存池合约的ABI。
ABI_Factory工厂合约的ABI。
由于这些字符串很长,所以只是节选。这些内容就给程序提供了调用智能合约方法的标准(例如这个智能合约接口的参数有什么,几个参数,都是什么类型,返回数据是什么类型等)。
刚才我们也提过,一旦智能合约部署在以太坊,就有一个地址。
ContractV3Factory:记录了工厂合约的地址。
ContractV3SwapRouterV2:Uniswap V3的router V2地址,注意Uniswap有V1、V2,Uniswap V3 的router也有V1、V2之分,不同的合约地址是不一样的。
Part2:工具函数
1、computePoolPrice函数用来计算池中token的价格。
function computePoolPrice(decimals0, decimals1, sqrtPriceX96) {
[decimals0, decimals1, sqrtPriceX96] = [decimals0, decimals1, sqrtPriceX96].map(BigInt); // 使用BigInt函数处理,因为JavaScript语言数值精度的原因,需要使用FMZ的一个底层处理函数BigInt来处理
const TWO = BigInt(2); // 定义常量2用于计算
const TEN = BigInt(10); // 定义常量10用于计算
const SIX_TENTH = BigInt(1000000); // 定义常量10的6次方,即1e6
const Q192 = (TWO ** BigInt(96)) ** TWO; // 2^192
return (
Number((sqrtPriceX96 ** TWO * TEN ** decimals0 * SIX_TENTH) / (Q192 * TEN ** decimals1)) /
Number(SIX_TENTH)
);
}
假如交易对是ETH_USDT,那么token0是ETH,token1是USDT。decimals0就是token0的精度数据,decimals1就是token1的精度数据。sqrtPriceX96就是价格相关数据(并不是直接的价格数值),该数据可以从池合约的slot0方法获取。
sqrtPriceX96 : The current price of the pool as a sqrt(token1/token0) Q64.96 value
Q64.96 是一种数据处理储存标准。
decimals0,decimals1, sqrtPriceX96这三个数据作为参数传入computePoolPrice函数即可计算出交易对ETH_USDT的价格。该函数最后return语句中的算法就是把sqrtPriceX96还原成token1/token0的过程。例如此时池里token0(ETH)个数是1个,token1(USDT)个数是1100个。那么1100/1=1100,当前交易对ETH_USDT池中的价格就是1100。
2、toAmount函数用来把链上数值数据转换成可读数据。
function toAmount(s, decimals) {
return Number((BigDecimal(BigInt(s))/BigDecimal(Math.pow(10, decimals))).toString())
}
简单说,例如一个ETH代币在链上表示数量时是1e18,即10的18次方,因为ETH的精度数据是18。并非所有代币精度都是18,USDT的精度就和ETH的精度不一样。toAmount函数就是将1e18转换成1。
3、toInnerAmount函数则和toAmount相反,是将可读数据转换成链上使用的数值。
function toInnerAmount(n, decimals) {
return (BigDecimal(n)*BigDecimal(Math.pow(10,decimals))).toFixed(0)
}
接下来我们来一起剖析「Uniswap V3 交易类库」的代码。
Part3:Uniswap V3操作对象的构造函数
这个模板类库的核心就是Uniswap V3操作对象,这个对象实现了在Uniswap V3上的基本操作。后续可能还会升级更多的功能。通过剖析这个代码例子,即使不使用FMZ平台,也会增加对Uniswap这个DEX的各个环节流程、细节的认识和理解,目前我们就来学习下这些基本的功能是如何在FMZ上设计实现的。
Uniswap V3操作对象的构造函数代码:
javascript
$.NewUniswapV3 = function(e) {
e = e || exchange // 如果没有传参数e,就使用交易所对象exchange,即策略上第一个添加的交易所
if (e.GetName() !== 'Web3') { // 判断交易所对象是否是Web3,因为这个模板只支持Web3交易所对象
panic("only support Web3 exchange")
}
let self = { // 当前函数是一个构造函数,构造的对象就是self这个对象
tokenInfo: {}, // self对象的成员变量,用于记录token的注册信息
walletAddress: e.IO("address"), // 记录当前交易所对象绑定的钱包地址
pool: {} // 用于记录注册的池信息
}
// register
e.IO("abi", ContractV3Factory, ABI_Factory) // 注册工厂合约的ABI
e.IO("abi", ContractV3SwapRouterV2, ABI_Route) // 注册路由合约的ABI
self.addToken = function(name, address) { // 用于注册token
let ret = e.IO("api", address, "decimals") // 调用decimals方法,获取token精度信息
if (!ret) {
throw "get token decimals failed"
}
let decimals = Number(ret)
self.tokenInfo[name] = {
name: name,
decimals: decimals,
address: address
}
}
self.waitMined = function(tx) { // 用于等待以太坊上某个操作的结果,哈希为tx参数
while (true) {
Sleep(1000)
let info = e.IO("api", "eth", "eth_getTransactionReceipt", tx) // 查询结果使用eth_getTransactionReceipt方法,没有查询到,循环继续查询
if (info && info.gasUsed) {
return true
}
Log('Transaction not yet mined', tx)
}
}
self.swapToken = function(tokenIn, amountInDecimal, tokenOut, options) { // 用于token兑换
// options like {gasPrice: 11, gasLimit: 111, nonce: 111}
let tokenInInfo = self.tokenInfo[tokenIn] // 拿到兑换出去的token的信息
let tokenOutInfo = self.tokenInfo[tokenOut] // 拿到兑换回来的token的信息
if (!tokenInInfo) {
throw "not found token info " + tokenIn
}
if (!tokenOutInfo) {
throw "not found token info " + tokenOut
}
let amountIn = toInnerAmount(amountInDecimal, tokenInInfo.decimals) // 转换为智能合约上使用的数据
let recipientAddress = self.walletAddress
if (tokenInInfo.name != 'ETH') {
let allowanceAmount = e.IO("api", tokenInInfo.address, "allowance", self.walletAddress, ContractV3SwapRouterV2); // 查询授权的数量
let realAmount = toAmount(allowanceAmount, tokenInInfo.decimals)
if (realAmount < toAmount(amountIn, tokenInInfo.decimals)) { // 如果授权数量不足
Log("realAmount is", realAmount, "too small, try to approve large amount")
if (tokenInInfo.name == 'USDT') {
// As described in Tether code: To change the approve amount you first have to reduce the addresses allowance to 0 calling approve(spender, 0)
let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, 0) // 如果授权的token是USDT,需要先授权为0
if (!txApprove) {
throw "approve error"
}
Log("wait reduce approve", txApprove)
self.waitMined(txApprove)
}
let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); // 授权Router合约操作钱包的代币
if (!txApprove) {
throw "approve error"
}
Log("wait approve", txApprove)
self.waitMined(txApprove)
Log("approve success amountIn", amountIn)
} else {
Log("allowance", realAmount, "no need to approve")
}
}
if (tokenOutInfo.name == 'ETH' || tokenOutInfo.address.toLowerCase() == '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') {
/*
ADDRESS_THIS https://degencode.substack.com/p/uniswapv3-multicall
https://etherscan.io/address/0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45#code
*/
recipientAddress = '0x0000000000000000000000000000000000000002'
// 其它币换成 WETH的时候,要让合约HOLD住WETH才可以赎回
}
let swapToken = e.IO("encode", ContractV3SwapRouterV2, "swapExactTokensForTokens", amountIn, 1, [tokenInInfo.address, tokenOutInfo.address], recipientAddress) // 打包swapExactTokensForTokens调用
let data = [swapToken]
if (tokenOutInfo.name == 'ETH') { // 如果兑换时,兑换回来的token是ETH,这里实际是WETH,则需要解包
data.push(e.IO("encode", ContractV3SwapRouterV2, "unwrapWETH9(uint256,address)", 1, self.walletAddress)) // 所以这里再打包一个unwrapWETH9解包调用
}
let tx = e.IO("api", ContractV3SwapRouterV2, "multicall(uint256,bytes[])", (tokenInInfo.name == 'ETH' ? amountIn : 0), (new Date().getTime() / 1000) + 3600, data, options || {}) // 使用multicall执行这些打包的操作(swapExactTokensForTokens、unwrapWETH9)
if (tx) {
Log("tx: ", tx)
self.waitMined(tx)
Log("swap", tokenInInfo.name, "to", tokenOutInfo.name, "success")
return true
} else {
Log("trans error")
return false
}
}
self.getETHBalance = function(address) { // 查询钱包的ETH余额
return toAmount(e.IO("api", "eth", "eth_getBalance", address || self.walletAddress, "latest"), 18)
}
self.balanceOf = function(token, address) { // 查询钱包的某个token余额(根据参数确定)
let tokenInfo = self.tokenInfo[token]
if (!tokenInfo) {
throw "not found token info " + token
}
return toAmount(e.IO("api", tokenInfo.address, "balanceOf", address || self.walletAddress), tokenInfo.decimals)
}
self.sendETH = function(to, amount, options) { // 向某个地址发送ETH代币,即转账
return e.IO("api", "eth", "send", to, toInnerAmount(amount, 18), options || {})
}
self.getPrice = function(pair, fee) { // 获取交易对价格
let arr = pair.split('_')
let token0 = self.tokenInfo[arr[0]]
if (!token0) {
throw "token " + arr[0] + "not found"
}
let token1 = self.tokenInfo[arr[1]] // 首先拿到构成交易对的两个token信息
if (!token1) {
throw "token " + arr[1] + "not found"
}
let reverse = false
if (BigInt(token0.address) > BigInt(token1.address)) {
let tmp = token0
token0 = token1
token1 = tmp
reverse = true
}
let key = token0.address + '/' + token1.address
if (typeof(self.pool[key]) == 'undefined') {
let pool = e.IO("api", ContractV3Factory, "getPool", token0.address, token1.address, typeof(fee) === 'number' ? fee : 3000) // 调用工厂合约的getPool方法,获取兑换池的地址
if (pool) {
self.pool[key] = pool // 注册池地址,并注册池合约的ABI
// register pool address
e.IO("abi", pool, ABI_Pool)
}
}
if (typeof(self.pool[key]) == 'undefined') {
throw "pool " + pair + " not found"
}
let slot0 = e.IO("api", self.pool[key], "slot0") // 调用池合约的slot0方法,拿到价格相关信息
if (!slot0) {
return null
}
let price = computePoolPrice(token0.decimals, token1.decimals, slot0.sqrtPriceX96) // 计算出可读的价格
if (reverse) {
price = 1 / price
}
return price
}
return self
}
可能不熟悉FMZ的同学看到这个函数$.NewUniswapV3命名有些奇怪,带有$.开头的函数,表示这个函数是FMZ上模板类库的接口函数(何为模板类库可以查阅),简单说就是$.NewUniswapV3函数可以让其它引用了该模板类库的策略直接调用。策略就直接拥有了Uniswap V3的功能。
这个$.NewUniswapV3函数直接构造、创建一个对象,使用这个对象就可以进行一些操作:
- token兑换:由该对象的
swapToken方法实现。 - ETH余额查询:由该对象的
getETHBalance方法实现。 - token余额查询:由该对象的
balanceOf方法实现。 - 交易对价格查询:由该对象的
getPrice方法实现。 - 发送ETH进行转账:由该对象的
sendETH方法实现。
这个类库可能后续不局限于这些功能,甚至可以升级增加「添加流动性」等功能。我们来继续剖析代码:
e = e || exchange
if (e.GetName() !== 'Web3') {
panic("only support Web3 exchange")
}
let self = {
tokenInfo: {},
walletAddress: e.IO("address"),
pool: {}
}
// register
e.IO("abi", ContractV3Factory, ABI_Factory)
e.IO("abi", ContractV3SwapRouterV2, ABI_Route)
构造函数$.NewUniswapV3只有一个参数e,这个e表示交易所对象(在FMZ上的交易所配置)。因为在FMZ上策略可以设计成多exchange的,所以这里如果传入某个具体的exchange就表示创建出来的Uniswap V3对象是操作该交易所对象的。如果不传参数e,默认操作第一个添加的交易所对象。
配置节点服务地址、私钥(可以本地部署私钥,本地部署只用配置路径),就创建了一个交易所对象。在实盘的时候就可以添加在策略上,这个对象体现在策略代码中就是exchange也即exchanges[0],如果添加第二个就是exchanges[1],添加第三个为exchanges[2],...
截图中我配置的节点地址:https://mainnet.infura.io/v3/xxx 是用的infura的节点,这个可以个人申请,每个账号都有各自的具体地址,xxx这里是掩码,每个账户的xxx部分各不相同。
继续说代码,该构造函数开始判断交易所对象是不是Web3的,不是Web3就报错。然后创建了一个变量self,这个self就是构造函数最终返回的对象,后续构造函数给这个对象增加了各种函数,并且实现具体功能。self变量有3个属性:
- tokenInfo :记录注册在该对象的token代币信息,代币信息包括代币地址、代币精度、代币名称。
- walletAddress:当前交易所对象的钱包地址。
- pool:注册在该对象的兑换池信息,主要是兑换池名称和兑换池地址。
紧接着用到了我们上篇学习到的概念:
e.IO("abi", ContractV3Factory, ABI_Factory) // 注册Uniswap V3 工厂合约的ABI
e.IO("abi", ContractV3SwapRouterV2, ABI_Route) // 注册Uniswap Router V2 路由的ABI
为什么要注册这些接口信息呢?
因为后续要实现的一些功能需要调用这些智能合约的接口。接下来就是该构造函数给self对象增加各种方法了,self对象的方法除了上述提到的:兑换token、查询余额等,还有一些属于这个self对象的工具函数,我们这里先剖析这些工具函数。
self对象的工具函数
1、self.addToken = function(name, address)
观察这个函数的具体代码可知,这个函数功能是给当前对象self中记录token信息的成员tokenInfo增加(换种说法就是:注册)一个token(代币)信息。因为token(代币)的精度数据在后续计算时要经常用到,所以在这个函数增加(注册)token信息的时候,调用了let ret = e.IO("api", address, "decimals")函数,通过FMZ封装的exchange.IO函数(前边我们提过了e就是传入的exchange对象),调用token代币合约的"decimals"方法,从而获取token的精度。
所以self.tokenInfo是一个字典结构,每个键名是token名字,键值是这个token的信息,包括:地址、名称、精度。大概是这个样子:
{
"ETH": {name: "ETH", decimals: 18, address: "0x..."},
"USDT": {name: "USDT", decimals: 6, address: "0x..."},
...
}
2、self.waitMined = function(tx)
该函数用于等待以太坊上智能合约的执行结果,从这个函数的实现代码上可以看到,这个函数一直在循环调用let info = e.IO("api", "eth", "eth_getTransactionReceipt", tx),通过调用以太坊的RPC方法eth_getTransactionReceipt,来查询交易哈希返回交易的收据,参数tx即为交易哈希。
eth_getTransactionReceipt等相关资料可以查看:https://ethereum.org/zh/developers/docs/apis/json-rpc/#eth_gettransactionreceipt
可能有同学会问:为什么要用这个函数?
答:在执行一些操作时,例如token兑换,是需要等待结果的。
接下来我们再来看$.NewUniswapV3函数创建的对象self的其它主要功能实现,我们从最简单的讲起。
主要功能函数
1、self.getETHBalance = function(address)
查询token(代币)余额是有区分的,分为查询ETH(以太坊)余额,查询其它ERC20的token余额。self对象的getETHBalance函数是用来查询ETH余额的,当传入了具体钱包地址参数address时,查询这个地址的ETH余额。如果没有传address参数则查询self.walletAddress地址的ETH余额(即当前exchange上配置的钱包)。
这些通过调用以太坊的RPC方法eth_getBalance实现。
2、self.balanceOf = function(token, address)
查询除了ETH以外的token余额,需要传入参数token即代币名称,例如USDT。传入所要查询的钱包地址address,没有传入address则查询self.walletAddress地址的余额。观察这个函数实现的代码可知,需要事先通过self.addToken函数注册过的token才可以查询,因为调用token的合约的balanceOf方法时,需要用到token(代币)的精度信息和地址。
3、self.sendETH = function(to, amount, options)
该函数的功能为ETH转账,向某个钱包地址(使用to参数设置)转账一定数量的ETH(使用amount参数设置),可以再设置一个options参数(数据结构:{gasPrice: 111, gasLimit: 111, nonce: 111})用来指定gasLimit/gasPrice/nonce,不传入options参数即使用系统默认的设置。
gasLimit/gasPrice影响在以太坊上执行操作时消耗的ETH(以太坊上的一些操作是消耗gas的,即消耗一定ETH代币)。
4、self.getPrice = function(pair, fee)
该函数用来获取在Uniswap上某个交易对的价格,通过函数实现代码可以看到,在函数开始执行时会首先将交易对pair解析,得到baseCurrency和quoteCurrency。例如交易对是ETH_USDT,则会拆分为ETH和USDT。然后查询self.tokenInfo中是否有这两种token(代币)的信息,没有则报错。
在Uniswap上的兑换池地址是由参与的两种token(代币)地址、Fee(费率标准)计算构成的,所以在查询self.pool(self.pool之前我们提过,可以看下)中记录的池地址时,如果没有查询到就使用两种token的地址、Fee去计算池地址。所以一个交易对可能有多个池,因为Fee可能不同。
查询、计算兑换池的地址通过调用Uniswap V3的工厂合约的getPool方法获得(所以要在开始注册工厂合约的ABI)。
拿到这个交易对的池地址,就可以注册池合约的ABI。这样才能调用这个池(智能合约)的slot0方法,从而拿到价格数据。当然这个方法返回的数据并不是人类可读的价格,而是一个和价格相关的数据结构,需要进一步处理获取可读的价格,这个时候就使用到我们上篇中提到的computePoolPrice函数。
5、self.swapToken = function(tokenIn, amountInDecimal, tokenOut, options)
该函数的功能是token兑换,参数tokenIn是兑换时支付的代币名称,参数tokenOut是兑换时获得的代币名称,参数amountInDecimal是兑换数量(人类可读的数量),参数options和我们之前提到的一样,可以设置兑换时的gas消耗、nonce等。
函数执行时首先还是先通过self.tokenInfo变量中拿到token(代币)的信息,兑换也是很多细节的,首先如果参与兑换的token中,支付的token不是ETH则需要先给路由(负责兑换的智能合约)授权。授权之前要先查询是否已经有足够的授权额度。
let allowanceAmount = e.IO("api", tokenInInfo.address, "allowance", self.walletAddress, ContractV3SwapRouterV2);
使用token合约allowance方法查询已经授权的额度。通过比较已经授权的额度和当前兑换的数量,如果授权的额度足够兑换,则不用再授权。如果额度不够则执行授权处理。
这里授权也有一个细节,如果授权的token是USDT,则需要先重置授权数量为0,再进行授权。授权使用token合约的approve方法。注意approve授权方法是一个消耗gas的方法,会消耗一定量的ETH。所以需要使用self.waitMined函数等待处理结果。
为了避免频繁授权,支付不必要的ETH,这个授权操作一次性授权最大值。
let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
有足够的兑换额度,就可以进行兑换了。但是这里也有细节,如果参与兑换的token中,兑换后获取的token是ETH则需要修改接收地址:
recipientAddress = '0x0000000000000000000000000000000000000002'
具体原因比较复杂,这里不在赘述,可以参看:
ADDRESS_THIS https://degencode.substack.com/p/uniswapv3-multicall
https://etherscan.io/address/0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45#code
接着使用FMZ平台封装的打包函数e.IO("encode", ...,打包对于路由(智能合约)的swapExactTokensForTokens方法调用,如果兑换后获取的token是ETH则还需要增加一步WETH9的解包操作:
data.push(e.IO("encode", ContractV3SwapRouterV2, "unwrapWETH9(uint256,address)", 1, self.walletAddress))
因为参与兑换的是WETH,这个是ETH的一个包装后的代币。换成真正的ETH需要解包操作,把这个解包操作也打包之后就可以调用路由(智能合约)的multicall方法执行这一系列操作了。这里还有一个细节要额外注意,如果参与兑换的交易对,支付的token是ETH时是需要在如下步骤设置转账的ETH数量,如果不是ETH则设置0。
let tx = e.IO("api", ContractV3SwapRouterV2, "multicall(uint256,bytes[])", (tokenInInfo.name == 'ETH' ? amountIn : 0), (new Date().getTime() / 1000) + 3600, data, options || {})
这个设定体现在这里:(tokenInInfo.name == 'ETH' ? amountIn : 0)。小编就因为之前没弄清楚,没有在tokenIn不等于ETH代币时设置0,导致误转了ETH。所以编写转账代码时要格外小心。
Part4:Uniswap V3操作对象如何使用
这个模板中的代码在功能实现上实际不到200行,以下这一段实际是使用演示。
$.testUniswap = function() {
let ex = $.NewUniswapV3()
Log("walletAddress: ", ex.walletAddress)
let tokenAddressMap = {
"ETH": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH
"USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"1INCH": "0x111111111117dC0aa78b770fA6A738034120C302",
}
for (let name in tokenAddressMap) {
ex.addToken(name, tokenAddressMap[name])
}
Log(ex.getPrice('ETH_USDT'))
Log(ex.getPrice('1INCH_USDT'))
// swap 0.01 ETH to USDT
Log(ex.swapToken('ETH', 0.01, 'USDT'))
let usdtBalance = ex.balanceOf('USDT')
Log("balance of USDT", usdtBalance)
// swap reverse
Log(ex.swapToken('USDT', usdtBalance, 'ETH'))
Log("balance of ETH", ex.getETHBalance())
// Log(ex.sendETH('0x11111', 0.02))
}
$.testUniswap = function()这个函数仅仅只是一个演示,没有实际用途请勿调用。我们通过这个函数来看如何使用这个模板类库操作Uniswap V3的功能。
代码中首先执行let ex = $.NewUniswapV3()构造了一个Uniswap V3操作对象,如果想拿到当前exchange绑定的钱包地址,可以使用ex.walletAddress获取。接着代码中使用ex.addToken注册了三种token,分别是ETH、USDT、1INCH。
打印某个交易对的价格(token需要先注册):
Log(ex.getPrice('ETH_USDT'))
Log(ex.getPrice('1INCH_USDT'))
getPrice函数如果没有设置Fee,则使用的是默认3000这个费率,转换为可读数值是0.3%。
如果要把0.01个ETH兑换成USDT,然后查询余额,接着再兑换回来,则使用代码:
Log(ex.swapToken('ETH', 0.01, 'USDT'))
let usdtBalance = ex.balanceOf('USDT') // 查询兑换后的USDT余额
Log("balance of USDT", usdtBalance)
Log(ex.swapToken('USDT', usdtBalance, 'ETH')) // 把USDT兑换为ETH
Log("balance of ETH", ex.getETHBalance()) // 查询ETH余额
// Log(ex.sendETH('0x11111', 0.02)) // ETH转账操作
使用测试网 Goerli 测试
1、配置测试网交易所对象
注意设置节点就需要设置为测试网Goerli的节点。
2、编写一个策略,在测试网Goerli上测试。
function main() {
let ex = $.NewUniswapV3()
Log("walletAddress: ", ex.walletAddress)
let tokenAddressMap = {
"ETH" : "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", // WETH
"LINK" : "0x326C977E6efc84E512bB9C30f76E30c160eD06FB",
"UNI" : "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
}
for (let name in tokenAddressMap) {
ex.addToken(name, tokenAddressMap[name])
}
// ETH_UNI 、 UNI_ETH
Log("ETH_UNI:", ex.getPrice('ETH_UNI'))
Log("UNI_ETH:", ex.getPrice('UNI_ETH'))
// ETH
Log("balance of ETH", ex.getETHBalance())
// UNI
let uniBalance = ex.balanceOf('UNI')
Log("balance of UNI", uniBalance)
// LINK
let linkBalance = ex.balanceOf('LINK')
Log("balance of LINK", linkBalance)
// swap 0.001 ETH to UNI
Log(ex.swapToken('ETH', 0.001, 'UNI'))
// swap UNI to LINK
Log(ex.swapToken('UNI', ex.balanceOf('UNI') - uniBalance, 'LINK'))
}
测试代码中我们测试了打印钱包地址、注册token信息、打印资产余额、进行了一次连续兑换ETH -> UNI -> LINK。需要注意这里注册的代币地址是以太坊测试网Goerli上的,所以同样名称的代币地址是不同的,至于测试币可以用这个测试网的水龙头申请测试代币,具体可以谷歌查询。
注意要勾选「Uniswap V3 交易类库」模板才能使用$.NewUniswapV3()函数,如果你的FMZ账号还没有这个模板,可以点击这里获取。
策略运行日志:
Uniswap页面上显示的资产数值
在链上对应也能查询到这些操作:
ETH兑换为UNI执行了一次,对UNI授权执行一次,把UNI兑换为LINK执行了一次。
END
这个类库还有很多功能可以扩展,甚至可以扩展打包多次兑换实现tokenA -> tokenB -> tokenC路径兑换。具体可以根据需求优化、扩展,此类库代码主要提供教学为主。
更新
升级了swapToken函数,支持了tokenA -> tokenB -> tokenC ... -> tokenD连续兑换的功能。可以查看FMZ上策略广场公开的该模板最新的代码。
- 1








