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.
git clone https://github.com/warp-contracts/academy.git
cd academy/warp-academy-pst/challenge
3. Launch visual studio code:
yarn add arweave@1.11.4 arlocal@1.1.42 warp-contracts@1.0.1
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.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:
{ state: newState }
— when contract state is changing after specific interaction.{ result: someResult }
— when contract state is not changing after interaction.ContractError
exception.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 changedresult
— when the interaction was a read-only operationHead 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.
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.
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 };
};
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.
Run the following command :yarn build:contracts
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');
}
Run the following command: yarn test:node
The above screenshot shows the six test cases are running successfully on local 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.
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
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:
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.
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