白天搞智能合约,晚上撸前端代码,只要咖啡还续着,凌晨照样在线!会说 Solidity、PHP、Node.js,还有 Vue 和 HTML 的“方言”,代码不怕我不写,就怕写完跑太快。Bug?那都是小场面,一出手就搞定。我的宗旨是——交付准时,调试无敌,客户满意才是真理!

5. 合约交互

创建可写Contract变量

声明可写的Contract变量的规则:

const contract = new ethers.Contract(address, abi, signer)

其中address为合约地址,abi是合约的abi接口,signerwallet对象。注意,这里你需要提供signer,而在声明可读合约时你只需要提供provider

你也可以利用下面的方法,将可读合约转换为可写合约:

const contract2 = contract.connect(signer)

合约交互

我们在第三讲介绍了读取合约信息。它不需要gas。这里我们介绍写入合约信息,你需要构建交易,并且支付gas。该交易将由整个网络上的每个节点以及矿工验证,并改变区块链状态。

你可以用下面的方法进行合约交互:

// 发送交易
const tx = await contract.METHOD_NAME(args [, overrides])
// 等待链上确认交易
await tx.wait()

其中METHOD_NAME为调用的函数名,args为函数参数,[, overrides]是可以选择传入的数据,包括:

  • gasPrice:gas价格
  • gasLimit:gas上限
  • value:调用时传入的ether(单位是wei)
  • nonce:nonce

注意: 此方法不能获取合约运行的返回值,如有需要,要使用Solidity事件记录,然后利用交易收据去查询。

例子:与测试网WETH合约交互

WETH (Wrapped ETH)是ETH的带包装版本,将以太坊原生代币用智能合约包装成了符合ERC20的代币。

  1. 创建providerwallet变量。

    import { ethers } from "ethers";
    
    // 利用Alchemy的rpc节点连接以太坊网络
    const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l';
    const provider = new ethers.JsonRpcProvider(ALCHEMY_GOERLI_URL);
    
    // 利用私钥和provider创建wallet对象
    const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b'
    const wallet = new ethers.Wallet(privateKey, provider)
    
  2. 创建可写WETH合约变量,我们在ABI中加入了4个我们要调用的函数:

    • balanceOf(address):查询地址的WETH余额。
    • deposit():将转入合约的ETH转为WETH
    • transfer(address, uint256):转账。
    • withdraw(uint256):取款。
    // WETH的ABI
    const abiWETH = [
        "function balanceOf(address) public view returns(uint)",
        "function deposit() public payable",
        "function transfer(address, uint) public returns (bool)",
        "function withdraw(uint) public",
    ];
    // WETH合约地址(Goerli测试网)
    const addressWETH = '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6' // WETH Contract
    
    // 声明可写合约
    const contractWETH = new ethers.Contract(addressWETH, abiWETH, wallet)
    // 也可以声明一个只读合约,再用connect(wallet)函数转换成可写合约。
    // const contractWETH = new ethers.Contract(addressWETH, abiWETH, provider)
    // contractWETH.connect(wallet)
    
  3. 读取账户WETH余额,可以看到余额为1.001997

    const address = await wallet.getAddress()
    // 读取WETH合约的链上信息(WETH abi)
    console.log("\n1. 读取WETH余额")
    const balanceWETH = await contractWETH.balanceOf(address)
    console.log(`存款前WETH持仓: ${ethers.formatEther(balanceWETH)}\n`)
    

    读取WETH余额

  4. 调用WETH合约的deposit()函数,将0.001 ETH转换为0.001 WETH,打印交易详情和余额。deposit()函数没有参数,可以看到余额变为1.002997

        console.log("\n2. 调用desposit()函数,存入0.001 ETH")
        // 发起交易
        const tx = await contractWETH.deposit({value: ethers.parseEther("0.001")})
        // 等待交易上链
        await tx.wait()
        console.log(`交易详情:`)
        console.log(tx)
        const balanceWETH_deposit = await contractWETH.balanceOf(address)
        console.log(`存款后WETH持仓: ${ethers.formatEther(balanceWETH_deposit)}\n`)
    

    调用deposit

  5. 调用WETH合约的transfer()函数,给Vitalik转账0.001 WETH,并打印余额。可以看到余额变为1.001997

        console.log("\n3. 调用transfer()函数,给vitalik转账0.001 WETH")
        // 发起交易
        const tx2 = await contractWETH.transfer("vitalik.eth", ethers.parseEther("0.001"))
        // 等待交易上链
        await tx2.wait()
        const balanceWETH_transfer = await contractWETH.balanceOf(address)
        console.log(`转账后WETH持仓: ${ethers.formatEther(balanceWETH_transfer)}\n`)
    

    给Vitalik转WETH

注意:观察deposit()函数和balanceOf()函数,为什么他们的返回值不一样?为什么前者返回一堆数据,而后者只返回确定的值?这是因为对于钱包的余额,它是一个只读操作,读到什么就是什么。而对于一次函数的调用,并不知道数据何时上链,所以只会返回这次交易的信息。总结来说,就是对于非pure/view函数的调用,会返回交易的信息。如果想知道函数执行过程中合约变量的变化,可以在合约中使用emit输出事件,并在返回的transaction信息中读取事件信息来获取相应的值。

完整代码

// 声明只可写合约的规则:
// const contract = new ethers.Contract(address, abi, signer);
// 参数分别为合约地址`address`,合约ABI `abi`,Signer变量`signer`

import { ethers } from "ethers";
// playcode免费版不能安装ethers,用这条命令,需要从网络上import包(把上面这行注释掉)
// import { ethers } from "https://cdn-cors.ethers.io/lib/ethers-5.6.9.esm.min.js";

// 利用Alchemy的rpc节点连接以太坊网络

const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l';
const provider = new ethers.JsonRpcProvider(ALCHEMY_GOERLI_URL);

// 利用私钥和provider创建wallet对象
const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b'
const wallet = new ethers.Wallet(privateKey, provider)

// WETH的ABI
const abiWETH = [
    "function balanceOf(address) public view returns(uint)",
    "function deposit() public payable",
    "function transfer(address, uint) public returns (bool)",
    "function withdraw(uint) public",
];
// WETH合约地址(Goerli测试网)
const addressWETH = '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6'
// WETH Contract

// 声明可写合约
const contractWETH = new ethers.Contract(addressWETH, abiWETH, wallet)
// 也可以声明一个只读合约,再用connect(wallet)函数转换成可写合约。
// const contractWETH = new ethers.Contract(addressWETH, abiWETH, provider)
// contractWETH.connect(wallet)

const main = async () => {

    const address = await wallet.getAddress()
    // 1. 读取WETH合约的链上信息(WETH abi)
    console.log("\n1. 读取WETH余额")
    const balanceWETH = await contractWETH.balanceOf(address)
    console.log(`存款前WETH持仓: ${ethers.formatEther(balanceWETH)}\n`)
    //读取钱包内ETH余额
    const balanceETH = await provider.getBalance(wallet)

    // 如果钱包ETH足够
    if(ethers.formatEther(balanceETH) > 0.0015){

        // 2. 调用deposit()函数,将0.001 ETH转为WETH
        console.log("\n2. 调用deposit()函数,存入0.001 ETH")
        // 发起交易
        const tx = await contractWETH.deposit({value: ethers.parseEther("0.001")})
        // 等待交易上链
        await tx.wait()
        console.log(`交易详情:`)
        console.log(tx)
        const balanceWETH_deposit = await contractWETH.balanceOf(address)
        console.log(`存款后WETH持仓: ${ethers.formatEther(balanceWETH_deposit)}\n`)

        // 3. 调用transfer()函数,将0.001 WETH转账给 vitalik
        console.log("\n3. 调用transfer()函数,给vitalik转账0.001 WETH")
        // 发起交易
        const tx2 = await contractWETH.transfer("vitalik.eth", ethers.parseEther("0.001"))
        // 等待交易上链
        await tx2.wait()
        const balanceWETH_transfer = await contractWETH.balanceOf(address)
        console.log(`转账后WETH持仓: ${ethers.formatEther(balanceWETH_transfer)}\n`)

    }else{
        // 如果ETH不足
        console.log("ETH不足,去水龙头领一些Goerli ETH")
        console.log("1. chainlink水龙头: https://faucets.chain.link/goerli")
        console.log("2. paradigm水龙头: https://faucet.paradigm.xyz/")
    }
}

main()