{"content":{"title":"Sui Move初体验(2) -- 创建 NFT 剑","body":"![img](https:\/\/img.learnblockchain.cn\/2022\/09\/30\/1*MofZ6s6_RgqBCjKipTpdnA.png)\r\n\r\n\r\n\r\n**Sui Move 初体验系列文章包含：**\r\n\r\n1. [介绍和铸造一个简单的NFT](https:\/\/learnblockchain.cn\/article\/4812)\r\n2. **建立一个简单的剑的例子**。\r\n3. 构建一个带有前端的简单井字游戏实例\r\n\r\n## 示例 -- 铸造一把剑\r\n\r\n让我们简单的想像一个游戏示例来进行研究。请确保你的工作电脑有[Sui binaries](https:\/\/docs.sui.io\/build\/install#binaries)，克隆相应的代码库，因为本教程假设你是在Sui版本库上工作。\r\n\r\n```bash\r\n    $ git clone <https:\/\/github.com\/MystenLabs\/sui.git> --branch devnet\r\n    $ which sui-move \/\/ make sure the path to Sui binaries (~\/.cargo\/bin)\r\n```\r\n\r\n现在的目录结构应该是下面的样子。你可以参考[这里](https:\/\/github.com\/sigridjineth\/awesome-web3dev\/tree\/main\/sui-example)或[官方文档](https:\/\/docs.sui.io\/build\/move#entry-functions)中的代码示例。在当前目录下创建一些文件，该目录与 [Sui 代码库](https:\/\/github.com\/MystenLabs\/sui) 平级。\r\n\r\n```\r\n    $ mkdir -p my_move_package\/sources\r\n    touch my_move_package\/sources\/m1.move\r\n    touch my_move_package\/Move.toml\r\n    current_directory\r\n    ├── sui\r\n    ├── my_move_package\r\n        ├── Move.toml\r\n        ├── sources\r\n            ├── m1.move\r\n\r\n```\r\n\r\n对于初学者来说，应该先声明模块。我们希望一把剑是一个可升级的对象，可能被几个用户使用。\r\n\r\n```rust\r\nmodule my_first_package::m1 {\r\n  \/\/ Please note that we need to import the ID package from Sui framework  \r\n  \/\/ to gain access to the VersionedID struct type defined in this package.  \r\n  use sui::id::VersionedID;\r\n  use sui::tx_context::TxContext;\r\n  \r\n  struct Sword has key, store {\r\n    id: VersionedID,\r\n    magic: u64,\r\n    strength: u64,\r\n  }\r\n}\r\n```\r\n\r\n如果开发者希望从不同的包中访问剑的特性，该结构体必须在其模块中包括`public`访问器函数。该资产还具有`magic`和`strength`字段，描述其各种属性字段的值，此外还有必要的id字段以及`key`和`store`能力。\r\n\r\n```rust\r\npublic fun magic(self: & Sword): u64 {\r\n  self.magic\r\n}\r\npublic fun strength(self: & Sword): u64 {\r\n  self.strength\r\n}\r\n```\r\n\r\n让我们先写一些测试方法。为了给我们的剑构建一个唯一的标识符，我们必须首先创建一个`TxContext`结构的模拟。接下来，我们创建实体剑。最后，我们使用它的访问器方法，以确保它们提供预期的结果。\r\n\r\n注意，剑本身是作为一个只读的引用参数提供给函数的访问器方法的，而假（dummy）上下文是作为一个可变的引用参数`&mut`传递给`tx_context::new_id`函数。\r\n\r\n```rust\r\n#[test]\r\npublic fun test_sword_create() {\r\n  use sui::tx_context;\r\n  use sui::transfer;\r\n  \/\/ create a dummy instance of TXContext so that to create sword object  \r\n  let ctx = tx_context::dummy();\r\n  \/\/ create a sword  \r\n  let sword = Sword {\r\n    \/\/ dummy context is passed to `tx_context::new_id`  \r\n    id: tx_context::new_id( &mut ctx),\r\n    magic: 42,\r\n    strength: 7\r\n  };\r\n  \/\/ check if accessor function returns correct values  \r\n  assert!(magic( & sword) == 42 && strength( & sword) == 7, 1);\r\n  \/\/ create a dummy address and transfer the sword  \r\n  let dummy_address = @0xCAFE;\r\n  transfer::transfer(sword, dummy_address);\r\n}\r\n```\r\n\r\n现在让我们进入新测试函数的具体内容。首先是生成几个地址，代表测试环境中的用户。我们有一个管理员用户和两个参加游戏的普通用户。第一种情况是通过代表管理员发起第一笔交易来创建，这也会生成一把剑并将所有权转移给所有者。\r\n\r\n```rust\r\n#[test]\r\nfun test_sword_transactions() {\r\n    use sui::test_scenario;\r\n    let admin = @0xABBA;\r\n    let initial_owner = @0xCAFE;\r\n    let final_owner = @0xFACE;\r\n    \/\/ first transaction executed by admin  \r\n    let scenario = &mut test_scenario::begin( &admin); \r\n    {\r\n      \/\/ create the sword and transfer it to the initial owner  \r\n      sword_create(42, 7, initial_owner, test_scenario::ctx(scenario));\r\n    };\r\n    (...)\r\n```\r\n\r\n第二笔交易是由第一个所有者进行的。注意，初始所有者是作为参数提供给`test_scenario::next_tx`函数的。他们将剑的所有权转移给最终的所有者。\r\n\r\n这里使用了`test_scenario`模块，它的`take_owned`方法使Sword的一个对象被一个在当前交易内执行的地址所拥有。在此案例中，从存储器中检索的对象被移到一个不同的地址。\r\n\r\n```rust\r\n#[test]\r\nfun test_sword_transactions() {\r\n  (...)\r\n  \/\/ second transaction executed by the initial sword owner  \r\n  test_scenario::next_tx(scenario, & initial_owner); \r\n  {\r\n    \/\/ extract the sword owned by the initial owner  \r\n    let sword = test_scenario::take_owned < Sword > (scenario);\r\n    \/\/ transfer the sword to the final owner  \r\n    sword_transfer(sword, final_owner, test_scenario::ctx(scenario));\r\n  };\r\n  (...)\r\n}\r\n```\r\n\r\n最后一笔交易是由最后的所有者进行的，他从存储中获得**Sword**对象，并验证它是否具有相应的特性。值得注意的是，一旦一个对象通过创建或从模拟存储中获取而变得可用，它就不能简单地消失。\r\n\r\n`test_scenario`包提供了一个简单的方法，类似于在Sui的上下文中执行Move时的情况--只是使用`test_scenario::return_owned`函数将剑返回到对象池中。\r\n\r\n```rust\r\n#[test]\r\nfun test_sword_transactions() {\r\n  (...)\r\n  \/\/ third transaction executed by the final sword owner  \r\n  test_scenario::next_tx(scenario, & final_owner); {\r\n    \/\/ extract the sword owned by the final owner  \r\n    let sword = test_scenario::take_owned < Sword > (scenario);\r\n    \/\/ verify that the sword has expected properties  \r\n    assert!(magic( & sword) == 42 && strength( & sword) == 7, 1);\r\n    \/\/ return the sword to the object pool (it cannot be simply \"dropped\")  \r\n    test_scenario::return_owned(scenario, sword)\r\n  }\r\n}\r\n```\r\n\r\n**Move.toml**文件必须包含某些元数据，以便创建包含这个直接模块的包，包括包的**名称**、**版本**、本地**依赖**路径（用于查找Sui框架代码），以及包的数字ID，对于用户定义的模块必须是`0x0`，以便于包发布。\r\n\r\n**Move.toml**文件的**MoveStdlib**依赖的本地文件路径必须被改变，以使其指向正确的Move标准库。为了做到这一点，我使用git克隆了[sui 库代码](https:\/\/github.com\/MystenLabs\/sui)，这使我能够在本地安装正确的**MoveStdlib**。然后我对**Move.toml**做了如下修改。\r\n\r\n```toml\r\n    [package]\r\n    name = \"MyFirstPackage\"\r\n    version = \"0.0.1\"\r\n    [dependencies]\r\n    Sui = { local = \"..\/sui\/crates\/sui-framework\" }\r\n    MoveStdlib = { local = \"\/path\/to\/git-repo\/move\/deps\/sui\/crates\/sui-framework\/deps\/move-stdlib\/\",\r\n    addr_subst = { \"std\" = \"0x1\" } }\r\n    [addresses]\r\n    my_first_package = \"0x0\"\r\n```\r\n\r\n确保软件包在正确目录中，然后用以下命令构建：\r\n\r\n```bash\r\n    $ sui move build\r\n```\r\n\r\n结果应该是下面的样子：\r\n\r\n```markdown\r\n    Build Successful\r\n    Artifacts path: \".\/build\"\r\n```\r\n\r\n\r\n这里只有一个Move单元测试，一个带有`#[test]`注解的公共函数，没有参数或返回值。运行下面的命令进行测试，测试应该在`my_move_package`目录下执行，测试框架将执行上述的功能。\r\n\r\n```\r\n    $ sui move test\r\n```\r\n\r\n结果应该类似于下面。而且，不出所料，单元测试将失败了，因为我们还没有编写任何实现逻辑。\r\n\r\n```\r\n    error[E03005]: unbound unscoped name\r\n        ┌─ .\/sources\/m1.move:145:17\r\n        │\r\n    145 │                 sword_create(\r\n        │                 ^^^^^^^^^^^^ Unbound function 'sword_create' in current scope\r\n    \r\n```\r\n\r\n测试驱动开发，这听起来很简单啊？为了通过测试，需要在上面的结构上编写`sword_create`方法。这个新方法是不言而喻的，它利用了与前面部分相同的Sui内部模块，`TxContext`和`Transfer`。\r\n\r\n```rust\r\n    use sui::tx_context::TxContext;\r\n```\r\n\r\n继续我们的想像这个游戏。这里将引入一个将参与铸剑过程的Forge对象。比方说，Forge对象需要跟踪已经铸造了多少把剑。将Forge结构定义为如下。然后，该模块有一个getter，返回铸剑的数量。\r\n\r\n```rust\r\nstruct Forge has key, store {\r\n  id: VersionedID,\r\n  swords_created: u64,\r\n}\r\npublic fun sword_created(self: & Forge): u64 {\r\n  self.swords_created\r\n}\r\n```\r\n\r\n为了使`TxContext`结构可用于函数声明，我们必须在模块级别添加一个额外的导入行，以使代码可以构建。\r\n\r\n```rust\r\npublic entry fun sword_create(forge: &mut Forge, magic: u64, strength: u64,  recipient: address, ctx: &mut TxContext) {\r\n  use sui::transfer;\r\n  \/\/ <https:\/\/github.com\/MystenLabs\/sui\/blob\/main\/crates\/sui-framework\/sources\/tx_context.move>\r\n  use sui::tx_context;\r\n  \/\/ create a sword  \r\n  let sword = Sword {\r\n    id: tx_context::new_id(ctx),\r\n    magic,\r\n    strength,\r\n  };\r\n  \/\/ In order to use the forge, we need to modify the sword_create function to take the forge as a parameter\r\n  \/\/ and to update the number of created swords at the end of the function:  \r\n  forge.swords_created = forge.swords_created + 1;\r\n  \/\/ transfer the sword  \r\n  transfer::transfer(sword, recipient);\r\n}\r\n\r\npublic entry fun sword_transfer(sword: Sword, recipient: address, _ctx: &mut TxContext) {\r\n  use sui::transfer;\r\n  \/\/ transfer the sword  \r\n  transfer::transfer(sword, recipient);\r\n}\r\n```\r\n\r\n现在我们可以再次运行测试命令，看到我们的模块现在有两个成功的测试。\r\n\r\n```\r\n    BUILDING MoveStdlib\r\n    BUILDING Sui\r\n    BUILDING MyFirstPackage\r\n    Running Move unit tests\r\n    [ PASS    ] 0x0::M1::test_sword_create\r\n    [ PASS    ] 0x0::M1::test_sword_transactions\r\n    Test result: OK. Total tests: 2; passed: 2; failed: 0\r\n    \r\n```\r\n\r\nMove 作为一种面向对象的编程语言，而我们现在还没有创建一个`构造器`方法（用于初始化）。一个包中的每个模块都可以有一个自定义的初始化函数，当包被发布时将会被调用。现在你可以写一个方法来测试模块的初始化。\r\n\r\n正如你在测试方法中看到的，我们在第一个交易中明确地调用初始化函数，然后在下一个交易中验证**Forge**对象是否已经被创建并正确初始化。\r\n\r\n```rust\r\n#[test]\r\npublic fun test_module_init() {\r\n  use sui::test_scenario;\r\n  \/\/ create test address representing game admin  \r\n  let admin = @0xABBA;\r\n  \/\/ first transaction to emulate module initialization  \r\n  let scenario = &mut test_scenario::begin( &admin); \r\n  {\r\n    init(test_scenario::ctx(scenario));\r\n  };\r\n  \r\n  \/\/ second transaction to check if the forge has been created  \r\n  \/\/ and has initial value of zero swords created  \r\n  test_scenario::next_tx(scenario, &admin); \r\n  {\r\n    \/\/ extract the Forge object  \r\n    let forge = test_scenario::take_owned < Forge > (scenario);\r\n    \/\/ verify number of created swords  \r\n    assert!(swords_created( & forge) == 0, 1);\r\n    \/\/ return the Forge object to the object pool  \r\n    test_scenario::return_owned(scenario, forge)\r\n  }\r\n}\r\n```\r\n\r\n当你运行这个测试时，肯定会抛出一个错误，说没有实现`init`模块：\r\n\r\n```\r\n    error[E03005]: unbound unscoped name\r\n        ┌─ .\/sources\/m1.move:135:17\r\n        │\r\n    135 │                 init(test_scenario::ctx(scenario1));\r\n        │                 ^^^^ Unbound function 'init' in current scope\r\n```\r\n\r\n为了在发布（或部署）模块时执行，请将模块的初始化方法写成以下样子：\r\n\r\n```rust\r\n\/\/ module initializer to be executed when this module is published  \r\nfun init(ctx: &mut TxContext) {\r\n  use sui::transfer;\r\n  use sui::tx_context;\r\n  let admin = Forge {\r\n    id: tx_context::new_id(ctx),\r\n    swords_created: 0,\r\n  };\r\n  \/\/ transfer the forge object to the module\/package publisher  \r\n  \/\/ (presumably the game admin)  \r\n  transfer::transfer(admin, tx_context::sender(ctx));\r\n}\r\n```\r\n\r\n运行你指定的单元测试，它们应该被通过。\r\n\r\n```\r\n    INCLUDING DEPENDENCY MoveStdlib\r\n    INCLUDING DEPENDENCY Sui\r\n    BUILDING MyFirstPackage\r\n    Running Move unit tests\r\n    [ PASS    ] 0x0::m1::test_module_init\r\n    [ PASS    ] 0x0::m1::test_sword_create\r\n    [ PASS    ] 0x0::m1::test_sword_transactions\r\n    Test result: OK. Total tests: 3; passed: 3; failed: 0\r\n   \r\n```\r\n\r\n让我们开始游戏吧。在发布模块之前，让我们用命令看一下我们在CLI客户端拥有的账户地址。因为这些地址是随机产生的，所以它们会和你看到的不同。\r\n\r\n```\r\n    $ sui client addresses\r\n    Showing 5 results.\r\n    0x76705799eaef88a5378bf616fab44c96e0b8dc05\r\n    0x9f67952f0fe64b790c169d6bc3e97f0275f9876d\r\n    0x86e29705c42a304a85c16b095892e5c877768113\r\n    0xc6fce00f0ec0b8bb66c6e03bbeb0f9c2c6c7a555\r\n    0xa57bf4d15156534533f20576bea6961e1e0696ad\r\n```\r\n\r\n因为我们将在整个发布过程中反复利用地址和Gas对象，让我们声明它们是环境变量，这样我们就不必每次都把它们放进去。\r\n\r\n```\r\n    export ADMIN=0x76705799eaef88a5378bf616fab44c96e0b8dc05 \/\/ YOUR ANY ARBITRARY ADDRESS SHOWN ABOVE\r\n    export RECIPIENT=0x9f67952f0fe64b790c169d6bc3e97f0275f9876d\r\n    \r\n```\r\n\r\n对于上面的地址，让我们发现Gas对象。如果你没有Gas对象，在Discord的[#devnet-faucet](https:\/\/discord.com\/channels\/916379725201563759\/971488439931392130)上申请测试SUI代币。\r\n\r\n```\r\n    $ sui client gas --address $ADMIN\r\n     Object ID                                  |  Version   |  Gas Value \r\n    ----------------------------------------------------------------------\r\n     0x152045509c335d7c0ee9c093513282f375cbe4cd |     13     |    39063   \r\n     0x17f40dc99e5e0097fa4693e5dfd1d493d1803aa2 |     2      |    50000   \r\n     0x1abf044fa3243c33454d9d01400c0f7ea0182b4d |     2      |    50000   \r\n     0x7ce36b6a4f0ac1037877408c75cbecebfeddeaf8 |     2      |    50000   \r\n     0xd6af71588d85618a3b83941a8d2183c062c40c81 |     2      |    50000\r\n    \r\n```\r\n\r\n选择第一个有Gas的账号 。我已经事先发布了大量的模块，这样初始Gas 值就会减少到39063。让把他们也记录到环境变量中。\r\n\r\n```\r\n    export ADMIN_GAS=0x152045509c335d7c0ee9c093513282f375cbe4cd \/\/ YOUR ANY ARBITRARY GAS OBJECT SHOWN ABOVE\r\n   \r\n```\r\n\r\n现在发布模块! 运行下面的命令。确保你在`--path`标志后输入正确的模块路径。模块的根目录是Move.toml所在的目录。在我这里，我直接在模块的根目录下运行该命令：\r\n\r\n```\r\n    $ sui client publish --path . --gas $ADMIN_GAS --gas-budget 30000\r\n    ----- Certificate ----\r\n    Transaction Hash: CoyjjuBV6vsD\/xvHzu51wgU\/MFFGXK9+gxlr7XnlknE=\r\n    Transaction Signature: cuv3o997JY36DkwHXSD2r+m1zFORgIB64duNgFGWqfLnIFrVVqQDRjw3sQ6du\/4dymxNcC2kF\/wS+x7k+0TIAw==@owVFDiFH79k1g9wNnwlf56i12jPHJv37C9Q99mYM47M=\r\n    Signed Authorities : [k#e97638a9e10b9cc46d85b81191a60a6a6fcae63dacd811c07502db9c5cfc55b5, k#1584ef2b677d6f5e96b57dac2744e9278aa24e117c7bfd6ac00bf43c1129c7fd, k#839e99f8b03f0f5563d6cd9cc39e10a8cf483ad2775aecbb927031370925ef4e]\r\n    Transaction Kind : Publish\r\n    ----- Publish Results ----\r\n    The newly published package object ID: 0x529830305d84d2335c5394c7f786e01f241c8372\r\n    List of objects created by running module initializers:\r\n    ----- Move Object (0x954fa256f139900d995d807392a56ddc4442caa9[1]) -----\r\n    Owner: Account Address ( 0x76705799eaef88a5378bf616fab44c96e0b8dc05 )\r\n    Version: 1\r\n    Storage Rebate: 12\r\n    Previous Transaction: CoyjjuBV6vsD\/xvHzu51wgU\/MFFGXK9+gxlr7XnlknE=\r\n    ----- Data -----\r\n    type: 0x529830305d84d2335c5394c7f786e01f241c8372::m1::Forge\r\n    id: 0x954fa256f139900d995d807392a56ddc4442caa9[1]\r\n    swords_created: 0\r\n    Updated Gas : Coin { id: 0x152045509c335d7c0ee9c093513282f375cbe4cd, value: 48687 }\r\n\r\n```\r\n\r\n我的交易哈希值是`AdSdjYdaCgfnljF0bk50ZWVdk+\/cgafSYB1eA\/d+Ca8=`。Sui Explorer上的[交易哈希](https:\/\/explorer.devnet.sui.io\/transactions\/AdSdjYdaCgfnljF0bk50ZWVdk%2B%2FcgafSYB1eA%2Fd%2BCa8%3D)可能包括Sui的区块链状态上发布的字节码。该软件包已成功发布。一些Gas被收取，导致初始Gas值发生变化。在发布过程中，`init`函数被调用，因此模块初始化函数已经运行。`sword_created`值被初始化为零。\r\n\r\n![img](https:\/\/img.learnblockchain.cn\/2022\/09\/30\/1*G8ilIsNV-wjeFW5ulvlr-w.png)\r\n\r\n> 上面的软件包现在在资源管理器上找不到了，由于 devnet 经常清空重启。\r\n\r\n新发布的软件包的ID是`0x5f35e44748ddc37ab57f7d97435b3b984d13d8e6`，这与你那边不同。我们将该软件包添加到另一个环境变量中。\r\n\r\n```\r\n    $ export PACKAGE=0x5f35e44748ddc37ab57f7d97435b3b984d13d8e6\r\n\r\n```\r\n\r\n你会得到**PACKAGE**的ID和新创建的**Forge**对象ID。使用 `sui client call `功能，在`--function `标志后调用指定的函数。你也可以在`--args`标志后传递参数。在这个例子中，第一个参数是 Forge 对象，因此传递已发布的 *Forge Id* 将是准确的。\r\n\r\n```\r\n    \/\/ m1.move\r\n    public entry fun sword_create(forge: &mut Forge, magic: u64, strength: u64, recipient: address, ctx: &mut TxContext)\r\n```\r\n\r\n\r\n第二个和第三个参数是传递一个无符号整数来指定魔法和强度。第三个参数是收件人的地址，我们将使用上面声明的`$RECIPIENT`环境变量作为参数。最后一个参数是 `TxContext`，它将有 Sui CLI 自动注入。\r\n\r\n```\r\n    $ sui client call --package $PACKAGE --module m1 --function sword_create --args 0x954fa256f139900d995d807392a56ddc4442caa9 30 7 $RECIPIENT --gas-budget 3000\r\n    ----- Certificate ----\r\n    Transaction Hash: t9Q5C9tHuxLs9lDgjRn+vRaIWO+gnSiGIeZ+b7EjGEo=\r\n    Transaction Signature: XQTRhTodzSbgUu8q+9YH6BcXwTgdHqz5stEv4OoMBVOg0lC8m3paEADBR13887DQj2u8UICXZIxVujjwBp8OCA==@owVFDiFH79k1g9wNnwlf56i12jPHJv37C9Q99mYM47M=\r\n    Signed Authorities : [k#1584ef2b677d6f5e96b57dac2744e9278aa24e117c7bfd6ac00bf43c1129c7fd, k#e97638a9e10b9cc46d85b81191a60a6a6fcae63dacd811c07502db9c5cfc55b5, k#839e99f8b03f0f5563d6cd9cc39e10a8cf483ad2775aecbb927031370925ef4e]\r\n    Transaction Kind : Call\r\n    Package ID : 0x529830305d84d2335c5394c7f786e01f241c8372\r\n    Module : m1\r\n    Function : sword_create\r\n    Arguments : [\"0x954fa256f139900d995d807392a56ddc4442caa9\", 30, \"\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\\\\u0000\", \"0x76705799eaef88a5378bf616fab44c96e0b8dc05\"]\r\n    Type Arguments : []\r\n    ----- Transaction Effects ----\r\n    Status : Success\r\n    Created Objects:\r\n      - ID: 0x1f403b24ae0fea26c27b16d9b57fd7e88398610d , Owner: Account Address ( 0x76705799eaef88a5378bf616fab44c96e0b8dc05 )\r\n    Mutated Objects:\r\n      - ID: 0x152045509c335d7c0ee9c093513282f375cbe4cd , Owner: Account Address ( 0x76705799eaef88a5378bf616fab44c96e0b8dc05 )\r\n      - ID: 0x954fa256f139900d995d807392a56ddc4442caa9 , Owner: Account Address ( 0x76705799eaef88a5378bf616fab44c96e0b8dc05 )\r\n    ``\r\n\r\n\r\n```\r\n\r\n你的**Forge**对象有一个递增的`sword_created`值，因为我们在[m1.move](https:\/\/github.com\/sigridjineth\/awesome-web3dev\/blob\/main\/sui-example\/sources\/m1.move)文件的`sword_create`函数中声明，在创建一个新剑后，`swords_created`值递增1。看一下Sui Explorer上的示例交易，你可以找到新创建的`objectId`。它肯定是新的Sword对象。\r\n\r\n这就是了! 你已经成功地为Sui编写了合约模块，发布了它，并调用了该函数。在第三章中，我们将研究另一个简单的井字游戏的例子，以进一步了解Move语言及其生态系统。\r\n\r\n原文： https:\/\/medium.com\/dsrv\/my-first-impression-of-sui-move-2-building-a-sword-example-a8af707d3bed"},"author":{"user":"https:\/\/learnblockchain.cn\/people\/11436","address":null},"history":"QmUGD8yAWSPXZ4ZyHsqEBeRmrps91fmWxYrinBCAtCF8fC","timestamp":1665221776,"version":1}