“fromBlock” : fromBlock, “toBlock” : toBlock, “address” : self.contractAddress, “topics” : [self.eventHash] } // Log(“fromBlockNumber:”, self.fromBlockNumber, “, currBlockNumber:”, currBlockNumber, “#FF0000”)
var logs = exchange.IO("api", "eth", "eth_getLogs", params)
if (!logs) {
return
}
for (var i = 0; i < logs.length; i++) {
if (toAmount(logs[i].blockNumber, 0) > self.latestBlockNumber) {
/* TODO: test
if (self.isFirst) {
self.firstBlockNumber = toAmount(logs[i].blockNumber, 0)
Log("firstBlockNumber:", self.firstBlockNumber)
self.isFirst = false
}
*/
callBack(logs[i])
}
}
self.latestBlockNumber = currBlockNumber
self.fromBlockNumber = self.latestBlockNumber - 1
}
self.latestBlockNumber = self.getBlockNumber()
self.fromBlockNumber = self.latestBlockNumber - 1
return self
}
var listener = null function main() { var event = “Transfer(address,address,uint256)” var contractAddress = “0xdac17f958d2ee523a2206206994597c13d831ec7” var decimals = exchange.IO("api", contractAddress, “decimals”) Log(exchange.IO("api", contractAddress, “name”), " decimals:", decimals)
listener = addEventListener(contractAddress, event, function(log) {
var fromAddress = "0x" + exchange.IO("encodePacked", "address", log.topics[1])
var toAddress = "0x" + exchange.IO("encodePacked", "address", log.topics[2])
Log("Transfer:", fromAddress, "->", toAddress, ", value:", toAmount(log.data, decimals), ", blockNumber:", toAmount(log.blockNumber, 0))
/* TODO: test
arrLog.push(log)
*/
})
while (true) {
listener.run()
Sleep(5000)
}
}
/* TODO: test var arrLog = [] function onexit() { Log(“结束运行,验证记录”) var firstBlockNumber = listener.firstBlockNumber var endBlockNumber = listener.latestBlockNumber
Log("getLogs, from:", firstBlockNumber, " -> to:", endBlockNumber)
var fromBlock = "0x" + (firstBlockNumber).toString(16)
var toBlock = "0x" + (endBlockNumber).toString(16)
var params = {
"fromBlock" : fromBlock,
"toBlock" : toBlock,
"topics" : ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
"address" : "0xdac17f958d2ee523a2206206994597c13d831ec7"
}
var logs = exchange.IO("api", "eth", "eth_getLogs", params)
Log("arrLog:", arrLog.length)
Log("logs:", logs.length)
if (arrLog.length != logs.length) {
Log("长度不等!")
return
}
for (var i = 0; i < arrLog.length; i++) {
Log("判断blockNumber:", logs[i].blockNumber == arrLog[i].blockNumber, ",判断from:", logs[i].topics[1] == arrLog[i].topics[1],
"判断to:", logs[i].topics[2] == arrLog[i].topics[2])
}
} */
实盘运行:
![img](/upload/asset/16ffd65adc050d33056d.png)
对于执行结果,代码中也编写了验证部分(TODO: test)。经过简单验证可以看到持续监控USDT合约的```Transfer```事件并且记录数据,用这个数据和一次性获取的事件数据对比可以观察出数据一致:
![img](/upload/asset/16e07390a11a606276b1.png)
### 事件过滤
在上一节课程「监听合约事件」的基础上,我们拓展一下,在监听的过程中增加过滤器,监听指定地址的转入转出。当智能合约创建日志时(即释放事件),日志数据中```topics```最多包含4条信息。所以我们设计一个过滤规则,以```[[A1, A2, ...An], null, [C1], D]```为例子。
1、```[A1, A2, ...An]```对应```topics[0]```位置的数据。
2、```null```对应```topics[1]```位置的数据。
3、```[C1]```对应```topics[2]```位置的数据。
4、```D```对应```topics[3]```位置的数据。
- 如果条件结构中的元素设置```null```表示不被过滤,例如```null```对应```topics[1]```,任何值都匹配。
- 如果条件结构中的元素设置单个值表示该位置必须匹配,例如```[C1]```对应```topics[2]```或者```D```对应```topics[3]```,不匹配的日志被过滤。
- 如果条件结构中的元素是一个数组,表示数组中的元素至少有一个要匹配,例如```[A1, A2, ...An]```对应```topics[0]```,```[A1, A2, ...An]```中有任意一个和```topics[0]```匹配,则日志不会被过滤。
**监听交易所的USDT转账**
监控从币安交易所转出、转入币安交易所```USDT```的交易:
```javascript
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)
}
function addEventListener(contractAddress, event, callBack) {
var self = {}
self.eventHash = "0x" + Encode("keccak256", "string", "hex", event)
self.contractAddress = contractAddress
self.latestBlockNumber = 0
self.fromBlockNumber = 0
self.firstBlockNumber = 0
self.filters = []
self.setFilter = function(filterCondition) {
if (filterCondition.length > 4) {
throw "filterCondition error"
}
self.filters.push(filterCondition)
Log("设置过滤条件:", filterCondition)
}
self.getTokenBalanceOfWallet = function(walletAddress, tokenAddress, tokenDecimals) {
var balance = exchange.IO("api", tokenAddress, "balanceOf", walletAddress)
if (balance) {
return toAmount(balance, tokenDecimals)
}
return null
}
self.getBlockNumber = function() {
var maxTry = 10
for (var i = 0; i < maxTry; i++) {
var ret = exchange.IO("api", "eth", "eth_blockNumber")
if (ret) {
return toAmount(ret, 0)
}
Sleep(5000)
}
throw "getBlockNumber failed"
}
self.run = function() {
var currBlockNumber = self.getBlockNumber()
var fromBlock = "0x" + self.fromBlockNumber.toString(16)
var toBlock = "0x" + currBlockNumber.toString(16)
var params = {
"fromBlock" : fromBlock,
"toBlock" : toBlock,
"address" : self.contractAddress,
"topics" : [self.eventHash]
}
var logs = exchange.IO("api", "eth", "eth_getLogs", params)
if (!logs) {
return
}
for (var i = 0; i < logs.length; i++) {
if (toAmount(logs[i].blockNumber, 0) > self.latestBlockNumber) {
// 检查过滤条件,设置了过滤条件则执行判断
if (self.filters.length != 0) {
// 初始过滤标记
var isFilter = true
// 遍历过滤条件设置
for (var j = 0; j < self.filters.length; j++) {
// 取一个过滤设置,例如:[[A1, A2, ...An], null, [C1], D]
var cond = self.filters[j]
// 遍历这个过滤设置
var final = true
for (var topicsIndex = 0; topicsIndex < cond.length; topicsIndex++) {
// 拿到这个过滤设置中的某一个条件,如果是第一个条件:即要和topics[0]对比的数据
var condValue = cond[topicsIndex]
// 日志中的数据
if (topicsIndex > logs[i].topics.length - 1) {
continue
}
var topicsEleValue = logs[i].topics[topicsIndex]
// 如果是Transfer事件,需要处理from和to
if (logs[i].topics[0] == "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") {
if (topicsIndex == 1 || topicsIndex == 2) {
topicsEleValue = "0x" + exchange.IO("encodePacked", "address", topicsEleValue)
}
}
// 如果condValue类型是数组,表示该位置的对比条件有多个,多个条件对比是逻辑或关系
if (Array.isArray(condValue) && condValue.length > 1) {
// 判断 condValue[0] == topicsEleValue || condValue[1] == topicsEleValue
final = final && condValue.some(element => element === topicsEleValue)
}else if (condValue === null) {
final = final && true
} else {
final = final && (condValue === topicsEleValue)
}
}
if (final) {
isFilter = false
}
}
if (isFilter) {
continue
}
}
callBack(logs[i])
}
}
self.latestBlockNumber = currBlockNumber
self.fromBlockNumber = self.latestBlockNumber - 1
}
self.latestBlockNumber = self.getBlockNumber()
self.fromBlockNumber = self.latestBlockNumber - 1
return self
}
var listener = null
function main() {
// 初始清理日志
LogReset(1)
LogProfitReset()
var event = "Transfer(address,address,uint256)" // 监听事件
var contractAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7" // USDT合约地址
var decimals = exchange.IO("api", contractAddress, "decimals") // 获取USDT token的精度信息
var accountBinanceAddress = "0x28C6c06298d514Db089934071355E5743bf21d60" // Binance 热钱包地址
accountBinanceAddress = accountBinanceAddress.toLowerCase() // 地址处理为小写
Log(exchange.IO("api", contractAddress, "name"), " decimals:", decimals)
// 创建监听对象
listener = addEventListener(contractAddress, event, function(log) {
var fromAddress = "0x" + exchange.IO("encodePacked", "address", log.topics[1])
var toAddress = "0x" + exchange.IO("encodePacked", "address", log.topics[2])
if (fromAddress == accountBinanceAddress) {
Log("币安转出 - ", " Transfer:", fromAddress, "->", toAddress, ", value:", toAmount(log.data, decimals), ", blockNumber:", toAmount(log.blockNumber, 0), "#CD32CD")
} else if (toAddress == accountBinanceAddress) {
Log("转入币安 - ", " Transfer:", fromAddress, "->", toAddress, ", value:", toAmount(log.data, decimals), ", blockNumber:", toAmount(log.blockNumber, 0), "#FF0000")
}
})
// 设置事件过滤
listener.setFilter([null, accountBinanceAddress, null]) // Binance -> USDT
listener.setFilter([null, null, accountBinanceAddress]) // USDT -> Binance
var preBalance = 0
while (true) {
listener.run()
var balance = listener.getTokenBalanceOfWallet(accountBinanceAddress, contractAddress, decimals)
if (balance) {
var direction = ""
if (preBalance != 0 && preBalance > balance) {
direction = " ↓ " + (preBalance - balance) + "#CD32CD"
} else if (preBalance != 0 && preBalance < balance) {
direction = " ↑ " + (balance - preBalance) + "#FF0000"
}
Log("币安钱包地址:", accountBinanceAddress, " 余额:", balance, direction)
LogProfit(balance, "&") // 只画图,不打印日志
preBalance = balance
}
LogStatus(_D(), "币安钱包地址:", accountBinanceAddress, ", 余额:", balance)
Sleep(5000 * 3)
}
}
以上代码在实盘运行:
这一节课程,我们介绍了如何设计事件过滤器。并且用它监听了与币安交易所热钱包相关的USDT
交易。你可以修改、扩展这个范例程序,用来监听任何你感兴趣的事件,看看smart money
做了哪些新交易,NFT
大佬冲了哪些新项目等等。
以太坊相关的许多计算中,数值都超出了JavaScript
语言的最大安全整数。因此在发明者量化交易平台上需要一些方法来处理大数值,这些方法在之前的课程中我们也具体用到过,没有详细讲解。那么本章节就详细讨论一下这方面的内容。
打印JavaScript
语言中定义的最大安全整数:
function main() {
Log("Number.MAX_SAFE_INTEGER:", Number.MAX_SAFE_INTEGER)
}
运行结果:
Number.MAX_SAFE_INTEGER: 9007199254740991
在以太坊中定义的最小单位是1wei
,定义1Gwei
等于1000000000 wei
。1Gwei
在以太坊相关计算中其实并不算一个很大的数,有些数据比它大得多。那么这些值超大的数据就很容易超过Number.MAX_SAFE_INTEGER: 9007199254740991
。
在发明者量化交易平台,我们使用平台的BigInt
对象来表示这些超大的整数数据。使用构造函数BigInt()
来构造BigInt
对象。可以使用数值、十六进制数值字符串作为参数构造BigInt
对象。使用BigInt
对象的toString()
方法将对象表示的数据输出为字符串形式。
BigInt
对象支持的运算:
+
-
*
/
%
**
参考以下代码例子:
function main() {
// 1Gwei的十进制表示
var oneGwei = 1000000000
// 1Gwei的十进制转换为十六进制表示
var oneGweiForHex = "0x" + oneGwei.toString(16)
Log("oneGwei : ", oneGwei)
Log("oneGweiForHex : ", oneGweiForHex)
// 构造BigInt对象
Log("1Gwei / 1Gwei : ", (BigInt(oneGwei) / BigInt(oneGweiForHex)).toString(10))
Log("1Gwei * 1Gwei : ", (BigInt(oneGwei) * BigInt(oneGweiForHex)).toString(10))
Log("1Gwei - 1Gwei : ", (BigInt(oneGwei) - BigInt(oneGweiForHex)).toString(10))
Log("1Gwei + 1Gwei : ", (BigInt(oneGwei) + BigInt(oneGweiForHex)).toString(10))
Log("(1Gwei + 1) % 1Gwei : ", (BigInt(oneGwei + 1) % BigInt(oneGweiForHex)).toString(10))
Log("1Gwei ** 2 : ", (BigInt(oneGwei) ** BigInt(2)).toString(10))
Log("100 的平方根 : ", (BigInt(100) ** BigFloat(0.5)).toString(10))
Log("Number.MAX_SAFE_INTEGER : ", BigInt(Number.MAX_SAFE_INTEGER).toString(10))
Log("Number.MAX_SAFE_INTEGER * 2 : ", (BigInt(Number.MAX_SAFE_INTEGER) * BigInt("2")).toString(10))
}
调试工具测试:
2023-06-08 11:39:50 信息 Number.MAX_SAFE_INTEGER * 2 : 18014398509481982
2023-06-08 11:39:50 信息 Number.MAX_SAFE_INTEGER : 9007199254740991
2023-06-08 11:39:50 信息 100 的平方根 : 10
2023-06-08 11:39:50 信息 1Gwei ** 2 : 1000000000000000000
2023-06-08 11:39:50 信息 (1Gwei + 1) % 1Gwei : 1
2023-06-08 11:39:50 信息 1Gwei + 1Gwei : 2000000000
2023-06-08 11:39:50 信息 1Gwei - 1Gwei : 0
2023-06-08 11:39:50 信息 1Gwei * 1Gwei : 1000000000000000000
2023-06-08 11:39:50 信息 1Gwei / 1Gwei : 1
2023-06-08 11:39:50 信息 oneGweiForHex : 0x3b9aca00
2023-06-08 11:39:50 信息 oneGwei : 1000000000
BigFloat
对象与BigInt
对象使用类似,用于表示数值较大的浮点数,也支持加减乘除运算。
BigFloat
对象支持toFixed()
方法。
参考以下代码例子:
function main() {
var pi = 3.14
var oneGwei = "1000000000"
var oneGweiForHex = "0x3b9aca00"
Log("pi + oneGwei : ", (BigFloat(pi) + BigFloat(oneGwei)).toFixed(2))
Log("pi - oneGweiForHex : ", (BigFloat(pi) - BigFloat(oneGweiForHex)).toFixed(2))
Log("pi * 2.0 : ", (BigFloat(pi) * BigFloat(2.0)).toFixed(2))
Log("pi / 2.0 : ", (BigFloat(pi) / BigFloat(2.0)).toFixed(2))
}
调试工具测试:
2023-06-08 13:56:44 信息 pi / 2.0 : 1.57
2023-06-08 13:56:44 信息 pi * 2.0 : 6.28
2023-06-08 13:56:44 信息 pi - oneGweiForHex : -999999996.86
2023-06-08 13:56:44 信息 pi + oneGwei : 1000000003.14
BigDecimal
对象兼容了整型数值、浮点型数值,支持使用BigInt
对象、BigFloat
对象初始化,也支持加减乘除运算。
参考以下代码例子:
function main() {
var pi = 3.1415
var oneGwei = 1000000000
var oneGweiForHex = "0x3b9aca00"
Log("pi : ", BigDecimal(pi).toFixed(2))
Log("oneGwei : ", BigDecimal(oneGwei).toString())
Log("oneGweiForHex : ", BigDecimal(BigInt(oneGweiForHex)).toString())
Log("BigInt(oneGwei) : ", BigDecimal(BigInt(oneGwei)).toString())
Log("BigFloat(pi) : ", BigDecimal(BigFloat(pi)).toFixed(4))
Log("oneGwei + pi : ", (BigDecimal(oneGwei) + BigDecimal(pi)).toString())
Log("oneGwei - pi : ", (BigDecimal(oneGwei) - BigDecimal(pi)).toString())
Log("2.0 * pi : ", (BigDecimal(2.0) * BigDecimal(pi)).toString())
Log("pi / pi : ", (BigDecimal(pi) / BigDecimal(pi)).toString())
}
调试工具中运行:
2023-06-08 14:52:53 信息 pi / pi : 1
2023-06-08 14:52:53 信息 2.0 * pi : 6.283
2023-06-08 14:52:53 信息 oneGwei - pi : 999999996.8585
2023-06-08 14:52:53 信息 oneGwei + pi : 1000000003.1415
2023-06-08 14:52:53 信息 BigFloat(pi) : 3.1415
2023-06-08 14:52:53 信息 BigInt(oneGwei) : 1e+9
2023-06-08 14:52:53 信息 oneGweiForHex : 1e+9
2023-06-08 14:52:53 信息 oneGwei : 1e+9
2023-06-08 14:52:53 信息 pi : 3.14
以下这两个函数:toAmount()
、toInnerAmount()
我们在之前的课程中多次使用过,这两个函数主要用于数据精度转换。
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)
}
toAmount()
函数将一个变量s
,按照精度参数decimals
进行(缩小)转换。在web3实际开发中,经常需要处理一些链上的十六进制数据。
在我们之前的课程中就经常遇到,例如智能合约的Transfer(address,address,uint256)
事件中的data
字段数据:
{
"data": "0x00000000000000000000000000000000000000000000000001c1a55000000000",
"topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000006b75d8af000000e20b7a7ddf000ba900b4009a80", "0x000000000000000000000000bcb095c1f9c3dc02e834976706c87dee5d0f1fb6"],
"transactionHash": "0x27f9bf5abe3148169b4b85a83e1de32bd50eb81ecc52e5af006157d93353e4c4",
"transactionIndex": "0x0",
"removed": false,
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"blockHash": "0x847be24a7b159c292bda030a011dfec89487b70e71eed486969b032d6ef04bad",
"blockNumber": "0x109b1cc",
"logIndex": "0x0"
}
处理数据"data": "0x00000000000000000000000000000000000000000000000001c1a55000000000"
,这个时候使用toAmount()
函数。这样的处理设计就可以很好的把data
字段数据转换为可读数值。
function toAmount(s, decimals) {
return Number((BigDecimal(BigInt(s)) / BigDecimal(Math.pow(10, decimals))).toString())
}
function main() {
var data = "0x00000000000000000000000000000000000000000000000001c1a55000000000"
Log(toAmount(data, 18)) // 打印出 0.12656402755905127
}
1个ETH代币我们知道是1e18 wei
,如果我们拿到了一个以wei
为单位的数据126564027559051260
如何换算成ETH代币个数?
使用toAmount(, 18)
函数就可以很简单的转换了。toInnerAmount()
函数则是toAmount()
函数的反向操作(根据精度,放大),使用这两个函数就很方便的可以转换数据了。
需要注意的是JavaScript语言中的整型数值安全范围,即Number.MAX_SAFE_INTEGER
,以下这个例子说明一个数据转换时比较隐蔽的问题:
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)
}
function main() {
var amount = 0.01
var innerAmount = Number(toInnerAmount(amount, 18))
Log("Number.MAX_SAFE_INTEGER:", Number.MAX_SAFE_INTEGER) // 9007199254740991
Log("innerAmount:", innerAmount) // 10000000000000000
Log("typeof(innerAmount):", typeof(innerAmount), ", innerAmount:", innerAmount)
// 十进制数值 10000000000000000 -> 十六进制数值 0x2386f26fc10000
Log("转换", innerAmount, "为十六进制:", innerAmount.toString(16))
Log("转换", BigInt(10000000000000000).toString(10), "为十六进制:", BigInt(10000000000000000).toString(16))
Log("0x" + BigInt(10000000000000000).toString(16), "转换为10进制:", toAmount("0x" + BigInt(10000000000000000).toString(16), 0))
}
可以在调试工具中运行:
2023-06-15 16:21:40 信息 0x2386f26fc10000 转换为10进制: 10000000000000000
2023-06-15 16:21:40 信息 转换 10000000000000000 为十六进制: 2386f26fc10000
2023-06-15 16:21:40 信息 转换 10000000000000000 为十六进制: 10000000000000000
2023-06-15 16:21:40 信息 typeof(innerAmount): number , innerAmount: 10000000000000000
2023-06-15 16:21:40 信息 innerAmount: 10000000000000000
2023-06-15 16:21:40 信息 Number.MAX_SAFE_INTEGER: 9007199254740991
通过观察我们发现:
Log("转换", innerAmount, "为十六进制:", innerAmount.toString(16))
这一行代码对应的日志输出:转换 10000000000000000 为十六进制: 10000000000000000
,竟然没有正确转换。原因自然是因为10000000000000000超出了Number.MAX_SAFE_INTEGER
。
但是当十进制数值在安全范围内,即小于Number.MAX_SAFE_INTEGER
时,toString(16)
函数转换又是正常的,例如:
function main() {
var value = 1000
Log("把value转换为十六进制:", "0x" + value.toString(16)) // 0x3e8
Log("把0x3e8转换为十进制:", Number("0x3e8")) // 1000
}
在区块链领域即使0.01
个ETH换算成以wei
为单位的数值10000000000000000
也会超出Number.MAX_SAFE_INTEGER
,所以对于此类情况比较安全的转换是:BigInt(10000000000000000).toString(16)
。
在以太坊上执行交易、调用智能合约的Write
方法需要消耗一定gas费用,并且有时候有失败的风险。在发送交易、调用之前知道哪些交易可能会失败非常重要。在以太坊上有模拟调用的方法用来测试。
以太坊的RPC方法eth_call
:可以模拟一笔交易,并返回可能的交易结果,但是并不会真正的在区块链上执行这次交易。
eth_call
方法的参数有2个,第一个参数为一个字典结构,transactionObject
:
// transactionObject
{
"from" : ..., // The address from which the transaction is sent
"to" : ..., // The address to which the transaction is addressed
"gas" : ..., // The integer of gas provided for the transaction execution
"gasPrice" : ..., // The integer of gasPrice used for each paid gas encoded as hexadecimal
"value" : ..., // The integer of value sent with this transaction encoded as hexadecimal
"data" : ..., // The hash of the method signature and encoded parameters. For more information, see the Contract ABI description in the Solidity documentation
}
第二个参数为blockNumber
:可以传标签latest
/pending
/earliest
等:
/* blockNumber
The block number in hexadecimal format or the string latest, earliest, pending, safe or
finalized (safe and finalized tags are only supported on Ethereum, Gnosis, Arbitrum,
Arbitrum Nova and Avalanche C-chain), see the default block parameter description in
the official Ethereum documentation
*/
接下来我们以代币DAI
的智能合约方法approve
、transfer
调用为例子进行模拟调用,以下测试环境为以太坊主网。
对于ERC20合约的approve
方法我们都已经很熟悉了,之前的课程中我们也实践过。由于ERC20合约在FMZ平台ABI已经内置,所以不用注册所要模拟调用的智能合约的ABI。
function main() {
var contractAddressUniswapV3SwapRouterV2 = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
var contractAddress_DAI = "0x6b175474e89094c44da98b954eedeac495271d0f"
var wallet = exchange.IO("address")
// encode approve
var data = exchange.IO("encode", contractAddress_DAI, "approve(address,uint256)",
contractAddressUniswapV3SwapRouterV2, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
Log("ERC20 token DAI approve encode, data:", data)
var transactionObject = {
"from" : wallet,
"to" : contractAddress_DAI,
// "gasPrice" : "0x" + parseInt("21270894680").toString(16),
// "gas" : "0x" + parseInt("21000").toString(16),
"data" : "0x" + data,
}
var blockNumber = "latest"
var ret = exchange.IO("api", "eth", "eth_call", transactionObject, blockNumber)
Log("ret:", ret)
}
例子中的代码首先将approve(address,uint256)
方法、参数进行编码,approve
方法的参数值0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
表示授权最大数量。授权给智能合约,地址0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
即Uniswap V3
的路由合约。最后调用以太坊RPC方法eth_call
进行模拟。可以看到transactionObject
参数中的gasPrice
、gas
字段是可以省略的。
调试工具运行,模拟调用approve方法授权成功(并不会真正授权):
2023-06-09 11:58:39 信息 ret: 0x0000000000000000000000000000000000000000000000000000000000000001
2023-06-09 11:58:39 信息 ERC20 token DAI approve encode, data: 095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
也可以模拟一些失败的场景,当我们调整gasPrice
和gas
参数时,如果钱包中的ETH不足以支付gas费用会报错:
insufficient funds
当设置了过低的gas费用,会报错:
intrinsic gas too low: have 21000, want 21944 (supplied gas 21000)
对于ERC20的transfer
方法我们也并不陌生,这个方法可以给某个钱包地址转账ERC20代币,我们就尝试模拟给V神转账1000个DAI。
function toInnerAmount(n, decimals) {
return (BigDecimal(n) * BigDecimal(Math.pow(10, decimals))).toFixed(0)
}
function main() {
var walletVitalik = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
var contractAddress_DAI = "0x6b175474e89094c44da98b954eedeac495271d0f"
var wallet = exchange.IO("address")
// 转账给V神
var decimals_DAI = exchange.IO("api", contractAddress_DAI, "decimals")
var transferAmount = toInnerAmount(1000, decimals_DAI)
Log("转账金额:", 1000, "DAI, 使用 toInnerAmount 转换为:", transferAmount)
// encode transfer
var data = exchange.IO("encode", contractAddress_DAI, "transfer(address,uint256)",
walletVitalik, transferAmount)
var transactionObject = {
"from" : wallet,
"to" : contractAddress_DAI,
"data" : "0x" + data,
}
var blockNumber = "latest"
var ret = exchange.IO("api", "eth", "eth_call", transactionObject, blockNumber)
return ret
}
由于我这个测试钱包没有DAI代币,所以在调试工具中运行的结果意料之中的报错了:
execution reverted: Dai/insufficient-balance
查看V神的这个钱包地址:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
可知,这个钱包是有DAI代币的。那么我们来对调一下模拟调用的转账方向,模拟一下V神给我们转账1000DAI。
修改一下代码,在修改的地方我作了注释:
function toInnerAmount(n, decimals) {
return (BigDecimal(n) * BigDecimal(Math.pow(10, decimals))).toFixed(0)
}
function main() {
var walletVitalik = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
var contractAddress_DAI = "0x6b175474e89094c44da98b954eedeac495271d0f"
var wallet = exchange.IO("address")
var decimals_DAI = exchange.IO("api", contractAddress_DAI, "decimals")
var transferAmount = toInnerAmount(1000, decimals_DAI)
Log("转账金额:", 1000, "DAI, 使用 toInnerAmount 转换为:", transferAmount)
// encode transfer
var data = exchange.IO("encode", contractAddress_DAI, "transfer(address,uint256)",
wallet, transferAmount) // 使用wallet变量作为参数,转账接收方地址改为我自己
var transactionObject = {
"from" : walletVitalik, // 使用walletVitalik变量作为from字段的值,模拟这个调用是由V神钱包地址发出
"to" : contractAddress_DAI,
"data" : "0x" + data,
}
var blockNumber = "latest"
var ret = exchange.IO("api", "eth", "eth_call", transactionObject, blockNumber)
Log(ret)
}
调试工具测试:
2023-06-09 13:34:31 信息 0x0000000000000000000000000000000000000000000000000000000000000001
2023-06-09 13:34:31 信息 转账金额: 1000 DAI, 使用 toInnerAmount 转换为: 1000000000000000000000
使用发明者量化交易平台,可以很方便地模拟交易的结果,并避免发送可能失败的交易造成不必要的gas费用损失。我们使用本章节课程中的例子代码模拟了向V神钱包转账、V神钱包向我们转账的调用。当然,这个eth_call
方法还有更多用处。发挥你的想象力,你会将eth_call
方法用在什么地方呢?
我们知道ETH、BTC之类的token都属于同质化token,你手中的token和我手中的token并没有不同。但是世界上有很多东西是不同质的,例如:房产、古董、虚拟艺术品等,这些无法用同质化代币来抽象代表。因此,就有了ERC721标准来抽象非同质化的对象,就有了NFT以及相关概念。 那么在以太坊上部署的众多智能合约中,我们如何识别哪些智能合约是ERC721标准的智能合约呢?
要识别ERC721,那么首先要了解ERC165标准。
通过ERC165标准,智能合约可以声明它支持的接口供其它合约检查。ERC165接口合约只有一个函数:supportsInterface(bytes4 interfaceId)
,参数interfaceId
即要查询的接口Id。如果合约实现了该接口Id返回布尔值真值,否则返回假值。
下面我们要讲一下这个interfaceId
具体是如何计算、编码的。
ERC165标准给出了一个例子:
pragma solidity ^0.4.20;
interface Solidity101 {
function hello() external pure;
function world(int) external pure;
}
contract Selector {
function calculateSelector() public pure returns (bytes4) {
Solidity101 i;
return i.hello.selector ^ i.world.selector;
}
}
对于接口的函数签名(由函数名和参数类型列表组成)进行异或运算,对于合约只有一个函数的ERC165接口合约来说:
pragma solidity ^0.4.20;
interface ERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
The interface identifier for this interface is 0x01ffc9a7. You can calculate this by running bytes4(keccak256(‘supportsInterface(bytes4)’)); or using the Selector contract above.
直接计算函数签名,取其前4个byte得出的就是interfaceId
。
function main() {
var ret = Encode("keccak256", "string", "hex", "supportsInterface(bytes4)")
Log("supportsInterface(bytes4) interfaceId:", "0x" + ret.slice(0, 8))
}
在调试工具中可以运行测试:
2023-06-13 14:53:35 信息 supportsInterface(bytes4) interfaceId: 0x01ffc9a7
可以看到计算出的结果和ERC165标准文档中描述一致。
接下来我们看ERC721合约标准的接口定义:
interface ERC721 /* is ERC165 */ {
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
如果要判断某个智能合约是否是ERC721合约,我们首先要知道ERC721合约的interfaceId
,然后才能尝试使用supportsInterface(bytes4 interfaceId)
方法判断,之前的课程我们已经熟悉了ERC165标准的一些概念和计算interfaceId
的算法,我们直接写代码计算:
function calcSelector(arrSelector) {
var ret = null
if (Array.isArray(arrSelector)) {
if (arrSelector.length == 1) {
ret = Encode("keccak256", "string", "hex", arrSelector[0])
} else if (arrSelector.length == 0) {
throw "错误:数组中元素个数为0"
} else {
var viewEncodeData = null
for (var i = 0; i < arrSelector.length; i++) {
if (i == 0) {
ret = new Uint8Array(Encode("keccak256", "string", "raw", arrSelector[i]))
} else {
viewData = new Uint8Array(Encode("keccak256", "string", "raw", arrSelector[i]))
if (viewData.length != ret.length) {
throw "错误:TypeArray view长度不同"
}
for (var index = 0; index < ret.length; index++) {
ret[index] ^= viewData[index]
}
}
}
ret = Encode("raw", "raw", "hex", ret.buffer)
}
} else {
throw "错误:参数需要数组类型。"
}
return "0x" + ret.slice(0, 8)
}
function main() {
// supportsInterface(bytes4): 0x01ffc9a7
// var ret = calcSelector(["supportsInterface(bytes4)"])
// ERC721Metadata: 0x5b5e139f
/*
var arrSelector = [
"name()",
"symbol()",
"tokenURI(uint256)"
]
var ret = calcSelector(arrSelector)
*/
// ERC721: 0x80ac58cd
// /*
var arrSelector = [
"balanceOf(address)",
"ownerOf(uint256)",
"safeTransferFrom(address,address,uint256,bytes)",
"safeTransferFrom(address,address,uint256)",
"transferFrom(address,address,uint256)",
"approve(address,uint256)",
"setApprovalForAll(address,bool)",
"getApproved(uint256)",
"isApprovedForAll(address,address)",
]
var ret = calcSelector(arrSelector)
// */
Log(ret)
}
代码中使用了Encode()
函数进行函数签名计算(keccak256
算法),对于以上代码例子中的计算来说,指定Encode()
函数的输出参数为"raw"
,该函数则返回JavaScript
语言的ArrayBuffer
类型。
如果要对两个ArrayBuffer
对象进行^
(异或)运算,需要基于ArrayBuffer
对象创建TypedArray
视图,然后遍历其中的数据,逐个进行异或运算。
调试工具中运行:
2023-06-13 15:04:09 信息 0x80ac58cd
可以看到算出的结果和eip-721中描述的一致。
pragma solidity ^0.4.20;
/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
/// Note: the ERC-165 identifier for this interface is 0x80ac58cd.
interface ERC721 /* is ERC165 */ {
/// @dev This emits when ownership of any NFT changes by any mechanism.
/// This event emits when NFTs are created (`from` == 0) and destroyed
/// (`to` == 0). Exception: during contract creation, any number of NFTs
/// may be created and assigned without emitting Transfer. At the time of
/// any transfer, the approved address for that NFT (if any) is reset to none.
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
...
有了ERC721的接口Id,我们就可以判断某个合约是不是ERC721标准的合约了。我们使用BAYC
做测试,这是一个遵循ERC721的合约,首先我们需要注册ABI,由于我们只调用以下三个方法,可以只注册这三个方法:
具体代码如下:
function main() {
// ERC721的合约地址,这里用的BAYC
var testContractAddress = "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"
var testABI = `[{
"inputs": [{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}],
"name": "supportsInterface",
"outputs": [{
"internalType": "bool",
"name": "",
"type": "bool"
}],
"stateMutability": "view",
"type": "function"
}, {
"inputs": [],
"name": "symbol",
"outputs": [{
"internalType": "string",
"name": "",
"type": "string"
}],
"stateMutability": "view",
"type": "function"
}, {
"inputs": [],
"name": "name",
"outputs": [{
"internalType": "string",
"name": "",
"type": "string"
}],
"stateMutability": "view",
"type": "function"
}]`
// ERC721接口Id,在之前的课程中计算得出
var interfaceId = "0x80ac58cd"
// 注册ABI
exchange.IO("abi", testContractAddress, testABI)
// 调用supportsInterface方法
var isErc721 = exchange.IO("api", testContractAddress, "supportsInterface", interfaceId)
// 输出信息
Log("合约地址:", testContractAddress)
Log("合约名称:", exchange.IO("api", testContractAddress, "name"))
Log("合约代号:", exchange.IO("api", testContractAddress, "symbol"))
Log("合约是否为ERC721标准:", isErc721)
}
在调试工具中可以运行测试:
2023-06-13 16:32:57 信息 合约是否为ERC721标准: true
2023-06-13 16:32:57 信息 合约代号: BAYC
2023-06-13 16:32:57 信息 合约名称: BoredApeYachtClub
2023-06-13 16:32:57 信息 合约地址: 0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d
判断出地址0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d
的合约为ERC721标准。
这一讲,我们介绍了如何判断ERC721合约,那么ERC20这种不支持ERC165的标准的合约,就要用别的方法识别了。你知道怎么检查一个合约是否为ERC20标准吗?
什么是calldata
?通过作者的理解,这里简单通俗的描述就是:
"calldata"是以太坊中对某个函数调用、参数的编码,"calldata"是根据合约的ABI(Application Binary Interface)规范进行编码的。
例如我们可以把之前课程中学习过的ERC20合约的balanceOf
、transfer
方法调用、连同调用时的参数一起编码为一个calldata
。在一些应用场景例如:合约间交互,这种场景下就会用到calldata
,当然还有很多其它应用场景这里就不一一列举了。
如何编码一个智能合约函数调用,得到calldata
?
在发明者量化交易平台可以使用exchange.IO("encode", ...)
对智能合约函数调用进行编码,使用也非常简单。exchange.IO("encode", ...)
函数的第一个参数为固定字符串"encode"
;第二个参数为智能合约的地址;第三个参数为所要编码的智能合约方法名;其余参数传入这个所要编码的智能合约方法的具体参数值。
当我们编码了一个智能合约方法的调用并生成了对应的calldata
数据时,如果这个智能合约方法是一个Write方法(即:写操作),我们需要将生成的calldata
数据作为交易的数据字段,然后使用以太坊的RPC方法eth_sendRawTransaction
发送一个包含该交易的原始数据的请求到以太坊网络。
eth_sendRawTransaction
方法的参数只有一个data
:
data: The signed transaction (typically signed with a library, using your private key)
这个data
参数是一个交易数据经过签名计算后的数据,以太坊的交易数据结构主要有以下字段:
{
"nonce": "0x1", // 交易发送方的账户交易次数
"gasPrice": "0x12a05f200", // 交易的Gas价格
"gasLimit": "0x5208", // 交易的Gas限制
"to": "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", // 目标合约地址或接收方地址
"value": "0x4563918244F40000", // 转账的以太币数量
"data": "0x0123456789ABCDEF", // 要发送给合约的数据
}
如何对一个以太坊交易进行签名呢?
在发明者量化交易平台我们使用Encode()
函数来进行签名计算,具体例子我们写在后续课程「执行Write方法calldata」。
对于Read方法的calldata
执行,我们使用之前学习过的RPC方法:eth_call
来执行,之前我们讲解eth_call
这个以太坊的RPC方法只做了智能合约Write
方法的演示,借此章节使用calldata
的方式演示一下执行智能合约Read方法的调用。我们来使用WETH合约的balanceOf
方法读取当前钱包的WETH代币余额。
我们使用调试工具,在以太坊主网测试:
function toAmount(s, decimals) {
return Number((BigDecimal(BigInt(s)) / BigDecimal(Math.pow(10, decimals))).toString())
}
function main() {
// WETH合约的ABI
var abiWETH = `[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"}]`
// WETH合约地址
var wethAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
// 注册WETH合约的ABI
exchange.IO("abi", wethAddress, abiWETH)
// 当前配置的交易所对象的钱包地址
var walletAddress = exchange.IO("address")
// 编码WETH合约的deposit方法调用
var calldataForDeposit = exchange.IO("encode", wethAddress, "balanceOf(address)", walletAddress)
Log("calldataForDeposit:", "0x" + calldataForDeposit)
// 构造transaction,作为eth_call的第一个参数
var transaction = {
"from" : walletAddress,
"to" : wethAddress,
"data" : "0x" + calldataForDeposit,
}
// eth_call的第二个参数
var blockNumber = "latest"
// 使用eth_call调用
var ret = exchange.IO("api", "eth", "eth_call", transaction, blockNumber)
var wethBalance = exchange.IO("decode", "uint256", ret) // 可以使用exchange.IO("decode", ...) 函数解码
Log("wethBalance:", toAmount(wethBalance, 18)) // 从以wei为单位,换算成WETH个数为单位
}
调试工具中运行:
2023-06-15 11:51:31 信息 wethBalance: 0.015
2023-06-15 11:51:31 信息 calldataForDeposit: 0x70a082310000000000000000000000006b3f11d807809b0b1e5e3243df04a280d9f94bf4
如果智能合约的方法有返回值,可以使用exchange.IO("decode", ...)
函数解码。可以看到通过传递calldata
的方式和直接调用智能合约的balanceOf
方法是一样的,拿到了我这个测试钱包的WETH余额为0.015个WETH。
对于Write方法的calldata执行,就需要使用RPC方法:eth_sendRawTransaction
了。
我们使用调试工具,在以太坊主网测试:
function toAmount(s, decimals) {
return Number((BigDecimal(BigInt(s)) / BigDecimal(Math.pow(10, decimals))).toString())
}
function toInnerAmount(s, decimals) {
return (BigDecimal(s)*BigDecimal(Math.pow(10, decimals))).toFixed(0)
}
function main() {
// WETH合约的ABI
var abiWETH = `[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed