Skip to content

Latest commit

 

History

History
 
 

move-dapp

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

0x01 最小必要知识

1.1 OverView

// TODO

1.2 Setup a node

下载最新的发行版 Starcoin 节点程序(MacOS 将其拷贝至/usr/local/bin目录即可):

https://github.com/starcoinorg/starcoin/releases

在此以Starcoin网络为基础,展现如何启动一个节点:

https://starcoinorg.github.io/starcoin-cookbook/docs/getting-started/setup/

太长不看版——关键命令合集:

# 启动一个本地 dev 节点
$ starcoin -n dev
# 启动一个本地 dev 节点的同时打开控制台,-d 参数可以确保每次打开控制台时都保有历史的数据而不是重开
$ mkdir [folder_name]
$ cd [folder_name]
$ pwd
$ starcoin -d [path_to_your_data_folder] -n dev console

starcoin 控制台命令:

# 指定账户获得测试代币
starcoin% dev get-coin 0xb7c46353c6c0e3a2559d5b12cad981e4 -v 100STC
# 账户列表
starcoin% account list
# 单一账户情况查看
starcoin% account show 0xb7c46353c6c0e3a2559d5b12cad981e4
# 创建新账户
starcoin% account create -p [pwd]

新账户记得同上获取测试代币STC,否则无法在该账户下进行任何需要消耗STC的操作,如:部署合约

1.3 Contract Framework

最小可实践例子:

https://github.com/starcoinorg/starcoin-cookbook/blob/main/examples/my-counter

module MyCounterAddr::MyCounter {
     use StarcoinFramework::Signer;

     struct Counter has key, store {
        value:u64,
     }
     public fun init(account: &signer){
        move_to(account, Counter{value:0});
     }
     public fun incr(account: &signer) acquires Counter {
        let counter = borrow_global_mut<Counter>(Signer::address_of(account));
        counter.value = counter.value + 1;
     }

     public(script) fun init_counter(account: signer){
        Self::init(&account)
     }

     public(script) fun incr_counter(account: signer)  acquires Counter {
        Self::incr(&account)
     }
}

MyCounter 源码分析

module 是发布在特定地址下的打包在一起的一组函数和结构体。使用script时需要与已发布的module或标准库一起运行,而标准库本身就是在 0x1 地址下发布的一组module。

module MyCounterAddr::MyCounter{ } 则在该MyCounterAddr地址下(对应Move.toml下的MyCounterAddr = "0xb7c46353c6c0e3a2559d5b12cad981e4")创建一个module。

use StarcoinFramework::Signer,是使用标准库下的Signer module,Signer 是一种原生的类似 Resource 的不可复制的类型,它包含了交易发送者的地址。引入signer类型的原因之一是要明确显示哪些函数需要发送者权限,哪些不需要。因此,函数不能欺骗用户未经授权访问其 Resource。具体可参考源码

struct Counter has key, store {
        value:u64,
     }

使用struct定义了一个叫做Counter的结构体,同时被 key,store两种限制符修饰,Move的类型系统灵活,每种类型都可以被四种限制符所修饰。这四种限制符我们称之为 abilities,它们定义了类型的值是否可以被复制、丢弃和存储。 这四种 abilities 限制符分别是: Copy, Drop, Store 和 Key。

它们的功能分别是:

  • Copy - 被修饰的值可以被复制。
  • Drop - 被修饰的值在作用域结束时可以被丢弃。
  • Key - 被修饰的值可以作为键值对全局状态进行访问。
  • Store - 被修饰的值可以被存储到全局状态。

这里用key、store修饰,则表示它不能被复制,也不能被丢弃或重新使用,但是它却可以被安全地存储和转移。

下面则是定义的方法,

public fun init(account: &signer){
    move_to(account, Counter{value:0});
}
public fun incr(account: &signer) acquires Counter {
    let counter = borrow_global_mut<Counter>(Signer::address_of(account));
    counter.value = counter.value + 1;
}

定义格式则是:

public fun 函数名(参数:参数类型){ }

move函数默认是私有函数,只能在定义它们的模块中访问。关键字 public 将更改函数的默认可见性并使其公开,即可以从外部访问。

init方法参数是一个&signer,意味着该方法必须是一个账户合法签名过后才可以调用,move_to则是move的一个原语,作用是发布、添加Counter资源到 signer 的地址下。Move的账户模型,code和data是存储在一个账户地址下的。

下面是列举的常用原语

  • move_to< T >(&signer, T):发布、添加类型为 T 的资源到 signer 的地址下。
  • move_from< T >(addr: address): T - 从地址下删除类型为 T 的资源并返回这个资源。
  • borrow_global< T >(addr: address): &T - 返回地址下类型为 T 的资源的不可变引用。
  • borrow_global_mut< T >(addr: address): &mut T - 返回地址下类型为 T 的资源的可变引用。
  • exists< T >(address): bool:判断地址下是否有类型为 T 的资源。。

incr方法参数也是一个&signer,意味着该方法必须是一个账户合法签名过后才可以调用,

关键字 acquires,放在函数返回值之后,用来显式定义此函数获取的所有 Resource。

Signer::address_of(account) 从签名者中拿到address

borrow_global_mut上面有介绍到,可变借用到address下到resource Counter,然后将Counter结构体下的value进行+1操作。

这下面的两个方法则是script方法,它与上面两个函数有什么区别呢?

  • public fun : 方法可以在任何模块中被调用。
  • public(script) fun:script function 是模块中的入口方法,表示该方法可以通过控制台发起一个交易来调用,就像本地执行脚本一样

下个版本的 Move 会用 public entry fun 替代 public(script) fun

Self则是代表自身module。

  public(script) fun init_counter(account: signer){
        Self::init(&account)
     }

     public(script) fun incr_counter(account: signer)  acquires Counter {
        Self::incr(&account)
     }

1.3.1 编译

下载第一个实例的源码:

$ git clone [email protected]:WeLightProject/Web3-dApp-Camp.git
$ cd Web3-dApp-Camp/move-dapp/my-counter

Move的包管理工具为Move Package Manager(mpm),它类似于Rust的Cargo或者Node的NPM。 可以通过mpm package new my-counter来创建一个新项目,典型的目录结构为:

my-counter
├── Move.toml
└── sources
    └── MyCounter.move 
  • sources用来存档Move的模块,它类似于与Java中的类文件。
  • Move.toml-用来存放配置文件:包括包的原数据、依赖和命名地址。
  • 上述文件构成一个Move包(Move Package) 更详细的Move包管理参考文档

修改move.toml中的地址为你用来部署的地址。

image-20220727123922351

编译:

$ mpm release

接下来会在release文件夹中,看到你编译好的二进制文件。

image-20220727124134402

1.3.2 控制台部署

在 Starcoin Console 中执行如下命令即可部署:

starcoin% dev deploy [path to blob] -s [addr] -b

-s 即--sender,-b即--blocking,表示阻塞等待命令执行完成

如果遇到账户被锁,用 unlock命令解锁即可。

account unlock [addr] -p [pwd]

其中pwd即是在1.2中创建的密码。

部署成功后能看到:

image-20220727124625807

💡需要注意的是,在Move中代码存储在个人的地址上,而非像以太坊那样的公共地址上。因此合约部署后并不会创建新地址,当我们想要调用合约时需要采用部署合约人的地址+合约名来调用改合约。

1.3.3 控制台调用

https://starcoinorg.github.io/starcoin-cookbook/docs/move/interacting-with-the-contract

  1. 调用 init_counter 脚本函数来初始化资源。
starcoin% account execute-function --function {MyCounterAddr-in-Move.toml}::MyCounter::init_counter -s 0x23dc2c167fcd16e28917765848e189ce -b # call the script fun

# starcoin% dev call --function 0x1168e88ffc5cec53b398b42d61885bbb::EthSigVerifier::verify_eth_sig --arg x"90a938f7457df6e8f741264c32697fc52f9a8f867c52dd70713d9d2d472f2e415d9c94148991bbe1f4a1818d1dff09165782749c877f5cf1eff4ef126e55714d1c" --arg x"29c76e6ad8f28bb1004902578fb108c507be341b" --arg x"b453bd4e271eed985cbab8231da609c4ce0a9cf1f763b6c1594e76315510e0f1" # call the fun(no script)

其中:

  • {MyCounterAddr-in-Move.toml}::MyCounter::init_counter为完整的函数链上地址,包括合约所在地址+包名+函数名。
  • -s 即--sender,-b即--blocking,表示阻塞等待命令执行完成
  1. 查看Counter资源
starcoin% state get resource 0x23dc2c167fcd16e28917765848e189ce 0x23dc2c167fcd16e28917765848e189ce::MyCounter::Counter

在Move中合约的数据被称为资源(resource),由于读取数据不改变链上状态,因此不需要-s -b,不会执行交易,也不消耗状态。

感兴趣的同学可以试着调用incr_counter,并再次查看Counter是否+1。

1.4 Your First Move dApp / Starcoin dApp

下载starcoin-test-dapp-react

$ git clone [email protected]:starcoinorg/starcoin-test-dapp-react.git

1.4.1 极速启动

$ yarn
$ yarn start

image-20220729090935566

1.4.2 配置 Starmask

Starmask 是和 Metamask 一样的浏览器插件。

因此,我们可以使用相同的方式去配置:

  • 确保节点 RPC 端口能访问
$ lsof -i:9851

image-20220729092714792

  • 添加端口为9851 的本地网络

image-20220729092609290

  • 在 Starmask 中导入测试账户

控制台中的导出私钥命令:

starcoin% account export 0x23dc2c167fcd16e28917765848e189ce

然后通过导入账户功能导入:

image-20220729092931382

  • 余额显示

此时 Starmask、Starcoin Console 与 RPC 接口所查询到同一账户的 STC 余额应该一致。

其中 Starcoin RPC 的 Postman Collection 链接如下:

https://www.postman.com/starcoinorg/workspace/starcoin-blockchain-api/request/13565741-fa891c12-6684-452a-86cb-6d938fc72f4e

image-20220729093042286

image-20220729093116486

image-20220729093132604

1.4.3 修改调用代码

调整 demo 中的合约。首先我们定位到相关代码处:

src/app.jsx

找到标签 {/* Contracts Function */}

{/* Contracts Function */}
                <div className="mt-4 shadow-2xl rounded-2xl border-2 border-slate-50 p-2">
                  <div className="font-bold">Contract Function</div>
                  <div
                    className="mt-4 rounded-2xl bg-blue-900 flex justify-center text-white p-4 font-bold cursor-pointer hover:bg-blue-700 duration-300"
                    onClick={() => {
                      makeModal({
                        children: ({ onClose }) => {
                          return (
                            <>
                              <Mask onClose={onClose} />
                              <Account />
                            </>
                          );
                        },
                      });
                    }}
                  >
                    0x1::TransferScripts::peer_to_peer_v2
                  </div>
                </div>

0x1::TransferScripts::peer_to_peer_v2改为Init_counter

定位到src/modal.jsx,修改!! KEY PLACE为相应的 func:

try {
      // !! KEY PLACE
      const functionId = "0x2fe27a8d6a04d57583172cdda79df0e9::MyCounter::init_counter";
      // !! KEY PLACE
      const strTypeArgs = [];
      
      const tyArgs = utils.tx.encodeStructTypeTags(strTypeArgs);
      const sendAmount = parseFloat(amount, 10);
      if (!(sendAmount > 0)) {
        window.alert("Invalid sendAmount: should be a number!");
        return false;
      }
      const BIG_NUMBER_NANO_STC_MULTIPLIER = new BigNumber("1000000000");
      const sendAmountSTC = new BigNumber(String(sendAmount), 10);
      const sendAmountNanoSTC = sendAmountSTC.times(
        BIG_NUMBER_NANO_STC_MULTIPLIER
      );
      const sendAmountHex = `0x${sendAmountNanoSTC.toString(16)}`; // Multiple BcsSerializers should be used in different closures, otherwise, the latter will be contaminated by the former.
      const amountSCSHex = (function () {
        const se = new bcs.BcsSerializer();
        // eslint-disable-next-line no-undef
        se.serializeU128(BigInt(sendAmountNanoSTC.toString(10)));
        return hexlify(se.getBytes());
      })();

      // !! KEY PLACE
      const args = [];

      // const args = [arrayify(account), arrayify(amountSCSHex)];

      const scriptFunction = utils.tx.encodeScriptFunction(
        functionId,
        tyArgs,
        args
      );

1.4.4 调用函数

打开http://localhost:3000,即可成功调用智能合约的函数。

image

image

1.4.5 操作资源

在Move中合约的变量被称为资源,比如Counter,资源只能通过脚本间接来调用合约中的内容,而不能直接操作资源。本节的完整代码参见MyCounter实例。本节完成后共需要提交三个截图,在下文予以说明。 1.首先实现Counter资源的读取. 将上一节的Modal.js中的内容覆盖掉,主要增加了三个按钮Get CounterIncr_counterIncr_counter_by;其中app.jsx中的下面这行函数调用了读取Counter资源的工具函数。

  const getCounter = async () => {
    let res = await getResource(COUNTER_ADDRESS, COUNTER_RESOURCE_ID)
    setCounter(res.value)
  }

下面重点实现上面这个函数,创建src/txs/counter.tx.js:

import { utils, bcs, encoding, } from "@starcoin/starcoin"
import { starcoinProvider } from "../app";
import { arrayify, hexlify } from '@ethersproject/bytes'

export async function getResource(address, functionId) {
    const resourceType = `${address}::${functionId}`
    const resource = await starcoinProvider.getResource(address, resourceType)
    console.log(resource)
    return resource
}

通过getResource读取,其参数为资源所在账户的地址(回忆一下Move中资源存储在个人账户而非公共合约账户)和完整的资源地址,它由账户地址+module名+资源名构成,这里funcitonIdmodule名+资源名组合了起来。

为了方便后续对资源的读取,我们单独创建/src/txs/config.js定义所有相关的地址和functionId:

export const COUNTER_ADDRESS = "0x07Ffe973C72356C25e623E2470172A69"
export const COUNTER_RESOURCE_ID = "MyCounter::Counter"
export const INCR_COUNTER_FUNC_NAMW = "MyCounter::incr_counter"
export const INCR_COUNTERBY_FUNC_NAME = "MyCounter::incr_counter_by"

修改其中的COUNTER_ADDRESS为您的账户地址。

为了避免报错在src/modal.jsx最下面加入如下两行:

export const Counter = () => {}
export const IncreaseCounterBy = () => {}

OK,现在点击Get Counter可以得到以下截图(截图任务1):

  1. 实现incr 这包括两部分:合约的对Counter资源的修改和前端显示。 首先实现合约调用,在/src/txs/conter.tx.js中增加以下内容:
export async function executeFunction(address, functionName, strTypeArgs = [], args = []) {

    const functionId = `${address}::${functionName}`;
    const tyArgs = utils.tx.encodeStructTypeTags(strTypeArgs);
    if (args.length > 0) {
        args[0] = (function () {
            const se = new bcs.BcsSerializer();
            se.serializeU64(BigInt(args[0].toString(10)));
            return hexlify(se.getBytes());
        })();
    }
    args = args.map(arg => arrayify(arg))
    const scriptFunction = utils.tx.encodeScriptFunction(functionId, tyArgs, args);

    const payloadInHex = (() => {
        const se = new bcs.BcsSerializer();
        scriptFunction.serialize(se);
        return hexlify(se.getBytes());
    })();

    const txParams = {
        data: payloadInHex,
    };

    const transactionHash = await starcoinProvider
        .getSigner()
        .sendUncheckedTransaction(txParams);
    return transactionHash
}

Starcoin中合约的调用分为四部分:

  1. 链接钱包(在app.jsx中我们已经连好了,只需要引入starcoinProvider)
  2. 生成交易内容
  3. 调用合约
  4. 等待交易确认 现在关注的是交易内容生成,它主要包括三部分:
  • functionId:函数签名,本例为账户地址+模块名+函数名
  • tyArgs:这个比较晦涩,实际上定义的是转账的token类型而非参数类型,不需要转账时设置为[]即可,需要转账时设置为0x01::STC::STC
  • args: 函数的参数,本例为[],后面我们会展示包含一个参数的例子

调用交易则是最下面几行代码,调用后会返回交易的hash:

    const transactionHash = await starcoinProvider
        .getSigner()
        .sendUncheckedTransaction(txParams);

其次交易状态读取,这需要实现app.jsx中的Counter组件,删掉原来的: export const Counter = () => {}并在Modal.js最下面加入一下内容:

import { executeFunction } from "./txs/counter.tx";
import { COUNTER_ADDRESS, INCR_COUNTER_FUNC_NAMW, INCR_COUNTERBY_FUNC_NAME } from "./txs/config";
...
export const Counter = (props) => {
  const [hash, setHash] = useState('')
  const [txStatus, setTxStatus] = useState()
  useEffect(() => {
    const incr_counter = async () => {
      let txHash = await executeFunction(COUNTER_ADDRESS, INCR_COUNTER_FUNC_NAMW)
      setHash(txHash)
      let timer = setInterval(async () => {
        const txnInfo = await starcoinProvider.getTransactionInfo(txHash);
        setTxStatus(txnInfo.status)
        if (txnInfo.status === "Executed") {
          clearInterval(timer);
        }
      }, 500);
    }
    incr_counter()

  }, [])

  const { isShow } = useFadeIn();
  return <div className={classnames(
    "fixed top-2/4 left-2/4 -translate-x-2/4 -translate-y-2/4 rounded-2xl shadow-2xl w-3/4 p-6 bg-white duration-300",
    isShow ? "opacity-100 scale-100" : "opacity-0 scale-50"
  )}>
    {hash && (
      <div className="text-center mt-2 text-gray-500 break-all">
        Transaction Hash: {hash}
      </div>

    )}
    {txStatus ? <div style={{ "textAlign": "Center" }}>{txStatus}</div> : null}
  </div>
}
export const IncreaseCounterBy = () => {}

我们不断轮询读取交易状态await starcoinProvider.getTransactionInfo(txHash);,知道交易成功。点击Incr_counter交易成功后应该看到如下界面(截图任务2):

  1. 实现带参数的资源调用 我们的目的是增加一个函数,可以输入需要增加的值X,然后对Counter加上X。首先修改上节的MyCounter.move增加以下代码:
     public fun incr_by(account: &signer, increasement: u64) acquires Counter {
        let counter = borrow_global_mut<Counter>(Signer::address_of(account));
        counter.value = counter.value + increasement;
     }
     
     public(script) fun incr_counter_by(account: signer,increasement: u64)  acquires Counter {
        Self::incr_by(&account, increasement)
     }

然后到my-counter文件夹下进行编译mpm release, 再进行部署,注意一定要先把dev测试网启动了,账户锁定需要解锁:

dev deploy [path to blob] -s [addr] -b

然后实现对改合约的调用,由于我们的counter.tx.js是一个通用的合约调用函数,因此不需要再针对incr_counter_by单独实现调用函数。只需要修改/src/Modal.jsx中的IncreaseCounterBy函数的内容,传入正确的合约调用参数即可:

export const IncreaseCounterBy = (props) => {
  const [plus, setPlus] = useState(2)
  const [txHash, setTxHash] = useState()
  const [disabled, setDisabled] = useState(false)
  const [txStatus, setTxStatus] = useState()
  const handleCall = () => {
    setDisabled(true)
    setTxStatus("Pending...")
    const incr_counter_by = async () => {
      const tyArgs = []
      const args = [parseInt(plus)]
      let txHash = await executeFunction(COUNTER_ADDRESS, INCR_COUNTERBY_FUNC_NAME, tyArgs, args)
      setTxHash(txHash)
      let timer = setInterval(async () => {
        const txnInfo = await starcoinProvider.getTransactionInfo(txHash);
        setTxStatus(txnInfo.status)
        if (txnInfo.status === "Executed") {
          setDisabled(false)
          clearInterval(timer);
        }
      }, 500);
    }
    incr_counter_by()

  }
  const { isShow } = useFadeIn();

  return (
    <div
      className={classnames(
        "fixed top-2/4 left-2/4 -translate-x-2/4 -translate-y-2/4 rounded-2xl shadow-2xl w-3/4 p-6 bg-white duration-300",
        isShow ? "opacity-100 scale-100" : "opacity-0 scale-50"
      )}
    >
      <div className="font-bold">To</div>
      <div className="mt-2 mb-2">
        <input
          type="text"
          className="focus:outline-none rounded-xl border-2 border-slate-700 w-full p-4"
          value={plus}
          onChange={(e) => setPlus(e.target.value)}
        />
      </div>
      <div
        className="mt-6 p-4 flex justify-center font-bold bg-blue-900 text-white rounded-lg hover:bg-blue-700 cursor-pointer"
        onClick={handleCall}
        disabled={disabled}
      >
        CALL
      </div>
      {txHash && (
        <div className="text-center mt-2 text-gray-500 break-all">
          Transaction: {txHash}
        </div>
      )}
      {txStatus ? <div style={{ "textAlign": "Center" }}>{txStatus}</div> : null}
    </div>
  );
};

此时点击Incr_counter_by按钮,会弹出如下交易界面(截图任务3): 。等待交易成功即可。

本节的内容有点多,感谢大家follow到了最后,希望大家耐心完成并理解上述内容。

0x02 Move Contract + dApp 案例

2.1 MyLibrary

2.1.1 Types with Abilities

https://move-book.com/advanced-topics/types-with-abilities.html#types-with-abilities

https://move-book.com/cn/advanced-topics/types-with-abilities.html

Move 的类型系统非常灵活,每种类型都可以定义四种能力(abilities)。它们决定了类型的值是否可以被「使用、丢弃和存储」。

这四种 abilities 能力分别是: Copy, Drop, Store 和 Key。

它们的功能分别是:

  • Copy - 值可以被复制
  • Drop - 在作用域(Scope)结束时值可以被丢弃
  • Key - 值可以作为键值(Key)被「全局存储操作( global storage operations)」进行访问
  • Store - 值可以被 存储 到全局状态。

1.3 一节中,我们已经初步接触到了 Abilities。在本实例中,我们将进一步的通过 Play with Abilities 掌握其原理。

2.1.2 Abilities 的语法

基本类型和内建类型的 abilities 是预先定义好的并且不可改变: integers, vector, addresses 和 boolean 类型的值先天具有 copy、drop 和 store ability。

然而,结构体的 ability 可以按照下面的语法进行添加:

struct NAME has ABILITY [, ABILITY] { [FIELDS] }

一个图书馆的例子:

module Library {
    
    // each ability has matching keyword
    // multiple abilities are listed with comma
    struct Book has store, copy, drop {
        year: u64
    }

    // single ability is also possible
    struct Storage has key {
        books: vector<Book>
    }

    // this one has no abilities 
    struct Empty {}
}

2.1.3 合约实战

2.1.4 dApp实战

2.1.5 知识点分析

2.2 MyToken

2.2.1 合约实战

2.2.2 dApp实战

2.2.3 知识点分析

2.3 MyNFT

2.3.1 合约实战

2.3.2 dApp实战

2.3.3 知识点分析

0x03

1.5 Variables

// TODO

1.6 Basic Operations

// TODO

1.7 Functions

// TODO

1.8 Sructs

// TODO

1.9 Impl - DNA Generator

// TODO

1.10 Buidl A XiuXian Role

// TODO