一份优质的学习总结
撰写人背景:Web3 萌新,无 Rust 经验,无 Solidity 经验,没搞过 NFT,没做过 Dapp ref: https://mp.weixin.qq.com/s/4ucdWPhOuJagRGi6GiLcyg
在线调试:
共学课链接:
Web2 to 3 第一站,共学最新合约语言 Move | 706 Creators × NonceGeek
Aptos 是全新的 Layer 1 公链
ff: “diem 前身是 libra ,libra因为监管问题 更名为diem,之后 diem 卖给了meta 然后那伙人就出来搞了 aptos 和 sui 。“ 狗哥:”这种 Web3 狗血剧情请来一百集“
Move 认为 Token 资产是一种很特殊且重要的数据,不应该用普通的数值类型来定义和表示,所以单独创建了 Resource 来定义链上资产。这种方式呈现出三个特性:
综上所述,Move 是一种更加原生且贴合的专用于发行数字资产的编程语言,它实现了程序与数字资产的直接集成。
Modules
Scripts
About  —— public(script)
public fun : 方法可以在任何模块中被调用。public(script) fun:script function 是模块中的入口方法,表示该方法可以通过控制台发起一个交易来调用,就像本地执行脚本一样public entry fun 替代 public(script) fun  public(script) fun init_counter(account: signer){  
        Self::init(&account)  
     }  
     public(script) fun incr_counter(account: signer)  acquires Counter {  
        Self::incr(&account)  
     }Move 里中文注释会报错,so write comments in English.
script{
  use 0x1::Debug  // import other modules, 0x1 is an offical modules library.
  fun main(){
    let num:u64 = 1024;
    Debug::print(&num);
  }
}脚本执行:
Move 不支持字符串类型,只支持 ”数值、bool、和 address “ .
整形(无符号整形)
bool : true or false
address :
原生数据类型,只有一种 ability :drop
struct signer has drop { a: address }Signer 代表发送交易的人
不能在代码中创建,必须通过脚本调用传递
use StarcoinFramework::Signer,是使用标准库下的 Signer module,Signer 是一种原生的类似 Resource 的不可复制的类型,它包含了交易发送者的地址。引入 signer 类型的原因之一是要明确显示哪些函数需要发送者权限,哪些不需要。因此,函数不能欺骗用户未经授权访问其 Resource。具体可参考源码。
API:
address_of ( &Signer ): address   返回 signer 中的地址borrow_address(&Signer): &address   返回 SIgner 中地址的引用script {
  use 0x1::Signer;  
  use 0x1::Debug;
  fun main_signer( sn: signer) {  // if multi params, place signer first
    // let a: signer = @0x42;     // wrong, no constructor for signer
    let addr = Signer::address_of(&sn);
    Debug::print(&addr);
  }
}Signer 是 librarysigner 是数据类型public fun init(account: &signer){  
    move_to(account, Counter{value:0});  
}如上,init 方法参数是一个 &signer, 意味着该方法必须是一个账户合法签名过后才可以调用
API :
| name | description | abort | 
|---|---|---|
| move_ro<T>(&Signer, T) | 将 Resource T 发布给 Signer | if Signer 早已具有了 T | 
| move_from<T>(address): T | 删除 Signer 的 T 资源并返回 | if Signer 没有 T | 
| borrow_global_mut<T>(address): &mut T | 返回 Signer 下 T 的可变引用 | if Signer 没有 T | 
| borrow_global(address): &T | 返回 Signer 下 T 的不可变引用 | if Signer 没有 T | 
| exists <T>(address): bool | 返回 address 下的是否具有 T 类型的资源 | Never | 
① move_to<T>(&signer, T):发布、添加类型为 T 的资源到 signer 的地址下。② exists<T>(address): bool:判断地址下是否有类型为 T 的资源。。③ move_from<T>(addr: address): T ——  从地址下删除类型为 T 的资源并返回这个资源。④ borrow_global< T >(addr: address): &T ——  返回地址下类型为 T 的资源的不可变引用。⑤ borrow_global_mut< T >(addr: address): &mut T —— 返回地址下类型为 T 的资源的可变引用。
// resouces/collection.move
module 0x1::collection{
  use 0x1::Vector;
  use 0x1::Signer;
  struct Item has store, drop {}
  // define resouce,
  // abilities of resouce only store & kkry
  struct Collection has store, key {
    items: vector<Item>,   // vector not Vector
  }
  // move resource to account.
  public fun start_collection(account: &signer){
    move_to<Collection>(account, Collection{
      items: Vector::empty<Item>()
    })
  }
  // judge exists ?
  public fun exists_account(at: address): bool {
    exists<Collection>(at)
  }
}vector<Item> 表示容器里面存放的是 Item 类型;// scripts/test-resource.move
script {
  use 0x1::collection as cl;
  use 0x1::Debug;
  use 0x1::Signer;
  fun test_resource(account: signer) {
    cl::start_collection(&account);
    let addr = Signer::address_of(&account);
    let isExists = cl::exists_account(addr);
    Debug::print(&isExists);
  }
}module 0x1::collection{
  use 0x1::Vector;
  use 0x1::Signer;
  struct Item has store, drop {}
  // define resouce,
  // abilities of resouce only store & kkry
  struct Collection has store, key {
    items: vector<Item>,   // vector not Vector
  }
  // move resource
  public fun start_collection(account: &signer){
    move_to<Collection>(account, Collection{
      items: Vector::empty<Item>()
    })
  }
  // judge exists ?
  public fun exists_account(at: address): bool {
    exists<Collection>(at)
  }
  // modify
  // acquires return resource list
  public fun add_item(account: &signer) acquires Collection{
    // get the resource mutable quote
    let addr = Signer::address_of(account);
    let collection = borrow_global_mut<Collection>(addr);
    Vector::push_back(&mut collection.items, Item{});
  }
  // return resources length 
  public fun size(account: &signer): u64 acquires Collection {
    let addr = Signer::address_of(account);
    let collection = borrow_global<Collection>(addr);
    Vector::length(&collection.items)
  }
}测试:
① 取消 cl::start_collection(&account); 的注释,先创建
② 注释掉 cl::start_collection(&account); ,执行后续代码
script {
  use 0x1::collection as cl;
  use 0x1::Debug;
  use 0x1::Signer;
  fun test_resource(account: signer) {
    cl::start_collection(&account);
    // let addr = Signer::address_of(&account);
    // let isExists = cl::exists_account(addr);
    // Debug::print(&isExists);
    let addr = Signer::address_of(&account);
    cl::add_item(&account);
    let lsize = cl::size(&account);
    Debug::print(&lsize);
  }
}module 0x1::collection{
  use 0x1::Vector;
  use 0x1::Signer;
  struct Item has store, drop {}
  // ...
  // Destroy
  public fun destroy(account: &signer) acquires Collection{
    let addr = Signer::address_of(account);
    // remove collection from address
    let collection = move_from<Collection>(addr);
    // DESTROY:
    let Collection{ items: _ } = collection;
  }
}是一种新的数据类型,可以指定 “能力 ability ”
struct Name [has ${ability}] {   // 首字母大写
  filed1: TYPE1,
  filed2: TYPE2,
  filed3: TYPE3,
}举例—— 学生类型:
//  sources/student.move
address 0x1 {
  module student {
    struct Empty {}
    struct Student {
      id: u64,
      age: u8,
      sex: bool,
    }
    public fun newStudent(id: u64, age: u8, sex: bool): Student {  //
      return Student {
        id: id,    // 或者直接写 id, 跟 js 一样 
        age: age,  // age: age,  means  age ,
        sex: sex,  // sex
      }
    }
  }
}
// scripts/05-struct.move
script {
  use 0x1::student;
  use 0x1::Debug;
  fun main() {
    let stu1 = student::newStudent(10001, 24, true);
    let id = student::getId(stu1);
    Debug::print(&id);
  }
}return 的那一句 不要加分号 ;
函数原型:
[public] fun funcName(param1: Type, ... ): ReturnType{
  // body ...
}[public] 是函数的访问权限,无 Public 则仅限于 module /ˈmɒdjuːl/  内访问。在 Address 处定义一个地址空间 Sender,后续编译时,会自动将地址空间转换为地址 0x1。
// module 0x01::Math{
module Sender::Math{
  public fun sum(a:u64, b:u64): u64 {
    // return a+b  // it's ok .
    a + b       // `return` can be omitted
  }
}
script{
  use 0x1::Debug;
  // use 0x1::Math;
  use Sender::Math;
  fun testSum(a:u64,b:u64){
    let c:u64 = Math::sum(a, b);
    Debug::print(&c);
  }
}我发现传入的参数是 (a:u8, b:u8) , return 的也需要是 u8 类型
if 、 else
public fun max(a:u64, b:u64): u64 {
  // a>b ? a : b
  if (a >= b){
    a
  } else {
    b
  }
}
// 03-text-func.move
script {
  use 0x1::Debug;
  use:Sender::Math as MM;
  fun textMax(a:u64, b:u64) {
    Debug::print(&MM::max(a, b));
  }
}while :
// 计算 1 +3 +5 ...
  public fun sum99() :u64 {
    let idx:u64 = 0;
    let res:u64 = 0;
    while ( idx <= 99) {
      idx = idx + 1;   // not support `+=` ?
      if ( idx % 2 == 0) continue;
      res = res + idx
    };    // Attention  `;`
    return res
}//  sources/Math.move
module Sender::Math {
  public fun sum(a:u64, b:u64):u64 {
    a + b  // `+` has higher priority than `as`
  } 
  public fun sum2(a:u64, b:u8):u64 {
    a + (b as u64)  // `+` has higher priority than `as`
  } 
}
// scripts/02-test-as.move
script {
  use 0x1::Debug;
  use Sender::Math;
  fun testSum(a:u64, b:u8) {
    let c:u64 = Math::sum2(a, b);
    Debug::print(&c)
  }
}as 还可用于 module 令命名:
💡 use Sender::Math as SM;
script {
  use 0x1::Debug;
  use Sender::Math as SM;
  fun testSum(a:u64, b:u8) {
    let c:u64 = SM::sum2(a, b);
    Debug::print(&c)
  }
}作用域和生命周期
全局常量:
const
module内 或 script 内
首字母 A-Z
局部变量:
地址类型,如果放到表达式里面,必须用 @ 符号
script {
  use 0x1::Debug;
  use Sender::Math as SM;
  fun testSum() {
    // let c:u64 = SM::sum2(a, b);
    //Debug::print(&SM::sum99());
    let dd:address = @Sender;
    Debug::print(&dd);
  }
}abort 断言( abort v. 退出, 舍弃)
用法 :abort 0
打断当前脚本的执行,打断完成后,脚本会恢复初始的状态。
assert 断言
assert!(condition, code)如下示例,Debug::print(&temp); 没有在 abort(1) 后接着执行。
    let temp:u8 = 10;
    if (temp == 10)  abort(1);
    Debug::print(&temp);使用 assert 同样可达到如上的效果:
loginStatus == false, 则抛出 code == 401 Unauthorized    // let loginStatus:bool = false;
    assert!(loginStatus, 401);
    Debug::print(&loginStatus);元组:
let (a, _) = (1, false); // _  is 占位符 // let (a:u8, b:bool) = (1, false);  // Error 不需要写类型...  迷惑 ...
    let (a, b) = (1, false);C++ 经常用
&引用去替代指针*。 因为指针会直接暴露地址,但是 quote 不会。
引用 quote :
Move 的 2 种引用方式:
不可变引用 & ( 只读,不可修改 —— 安全!
可变引用 &mut
&mut* 代表解除引用,指向引用对应的值使用 quote 实现交换数值:
最大用处:间接赋值
// modules/Math.move
module Sender::Math {
  // ..
  public fun swap(a:&mut u64, b:&mut u64) { // no return
    let temp = *a;
    *a = *b;
    *b = temp;
  }
}
// scripts/02-test.move
script {
  use 0x1::Debug;
  use Sender::Math as SM;
  fun testSum() {
    let (a, b) =  (1, 10);
    SM::swap(&mut a, &mut b);
    Debug::print(&a);
    Debug::print(&b);
  }
}&mut a 将 a 解构为 可变引用fun swap 使用 ( a:&mut u64, ... ) 来接受可变引用*a 解除引用,此时 *a 指向引用对应的值即 1**fun funcName<Type>(x: Type)**下面实现一个比较 low 的 show 函数,打印不同类型:
module Sender::Math {
  use 0x1::Debug;
  // ..
  public fun show<T>(a: T) {
    Debug::print(&a);
  }
}执行后发现报错:
The type 'T' does not have the ability 'drop’
加上 drop ability,Compile passed :
  public fun show<T:drop>(a: T) {
    Debug::print(&a);
  }后续使用结构体泛型实现进阶版的能力。
// modules/user.move
address 0x1 {
  module user {
    struct Student has drop {
      id: u64,
      age: u8,
      sex: bool,
    }
    struct User<T1, T2> has drop {
      id: T1,
      age: T2,
    }
    public fun newUser<T1, T2>(id: T1, age: T2): User<T1, T2> {
      return User { id , age }
    }
  }
}
// scripts/test.move
script {
  use 0x1::user;
  use 0x1::Debug;
  fun main() {
    // let user1 = user::newUser(10001, 23);  // auto cognize
    let user1 = user::newUser(10001: u64, 23: u8);  // good
    Debug::print(&user1)
  }
}vector 是 Move 提供的泛型容器
let str_ = b"hello";             // cast "hello" as Vector of Ascii
let v2 = Vector::empty<u64>();
Vector::push_back(&mut v2, 10);    let str_ = b"Hello World";
    Debug::print(&str_);
    SM::show(str_);   // Attention No &str_ , but str_ , why ?script {
  use 0x1::Debug;
  use Sender::Math as SM;
  use 0x1::Vector;   // maybe lowcase as 'vector', need test ?
  fun testSum() {
    let v2 = Vector::empty<u64>();
    Vector::push_back<u64>(&mut v2, 1);
    Vector::push_back<u64>(&mut v2, 10);
    Debug::print(&v2);
  }
}
终止:即会不会产生 abort 类型的终止异常
| empty<T>(): vector<T> | |
|---|---|
| singleton<T>(t: T):vector<T> | |
| borrow<T>(v: &vector<T>, i:u64): &T | 返回在 index i 处对 T 的不可变引用 | 

Move 类型的几种能力
用 key,store 修饰,则表示它不能被复制,也不能被丢弃或重新使用,但是它却可以被安全地存储和转移。
struct Counter has key, store {  
 value:u64,  
}蓝色:整数、布尔等原生类型天然具有 copy、drop、store 能力。
public fun show<T:drop>(val: T) {
  Debug::print(&val);
}在调用 show 函数时, val 这个变量就归属于这个函数了, 函数执行完毕后,val 这个变量的生命周期就结束了,就需要被丢弃掉,所以编译器会要求 show 传递的类型 T 具有被 drop 的能力。
每个变量都有所有权
所有权的主人(属主)是 作用域
  fun main() {
    let tmp = 10;
    Math::show(move tmp);
    Math::show(tmp);main 函数将 tmp 的属主转移给了 show 函数, main 就没有了变量的所有权。
后面 Math::show(tmp); 再次调用就会报错。
如下这 2 种写法都是没问题的。
  fun main() {
    let tmp = 10;
    Math::show(move tmp);
    Math::show(tmp);
let tmp = 10;
    Math::show(copy tmp);
    Math::show(copy tmp);即 https://playground.pontem.network/ 的示例代码。
scripts 文件夹包含使用存储模块的示例。
Storage.move 利用 Move 语言泛型将用户帐户下的任何类型的数据存储为资源(resource)。
module Sender::Storage {
  use Std::Signer;
  //The resource would store `T` (generic) kind of data under `val` field.
  struct Storage<T: store> has key {
    val: T,
  }
  // Store the `val` under user account in `Storage` resource.
  // `signer` - transaction sender.
  // `val` - the value to store.
  public fun store<T: store>(account: &signer, val: T){
    // Get address of `signer` by utilizing `Signer` module of Standard Library
    let addr = Signer::address_of(account);
    // Check if resource isn't exists already, otherwise throw error with code 101.
    assert!(!exists<Storage<T>>(addr), 101);
    // Create `Storage` resource contains provided value.
    let to_store = Storage{
      val,  
    };
    // 'Move' the Storage resource under user account,
    // so the resource will be placed into storage under user account.
    move_to(account, to_store);
  // Get stored value under signer account.
  // `signer` - transaction sender, which stored value.
  public fun get<T: store>(account: &signer): T acquires Storage{
    let addr = Signer::address_of(account);
    assert!(exists<Storage<T>>(addr), 102);
    let Storage { val } = move_from<(Storage)<T>>(addr);
    return val
  }
} public fun store :
use Std::Signer;
struct Storage :
public fun store :
T: store :要求 T 类型拥有 store 的能力。signer 是交易的发送者, val 是待存储的数据;Signer 模块获取  signer 的地址,let to_store = Storage{ val, }; 即 {val: val ,} 跟 js 的 Object 一样move_to(account, to_store); 资源存储。
public fun get
acquires 关键字let Storage { val } 主要不是 let { val }测试一下上面写的 Storage.move
在 ./scripts/store_bytes.move中,这个脚本可以存储 bytes 数据:
script {
  use Sender::Storage;
  // Script to store `vector<u8>` (bytes).
  fun store_bytes(account: signer, val: vector<u8>) {
    Storage::store(&account, val);
  }
}这一个在没有 Aptos 框架的情况下,在 Move 中实现 Balance 和 Coin 逻辑的示例
module Sender::Coins {
  use Std::Signer;
  // In Move, all struct objects can have "abilities" from a hardcoded set of {copy, key, store, drop}.
  struct Coins has store { val: u64 }
  // resource object which is marked by `key` ability. It can be added to the Move Storage directly.
  struct Balance has key {
    // It contains an amount of `Coins` inside.
    coins: Coins
  }
  // Error Code Definition
  // 1. when `Balance` doesn't exist on account.
  const ERR_BALANCE_NOT_EXISTS: u64 = 101;
  // 2. Error when `Balance` already exists on account.
  const ERR_BALANCE_EXISTS: u64 = 102;
      // In Move you cannot directly create an instance of `Coin` from script,
    // Instead you need to use available constructor methods. In general case, those methods could have some permission
    // restrictions, i.e. `mint(acc, val: u64)` method would require `&signer` of coin creator as the first argument
    // which is only available in transactions signed by that account.
    //
    // In the current example anyone can mint as many coins as want, but usually you can add restrictions (MintCapabilities),
    // for details look at standard library (links in the herd of the file).
    public fun mint(val: u64): Coins {
      let new_coin = Coins{ val };
      new_coin
    }
    /// If struct object does not have `drop` ability, it cannot be destroyed at the end of the script scope,
    /// and needs explicit desctructuring method.
    public fun burn(coin: Coins) {
      let Coins{ val: _ } = coin;
    }
    public fun create_balance(acc: &signer) {
      let acc_addr = Signer::address_of(acc);
      assert!(!balance_exists(acc_addr), ERR_BALANCE_EXISTS);
      let zero_coins = Coins{ val: 0 };
      move_to(acc, Balance { coins: zero_coins});
 }
    // Check if `Balance` resource exists on account.
    public fun balance_exists(acc_addr: address): bool {
        exists<Balance>(acc_addr)
    }
    // Create `Balance` resource to account.
    // In Move to store resource under account you have to provide user signature (`acc: &signer`).
    // So before starting work with balances (use `deposit`, `withdraw`), account should add Balance resource
    // on it's own account.
    public fun create_balance(acc: &signer) {
        let acc_addr = Signer::address_of(acc);
        assert!(!balance_exists(acc_addr), ERR_BALANCE_EXISTS);
        let zero_coins = Coins{ val: 0 };
        move_to(acc, Balance { coins: zero_coins});
    }
    // Check if `Balance` resource exists on account.
    public fun balance_exists(acc_addr: address): bool {
        exists<Balance>(acc_addr)
    }
 // Deposit coins to user's balance (to `acc` balance).
    public fun deposit(acc_addr: address, coin: Coins) acquires Balance {
        assert!(balance_exists(acc_addr), ERR_BALANCE_NOT_EXISTS);
        let Coins { val } = coin;
        let balance = borrow_global_mut<Balance>(acc_addr);
        balance.coins.val = balance.coins.val + val;
    }
    // Withdraw coins from user's balance (withdraw from `acc` balance).
    public fun withdraw(acc: &signer, val: u64): Coins acquires Balance {
        let acc_addr = Signer::address_of(acc);
        assert!(balance_exists(acc_addr), ERR_BALANCE_NOT_EXISTS);
        let balance = borrow_global_mut<Balance>(acc_addr);
        balance.coins.val = balance.coins.val - val;
        Coins{ val }
    }
    // Get balance of an account.
    public fun balance(acc_addr: address): u64 acquires Balance {
        borrow_global<Balance>(acc_addr).coins.val
    }
}在 Move 中,所有 struct object 都可以从 hardcoded set 的{copy, key, store, drop} 中获得 “abilities” 。
Balance Resource 代表存储在帐户下的用户余额,由 key 能力标记。它可以直接添加到移动存储中。
可以看到, Balance struct 里面调用了其上一步生成的 Coin( 可能是为了拓展性?)
fun burn :  如果 struct 对象没有 drop 能力,它就不能在脚本范围结束时被销毁,所以需要显式的解构方法。
let Coins{ val: _} = coin :这里用 _ 来接受原来的 coin 信息,也就变相地对原来的 Coin 进行了销毁。create_balance :创建  Balance 资源到帐户,在帐户下存储资源时,您必须提供user signature(acc: &signer),所以在使用 deposit, withdraw 等方法之前,account should add Balance resource on it's own account.
move_to(acc, Balance { coins: Coins{ val: 0 }}); :创建余额 即把一个余额为 0 的对象添加进去,完成创建余额的操作。public fun deposit   :存款,
borrow_global_mut<Balance>(acc_addr); 取回一个可变对象做修改—— 增加余额。public fun withdraw  :提现(取款)
return Coins{ val } :提取的金额需要返回。对比 deposit 和 withdraw :
deposit(acc_addr: address) 传入的是一个 addresswithdraw(acc: &signer, val: u64)  传入的是一个 signermint.move
script {
    use Std::Signer;
    use Sender::Coins;
    // Mint new coins and deposit to account.
    fun mint(acc: signer, amount: u64) {
        let acc_addr = Signer::address_of(&acc);
        let coins = Coins::mint(amount);
        if (!Coins::balance_exists(acc_addr)) {
            Coins::create_balance(&acc);
        };
        Coins::deposit(acc_addr, coins);
        assert!(Coins::balance(acc_addr) == amount, 1);
    }
}转账:从 acc 账号 withdraw 出 Coins,然后 deposit 存到 recipient 账号中去。
script{
  use Sender::Coins;
  // Script to mint coins and deposit coins to account. 
  // Recipient should has created `Balance` resources on his account.
  fun mint_and_deposit(acc: signer, recipient: address, amount: u64){
    let coins = Coins::withdraw(&acc, amount);
    Coins::deposit(recipient, coins);
  }
}测试函数:mint_and_deposit(0x41, 0x51, 25)
但是贸然这样执行会报错:
因为 0x51 还没有余额 Balance。
mint_coins(0x51, 0) 添加余额

然后就可以执行转账了:
mint_and_deposit(0x41, 0x51, 25)
sources/AptosCoin.move有点难,先不搞了
球星卡信息 - 功能:
address 0x1 {
    module football {
        use Std::Signer;
        // error code
        const STAR_ALREADY_EXISTS: u64 = 100;
        const STAR_NOT_EXISTS: u64 = 101;
        struct FootBallStar has key {
            name: vector<u8>,
            country: vector<u8>,
            position: u8,
            value: u64
        }
        public fun newStar(
            name: vector<u8>,
            country: vector<u8>,
            position: u8
            ): FootBallStar {
            FootBallStar {
                name, country, position,
                value: 0
            }
        }
        public fun mint(recipient: &signer, card: FootBallStar) {
            let addr = Signer::address_of(recipient);
            assert!(!exists<FootBallStar>(addr), STAR_ALREADY_EXISTS);
            move_to<FootBallStar>(recipient, card);
        }
        // Query, dont need signer, address is ok.
        public fun get(owner: address, ): (vector<u8>, u64) acquires FootBallStar {
            // query dont need assert
            let card = borrow_global<FootBallStar>(owner);
            (card.name, card.value)  // return a tuple
        }
        // modify need to get the real data, so acquires ..
        public fun setPrice(owner: address, price: u64) acquires FootBallStar  {
            assert!(exists<FootBallStar>(owner), STAR_NOT_EXISTS);
            let card = borrow_global_mut<FootBallStar>(owner);
            card.value = price;
        }
        // in every transaction, price will go rise $20
        public fun transfer(owner: address, recipient: &signer) acquires FootBallStar {
            assert!(exists<FootBallStar>(owner), STAR_NOT_EXISTS); // is owner hold it ?
            let card = move_from<FootBallStar>(owner);
            card.value = card.value + 20;
            let reci_address = Signer::address_of(recipient);
            assert!(!exists<FootBallStar>(owner), STAR_ALREADY_EXISTS); // is recipient hold it ?
            move_to<FootBallStar>(recipient, card);
        }
    }
} 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!