优雅简洁!在FMZ上用200行代码接入了Uniswap V3

Author: 小小梦, Created: 2023-01-30 15:38:02, Updated: 2023-09-18 19:39:40

}

    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上模板类库的接口函数(何为模板类库可以[查阅](https://www.fmz.com/api#%E6%A8%A1%E6%9D%BF%E7%B1%BB%E5%BA%93)),简单说就是```$.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]```,...

![img](/upload/asset/166c103f51e64febcaa0.png) 

截图中我配置的节点地址: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的节点。

![img](/upload/asset/16bab01a6aa0a466294e.png) 

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上的,所以同样名称的代币地址是不同的,至于测试币可以用这个测试网的水龙头申请测试代币,具体可以谷歌查询。

![img](/upload/asset/16c0a485aa2171a99a60.png) 

注意要勾选「Uniswap V3 交易类库」模板才能使用```$.NewUniswapV3()```函数,如果你的FMZ账号还没有这个模板,可以点击[这里获取](https://www.fmz.com/strategy/397260)。

策略运行日志:

![img](/upload/asset/15ff29aa0cd1d83e091a.png) 

![img](/upload/asset/1695370b522d48baeda2.png) 

Uniswap页面上显示的资产数值

> https://app.uniswap.org/

![img](/upload/asset/166b2137def9f179438e.png) 

在链上对应也能查询到这些操作:

> https://goerli.etherscan.io/

![img](/upload/asset/1704feaa0f8d0892fb4a.png) 

ETH兑换为UNI执行了一次,对UNI授权执行一次,把UNI兑换为LINK执行了一次。

### END 

这个类库还有很多功能可以扩展,甚至可以扩展打包多次兑换实现```tokenA -> tokenB -> tokenC```路径兑换。具体可以根据需求优化、扩展,此类库代码主要提供教学为主。

### 更新

升级了```swapToken```函数,支持了```tokenA -> tokenB -> tokenC ... -> tokenD```连续兑换的功能。可以查看FMZ上策略广场公开的该模板最新的代码。

Related

More

15559953001 梦总,有python版本的吗

浮生若佛 学习中,mark

小小梦 可以回头移植一个,调用都一样。