使用FMZ轻松入门基于以太坊的web3开发

Author: 小小梦, Created: 2023-03-28 13:32:48, Updated: 2023-09-18 20:27:13

[TOC]

img

EtherEaseWithFMZ教程

使用FMZ轻松入门基于以太坊的web3开发

以太坊是一个基于区块链技术的智能合约平台,它提供了一种去中心化的方式来编写和部署智能合约。智能合约是一种特殊的计算机程序,它可以在区块链上自动执行,并且可以在不需要信任第三方的情况下实现各种业务逻辑。

发明者量化交易平台(FMZ.COM)提供了易于使用的 API,使得开发人员可以更轻松地与以太坊区块链及其生态系统进行交互。实现接入去中心化交易所(DEX),获取链上数据,发送交易等功能。

本篇教程中的例子使用JavaScript语言编写,测试环境使用以太坊主网Goerli测试网。在FMZ平台的API文档中也可以查看教程中使用到的API接口以及相关描述、代码范例。


FMZ使用入门

在学习使用FMZ量化交易平台之前,我们需要熟悉几个基本概念:

1、FMZ量化交易平台架构

在FMZ量化交易平台官网(https://www.fmz.com)注册、登录后就可以使用平台的各种功能了。FMZ网站是整个系统的管理端,用户编写的程序实际上是运行在托管者上。托管者这个软件程序可以部署在各种设备上,例如服务器、电脑等。当用户在FMZ网站上编写好程序创建运行实例时,FMZ平台就会和托管者通信,在托管者上启动一个程序实例。

img

2、托管者

如果想要运行程序实例,就必须要部署一个托管者,托管者部署也非常简单,在平台上有部署教程。也可以使用FMZ上提供的「一键部署托管者」使用FMZ代为租用的服务器自动部署。

  • 在个人设备上部署托管者

    可以在服务器、个人电脑等设备上部署运行托管者程序,只要确保网络正常即可(需要能访问到对应的目标,例如某交易所接口,节点地址等)。部署的主要步骤为:

    1、登录或者打开要部署托管者程序的设备,例如登录到服务器或者打开电脑进入操作系统。 2、下载对应版本的托管者程序(根据设备操作系统而定),下载页面:https://www.fmz.com/m/add-node img 3、下载的是一个压缩包,需要解压缩。 4、运行这个托管者程序,托管者程序是一个名为robot的可执行文件。配置托管者通信地址,这个通信地址是每个FMZ账号唯一的,登录FMZ后在https://www.fmz.com/m/add-node页面可以查看到自己的地址(即./robot -s node.fmz.com/xxxxx这一串地址,这里xxxxx位置的内容,每个FMZ账号显示都不同)。最后还需要输入FMZ账号的密码,配置好这些之后运行托管者程序即可。

  • 使用FMZ平台的「一键部署托管者」功能

    在FMZ平台上添加托管者页面,地址:https://www.fmz.com/m/add-node

    img

3、调试工具

FMZ量化交易平台提供了一个免费的调试工具,支持JavaScriptTypeScript,页面是:https://www.fmz.com/m/debug ,因为创建实例运行是计费的。初学期间可以使用这个调试工具进行测试、学习。调试工具除了限制运行时间最长为3分钟,其它方面和创建实例运行没有区别。

使用TypeScript语言时,需要在代码第一行书写// @ts-check用于切换到TypeScript模式,不切换默认是JavaScript语言。

4、交易所

在FMZ上「交易所」是一个泛指概念,对于CEX交易所来说就是指某个具体的交易所账户配置。对于web3来说,这个交易所指的是一个配置信息,包含节点地址、私钥配置。

在登录FMZ平台的状态下,在https://www.fmz.com/m/add-platform页面,可以配置交易所信息,这里交易所是泛指概念。

img

选择Web3,配置RPC节点地址,配置私钥即可,可以点击右下角「敏感信息使用独立私钥加密保存」查看安全机制。

节点可以用自建的节点,也可以用节点服务商提供的节点。节点服务商有很多,例如:Infura。注册后,可以查看到自己账号的节点地址。主网、测试网都有,比较方便,把这个节点地址配置在上图中Rpc Address的控件中。标签可以自己取名,用来区分配置的交易所对象。

img

图中https://mainnet.infura.io/v3/xxxxxxxxxxxxx就是私有的Infura的ETH主网RPC节点地址。


使用FMZ和以太坊交互

在部署好托管者程序、配置好交易所对象的前提下,就可以使用FMZ.COM的「调试工具」进行测试了。调用以太坊RPC方法和以太坊交互,除了本章节列举介绍的几个RPC方法,其它RPC方法可以查询资料了解,例如https://www.quicknode.com/docs

我们列举几个简单例子,从基础开始讲起。对于各种语言、工具来说都有访问web3的方式,如图:

img

在FMZ上对于RPC方法调用也做了封装,这些功能封装在了FMZ的API函数exchange.IO中。调用方式为exchange.IO("api", "eth", ...)。第一个参数固定传入"api",第二个参数固定传入"eth",其它参数根据具体调用的RPC方法而定。

输出信息我们就使用FMZ平台的Log函数,Log函数可以传入多个参数,然后输出在FMZ平台「调试工具」或者「实盘」页面中的日志区域,「调试工具」页面将是我们测试的主要工具。

eth_getBalance

以太坊的eth_getBalance方法用来查询某个以太坊上地址的ETH余额,这个方法需要传入2个参数。

  • 需要查询的地址。
  • 标签,一般使用"latest"。

我们来查询一下以太坊创始人V神的ETH钱包地址,已知地址为:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

function main() {
    let ethBalance = exchange.IO("api", "eth", "eth_getBalance", "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "latest")
    Log("ethBalance:", ethBalance)
}

已经部署托管者(图中:linux/amd64 …)并且配置好交易所对象(图中:Web3 test),在调试工具测试代码:

img

点击「执行」按钮,运行这段代码,显示结果:

ethBalance: 0x117296558f185bbc4c6

Log函数打印出来的ethBalance变量值为:0x117296558f185bbc4c6,是字符串类型。是十六进制值的 ETH余额,以wei为单位,1e18 wei为1ETH。所以还需要转换,才能变成可读的十进制ETH余额。

ethBalance转换为可读的数据:

function main() {
    let ethBalance = exchange.IO("api", "eth", "eth_getBalance", "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "latest")
    Log("ethBalance:", ethBalance)
    
    // 将ethBalance转换为可读的数据
    let vitalikEthBalance = parseInt(ethBalance.substring(2), 16) / 1e18
    Log("vitalikEthBalance:", vitalikEthBalance)
}

img

在上https://etherscan.io/查询:

img

不过这样处理由于语言本身精度问题会有偏差,所以FMZ平台内置了两个函数用于处理数据:

  • BigInt :把十六进制字符串转换成BigInt对象。
  • BigDecimal :把数值类型对象转换成可以运算的BigDecimal对象。

再次调整代码:

function main() {
    let ethBalance = exchange.IO("api", "eth", "eth_getBalance", "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "latest")

    // ETH的精度单位为1e18
    let ethDecimal = 18
    Log("vitalikEthBalance:", Number((BigDecimal(BigInt(ethBalance)) / BigDecimal(Math.pow(10, ethDecimal))).toString()))
}

vitalikEthBalance: 5149.6244846875215

eth_chainId

eth_chainIdnet_version用途差不多,所以放在一起测试。这两个函数都返回当前RPC节点接入的区块链的Id,区别是net_version返回十进制的Id,eth_chainId返回十六进制的Id。

链Id对应的网络名称

1 - ethereum mainnet
2 - morden testnet (deprecated)
3 - ropsten testnet
4 - rinkeby testnet
5 - goerli testnet
11155111 - sepolia testnet
10 - optimism mainnet
69 - optimism kovan testnet
42 - kovan testnet
137 - matic/polygon mainnet
80001 - matic/polygon mumbai testnet
250 - fantom mainnet
100 - xdai mainnet
56 - bsc mainnet

img

使用配置好的以太坊测试网goerli节点测试:

function main() {
    let netVersionId = exchange.IO("api", "eth", "net_version")
    let ethChainId = exchange.IO("api", "eth", "eth_chainId")

    Log("netVersionId:", netVersionId)
    Log("ethChainId:", ethChainId, " ,转换:", parseInt(ethChainId.substring(2), 16))
}

img

eth_gasPrice

调用eth_gasPrice方法,查询当前链上的gas price

function toAmount(s, decimals) {
    return Number((BigDecimal(BigInt(s)) / BigDecimal(Math.pow(10, decimals))).toString())
}

function main() {
    let gasPrice = exchange.IO("api", "eth", "eth_gasPrice")
    Log("gasPrice:", gasPrice, " ,转换:", toAmount(gasPrice, 0))
}

这里我们把十六进制字符串转换为可读数值的操作写成一个函数:toAmount。另外需要注意的是gasPrice的单位是wei,所以把形参decimals对应的实参传数值0即可。

eth_blockNumbe

eth_blockNumbe用来查询区块高度。

function toAmount(s, decimals) {
    return Number((BigDecimal(BigInt(s)) / BigDecimal(Math.pow(10, decimals))).toString())
}

function main() {
    let blockNumber = exchange.IO("api", "eth", "eth_blockNumber")
    Log(toAmount(blockNumber, 0))
}

调试工具中运行:

img

https://etherscan.io/上查询:

img

eth_getBlockByNumber

查询区块信息。

function main() {
    let blockNumber = exchange.IO("api", "eth", "eth_blockNumber")    
    Log(blockNumber)
    let blockMsg = exchange.IO("api", "eth", "eth_getBlockByNumber", blockNumber, true)
    Log(typeof(blockMsg), blockMsg)
    
    // 由于Log输出的内容过多,会自动截断,所以遍历返回的区块信息各个字段,逐个打印
    for (let key in blockMsg) {
        Log("key:", key, ", val:", blockMsg[key])
    }
}

「调试工具」中执行可以获取以下信息:

img


读取合约信息

以太坊上运行着数量众多的智能合约应用,ENS便是其中之一。ENS,即以太坊域名服务(Ethereum Name Service),是基于以太坊区块链的去中心化域名解析服务。 还记得教程中我们查询以太坊创始人V神的钱包余额的例子吗?V神的一个钱包地址为:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045。那么我们是如何知道这个地址的呢?其实可以通过ENS智能合约,使用一个直观的名称vitalik.eth(vitalik即V神的名字)来进行查询。

本章节以下内容使用以太坊主网环境,根据ENS文档可知需要对查询的以太坊域名进行Hashing Names,使用以下代码对vitalik.eth名称进行处理。

function nameHash(name) {
    if (name == "") {
        return "0000000000000000000000000000000000000000000000000000000000000000"
    } else {
        let arr = name.split(".")
        let label = arr[0]
        
        arr.shift()
        let remainder = arr.join(".")
        return Encode("sha3.keccak256", "hex", "hex", nameHash(remainder) + Encode("sha3.keccak256", "raw", "hex", label))
    }
}

以上代码例子中我们又看到了一个陌生的函数Encode,这个函数是FMZ平台的API函数,是专门用来在FMZ平台上进行编码操作的,该函数支持多种编码方式,支持多种哈希算法。

Encode(algo, inputFormat, outputFormat, data, keyFormat, key string)

根据ENS文档中的描述,使用sha3.keccak256算法对数据进行处理。

调用nameHash函数,例如:Log(nameHash("vitalik.eth")),可得:ee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835,需要再加上"0x"前缀。0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835作为ENS智能合约的resolver方法的参数。

let ensNode = "0x" + nameHash("vitalik.eth")    // 准备好调用resolver方法的参数ensNode

查询ENS文档可知,ENS智能合约应用的合约地址为:0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e。在调用智能合约的resolver方法前,我们还需要准备好合约的ABI

注册ABI

学习到这里不禁会问,什么是智能合约的ABI呢?

ABI,即应用程序二进制接口(Application Binary Interface),是智能合约与外部世界进行通信的接口标准。
智能合约的 ABI 定义了合约的函数接口、参数类型、返回值等信息,以及调用合约的方式和参数传递方式等规范。

智能合约的 ABI 通常以 JSON 格式存储,包含以下信息:

合约的函数接口:函数名、参数列表、返回值等信息。
函数参数类型:如 uint256、bool、string 等。
函数的输入参数和输出参数的编码方式:智能合约使用一种称为 Solidity ABI 的编码方式来编码函数的输入参数和输出参数,
以便与以太坊网络进行交互。
在以太坊网络中,使用智能合约的 ABI 来调用合约的函数。当需要调用合约函数时,需要提供函数名和函数参数,以及将函数参数按照 ABI 编码方式编码后的字节码。
以太坊节点会将这些信息打包成一笔交易,并将交易发送到以太坊网络中执行。

智能合约的 ABI 在 Solidity 语言中可以通过 interface 关键字来定义。以太坊开发工具如 Remix IDE、Truffle 等也提供了 ABI 编辑和生成工具,
使得开发者可以方便地创建和使用智能合约的 ABI。

摘出ENS的ABI中的resolver方法的部分,也可以使用完整的ABI,可以在https://etherscan.io/上查询到合约的ABI,或者通过其它途径获取ABI(例如:相关项目文档)。

img

let abiENS_resolver = `[{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"resolver","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]`

这里我们又要学到在FMZ平台上一个新的调用方法,exchange.IO("abi", address, abiContent),使用该方法注册ABI,address参数是智能合约的地址,abiContent参数是对应的智能合约ABI(字符串)。

let abiENS_resolver = `[{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"resolver","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]`
exchange.IO("abi", "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", abiENS_resolver)  // 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e 是在以太坊主网上部署的ENS智能合约的地址

调用智能合约的方法

接下来就可以调用ENS智能合约的resolver方法了,该方法返回ENS: Public Resolver合约的地址。

img

let resolverAddress = exchange.IO("api", "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", "resolver", ensNode)

使用ENS: Public Resolver合约的addr方法获取V神的钱包地址。要调用ENS: Public Resolver合约依然需要先注册ABI。这个智能合约的ABI信息依然可以从https://etherscan.io/获取。

let abiENSPublicResolver = `[{"inputs":[{"internalType":"contract ENS","name":"_ens","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"uint256","name":"contentType","type":"uint256"}],"name":"ABIChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"a","type":"address"}],"name":"AddrChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"coinType","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"newAddress","type":"bytes"}],"name":"AddressChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"target","type":"address"},{"indexed":false,"internalType":"bool","name":"isAuthorised","type":"bool"}],"name":"AuthorisationChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"hash","type":"bytes"}],"name":"ContenthashChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"name","type":"bytes"},{"indexed":false,"internalType":"uint16","name":"resource","type":"uint16"},{"indexed":false,"internalType":"bytes","name":"record","type":"bytes"}],"name":"DNSRecordChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"name","type":"bytes"},{"indexed":false,"internalType":"uint16","name":"resource","type":"uint16"}],"name":"DNSRecordDeleted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"DNSZoneCleared","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"bytes4","name":"interfaceID","type":"bytes4"},{"indexed":false,"internalType":"address","name":"implementer","type":"address"}],"name":"InterfaceChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"string","name":"name","type":"string"}],"name":"NameChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes32","name":"x","type":"bytes32"},{"indexed":false,"internalType":"bytes32","name":"y","type":"bytes32"}],"name":"PubkeyChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"string","name":"indexedKey","type":"string"},{"indexed":false,"internalType":"string","name":"key","type":"string"}],"name":"TextChanged","type":"event"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"contentTypes","type":"uint256"}],"name":"ABI","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"addr","outputs":[{"internalType":"address payable","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"coinType","type":"uint256"}],"name":"addr","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"authorisations","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"clearDNSZone","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"contenthash","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"name","type":"bytes32"},{"internalType":"uint16","name":"resource","type":"uint16"}],"name":"dnsRecord","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"name","type":"bytes32"}],"name":"hasDNSRecords","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"interfaceImplementer","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"pubkey","outputs":[{"internalType":"bytes32","name":"x","type":"bytes32"},{"internalType":"bytes32","name":"y","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"contentType","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"setABI","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"coinType","type":"uint256"},{"internalType":"bytes","name":"a","type":"bytes"}],"name":"setAddr","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"a","type":"address"}],"name":"setAddr","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"isAuthorised","type":"bool"}],"name":"setAuthorisation","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes","name":"hash","type":"bytes"}],"name":"setContenthash","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"setDNSRecords","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes4","name":"interfaceID","type":"bytes4"},{"internalType":"address","name":"implementer","type":"address"}],"name":"setInterface","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"name","type":"string"}],"name":"setName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"x","type":"bytes32"},{"internalType":"bytes32","name":"y","type":"bytes32"}],"name":"setPubkey","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"value","type":"string"}],"name":"setText","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"key","type":"string"}],"name":"text","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]`
exchange.IO("abi", resolverAddress, abiENSPublicResolver)

img

最后调用ENS: Public Resolver合约的addr方法,参数依然为ensNode

let vitalikAddress = exchange.IO("api", resolverAddress, "addr", ensNode)
Log("vitalikAddress:", vitalikAddress)

Log函数输出:

img

vitalikAddress: 0xd8da6bf26964af9d7eed9e03e53415d37aa96045

调用ENS的完整代码

function nameHash(name) {
    if (name == "") {
        return "0000000000000000000000000000000000000000000000000000000000000000"
    } else {
        let arr = name.split(".")
        let label = arr[0]
        
        arr.shift()
        let remainder = arr.join(".")
        return Encode("sha3.keccak256", "hex", "hex", nameHash(remainder) + Encode("sha3.keccak256", "raw", "hex", label))
    }
}

function main() {
    // 计算名称
    let ensNode = "0x" + nameHash("vitalik.eth")

    // 注册ENS合约
    let abiENS_resolver = `[{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"resolver","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]`
    exchange.IO("abi", "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", abiENS_resolver)
    let resolverAddress = exchange.IO("api", "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", "resolver", ensNode)
    
    // 注册ENS Public Resolver合约
    let abiENSPublicResolver = `[{"inputs":[{"internalType":"contract ENS","name":"_ens","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"uint256","name":"contentType","type":"uint256"}],"name":"ABIChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"a","type":"address"}],"name":"AddrChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"coinType","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"newAddress","type":"bytes"}],"name":"AddressChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"target","type":"address"},{"indexed":false,"internalType":"bool","name":"isAuthorised","type":"bool"}],"name":"AuthorisationChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"hash","type":"bytes"}],"name":"ContenthashChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"name","type":"bytes"},{"indexed":false,"internalType":"uint16","name":"resource","type":"uint16"},{"indexed":false,"internalType":"bytes","name":"record","type":"bytes"}],"name":"DNSRecordChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"name","type":"bytes"},{"indexed":false,"internalType":"uint16","name":"resource","type":"uint16"}],"name":"DNSRecordDeleted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"DNSZoneCleared","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"bytes4","name":"interfaceID","type":"bytes4"},{"indexed":false,"internalType":"address","name":"implementer","type":"address"}],"name":"InterfaceChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"string","name":"name","type":"string"}],"name":"NameChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"bytes32","name":"x","type":"bytes32"},{"indexed":false,"internalType":"bytes32","name":"y","type":"bytes32"}],"name":"PubkeyChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"string","name":"indexedKey","type":"string"},{"indexed":false,"internalType":"string","name":"key","type":"string"}],"name":"TextChanged","type":"event"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"contentTypes","type":"uint256"}],"name":"ABI","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"addr","outputs":[{"internalType":"address payable","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"coinType","type":"uint256"}],"name":"addr","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"authorisations","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"clearDNSZone","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"contenthash","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"name","type":"bytes32"},{"internalType":"uint16","name":"resource","type":"uint16"}],"name":"dnsRecord","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"name","type":"bytes32"}],"name":"hasDNSRecords","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"interfaceImplementer","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"pubkey","outputs":[{"internalType":"bytes32","name":"x","type":"bytes32"},{"internalType":"bytes32","name":"y","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"contentType","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"setABI","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"coinType","type":"uint256"},{"internalType":"bytes","name":"a","type":"bytes"}],"name":"setAddr","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"a","type":"address"}],"name":"setAddr","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"isAuthorised","type":"bool"}],"name":"setAuthorisation","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes","name":"hash","type":"bytes"}],"name":"setContenthash","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"setDNSRecords","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes4","name":"interfaceID","type":"bytes4"},{"internalType":"address","name":"implementer","type":"address"}],"name":"setInterface","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"name","type":"string"}],"name":"setName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"x","type":"bytes32"},{"internalType":"bytes32","name":"y","type":"bytes32"}],"name":"setPubkey","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"value","type":"string"}],"name":"setText","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"key","type":"string"}],"name":"text","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]`
    exchange.IO("abi", resolverAddress, abiENSPublicResolver)
    let vitalikAddress = exchange.IO("api", resolverAddress, "addr", ensNode)
    Log("vitalikAddress:", vitalikAddress)
}

发送ETH

在之前的课程章节中我们已经学过了如何配置私钥,对于配置完成的交易所对象我们如何知道这个私钥对应的钱包地址呢?在FMZ上可以使用exchange.IO("address")函数获取配置的私钥对应的钱包地址。

由于本章节以下内容使用Goerli测试网环境,所以节点我使用的是:https://goerli.infura.io/v3/*******,infura给每个注册用户分配的节点地址都不同,这里*******隐藏了具体内容。

function main() {
    let walletAddress = exchange.IO("address")
    Log("测试网 goerli 钱包地址:", walletAddress)
}

img

知道了自己的钱包地址之后,就可以使用以太坊的RPC方法eth_getTransactionCount查询钱包地址的交易计数。在以太坊中这个计数十分常用,其实就是转账操作时需要传入的nonce参数,在以太坊中,nonce是用来确保每个交易都是唯一的数字。它是一个递增的数字,每次发送一个新的交易时就会自动增加。因此,当您向智能合约发送交易时,需要提供一个nonce,以确保交易是唯一的并且顺序正确。在一些资料、文档中我们就可以查询到:

https://goethereumbook.org/en/

img

这里Go语言的以太坊库中的PendingNonceAt函数实际就是调用的eth_getTransactionCount方法。在之前的课程中我们也学过了如何调用RPC方法,我们这里再次使用exchange.IO("api", "eth", ...)函数。

function toAmount(s, decimals) {
    return Number((BigDecimal(BigInt(s)) / BigDecimal(Math.pow(10, decimals))).toString())
}

function main() {
    let walletAddress = exchange.IO("address")
    Log("测试网 goerli 钱包地址:", walletAddress)

    /**
    * eth_getTransactionCount
    * @param address - string - The address from which the transaction count to be checked.
    * @param blockNumber - string - The block number as a string in hexadecimal format or tags.
    * @returns The integer of the number of transactions sent from an address encoded as hexadecimal.
    */
    let nonce = exchange.IO("api", "eth", "eth_getTransactionCount", walletAddress, "pending")
    Log("钱包地址:", walletAddress, "当前的 nonce:", nonce, ",转换为10进制:", toAmount(nonce, 0))
}

在讲解转账操作之前,我们简单了解一些概念,在以太坊上转账时会消耗一定的ETH代币(作为gas费用)。这个gas费用具体由两个参数决定:

  • gasPrice

    然而,以太坊网络上的燃气费用始终根据市场需求和用户愿意支付的费用而波动,因此在代码中写入固定的燃气费用有时不是理想的选择。可以使用我们之前学过的eth_gasPrice方法,该方法可以获取平均燃气价格。

  • gasLimit

    一个标准的以太币转账的燃气限制是21000个单位。

了解了noncegasPricegasLimit这些概念,就可以测试转账了。在FMZ上封装非常简单易用的转账函数。

exchange.IO("api", "eth", "send", toAddress, toAmount)

作为转账使用时,exchange.IO的第三个参数固定写为"send",toAddress参数为转账时接收ETH的地址,toAmount为转账的ETH数量。

noncegasPricegasLimit这些参数在FMZ上都可以使用系统默认自动获取的值。也可以指定:

exchange.IO("api", "eth", "send", toAddress, toAmount, {gasPrice: 5000000000, gasLimit: 21000, nonce: 100})

接下来我们在测试网 goerli 上向某个地址转账一定的ETH:

function toInnerAmount(s, decimals) {
    return (BigDecimal(s)*BigDecimal(Math.pow(10, decimals))).toFixed(0)
}

function main() {
    let walletAddress = exchange.IO("address")
    Log("测试网 goerli 钱包地址:", walletAddress)

    let ret = exchange.IO("api", "eth", "send", "0x4D75a08E870674E68cAE611f329A27f446A66813", toInnerAmount(0.01, 18))
    return ret    // 返回Transaction Hash : 0xa6f9f51b00d8ae850b0f204380b59da98f4bbce34b813577d3d948f61de4734e
}

因为以太坊转账数量的单位是wei,需要使用一个自定义函数toInnerAmount处理为以wei为单位的数值。

https://etherscan.io/上查询Transaction Hash:0xa6f9f51b00d8ae850b0f204380b59da98f4bbce34b813577d3d948f61de4734e

img

也可以编写代码查询转账哈希0xa6f9f51b00d8ae850b0f204380b59da98f4bbce34b813577d3d948f61de4734e,使用eth_getTransactionReceipt方法进行查询。

function main() {
    let transHash = "0xa6f9f51b00d8ae850b0f204380b59da98f4bbce34b813577d3d948f61de4734e"
    let info = exchange.IO("api", "eth", "eth_getTransactionReceipt", transHash)
    return info
}

查询结果:

{
	"cumulativeGasUsed": "0x200850",
	"effectiveGasPrice": "0x1748774421",
	"transactionHash": "0xa6f9f51b00d8ae850b0f204380b59da98f4bbce34b813577d3d948f61de4734e",
	"type": "0x0",
	"blockHash": "0x6bdde8b0f0453ecd24eecf7c634d65306f05511e0e8f09f9ed3f59eee2d06ac7",
	"contractAddress": null,
	"blockNumber": "0x868a50",
	"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
	"gasUsed": "0x5208",
	"to": "0x4d75a08e870674e68cae611f329a27f446a66813",
	"status": "0x1",
	"transactionIndex": "0x23",
	"from": "0x6b3f11d807809b0b1e5e3243df04a280d9f94bf4",
	"logs": []
}

每个字段对应的描述:

blockHash - 该交易所在区块的哈希值
blockNumber - 以十六进制编码的该交易所在区块的块号
contractAddress - 如果是合约创建,该合约的地址;否则为null
cumulativeGasUsed - 该交易在区块中执行时使用的总燃气量
effectiveGasPrice - 每单位燃气的总基础费用加小费
from - 发送者的地址
gasUsed - 该特定交易使用的燃气量
logs - 生成该交易的日志对象数组
  address - 生成该日志的地址
  topics - 0到4个32字节索引日志参数的数据数组。在Solidity中,第一个主题是事件签名的哈希值(例如Deposit(address,bytes32,uint256)),除非你使用匿名说明符声明该事件
  data - 日志的32字节非索引参数
  blockNumber - 该日志所在区块的块号
  transactionHash - 该日志创建时的交易哈希值。如果该日志处于待定状态,则为null
  transactionIndex - 该日志创建时的交易索引位置。如果该日志处于待定状态,则为null
  blockHash - 该日志所在区块的哈希值
  logIndex - 该日志在区块中的索引位置,以十六进制编码的整数。如果该日志处于待定状态,则为null
  removed - 如果该日志已被删除,则为true,由于链重组而被删除;如果是有效的日志,则为false
logsBloom - 用于检索相关日志的布隆过滤器
status - 以十六进制编码的值,它要么是1(成功),要么是0(失败)
to - 接收者的地址。如果是合约创建交易,则为null
transactionHash - 该交易的哈希值
transactionIndex - 以十六进制编码的该交易在区块中的索引位置
type - 值的类型

调用以太坊智能合约

我们在读取合约信息这一章节中通过一个完整的例子,调用以太坊上部署的ENS合约的方法获取了V神的钱包地址。这些方法属于Read方法,调用这些方法是不需要gas的(还记得我们之前讲的gas是什么吗?)。本章节我们将调用一些以太坊上的智能合约的Write方法并且支付gas。这些操作将由整个网络上的每个节点以及矿工验证,并改变区块链状态。

ERC20

对于ERC20合约来说(ERC20代币合约),FMZ平台把ERC20合约的ABI列为常用的ABI直接内置在系统中,省去了注册ABI这一步。对于ABI我们在之前的教程中也有所了解,我们调用ENS合约方法时先注册了ENS合约的ABI。

为了更加清楚的了解ABI,在使用前可以先查看一下,以下是ERC20合约的ABI:

[{"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"}]

本章节以下内容使用Goerli测试网环境。

balanceOf

接下来我们重新再练习一次如何调用合约的Read方法读取合约信息,调用ERC20合约的balanceOf方法查询代币余额,balanceOf方法只有一个参数,但是没有命名,通过类型可以看到是一个地址(即查询的代币地址)。由于返回的数据并不是以一个代币为单位的,所以还需要代币的精度数据来换算一下,代币的精度可以用ERC20合约的decimals方法获取。我们使用以太坊测试网goerli进行测试,注意在不同的链上代币合约地址可能也不同。

function toAmount(s, decimals) {
    return Number((BigDecimal(BigInt(s)) / BigDecimal(Math.pow(10, decimals))).toString())
}

function main() {
    let walletAddress = exchange.IO("address")
    
    // goerli WETH address 
    let wethAddress = "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"
    // goerli LINK address 
    let linkAddress = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB"

    // 由于是ERC20合约,FMZ已经内置ABI注册,所以这里不用注册ERC20 ABI
    let wethDecimals = exchange.IO("api", wethAddress, "decimals")
    let linkDecimals = exchange.IO("api", linkAddress, "decimals")

    let wethBalance = exchange.IO("api", wethAddress, "balanceOf", walletAddress)
    let linkBalance = exchange.IO("api", linkAddre

More