😭呜呜呜呜呜呜呜
手残把.env 提交上去,泄露私钥,导致涨了30%的ETH飞了
最近在忙着学习web3,参加黑客松的比赛,但是还没挣钱,先交了一把学费的感觉真不好😭
进入正题
记录一版 etherjs v6版本api
js
首先新建一个文件夹,然后初始化一下
pnpm init
安装 ethers
pnpm i ethers
Provider
Provider
类主要用于与以太坊区块链进行交互。它可以用来获取链上的数据,例如区块、交易、账户余额等。Provider
类不需要智能合约的 ABI(应用二进制接口)来操作链上的数据
ethers
默认提供defaultProvider
,除此以外,最常用的是jsonRpcProvider
,可以让用户连接到特定节点服务商的节点
内置Provider
js
import { ethers } from "ethers";
// ethers内置了一些公用rpc,方便用户连接以太坊
// ethers内置的rpc访问速度有限制,仅测试用,生产环境还是要申请个人rpc
const provider = ethers.getDefaultProvider();
const main = async () => {
const balance = await provider.getBalance("vitalik.eth");
console.log("balance (wei):", balance.toString(), "balance (ether):", ethers.formatEther(balance));
};
main();
服务商 RPC
这里我们使用的是infura提供的
打开infura,注册一个ApiKey,infura只允许注册一个,所以就不演示了
创建一个.env 添加环境变量,一定记得别把.env提交到git上,哭唧唧!
安装dotenv
js
pnpm add dotenv
js
import { ethers } from "ethers";
import dotenv from "dotenv";
dotenv.config();
/**
* 学会使用服务商提供的rpc
*/
// 这里我们连接的是sepolia测试网
const provider = new ethers.JsonRpcProvider(process.env.INFURA_ID)
// 用自己的provider可以做很多事情
async function main() {
// 1.查询余额
const balance = await provider.getBalance("vitalik.eth");
console.log("balance:", balance, ethers.formatEther(balance));
// 2.查询provider连接到了哪条链
const network = await provider.getNetwork();
console.log(network.toJSON());
// 3.查询当前区块高度
const blockNumber = await provider.getBlockNumber();
console.log(blockNumber);
// 4.查询钱包历史交易次数
const txCount = await provider.getTransactionCount('0x8d2E185C8422021D7A23C9bFAc3064e37348FAB8');
console.log(txCount);
// 5.查询当前建议的gas设置
const feeData = await provider.getFeeData();
console.log(feeData);
// 6.利用getBlock()查询区块信息,参数为要查询的区块高度
const block = await provider.getBlock(0);
console.log(block);
// 7.利用getCode()查询某个地址的合约bytecode,参数为合约地址,下面例子中用的Sepolia WETH的合约地址
// 地址不唯一
const code = await provider.getCode("0xf531B8F309Be94191af87605CfBf600D71C2cFe0");
console.log(code);
}
main()
Contract
Contract
类主要用于与以太坊智能合约进行交互。它需要智能合约的 ABI 和合约地址,以便调用合约的方法和事件。Contract
类依赖于Provider
或Signer
来执行与链上的交互。
Contract
对象分为两类,只读和可读写
只读Contract
创建只读Contract实例需要填入3
个参数,分别是合约地址,合约abi
和provider
变量
下面以读取Sepolia测试网上的WETH合约为例 测试网
js
import { ethers } from "ethers";
import dotenv from "dotenv";
dotenv.config();
const provider = new ethers.JsonRpcProvider(process.env.INFURA_ID)
/**
* 我用 uniswap 将账户中的WTh转成WETH
* WETH合约地址是 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14
* 获取abi的 网址是 https://sepolia.etherscan.io/address/0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14#code
*/
const 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" }]
const addressWETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
const contractWETH = new ethers.Contract(addressWETH, abiWETH, provider);
async function main() {
// 读取WETH合约的链上信息
const nameWETH = await contractWETH.name();
const symbolWETH = await contractWETH.symbol();
const totalSupplyWETH = await contractWETH.totalSupply();
console.log("\n1.读取WETH合约信息");
console.log("名称:", nameWETH);
console.log("合约地址:", addressWETH);
console.log("代号:", symbolWETH);
console.log("总供应量:", ethers.formatEther(totalSupplyWETH));
const balanceWETH = await contractWETH.balanceOf('0x8d2E185C8422021D7A23C9bFAc3064e37348FAB8');
console.log("我的WETH余额:", ethers.formatEther(balanceWETH));
}
main();
所以可以读取到我的账户在这个合约上的余额是
可写Contract
声明可写合约函数,我们需要一个新的参数wallet
const contractWETH = new ethers.Contract(addressWETH, abiWETH, wallet)
或者是声明一个只读合约,再用connect(wallet)
函数转换成可写合约
js
const contractWETH = new ethers.Contract(addressWETH, abiWETH, provider)
contractWETH.connect(wallet)
总之,我们需要先学习一下创建wallet
Wallet
在
ethers
中,Signer
签名者类是以太坊账户的抽象,可用于对消息和交易进行签名,并将签名的交易发送到以太坊网络,并更改区块链状态。Signer
类是抽象类,不能直接实例化,我们需要使用它的子类:Wallet
钱包类。
Wallet
类继承了Signer
类,并且开发者可以像包含私钥的外部拥有帐户(EOA
)一样,用它对交易和消息进行签名。
创建wallet的方式有很多,这里我们选择用私钥来创建,伤心往事再次calback~
js
import dotenv from "dotenv";
import { ethers, Wallet } from "ethers";
dotenv.config();
const provider = new ethers.JsonRpcProvider(process.env.INFURA_ID);
const main = async () => {
// 利用私钥和provider创建wallet对象
const privateKey = process.env.WALLET_PRIVATE_KEY;
const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY, provider);
// 发送ETH
// 我用我的账户2给我的账户1发送测试币
const Account1 = '0xc92Ad23a1dAC9d6C2CdE3943cFc41ec5b483Bb2b'
console.log(`i. 发送前余额`)
console.log(`钱包1: ${ethers.formatEther(await provider.getBalance(Account1))} ETH`)
console.log(`钱包2: ${ethers.formatEther(await provider.getBalance(wallet))} ETH`)
// ii. 构造交易请求,参数:to为接收地址,value为ETH数额
const tx = {
to: Account1,
value: ethers.parseEther("0.001")
}
// iii. 发送交易,获得收据
console.log(`\nii. 等待交易在区块链确认(需要几分钟)`)
const receipt = await wallet.sendTransaction(tx)
await receipt.wait() // 等待链上确认交易
console.log(receipt) // 打印交易详情
// iv. 打印交易后余额
console.log(`\niii. 发送后余额`)
console.log(`钱包1: ${ethers.formatEther(await provider.getBalance(Account1))} ETH`)
console.log(`钱包2: ${ethers.formatEther(await provider.getBalance(wallet))} ETH`)
};
main()
发送前账户信息
发送后账户信息
创建可写Contract
声明可写合约函数,我们需要通过传入wallet这个参数来实现
可写合约和可读合约函数的区别就是是否涉及到链上数据的变化,是否需要消耗gas开支持合约状态变化等
deposit
在WETH(Wrapped Ether)合约中,deposit函数的作用是将发送到合约的ETH转换为WETH,并相应地更新用户的WETH余额。
js
// 如果钱包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`)
} else {
// 如果ETH不足..
}
transfer
将指定数量的代币从调用者的地址转移到另一个地址。
js
// 3. 调用transfer()函数,将0.001 WETH转账给 Account1
const Account1 = '0xc92Ad23a1dAC9d6C2CdE3943cFc41ec5b483Bb2b'
console.log("\n3. 调用transfer()函数,给Account1转账0.001 WETH")
// 发起交易
const tx2 = await contractWETH.transfer(Account1, ethers.parseEther("0.001"))
// 等待交易上链
await tx2.wait()
const balanceWETH_transfer = await contractWETH.balanceOf(address)
console.log(`转账后WETH持仓: ${ethers.formatEther(balanceWETH_transfer)}\n`)
withdraw
withdraw
函数的作用是将WETH转换回ETH,并发送到用户的地址。
js
// 4. 调用withdraw()函数,将0.001 WETH转换回ETH
console.log("\n4. 调用withdraw()函数,将0.001 ETH转为0.001ETH")
// 发起交易
const tx3 = await contractWETH.withdraw(ethers.parseEther("0.001") )
// 等待交易上链
await tx3.wait()
const balanceWETH_transfer = await contractWETH.balanceOf(address)
console.log(`转账后WETH持仓: ${ethers.formatEther(balanceWETH_transfer)}\n`)
注: 我让chatGPT写了一个transfer函数,将abi转换为人类可读abi,不同的合约abi可能略有不同,粘贴abi时,要以当前合约为准
deploy
将合约部署到链上,这里我们用ERC20合约作为demo
至于合约的abi和bytecode,合约开发人员可以直接提供给前端,当然也可以去remix自己编译
js
import dotenv from "dotenv";
import { ethers, Wallet } from "ethers";
dotenv.config();
const provider = new ethers.JsonRpcProvider(process.env.INFURA_ID);
const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY, provider);
// ERC20的人类可读abi
const abiERC20 = [
"constructor(string memory name_, string memory symbol_)",
"function name() view returns (string)",
"function symbol() view returns (string)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint)",
"function transfer(address to, uint256 amount) external returns (bool)",
"function mint(uint amount) external",
];
// 填入合约字节码,在remix中,你可以在两个地方找到Bytecode
// 1. 编译面板的Bytecode按钮
// 2. 文件面板artifact文件夹下与合约同名的json文件中
// 里面"bytecode"属性下的"object"字段对应的数据就是Bytecode,挺长的,608060起始
// "object": "608060405260646000553480156100...
const bytecodeERC20 = "60806040526012600560006101000a81548160ff021916908360ff1602179055503480156200002d57600080fd5b5060405162001166380380620011668339818101604052810190620000539190620001bb565b81600390805190602001906200006b9291906200008d565b508060049080519060200190620000849291906200008d565b505050620003c4565b8280546200009b90620002d5565b90600052602060002090601f016020900481019282620000bf57600085556200010b565b82601f10620000da57805160ff19168380011785556200010b565b828001600101855582156200010b579182015b828111156200010a578251825591602001919060010190620000ed565b5b5090506200011a91906200011e565b5090565b5b80821115620001395760008160009055506001016200011f565b5090565b6000620001546200014e8462000269565b62000240565b905082815260208101848484011115620001735762000172620003a4565b5b620001808482856200029f565b509392505050565b600082601f830112620001a0576200019f6200039f565b5b8151620001b28482602086016200013d565b91505092915050565b60008060408385031215620001d557620001d4620003ae565b5b600083015167ffffffffffffffff811115620001f657620001f5620003a9565b5b620002048582860162000188565b925050602083015167ffffffffffffffff811115620002285762000227620003a9565b5b620002368582860162000188565b9150509250929050565b60006200024c6200025f565b90506200025a82826200030b565b919050565b6000604051905090565b600067ffffffffffffffff82111562000287576200028662000370565b5b6200029282620003b3565b9050602081019050919050565b60005b83811015620002bf578082015181840152602081019050620002a2565b83811115620002cf576000848401525b50505050565b60006002820490506001821680620002ee57607f821691505b6020821081141562000305576200030462000341565b5b50919050565b6200031682620003b3565b810181811067ffffffffffffffff8211171562000338576200033762000370565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b610d9280620003d46000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c806342966c681161007157806342966c681461016857806370a082311461018457806395d89b41146101b4578063a0712d68146101d2578063a9059cbb146101ee578063dd62ed3e1461021e576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b661024e565b6040516100c39190610b02565b60405180910390f35b6100e660048036038101906100e19190610a14565b6102dc565b6040516100f39190610ae7565b60405180910390f35b6101046103ce565b6040516101119190610b24565b60405180910390f35b610134600480360381019061012f91906109c1565b6103d4565b6040516101419190610ae7565b60405180910390f35b610152610583565b60405161015f9190610b3f565b60405180910390f35b610182600480360381019061017d9190610a54565b610596565b005b61019e60048036038101906101999190610954565b61066d565b6040516101ab9190610b24565b60405180910390f35b6101bc610685565b6040516101c99190610b02565b60405180910390f35b6101ec60048036038101906101e79190610a54565b610713565b005b61020860048036038101906102039190610a14565b6107ea565b6040516102159190610ae7565b60405180910390f35b61023860048036038101906102339190610981565b610905565b6040516102459190610b24565b60405180910390f35b6003805461025b90610c88565b80601f016020809104026020016040519081016040528092919081815260200182805461028790610c88565b80156102d45780601f106102a9576101008083540402835291602001916102d4565b820191906000526020600020905b8154815290600101906020018083116102b757829003601f168201915b505050505081565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516103bc9190610b24565b60405180910390a36001905092915050565b60025481565b600081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546104629190610bcc565b92505081905550816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546104b79190610bcc565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461050c9190610b76565b925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516105709190610b24565b60405180910390a3600190509392505050565b600560009054906101000a900460ff1681565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546105e49190610bcc565b9250508190555080600260008282546105fd9190610bcc565b92505081905550600073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516106629190610b24565b60405180910390a350565b60006020528060005260406000206000915090505481565b6004805461069290610c88565b80601f01602080910402602001604051908101604052809291908181526020018280546106be90610c88565b801561070b5780601f106106e05761010080835404028352916020019161070b565b820191906000526020600020905b8154815290600101906020018083116106ee57829003601f168201915b505050505081565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546107619190610b76565b92505081905550806002600082825461077a9190610b76565b925050819055503373ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516107df9190610b24565b60405180910390a350565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461083a9190610bcc565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461088f9190610b76565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516108f39190610b24565b60405180910390a36001905092915050565b6001602052816000526040600020602052806000526040600020600091509150505481565b60008135905061093981610d2e565b92915050565b60008135905061094e81610d45565b92915050565b60006020828403121561096a57610969610d18565b5b60006109788482850161092a565b91505092915050565b6000806040838503121561099857610997610d18565b5b60006109a68582860161092a565b92505060206109b78582860161092a565b9150509250929050565b6000806000606084860312156109da576109d9610d18565b5b60006109e88682870161092a565b93505060206109f98682870161092a565b9250506040610a0a8682870161093f565b9150509250925092565b60008060408385031215610a2b57610a2a610d18565b5b6000610a398582860161092a565b9250506020610a4a8582860161093f565b9150509250929050565b600060208284031215610a6a57610a69610d18565b5b6000610a788482850161093f565b91505092915050565b610a8a81610c12565b82525050565b6000610a9b82610b5a565b610aa58185610b65565b9350610ab5818560208601610c55565b610abe81610d1d565b840191505092915050565b610ad281610c3e565b82525050565b610ae181610c48565b82525050565b6000602082019050610afc6000830184610a81565b92915050565b60006020820190508181036000830152610b1c8184610a90565b905092915050565b6000602082019050610b396000830184610ac9565b92915050565b6000602082019050610b546000830184610ad8565b92915050565b600081519050919050565b600082825260208201905092915050565b6000610b8182610c3e565b9150610b8c83610c3e565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610bc157610bc0610cba565b5b828201905092915050565b6000610bd782610c3e565b9150610be283610c3e565b925082821015610bf557610bf4610cba565b5b828203905092915050565b6000610c0b82610c1e565b9050919050565b60008115159050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b60005b83811015610c73578082015181840152602081019050610c58565b83811115610c82576000848401525b50505050565b60006002820490506001821680610ca057607f821691505b60208210811415610cb457610cb3610ce9565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600080fd5b6000601f19601f8301169050919050565b610d3781610c00565b8114610d4257600080fd5b50565b610d4e81610c3e565b8114610d5957600080fd5b5056fea2646970667358221220f87d0662c51e3b4b5e034fe8e1e7a10185cda3c246a5ba78e0bafe683d67789764736f6c63430008070033";
async function main() {
const factoryERC20 = new ethers.ContractFactory(abiERC20, bytecodeERC20, wallet);
// 1. 利用contractFactory部署ERC20代币合约
console.log("\n1. 利用contractFactory部署ERC20代币合约")
// 部署合约,填入constructor的参数
const contractERC20 = await factoryERC20.deploy("Secret Token", "Secret")
console.log(`合约地址: ${contractERC20.target}`);
console.log("部署合约的交易详情")
console.log(contractERC20.deploymentTransaction())
console.log("\n等待合约部署上链")
await contractERC20.waitForDeployment()
// 也可以用 contractERC20.deployTransaction.wait()
console.log("合约已上链")
// 打印合约的name()和symbol(),然后调用mint()函数,给自己地址mint 10,000代币
console.log("\n2. 调用mint()函数,给自己地址mint 10,000代币")
console.log(`合约名称: ${await contractERC20.name()}`)
console.log(`合约代号: ${await contractERC20.symbol()}`)
let tx = await contractERC20.mint("10000")
console.log("等待交易上链")
await tx.wait()
console.log(`mint后地址中代币余额: ${await contractERC20.balanceOf(wallet)}`)
console.log(`代币总供给: ${await contractERC20.totalSupply()}`)
// 3. 调用transfer()函数,给Vitalik转账1000代币
console.log("\n3. 调用transfer()函数,给Vitalik转账1,000代币")
tx = await contractERC20.transfer("vitalik.eth", "1000")
console.log("等待交易上链")
await tx.wait()
console.log(`Vitalik钱包中的代币余额: ${await contractERC20.balanceOf("vitalik.eth")}`)
}
main()
可以看到。我先是创建了一个合约,然后铸造了一些Secret Token,然后转给了我自己的账户