8.MOVE从入门到实战-发布代币到Aptos网络并调用合约

2022-11-07 11:13:44 浏览数 (1)

本文作者:木头[1]

代币合约

合约参数

保存代币余额
代码语言:javascript复制
struct Coin has store {
    value : u128,
}
地址对印余额数据
代码语言:javascript复制
struct CoinStore has key {
    coin : Coin,
}
代币基础信息
代码语言:javascript复制
struct CoinInfo has key {
    // 名称
    name: string::String,
    // 符号
    symbol: string::String,
    // 精度
    decimals: u8,
    // 总发行量
    supply: u128,
    // 已发行量
    cap: u128,
}

assert 错误信息定义

assert只能给定 u64 错误信息,全局错误定义

代码语言:javascript复制
// 账户已经注册
const THE_ACCOUNT_HAS_BEEN_REGISTERED : u64 = 1;
// 无效的令牌所有者
const INVALID_TOKEN_OWNER : u64 = 2;
// 账户未注册
const THE_ACCOUNT_IS_NOT_REGISTERED : u64 = 3;
// 余额不足
const INSUFFICIENT_BALANCE : u64 = 4;
// 代币信息已发布
const ECOIN_INFO_ALREADY_PUBLISHED : u64 = 5;
// 超过总供应量
const EXCEEDING_THE_TOTAL_SUPPLY : u64 = 6;

合约查询方法

查询代币余额
代码语言:javascript复制
public fun getBalance(owner: address) : u128 acquires CoinStore{
    // 确定帐户是否已注册
    assert!(is_account_registered(owner), THE_ACCOUNT_IS_NOT_REGISTERED);
    // 返回余额
    borrow_global<CoinStore>(owner).coin.value
}
查询帐户是否已注册
代码语言:javascript复制
public fun is_account_registered(account_addr : address) : bool{
    exists<CoinStore>(account_addr)
}

合约公共方法

增加地址余额

代码语言:javascript复制
fun deposit(account_addr : address, coin : Coin) acquires CoinStore {
    // 确定该帐户是否已注册
    assert!(is_account_registered(account_addr), THE_ACCOUNT_IS_NOT_REGISTERED);
    // 转账之前余额
    let balance = getBalance(account_addr);
    // 获取可变资源余额
    let balance_ref = &mut borrow_global_mut<CoinStore>(account_addr).coin.value;
    // 更新余额
    *balance_ref = balance   coin.value;
    // 销毁资源
    let Coin { value:_ } = coin;
}

减少地址余额

代码语言:javascript复制
fun withdraw(account_addr : address, amount : u128) : Coin acquires CoinStore {
    // 确定该帐户是否已注册
    assert!(is_account_registered(account_addr), THE_ACCOUNT_IS_NOT_REGISTERED);
    // 转账之前余额
    let balance = getBalance(account_addr);
    // 余额是否足够
    assert!(balance >= amount, INSUFFICIENT_BALANCE);
    // 获取可变资源余额
    let balance_ref = &mut borrow_global_mut<CoinStore>(account_addr).coin.value;
    // 更新余额
    *balance_ref = balance - amount;
    // 返回余额资源
    Coin { value: amount }
}

合约执行函数

初始化代币信息

代码语言:javascript复制
public entry fun initialize(address : &signer, name : vector<u8>, symbol : vector<u8>, decimals : u8, supply : u128)  {
    // 是否有权限初始化代币信息
    assert!(signer::address_of(address) == MODULE_OWNER, INVALID_TOKEN_OWNER);
    // 确定是否已初始化(防止重复初始化)
    assert!(!exists<CoinInfo>(MODULE_OWNER), ECOIN_INFO_ALREADY_PUBLISHED);
    // 创建令牌信息
    move_to(address, CoinInfo{name : string::utf8(name), symbol : string::utf8(symbol), decimals, supply, cap : 0});
}

address:调用用者,name:名称,symbol:符号,decimals:精度,supply:总发行量

注册账号(在交易之前必须先注册账号)

代码语言:javascript复制
public entry fun register(address : &signer) {
    let account = signer::address_of(address);
    // 确定该帐户是否已注册(防止重复注册)
    assert!(!exists<CoinStore>(account), THE_ACCOUNT_HAS_BEEN_REGISTERED);
    // 初始化账号
    move_to(address, CoinStore{ coin : Coin{ value : 0 } });
}

铸币

代码语言:javascript复制
public entry fun mint(owner : &signer,to : address,amount : u128) acquires CoinStore,CoinInfo{
    // 是否有权限铸币
    assert!(signer::address_of(owner) == MODULE_OWNER, INVALID_TOKEN_OWNER);
    // 是否超过总发行量
    assert!(borrow_global<CoinInfo>(MODULE_OWNER).cap   amount <= borrow_global<CoinInfo>(MODULE_OWNER).supply,EXCEEDING_THE_TOTAL_SUPPLY);
    // 收款人增加余额
    deposit(to, Coin { value : amount });
    // 增加发行总量
    let cap = &mut borrow_global_mut<CoinInfo>(MODULE_OWNER).cap;
    *cap = *cap   amount;
}

owner:必须为模块拥有者才能有权限铸币,to:给 to 地址铸币,amount:铸币数量

转账

代码语言:javascript复制
public entry fun transfer(from : &signer, to : address, amount : u128) acquires CoinStore {
    // 先扣除账号余额
    let coin = withdraw(signer::address_of(from), amount);
    // 增加账号余额
    deposit(to, coin);
}

销毁

代码语言:javascript复制
public entry fun burn(owner : &signer, amount : u128) acquires CoinStore,CoinInfo {
    // 是否有权限销毁
    assert!(signer::address_of(owner) == MODULE_OWNER, INVALID_TOKEN_OWNER);
    // 扣除账号余额
    let coin = withdraw(signer::address_of(owner), amount);
    let Coin { value: amount } = coin;
    // 减少已发行量
    let cap = &mut borrow_global_mut<CoinInfo>(MODULE_OWNER).cap;
    *cap = *cap - amount;
    // 减少总发行量
    let supply = &mut borrow_global_mut<CoinInfo>(MODULE_OWNER).supply;
    *supply = *supply - amount;
}

Typescript 脚本

生成账号脚本

account_script.ts

代码语言:javascript复制
//节点URL  REST API地址
export const NODE_URL = "https://fullnode.devnet.aptoslabs.com";
//水龙头URL
export const FAUCET_URL = "https://faucet.devnet.aptoslabs.com";

//生成账号接口
import { AptosAccount, MaybeHexString } from "aptos";

//Aptos客户端 水龙头客户端
import { AptosClient, FaucetClient } from "aptos";

//创建节点客户端
const client = new AptosClient(NODE_URL);

//查询主链币余额
export async function accountBalance(accountAddress: MaybeHexString): Promise<number | null> {
  const resource = await client.getAccountResource(accountAddress, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>");
  if (resource == null) {
    return null;
  }

  return parseInt((resource.data as any)["coin"]["value"]);
}

//主方法
async function main() {
  //创建帐户
  const account = new AptosAccount();

  console.log("n=== 地址 ===");
  console.log(
    `地址: ${account.address()} key种子: ${Buffer.from(account.signingKey.secretKey).toString("hex").slice(0, 64)}`,
  );
  //水龙头领取代币
  const faucetClient = new FaucetClient(NODE_URL, FAUCET_URL);
  await faucetClient.fundAccount(account.address(), 50000);

  console.log("n=== 账户余额 ===");
  console.log(`${account.address()}余额: ${await accountBalance(account.address())}`);
}

if (require.main === module) {
  main();
}

调用合约脚本

coin_script.ts

代码语言:javascript复制
import assert from "assert";
import fs from "fs";
import { AptosAccount, AptosClient, TxnBuilderTypes, BCS, MaybeHexString, HexString, FaucetClient } from "aptos";

//节点URL  REST API地址
export const NODE_URL = "https://fullnode.devnet.aptoslabs.com";
//水龙头URL
export const FAUCET_URL = "https://faucet.devnet.aptoslabs.com";

// 创建aptos客户端
const client = new AptosClient(NODE_URL);

// 查询APT代币余额
export async function getAccountBalance(accountAddress: MaybeHexString): Promise<number | null> {
  const resource = await client.getAccountResource(accountAddress, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>");
  if (resource == null) {
    return null;
  }

  return parseInt((resource.data as any)["coin"]["value"]);
}

//发布模块
export async function publishModule(accountFrom: AptosAccount, moduleHex: string): Promise<string> {
  const moudleBundlePayload = new TxnBuilderTypes.TransactionPayloadModuleBundle(
    new TxnBuilderTypes.ModuleBundle([new TxnBuilderTypes.Module(new HexString(moduleHex).toUint8Array())]),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(accountFrom.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
    BigInt(sequenceNumber),
    moudleBundlePayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000)   10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
  const transactionRes = await client.submitSignedBCSTransaction(bcsTxn);

  return transactionRes.hash;
}

//注册代币账号=>初始化账号
async function registerCoin(contractAddress: HexString, accountFrom: AptosAccount): Promise<string> {
  const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
    TxnBuilderTypes.EntryFunction.natural(`${contractAddress.toString()}::coin`, "register", [], []),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(accountFrom.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
    BigInt(sequenceNumber),
    entryFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000)   10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);

  return pendingTxn.hash;
}

// 初始化token信息
async function initializeCoin(contractAddress: HexString, accountFrom: AptosAccount): Promise<string> {
  // 总发行量
  const supply = new BCS.Serializer();
  supply.serializeU128(1000);

  // 精度
  const decimals = new BCS.Serializer();
  decimals.serializeU8(0);

  const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
    TxnBuilderTypes.EntryFunction.natural(
      `${contractAddress}::coin`,
      "initialize",
      [],
      [BCS.bcsSerializeStr("CPI Coin"), BCS.bcsSerializeStr("CPI"), decimals.getBytes(), supply.getBytes()],
    ),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(accountFrom.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
    BigInt(sequenceNumber),
    entryFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000)   10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);

  return pendingTxn.hash;
}

//铸币
async function mintCoin(
  contractAddress: HexString,
  accountFrom: AptosAccount,
  receiverAddress: HexString,
  amount: number,
): Promise<string> {
  // 发行量
  const cap = new BCS.Serializer();
  cap.serializeU128(amount);

  const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
    TxnBuilderTypes.EntryFunction.natural(
      `${contractAddress.toString()}::coin`,
      "mint",
      [],
      [BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(receiverAddress.hex())), cap.getBytes()],
    ),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(accountFrom.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
    BigInt(sequenceNumber),
    entryFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000)   10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);

  return pendingTxn.hash;
}

//转账
async function transfer(
  contractAddress: HexString,
  accountFrom: AptosAccount,
  to: HexString,
  amount: number,
): Promise<string> {
  const amo = new BCS.Serializer();
  amo.serializeU128(amount);

  const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
    TxnBuilderTypes.EntryFunction.natural(
      `${contractAddress.toString()}::coin`,
      "transfer",
      [],
      [BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(to.hex())), amo.getBytes()],
    ),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(accountFrom.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
    BigInt(sequenceNumber),
    entryFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000)   10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);

  return pendingTxn.hash;
}

// 销毁
async function burn(contractAddress: HexString, accountFrom: AptosAccount, amount: number): Promise<string> {
  const amo = new BCS.Serializer();
  amo.serializeU128(amount);

  const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
    TxnBuilderTypes.EntryFunction.natural(`${contractAddress.toString()}::coin`, "burn", [], [amo.getBytes()]),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(accountFrom.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
    BigInt(sequenceNumber),
    entryFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000)   10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);

  return pendingTxn.hash;
}

//查询代币余额
async function getBalance(contractAddress: HexString, accountFrom: AptosAccount): Promise<string | number | any> {
  try {
    const resource = await client.getAccountResource(accountFrom.address(), `${contractAddress}::coin::CoinStore`);
    return parseInt((resource.data as any)["coin"]["value"]);
  } catch (_) {
    return 0;
  }
}

//查询代币信息
async function getCoinInfo(contractAddress: HexString): Promise<string | number | any> {
  try {
    const resource = await client.getAccountResource(contractAddress, `${contractAddress}::coin::CoinInfo`);
    return resource;
  } catch (_) {
    return 0;
  }
}

async function main() {

  assert(process.argv.length == 3, "Expecting an argument that points to the moon coin module");

  //创建客户端
  const client = new AptosClient(NODE_URL);
  const account = new AptosAccount(
    new HexString("3a64b8d0478799c570fb540fc2aabc917604b72f7cd44c75d8df4629e5af58ce").toUint8Array(),
  );
  console.log(`模块拥有者 ${account.address()}余额: ${await getAccountBalance(account.address())}APT`);

  //创建水龙头客户端
  const faucetClient = new FaucetClient(NODE_URL, FAUCET_URL);

  // 发布模块
  const modulePath = process.argv[2];
  const moduleHex = fs.readFileSync(modulePath).toString("hex");
  console.log("n==========发布模块========");
  let txHash = await publishModule(account, moduleHex);
  console.log("发布模块:"   txHash);
  await client.waitForTransaction(txHash);

  console.log("n==========初始化token信息========");
  txHash = await initializeCoin(account.address(), account);
  console.log("初始化token信息Hash:"   txHash);
  await client.waitForTransaction(txHash);

  console.log("n==========token信息========");
  let coinInfo: CoinInfo = await getCoinInfo(account.address());
  console.log(`模块类型:${coinInfo.type}`);
  console.log(`名称:${coinInfo.data.name}`);
  console.log(`符号:${coinInfo.data.symbol}`);
  console.log(`精度:${coinInfo.data.decimals}`);
  console.log(`总发行量:${coinInfo.data.supply}`);
  console.log(`已发行量:${coinInfo.data.cap}`);

  console.log("n==========创建测试账号A和B==========");
  const a = new AptosAccount();
  console.log(`a地址: ${a.address()}`);

  const b = new AptosAccount();
  console.log(`b地址: ${b.address()}`);

  // 领取gas费
  await faucetClient.fundAccount(a.address(), 5000);
  await faucetClient.fundAccount(b.address(), 5000);

  //注册代币账号=>初始化账号
  console.log("n==========注册代币账号========");
  txHash = await registerCoin(account.address(), a);
  console.log("a注册Hash:"   txHash);
  await client.waitForTransaction(txHash);
  txHash = await registerCoin(account.address(), b);
  console.log("b注册Hash:"   txHash);
  await client.waitForTransaction(txHash);
  txHash = await registerCoin(account.address(), account);
  console.log("模块拥有者注册Hash:"   txHash);
  await client.waitForTransaction(txHash);

  //铸币
  console.log("n==========铸造100个代币给A账号========");
  txHash = await mintCoin(account.address(), account, a.address(), 100);
  console.log("管理员给A账号铸币Hash:"   txHash);
  await client.waitForTransaction(txHash);
  let aBalance = await getBalance(account.address(), a);
  console.log(`a余额:`   aBalance);

  //转账
  console.log("n==========A账号转账50个代币给B账号========");
  txHash = await transfer(account.address(), a, b.address(), 50);
  console.log("转账Hash:"   txHash);
  await client.waitForTransaction(txHash);
  //查询余额
  aBalance = await getBalance(account.address(), a);
  console.log(`a余额:`   aBalance);
  let bBalance = await getBalance(account.address(), b);
  console.log(`b余额:`   bBalance);

  // 销毁
  console.log("n==========B账号转账50个代币给模块拥有者========");
  txHash = await transfer(account.address(), b, account.address(), 50);
  console.log("转账Hash:"   txHash);
  await client.waitForTransaction(txHash);
  //查询余额
  aBalance = await getBalance(account.address(), a);
  console.log(`a余额:`   aBalance);
  bBalance = await getBalance(account.address(), b);
  console.log(`b余额:`   bBalance);
  let accountBalance = await getBalance(account.address(), account);
  console.log(`模块拥有者余额:`   accountBalance);
  //开始销毁
  console.log("n==========销毁50个代币========");
  txHash = await burn(account.address(), account, 50);
  console.log("销毁Hash:"   txHash);
  await client.waitForTransaction(txHash);
  aBalance = await getBalance(account.address(), a);
  console.log(`a余额:`   aBalance);
  bBalance = await getBalance(account.address(), b);
  console.log(`b余额:`   bBalance);
  accountBalance = await getBalance(account.address(), account);
  console.log(`模块拥有者余额:`   accountBalance);
  coinInfo = await getCoinInfo(account.address());
  console.log(`总发行量:${coinInfo.data.supply}`);
  console.log(`已发行量:${coinInfo.data.cap}`);
}

if (require.main === module) {
  main();
}

// 代币信息
interface CoinInfo {
  type: string;
  data: Data;
}

interface Data {
  cap: string; //已发行量
  decimals: number; //精度
  name: string; //名称
  symbol: string; //符号
  supply: string; //总发行量
}

发布并调用合约

1.调用 account_script.ts 生成合约部署账号

代码语言:javascript复制
$ node --loader ts-node/esm account_script.ts
=== 地址 ===
地址: 0xe13c36e921448a601f2de9dc5341525ca6619a44e1444f302fba37fb39c5cf93 key种子: 3a64b8d0478799c570fb540fc2aabc917604b72f7cd44c75d8df4629e5af58ce

=== 账户余额 ===
0xe13c36e921448a601f2de9dc5341525ca6619a44e1444f302fba37fb39c5cf93余额: 50000

2.复制地址到合约项目 Move.toml 配置

代码语言:javascript复制
[addresses]
std = "0x1"
CoinToken = "0xe13c36e921448a601f2de9dc5341525ca6619a44e1444f302fba37fb39c5cf93"

生成 mv

代码语言:javascript复制
$ aptos move compile --package-dir . --named-addresses CoinToken=0xe13c36e921448a601f2de9dc5341525ca6619a44e1444f302fba37fb39c5cf93
{
  "Result": [
    "E13C36E921448A601F2DE9DC5341525CA6619A44E1444F302FBA37FB39C5CF93::coin"
  ]
}

3.复制合约项目 mv 到脚本项目根目录

4.复制账号的种子到 coin_script.ts 的 main

5.调用 coin_script.ts 脚本

代码语言:javascript复制
$ node --loader ts-node/esm coin_script.ts coin.mv

模块拥有者0xe13c36e921448a601f2de9dc5341525ca6619a44e1444f302fba37fb39c5cf93余额: 50000APT

==========发布模块========
发布模块:0x6d81ba53b5fe50d4d0a73022c8c4c2a8383d81e72768ea9e6066b425c93d5888

==========初始化token信息========
初始化token信息Hash:0x5a62958de06aa7407425c541a83c746e97a66ae427d8ac40a59ab8711c977fa4

==========token信息========
模块类型:0xe13c36e921448a601f2de9dc5341525ca6619a44e1444f302fba37fb39c5cf93::coin::CoinInfo
名称:CPI Coin
符号:CPI
精度:0
总发行量:1000
已发行量:0

==========创建测试账号A和B==========
a地址: 0x17f086d99a1fe096c397880173ae2356b95d63423ff75a6627827b3fc760e224
b地址: 0x922164511c6046c223e90653ebc26a68b0b914114a0c382f1c113319e394826b

==========注册代币账号========
a注册Hash:0x0256d8b19cd7551f582573ab8e4968ea7a0a25fed2ca901619187f1830d5966e
b注册Hash:0x837d60bc99e8143eb21370c6f1eb6cf9d7b0cbfddb3405c18697072cb4e8442e
模块拥有者注册Hash:0xdd93a94a785f0f85d38b143305880114314ef3ca30ad4d0e7e78a2063f9f514e

==========铸造100个代币给A账号========
管理员给A账号铸币Hash:0x60dbc50c4c61626a32c632a9a997ed9a147101c89c0387b3a57d93fc7ec31ddf
a余额:100

==========A账号转账50个代币给B账号========
转账Hash:0xa3a477466c687ddb86d80cfbff36b645f08edb31175ff9d85df80ab6719d1749
a余额:50
b余额:50

==========B账号转账50个代币给模块拥有者========
转账Hash:0x85205f4c94f922bd8b62c8a4cd68bbe4ea3a7581ffa05e9c81e2a12401028357
a余额:50
b余额:0
模块拥有者余额:50

==========销毁50个代币========
销毁Hash:0x529be7b0f235d8af769d7c00a8b78455ccdbee9e6d9b758a9538e349b0a1ce6f
a余额:50
b余额:0
模块拥有者余额:0
总发行量:950
已发行量:50

区块浏览器:https://explorer.devnet.aptos.dev/account/0xe13c36e921448a601f2de9dc5341525ca6619a44e1444f302fba37fb39c5cf93[2]

参考资料

[1]

木头: https://learnblockchain.cn/people/3015

[2]

https://explorer.devnet.aptos.dev/account/0xe13c36e921448a601f2de9dc5341525ca6619a44e1444f302fba37fb39c5cf93: https://explorer.devnet.aptos.dev/account/0xe13c36e921448a601f2de9dc5341525ca6619a44e1444f302fba37fb39c5cf93

0 人点赞