एफएमजेड पर 200 लाइन कोड के साथ यूनिस्वैप वी 3 तक पहुँचें

लेखक:छोटे सपने, बनाया गयाः 2023-01-30 15:38:02, अद्यतन किया गयाः 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 कारखाने अनुबंध के लिए पंजीकृत एबीआईe.IO("abi", ContractV3SwapRouterV2, ABI_Route) // Uniswap Router V2 रूटर के लिए पंजीकृत एबीआई


> 为什么要注册这些接口信息呢?

因为后续要实现的一些功能需要调用这些智能合约的接口。接下来就是该构造函数给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: {नामः ETH, दशमलव संख्याः 18, पताः 0x...}, USDT: {नामः USDT, दशमलवः 6, पताः 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", tokenInfo.address, allowance, self.walletAddress, ContractV3SwapRouterV2);


使用token合约allowance方法查询已经授权的额度。通过比较已经授权的额度和当前兑换的数量,如果授权的额度足够兑换,则不用再授权。如果额度不够则执行授权处理。

这里授权也有一个细节,如果授权的token是USDT,则需要先重置授权数量为0,再进行授权。授权使用token合约的approve方法。注意approve授权方法是一个消耗gas的方法,会消耗一定量的ETH。所以需要使用self.waitMined函数等待处理结果。

为了避免频繁授权,支付不必要的ETH,这个授权操作一次性授权最大值。

txApprove = अनुमति देंe.IO("api", tokenInInfo.address, approve, ContractV3SwapRouterV2, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);


有足够的兑换额度,就可以进行兑换了。但是这里也有细节,如果参与兑换的token中,兑换后获取的token是ETH则需要修改接收地址:

प्राप्तकर्ता पता = 0x0000000000000000000000000000000000000002


具体原因比较复杂,这里不在赘述,可以参看:

> ADDRESS_THIS https://degencode.substack.com/p/uniswapv3-multicall
> https://etherscan.io/address/0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45#code

接着使用FMZ平台封装的打包函数```e.IO("encode", ...```,打包对于路由(智能合约)的swapExactTokensForTokens方法调用,如果兑换后获取的token是ETH则还需要增加一步WETH9的解包操作:

डेटा.पुशe.IO("कोड", ContractV3SwapRouterV2, unwrapWETH9 ((uint256, पता) , 1, self.walletAddress))


因为参与兑换的是WETH,这个是ETH的一个包装后的代币。换成真正的ETH需要解包操作,把这个解包操作也打包之后就可以调用路由(智能合约)的multicall方法执行这一系列操作了。这里还有一个细节要额外注意,如果参与兑换的交易对,支付的token是ETH时是需要在如下步骤设置转账的ETH数量,如果不是ETH则设置0。

चलो tx =e.IO("api", अनुबंधV3SwapRouterV2, multicall(uint256,बाइट्स[]) , (tokenInInfo.name== ETH? राशिIn: 0), (नया दिनांक().getTime() / 1000) + 3600, डेटा, विकल्प {})


这个设定体现在这里:```(tokenInInfo.name == 'ETH' ? amountIn : 0)```。小编就因为之前没弄清楚,没有在tokenIn不等于ETH代币时设置0,导致误转了ETH。所以编写转账代码时要格外小心。

### Part4:Uniswap V3操作对象如何使用

这个模板中的代码在功能实现上实际不到200行,以下这一段实际是使用演示。

$.testUniswap = फ़ंक्शन (() { let ex = $.NewUniswapV3 (() लॉग (("walletAddress: ", ex.walletAddress) चलो tokenAddressMap = { ETH: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2, // WETH USDT: 0xdac17f958d2ee523a2206206994597c13d831ec7, 1INCH: 0x111111111117dC0aa78b770fA6A738034120C302, } के लिए ( tokenAddressMap में अक्षर नाम) { ex.addToken(नाम, टोकनAddressMap[नाम]) }

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需要先注册):

लॉग ((ex.getPrice(ETH_USDT)) लॉग ((ex.getPrice ((1INCH_USDT))


getPrice函数如果没有设置Fee,则使用的是默认3000这个费率,转换为可读数值是0.3%。

如果要把0.01个ETH兑换成USDT,然后查询余额,接着再兑换回来,则使用代码:

लॉग ((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()) // ईटीएच संतुलन का पता लगाएं

// Log ((ex.sendETH ((0x11111, 0.02)) // ETH स्थानांतरण ऑपरेशन


### 使用测试网 Goerli 测试

1、配置测试网交易所对象

注意设置节点就需要设置为测试网Goerli的节点。

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

2、编写一个策略,在测试网Goerli上测试。

मुख्य फंक्शन let ex = $.NewUniswapV3 (() लॉग (("walletAddress: ", ex.walletAddress) चलो tokenAddressMap = { ETH : 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6, // के साथ LINK : 0x326C977E6efc84E512bB9C30f76E30c160eD06FB, UNI : 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984, } के लिए ( tokenAddressMap में अक्षर नाम) { ex.addToken(नाम, टोकनAddressMap[नाम]) }

// 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上策略广场公开的该模板最新的代码。

संबंधित

अधिक

15559953001क्या कोई पायथन संस्करण है?

उभरता हुआ रोफ़रसीखने में, मार्क

छोटे सपनेएक को पीछे की ओर स्थानांतरित किया जा सकता है, और एक ही कॉल किया जा सकता है।