W3.Hitchhiker

发布于 2022-08-14到 Mirror 阅读

使用 Warp 部署一个 PST 代币合约

作者:Dan

English Version:「 Deploy a PST contract using Warp

在本教程中,我们将学习如何通过 Warp 建立一个简单的 PST 应用程序。为了实现这个目标,我们将编写一个 PST 合约,在用户本地测试网做一些测试后并把它部署到 Redstone 公共测试网。它将让我们铸造一些代币,以及在地址之间转移它们,并读取当前的余额。然后,我们将创建一个简单的 dApp,它将帮助我们以用户友好的方式与合约交互。

准备工作:

  • 安装好 node.js 16.5 或者以上版本
  • 安装好 yarn
  • 安装好 visual studio code
  • 安装好 python 3
  • 安装好 git

一、下载代码并启动 visual studio code

1、clone 代码到本地机器,使用如下命令:

git clone https://github.com/warp-contracts/academy.git

2、切换到工作目录,使用命令: cd academy/warp-academy-pst/challenge

3、用 visual studio code 打开源代码,使用命令 code . 启动 visual studio code

4、安装依赖,使用命令: yarn add [email protected] [email protected] [email protected]

二、编辑代码

1、定义状态类型

打开文件 warp-academy-pst/challenge/src/contracts/types/types.ts,复制以下代码:

export interface PstState {

  ticker: string;

  name: string;

  owner: string;

  balances: {

    [address: string]: number,

  };

}

PstState 代表合约的当前状态。是由开发者来决定,在我们的实现中,它将由四个属性组成:

  • ticker —— token 名字的缩写
  • name —— token 的名字
  • owner —— token 拥有者
  • balances —— 定义地址的代币余额

2、设置初始状态

打开文件,warp-academy-pst/challenge/src/contracts/initial-state.json

复制以下代码:

{

  "ticker": "FC",

  "name": "Federation Credits",

  "owner": "GH2IY_3vtE2c0KfQve9_BHoIPjZCS8s5YmSFS_fppKI",

  "balances": {

    "GH2IY_3vtE2c0KfQve9_BHoIPjZCS8s5YmSFS_fppKI": 1000,

    "33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA": 230

  }

}

3、编辑合约源代码

合约包括了一个 handle 函数,接受两个参数

第一个参数是 state,代表合约的当前状态,第二个参数是 action,包括两个属性,caller 代表合约的调用者,input 代表用户的输入

handle 函数必须通过下面三种方式结束:

  • 返回 { state: newState } —— 当合约状态在特定的交互后发生变化时。
  • 返回 { result: someResult } —— 当合约状态在交互后没有改变时。
  • 抛出 ContractError 异常

类型定义:

打开文件 warp-academy-pst/challenge/src/contracts/types/types.ts,添加以下类型:

export interface PstAction {

  input: PstInput;

  caller: string;

}

export interface PstInput {

  function: PstFunction;

  target: string;

  qty: number;

}

export interface PstResult {

  target: string;

  ticker: string;

  balance: number;

}

export type PstFunction = 'transfer' | 'mint' | 'balance';

export type ContractResult = { state: PstState } | { result: PstResult };

PstAction 代表合约的交互。如前所述,它有两个属性 —— 调用者和输入。输入由三个属性构成:

  • function —— 交互类型 (在我们的例子中可以是转移代币,mint 代币,读取余额)
  • target —— 目标地址
  • qty —— 代币数量

PstResult 当合约状态在交互后没有改变时返回的对象,包含三个属性:

  • target ——目标地址.
  • ticker —— token名字的缩写.
  • balance —— 余额

余额函数:

打开文件 warp-academy-pst/challenge/src/contracts/actions/read/balance.ts,添加以下代码:

declare const ContractError;

export const balance = async (

  state: PstState,

  { input: { target } }: PstAction

): Promise<ContractResult> => {

  const ticker = state.ticker;

  const balances = state.balances;

  if (typeof target !== 'string') {

    throw new ContractError('Must specify target to get balance for');

  }

  if (typeof balances[target] !== 'number') {

    throw new ContractError('Cannot get balance, target does not exist');

  }

  return { result: { target, ticker, balance: balances[target] } };

};

上述函数将帮助我们读取指定目标地址的余额。

铸造代币函数:

打开文件 warp-academy-pst/challenge/src/contracts/actions/write/mintTokens.ts,添加以下代码:

declare const ContractError;

export const mintTokens = async (

  state: PstState,

  { caller, input: { qty } }: PstAction

): Promise<ContractResult> => {

  const balances = state.balances;

  if (qty <= 0) {

    throw new ContractError('Invalid token mint');

  }

  if (!Number.isInteger(qty)) {

    throw new ContractError('Invalid value for "qty". Must be an integer');

  }

  balances[caller] ? (balances[caller] += qty) : (balances[caller] = qty);

  return { state };

};

这个函数将会 mint 代币到调用者地址。

转移代币函数:

打开文件 warp-academy-pst/challenge/src/contracts/actions/write/transferTokens.ts,添加以下代码:

declare const ContractError;

export const transferTokens = async (

  state: PstState,

  { caller, input: { target, qty } }: PstAction

): Promise<ContractResult> => {

  const balances = state.balances;

  if (!Number.isInteger(qty)) {

    throw new ContractError('Invalid value for "qty". Must be an integer');

  }

  if (!target) {

    throw new ContractError('No target specified');

  }

  if (qty <= 0 || caller === target) {

    throw new ContractError('Invalid token transfer');

  }

  if (!balances[caller]) {

    throw new ContractError(`Caller balance is not defined!`);

  }

  if (balances[caller] < qty) {

    throw new ContractError(

      `Caller balance not high enough to send ${qty} token(s)!`

    );

  }

  balances[caller] -= qty;

  if (target in balances) {

    balances[target] += qty;

  } else {

    balances[target] = qty;

  }

  return { state };

};

处理函数:

打开文件 warp-academy-pst/challenge/src/contracts/contract.ts,添加以下代码:

import { balance } from './actions/read/balance';

import { mintTokens } from './actions/write/mintTokens';

import { transferTokens } from './actions/write/transferTokens';

import { PstAction, PstResult, PstState } from './types/types';

declare const ContractError;

export async function handle(

  state: PstState,

  action: PstAction

): Promise<ContractResult> {

  const input = action.input;

  switch (input.function) {

    case 'mint':

      return await mintTokens(state, action);

    case 'transfer':

      return await transferTokens(state, action);

    case 'balance':

      return await balance(state, action);

    default:

      throw new ContractError(

        `No function supplied or function not recognised: "${input.function}"`

      );

  }

}

Handle 函数是合约的核心,接受两个参数分别是 state 和 action,对 action 进行判断去调用对应的函数。

三、用 ESBuild 将 ts 文件转化为 js 文件

运行命令 yarn build:contracts

四、编写测试文件

进入文件 warp-academy-pst/challenge/tests/contract.test.ts,复制以下代码:

import fs from 'fs';

import ArLocal from 'arlocal';

import Arweave from 'arweave';

import { JWKInterface } from 'arweave/node/lib/wallet';

import path from 'path';

import { addFunds, mineBlock } from '../utils/_helpers';

import {

  PstContract,

  PstState,

  Warp,

  WarpNodeFactory,

  LoggerFactory,

  InteractionResult,

} from 'warp-contracts';

describe('Testing the Profit Sharing Token', () => {

  let contractSrc: string;

  let wallet: JWKInterface;

  let walletAddress: string;

  let initialState: PstState;

  let arweave: Arweave;

  let arlocal: ArLocal;

  let warp: Warp;

  let pst: PstContract;

  beforeAll(async () => {

  // ~~ Declare all variables ~~

  // ~~ Set up ArLocal and instantiate Arweave ~~

  arlocal = new ArLocal(1820);

  await arlocal.start();

  arweave = Arweave.init({

  host: 'localhost',

  port: 1820,

  protocol: 'http',

  });

  // ~~ Initialize 'LoggerFactory' ~~

  LoggerFactory.INST.logLevel('error');

  // ~~ Set up Warp ~~

  warp = WarpNodeFactory.forTesting(arweave);

  // ~~ Generate wallet and add funds ~~

  wallet = await arweave.wallets.generate();

  walletAddress = await arweave.wallets.jwkToAddress(wallet);

  await addFunds(arweave, wallet);

  // ~~ Read contract source and initial state files ~~

  contractSrc = fs.readFileSync(path.join(__dirname, '../dist/contract.js'), 'utf8');

  const stateFromFile: PstState = JSON.parse(

  fs.readFileSync(path.join(__dirname, '../dist/contracts/initial-state.json'), 'utf8')

  );

  // ~~ Update initial state ~~

  initialState = {

  ...stateFromFile,

  ...{

  owner: walletAddress,

  },

  };

  // ~~ Deploy contract ~~

  const contractTxId = await warp.createContract.deploy({

  wallet,

  initState: JSON.stringify(initialState),

  src: contractSrc,

  });

  // ~~ Connect to the pst contract ~~

  pst = warp.pst(contractTxId);

  pst.connect(wallet);

  // ~~ Mine block ~~

  await mineBlock(arweave);

  });

  afterAll(async () => {

  // ~~ Stop ArLocal ~~

  await arlocal.stop();

  });

  it('should read pst state and balance data', async () => {

  expect(await pst.currentState()).toEqual(initialState);

  expect(

  (await pst.currentBalance('GH2IY_3vtE2c0KfQve9_BHoIPjZCS8s5YmSFS_fppKI'))

  .balance

  ).toEqual(1000);

  expect(

  (await pst.currentBalance('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'))

  .balance

  ).toEqual(230);

  });

  it('should properly mint tokens', async () => {

  await pst.writeInteraction({

  function: 'mint',

  qty: 2000,

  });

  await mineBlock(arweave);

  expect((await pst.currentState()).balances[walletAddress]).toEqual(2000);

  });

  it('should properly add tokens for already existing balance', async () => {

  });

  it('should properly transfer tokens', async () => {

  await pst.transfer({

  target: 'GH2IY_3vtE2c0KfQve9_BHoIPjZCS8s5YmSFS_fppKI',

  qty: 555,

  });

  await mineBlock(arweave);

  expect((await pst.currentState()).balances[walletAddress]).toEqual(

  2000 - 555

  );

  expect(

  (await pst.currentState()).balances[

  'GH2IY_3vtE2c0KfQve9_BHoIPjZCS8s5YmSFS_fppKI'

  ]

  ).toEqual(1000 + 555);

  });

  it('should properly view contract state', async () => {});

  it('should properly perform dry write with overwritten caller', async () => {

  const newWallet = await arweave.wallets.generate();

  const overwrittenCaller = await arweave.wallets.jwkToAddress(newWallet);

  await pst.transfer({

  target: overwrittenCaller,

  qty: 1000,

  });

  await mineBlock(arweave);

  const result: InteractionResult<PstState, unknown> = await pst.dryWrite(

  {

  function: 'transfer',

  target: 'GH2IY_3vtE2c0KfQve9_BHoIPjZCS8s5YmSFS_fppKI',

  qty: 333,

  },

  overwrittenCaller

  );

  expect(result.state.balances[walletAddress]).toEqual(

  2000 - 555 - 1000

  );

  expect(

  result.state.balances['GH2IY_3vtE2c0KfQve9_BHoIPjZCS8s5YmSFS_fppKI']

  ).toEqual(1000 + 555 + 333);

  expect(result.state.balances[overwrittenCaller]).toEqual(1000 - 333);

  });

});

以上代码会在本地启动一个 AR 的测试网,创建一个 AR 钱包,并将合约部署在本地的测试网上,并测试了铸造代币和转移代币等功能。

进入 warp-academy-pst\challenge\utils\_helpers.ts,复制以下代码:

import Arweave from 'arweave';

import { JWKInterface } from 'arweave/node/lib/wallet';

// ~~ Write function responsible for adding funds to the generated wallet ~~

export async function addFunds(arweave: Arweave, wallet: JWKInterface) {

  const walletAddress = await arweave.wallets.getAddress(wallet);

  await arweave.api.get(`/mint/${walletAddress}/1000000000000000`);

  }

// ~~ Write function responsible for mining block on the Arweave testnet ~~

export async function mineBlock(arweave: Arweave) {

  await arweave.api.get('mine');

  }

五、在本地命令行部署合约并运行测试

在命令行输入命令 yarn test:node

上图就是六个测试用例在本地测试网成功测试的结果。

六、编辑用于部署合约到 redstone 测试网的源文件

进入文件 warp-academy-pst\challenge\src\tools\deploy-test-contract.ts,复制以下代码:

import Arweave from 'arweave';

import { JWKInterface } from 'arweave/node/lib/wallet';

import { PstState } from '../contracts/types/types';

import { LoggerFactory, PstContract, Warp, WarpNodeFactory } from 'warp-contracts';

import fs from 'fs';

import path from 'path';

import { addFunds, mineBlock } from '../../utils/_helpers';

let contractSrc: string;

let wallet: JWKInterface;

let walletAddress: string;

let initialState: PstState;

let arweave: Arweave;

let warp: Warp;

(async () => {

// ~~ Declare variables ~~

// ~~ Initialize Arweave ~~

arweave = Arweave.init({

  host: 'testnet.redstone.tools',

  port: 443,

  protocol: 'https',

  });

// ~~ Initialize `LoggerFactory` ~~

LoggerFactory.INST.logLevel('error');

// ~~ Initialize Warp ~~

warp = WarpNodeFactory.memCached(arweave);

// ~~ Generate wallet and add some funds ~~

wallet = await arweave.wallets.generate();

walletAddress = await arweave.wallets.jwkToAddress(wallet);

await addFunds(arweave, wallet);

// ~~ Read contract source and initial state files ~~

contractSrc = fs.readFileSync(

  path.join(__dirname, '../../dist/contract.js'),

  'utf8'

  );

  const stateFromFile: PstState = JSON.parse(

  fs.readFileSync(

  path.join(__dirname, '../../dist/contracts/initial-state.json'),

  'utf8'

  )

  );

// ~~ Override contract's owner address with the generated wallet address ~~

initialState = {

  ...stateFromFile,

  ...{

  owner: walletAddress,

  },

  };

// ~~ Deploy contract ~~

const contractTxId = await warp.createContract.deploy({

  wallet,

  initState: JSON.stringify(initialState),

  src: contractSrc,

  });

// ~~ Log contract id to the console ~~

console.log(contractTxId);

//Mine block

await mineBlock(arweave);

})();

这段代码主要是创建一个连接 redstone 测试网的实例,创建一个钱包,领取 AR 测试代币,部署合约到 redstone 测试网,部署成功以后会输出部署的合约地址。

七、部署合约到 redstone 测试网

在命令行输入命令 yarn ts-node src/tools/deploy-test-contract.ts

上图说明合约已经成功部署到 redstone 测试网,合约地址为:XeJiSHIkj0dVU7ddGtOIE8kZoQEIPcqvLS_y5KPu62w

八、编辑前端文件的源代码

前端是使用 vue V2 开发的,以下是前端源代码的目录机构:

  • challenge/src/main.ts —— 是一个应用程序的入口。
  • challenge/src/pst-contract.ts —— 在这里我们定义 Arweave 和 SmartWeave 的实例并将其导出。
  • challenge/src/deployed-contracts.ts —— 在这里我们表明部署在 redstone 测试网上的合约ID。
  • challenge/src/constants.ts —— 所有的常量(包括 URL)。
  • challenge/src/assets —— 应用程序中使用的所有资产。
  • challenge/src/components —— 所有的组件,这些组件是 Vue 封装可重用代码的关键功能。
  • challenge/src/router —— 使用 vue-router 构建的应用程序的路由器。
  • challenge/src/store —— 使用 Vuex 构建应用程序的存储,Vuex 是一种状态管理模式和库。它可以作为应用程序中所有组件的集中存储。
  • challenge/src/views —— 应用程序的视图层。

进入 challenge/src/pst-contract.ts,复制以下代码:

import Arweave from 'arweave';

import {

  PstContract,

  PstState,

  Warp,

  WarpNodeFactory,

  LoggerFactory,

  InteractionResult,

  WarpWebFactory

  } from 'warp-contracts';

export const arweave: Arweave = Arweave.init({

  host: 'testnet.redstone.tools',

  port: 443,

  protocol: 'https',

  });

  export const warp: Warp = WarpWebFactory.memCachedBased(arweave).useArweaveGateway().build();

进入文件 challenge\src\deployed-contracts.ts,添加之前成功部署的合约的 ID:

这里的合约地址应该填入自己成功部署在 redstone 测试网的合约地址

进入文件 challenge\src\constants.ts,复制以下代码:

export const url = {

  warpGateway: '[https://gateway.redstone.finance](https://gateway.redstone.finance)',

};

进入文件challenge\src\store\index.ts,复制以下代码:

import Vue from 'vue';

import Vuex from 'vuex';

import { arweave, warp } from '../pst-contract';

import { deployedContracts } from '../deployed-contracts';

import { PstState } from '@/contracts/types/types';

import { Contract } from 'warp-contracts';

Vue.use(Vuex);

export default new Vuex.Store({

  state: {

  arweave,

  warp,

  state: {},

  validity: {},

  contract: null,

  walletAddress: null,

  },

  mutations: {

  setState(state, swState) {

  state.state = swState;

  },

  setValidity(state, validity) {

  state.validity = validity;

  },

  setContract(state, contract) {

  state.contract = contract;

  },

  setWalletAddress(state, walletAddress) {

  state.walletAddress = walletAddress;

  },

  },

  actions: {

  async loadState({ commit }) {

  // ~~ Generate arweave wallet ~~

  const wallet = await arweave.wallets.generate();

  // ~~ Get wallet address and mint some tokens ~~

  const walletAddress = await arweave.wallets.getAddress(wallet);

  await arweave.api.get(`/mint/${walletAddress}/1000000000000000`);

  // ~~ Connect deployed contract and wallet ~~

  const contract: Contract = warp

  .pst(deployedContracts.fc)

  .connect(wallet);

  commit('setContract', contract);

  // ~~ Set the state of the contract ~~

  const { state, validity } = await contract.readState();

  commit('setState', state);

  commit('setValidity', validity);

  commit('setWalletAddress', walletAddress);

  },

  },

  modules: {},

});

进入文件 challenge\src\components\Header\Header.vue,插入以下代码:

const txId = await this.contract.writeInteraction({

  function: 'mint',

  qty: parseInt(this.$refs.balanceMint.value),

  });

  await this.arweave.api.get('mine');

  // ~~ Set the balances by calling `currentState` method ~~

  const newResult = await this.contract.currentState();

具体插入代码的位置,请参考下图:

进入文件 challenge\src\components\BalancesList\BalancesList.vue,插入以下代码:

const tx = await this.contract.transfer({

  target: address,

  qty: parseInt(qty),

  });

  // ~~ Mine a block ~~

  await this.arweave.api.get('mine');

  // ~~ Set new balances list by calling `currentState` method

  let newResult = await this.contract.currentState();

具体插入的代码位置,请参考下图:

到现在为止,所有的源代码编辑工作完成。

九、通过前端与 redstone 测试网部署的合约进行交互

在命令行输入 yarn build 来 build 整个项目,

下图是 build 成功的截图:

在命令行输入 yarn serve 来在开发环境中运行,下图是成功运行的截图:

打开浏览器,在地址栏输入网址 http://localhost:8080/

打开后界面将会显示合约地址,钱包地址,以及代币余额等信息,这时候可以先 mint 一部分代币,然后再进行转账。

进行 mint 代币以及转账后代币的余额会发生变化。


声明:本文内容仅供参考、交流,不构成任何投资建议。若存在明显的理解或数据的错误,欢迎反馈。

本文内容系 W3.Hitchhiker 原创,如需转载请标明出处。

商务合作:[email protected]

官网:https://w3hitchhiker.com/

W3.Hitchhiker 官方推特:https://twitter.com/HitchhikerW3