Deploy a PST contract using Warp

Written by Dan

Revised by Evelyn

Chinese Version:《使用 Warp 部署一个 PST 代币合约

In this tutorial we're going to learn how to build a simple PST (Profit Sharing Token) application. In order to achieve that, we will write our first PST contract. After doing some testing on the user's local test network, we will deploy it to the Redstone public testnet. It will let us mint some tokens as well as transfer them between addresses and read current balances. We will then create a simple dApp which will help us interact with the contract in a user-friendly way.

prequisite:

  • installed node.js 16.5 or higher
  • installed yarn
  • installed visual studio code
  • installed python3
  • installed git

Step 1:Download the source code and launch visual studio code

1. Download code from github:

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

2. Change the working diretory:

cd academy/warp-academy-pst/challenge

3. Launch visual studio code:

4. Install dependencies:

yarn add arweave@1.11.4 arlocal@1.1.42 warp-contracts@1.0.1

Step 2:Edit the source code

1. Define the state types

Head to warp-academy-pst/challenge/src/contracts/types/types.ts and let's start writing! Copy the following code:

export interface PstState {

  ticker: string;

  name: string;

  owner: string;

  balances: {

    [address: string]: number,

  };

}

PstState represents the contract's current state. Its shape is not defined by any rules and it is the developer who decides what the state will look like. In case of our implementation it will consist of four properties:

  • ticker — abbreviation of token name.
  • name — name of the token.
  • owner — owner of the token.
  • balances — define the token balance of the address.

2. Set the initial state

Head to warp-academy-pst/challenge/src/contracts/initial-state.json, copy the following code:

{

  "ticker": "FC",

  "name": "Federation Credits",

  "owner": "GH2IY_3vtE2c0KfQve9_BHoIPjZCS8s5YmSFS_fppKI",

  "balances": {

    "GH2IY_3vtE2c0KfQve9_BHoIPjZCS8s5YmSFS_fppKI": 1000,

    "33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA": 230

  }

}

3. Edit the source code of the contract

Let's talk about contract. It exports one function — handle — which accepts two arguments:

  • state — contract's current state.
  • action — contract interaction with two properties:
    • caller — wallet address of user interacting with the contract.
    • input — user's input to the contract.

Handle function should end by:

  • returning { state: newState } — when contract state is changing after specific interaction.
  • returning { result: someResult } — when contract state is not changing after interaction.
  • throwing ContractError exception.

Contract source types:

Head to warp-academy-pst/challenge/src/contracts/types/types.ts, add the following code:

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 represents contract's interaction. As mentioned earlier it has two properties — caller and input. In our contract user will have an ability to write three types of inputs (PstInput):

  • function — type of interaction (in our case, it can be transfering tokens, minting tokens or reading balances - PstFunction)
  • target — target address.
  • qty — amount of tokens to be transferred/minted.

PstResult — object returned when the contract state has not changed after an interaction:

  • target — target address.
  • ticker — an abbreviation used to uniquely identify the token.
  • balance — specific address balance.

ContractResult — contract's handler function should be terminated by one of those:

  • state — when the state is being changed
  • result — when the interaction was a read-only operation

Balance fucntion:

Head to warp-academy-pst/challenge/src/contracts/actions/read/balance.ts, add the following code:

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] } };

};

The above function will help us read the balance of the inidicated target address.

Mint token function:

Head to warp-academy-pst/challenge/src/contracts/actions/write/mintTokens.ts,

add the following code:

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 };

};

This one will help us minting some tokens to the caller's address.

Transfer token function:

Head to warp-academy-pst/challenge/src/contracts/actions/write/transferTokens.ts, add the following code:

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 };

};

Handle function:

Head to warp-academy-pst/challenge/src/contracts/contract.ts, add the following code:

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 function is an asynchronous function and it returns a promise of type ContractResult. As mentioned above, it takes two arguments — state and action. It waits for one of the interactions to be called and returns the result of matching functions — the ones that we prepared earlier.

Step 3:Convert ts file to js file

Run the following command :yarn build:contracts

Step 4:Writing tests

Head to warp-academy-pst/challenge/tests/contract.test.ts, copy the following code:

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);

  });

});

The above code launches a local test network for AR, creates an AR wallet, deploys the contract on the local test network, and tests features such as minting tokens and transferring tokens.

Head to warp-academy-pst\challenge\utils\_helpers.ts, copy the following code:

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');

  }

Step 5:Deploy the contract to local testnet and run tests

Run the following command: yarn test:node

The above screenshot shows the six test cases are running successfully on local testnet.

Step 6:Edit the source code to deploy contract to redstone testnet

Head to warp-academy-pst\challenge\src\tools\deploy-test-contract.ts, copy the following code:

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);

})();

The above code will generate and fund the Arweave wallet, read, contract source and initial state files and deploy the contract to the testnet. After the contract is deployed successfully, you will be able to see the contract address in the command line.

Step 7:Deploy contract to redstone testnet

Run the following comand:yarn ts-node src/tools/deploy-test-contract.ts

The above screenshot shows the contract is deployed successfully to redstone testnet, and the contract address is:

XeJiSHIkj0dVU7ddGtOIE8kZoQEIPcqvLS_y5KPu62w

Step 8:Prepare frontend environment

As it is not a frontend tutorial, we've already got you covered. We will be working with Vue v.2 with typescript support.

To quickly walk you through the structure of the project:

  1. challenge/src/main.ts — a starting point for the application.
  2. challenge/src/pst-contract.ts — here we define Arweave and SmartWeave instances and export them.
  3. challenge/src/deployed-contracts.ts — here we indicate deployed contract id.
  4. challenge/src/constants.ts — all the constants (including urls).
  5. challenge/src/assets — all the assets used in the application.
  6. challenge/src/components — all the components that are key features of Vue to encapsulate reusable code.
  7. challenge/src/router — router of the application build with vue-router.
  8. challenge/src/store — build your application's storage using Vuex, a state management pattern and library. It can be used as a centralized storage for all components in an application.
  9. challenge/src/views — the view layer of the application.

Head to challenge/src/pst-contract.ts, copy the following code:

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();

Head to challenge\src\deployed-contracts.ts, add the contract which is deployed successfully in step 7.

The contract address should be the contract that you deploy successfully to redstone testnet.

Head to challenge\src\constants.ts, copy the following code:

export const url = {

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

};

Head to challenge\src\store\index.ts,copy the following code,

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: {},

});

Head to challenge\src\components\Header\Header.vue, insert the following code:

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();

Please insert the code as the below screenshot:

Head to challenge\src\components\BalancesList\BalancesList.vue, insert the following code:

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();

Please insert the code as the below screenshot:

By now, all source code editing is done.

Step 9:Interacte with the contract

Type yarn build at the command line to build the entire project, and the following screenshot shows a successful build:

Type yarn serve at the command line to run it in the development environment, and the following screenshot shows a successful run:

Open your browser and go to http://localhost:8080/,

Now open the interface, it will display the contract address, wallet address, and token balance information. At this point, you can mint some of the tokens first and then transfer them.

After some token minting and transfer, the PST token balances are updated.


Disclaimer: The content of this article is for information and communication purposes only and does not constitute any investment advice. If there are obvious errors of understanding or data, welcome feedback.

The content of this article was originally created by W3.Hitchhiker, please indicate the source if you need to reproduce.

Business cooperation: hello@w3hitchhiker.com

Official website: https://w3hitchhiker.com/

W3.Hitchhiker official Twitter: https://twitter.com/HitchhikerW3

Subscribe to W3.Hitchhiker
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.