获取示例合约

我们准备了一个非常简单的合约,里面只包含两个文件:config.yamlprotocol.proto

config.yaml

定义合约的名称,版本号,以及合约所依赖的几个文件,这里我们只有 1 个数据结构定义的文件。

---
name: create_product
version: 1
namespace: CoreTx
description: Create product that can be sold within the vending machine
type_urls:
  fg:t:create_product: ForgeAbi.CreateProductTx
  fg:x:product: ForgeAbi.Product
proto: protocol.proto

protocol.proto

定义合约里面会使用到的数据结构,和新的交易类型。

syntax = "proto3";
package forge_abi;

message CreateProductTx {
  Product product = 1;
}

message Product {
  string name = 1;
  string brand = 2;
}

上面的代码已经放到了 GitHub 上面,获取方式比较简单:

git clone https://github.com/wangshijun/forge-product-factory-contract.git
cd forge-product-factory-contract

准备编译环境

TODO: 这里应该需要额外安装几个工具

编译合约文件

接下来直接执行 forge contract:compile ./protocol,得到如下输出:

❯ forge contract:compile protocol
ℹ Contract meta: {"name":"create_product","version":1}

ℹ Generating elixir language support:
✔ Latest forge compiler version: v0.18.2
✔ forge-compiler info fetch success
ℹ Start download https://releases.arcblock.io/forge-compiler/0.18.2/forge-compiler
ℹ Downloading forge-compiler |████████████████████████████████████████ 100% || 2.84/2.84 MB
✔ Elixir itx generated: .compiled/create_product/elixir/create_product/create_product.itx.json

ℹ Generating JavaScript language support:
✔ Protobuf js generated: .compiled/create_product/javascript/protocol_pb.js
✔ JSON spec generated: .compiled/create_product/javascript/protocol_spec.json
✔ type_urls json generated: .compiled/create_product/javascript/protocol_url.json
✔ JavaScript entry file generated: .compiled/create_product/javascript/index.js

如果是第一次执行 forge contract:compile,Forge CLI 会自动去下载合约编译工具 forge-compiler

当前的 contract:compile 能产生能够给 Forge Elixir SDKForge JS SDK 使用的文件,也产生了可以直接用于部署到链节点的结果文件。

部署合约

如果要部署合约直接执行:forge protocol:deploy .compiled/create_product/elixir/create_product/create_product.itx.json,结果如下:

❯ forge contract:deploy .compiled/create_product/elixir/create_product/create_product.itx.json
ℹ Working on app-chain chain
ℹ Connect to grpc endpoint: tcp://127.0.0.1:28210
✔ Moderator checked success
ℹ Deploy contract to tcp://127.0.0.1:28210
Contract detail:
{ address: 'z2E3q3PUNCzMNvKi1RuT7KHJ7VnUGZMhjPQwF',
  name: 'create_product',
  version: 1,
  namespace: 'CoreTx',
  description: 'Create product that can be sold within the vending machine',
  typeUrlsList:
   [ { url: 'fg:t:create_product',
       module: 'ForgeAbi.CreateProductTx' },
     { url: 'fg:x:product', module: 'ForgeAbi.Product' } ],
  proto:
   'syntax = "proto3";\npackage forge_abi;\n\nmessage CreateProductTx {\n  Product product = 1;\n}\n\nmessage Product {\n  string name = 1;\n  string brand = 2;\n}\n',
  pipeline: '',
  sourcesList: [],
  codeList:
   [ { checksum: [Uint8Array], binary: [Uint8Array] },
     { checksum: [Uint8Array], binary: [Uint8Array] } ],
  tagsList: [],
  data: undefined }
✔ Contract deploy success
ℹ Inspect tx with forge tx 6800DF1ED99D3A5B9F41C2F54A2644CC4F34A42DAF57B8889E6418553A9EF6F1

相同名称、相同版本的合约是不允许重复部署的,所以,如果合约代码有改动,需要递增版本号,重新编译,然后再部署。

验证部署结果

执行 forge contract:ls | grep create_product,如果能看到结果,说明部署正常。

❯ forge contract:ls | grep create_product
│ create_product          │ z2E3q3PUNCzMNvKi1RuT7KHJ7VnUGZMhjPQwF  │ running       │ v1       │

使用编译后的代码

wangshijun/forge-product-factory-contract 有如何在 Forge JS SDK 中使用编译后代码的例子:

/* eslint-disable no-console */
require('./.compiled/create_product/javascript/index');

const moment = require('moment');
const GraphqlClient = require('@arcblock/graphql-client');
const { toAssetAddress } = require('@arcblock/did-util');
const { fromTokenToUnit } = require('@arcblock/forge-util');
const { fromRandom, WalletType } = require('@arcblock/forge-wallet');

const endpoint = process.env.FORGE_API_HOST || 'http://127.0.0.1:8210'; // testnet

const client = new GraphqlClient(`${endpoint}/api`);
const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout));

(async () => {
  try {
    const merchant = fromRandom();
    const customer = fromRandom();
    console.log({ merchant: merchant.toJSON(), customer: customer.toJSON() });

    // 1. declare merchant
    let res = await client.sendDeclareTx({
      tx: { itx: { moniker: 'merchant' } },
      wallet: merchant,
    });
    console.log('merchant declare tx', res);
    console.log('merchant view link', `${endpoint}/node/explorer/accounts/${merchant.toAddress()}`);

    // 2. create vendor machine
    const machine = {
      moniker: `Vending_Machine_${merchant.toAddress()}`,
      readonly: false, // if we want to update the machine, we should set this to false
      transferrable: false,
      data: {
        type: 'AssetFactory',
        value: {
          description: 'Unlimited Vending Machine',
          limit: 99999,
          price: {
            value: Buffer.from(fromTokenToUnit(1).toBuffer()),
          },
          template: JSON.stringify({ name: '{{name}}', brand: '{{brand}}' }),
          allowedSpecArgs: ['name', 'brand'],
          assetName: 'Product',
          attributes: {
            transferrable: false,
          },
        },
      },
    };
    const assetAddress = toAssetAddress(machine);
    machine.address = assetAddress;
    res = await client.sendCreateAssetTx({ tx: { itx: machine }, wallet: merchant });
    console.log('factory template', machine.data.value.template);
    console.log('view machine state', `${endpoint}/node/explorer/assets/${assetAddress}`);
    console.log('create machine tx', `${endpoint}/node/explorer/txs/${res}`);

    // wait for machine state consolidates
    await sleep(3000);

    // 3. read machine
    const { state } = await client.getAssetState({ address: assetAddress });
    console.log('machine state', state);

    // 4. declare customer
    res = await client.declare({ moniker: 'customer', wallet: customer });
    console.log('customer declare tx', res);
    console.log('customer view link', `${endpoint}/node/explorer/accounts/${customer.toAddress()}`);

    // 5. get some money
    res = await client.checkin({ wallet: customer });
    console.log('poke tx', res);
    console.log('poke link', `${endpoint}/node/explorer/txs/${res}`);
    await sleep(3000);

    // 6. create product and its address
    const product = {
      ttl: 0,
      readonly: true,
      transferrable: false,
      parent: machine.address,
      data: {
        type: 'Product',
        value: { name: 'Coca', brand: 'Pepsi' },
      },
    };
    const goodAddress = toAssetAddress(product);
    console.log('product value', product.data.value);

    // 7. acquire asset
    res = await client.sendAcquireAssetTx({
      tx: {
        itx: {
          to: machine.address,
          specs: [
            {
              address: goodAddress,
              data: JSON.stringify(product.data.value),
            },
          ],
        },
      },
      wallet: customer,
    });
    console.log('product spec', {
      address: goodAddress,
      data: JSON.stringify(product.data.value),
    });
    console.log('product address', goodAddress);
    console.log('acquire tx', res);
    console.log('acquire link', `${endpoint}/node/explorer/txs/${res}`);
  } catch (err) {
    console.error(err);
    console.log(JSON.stringify(err.errors));
  }
})();