第一个本地 Warp 合约

1. 安装运行 arlocal

npm i -g arlocal
npx arlocal start

Arlocal 模拟了一个本地 arweave gateway server,默认运行在1984端口。

Arocal 重启后状态就没了。

2. 创建 wallet

可以用 arconnect 生成一个地址,然后导出私钥,保存到本地目录,

或者用 arweave 节点程序生成密钥对,

或者用 arweave js 库生成密钥对。

const Arweave = require("arweave");

// 创建 Arweave 实例
const arweave = Arweave.init();

// 生成一个新的 JWK(JSON Web Key)钥匙对
const key = await arweave.wallets.generate();

// 从 JWK 获取 Arweave 地址
const address = await arweave.wallets.jwkToAddress(key);

console.log("Private Key:", key);
console.log("Address:", address);

Arweave 用的是 RSA 密钥对,私钥长这样:

{
    "kty": "RSA",
    "n": "*******************************",
    "e": "AQAB",
    "d": "*******************************",
    "p": "*******************************",
    "q": "*******************************",
    "dp": "*******************************",
    "dq": "*******************************",
    "qi": "*******************************"
}

3. 编写合约

写一个简单的 token 合约,功能就是记录余额和执行转账。合约本体就是 handle 函数,state 是合约状态,action.input 是调用合约的参数。

Arweave 合约的 state 和 code 是分开的,对 state 求值需要把 code,initial state,interactions 全部攒起来,warp 框架会把这些都搞定。(此处理解不深,可能不准确)

// file: token.js
export async function handle(state, action) {
    if (action.input.function === "transfer") {
        const { target, qty } = action.input;

        if (!target || !qty) {
            throw new ContractError("Invalid input");
        }

        const balance = state.balances[action.caller] || 0;

        if (balance < qty) {
            throw new ContractError("Insufficient balance");
        }

        // 执行转账
        state.balances[action.caller] -= qty;
        state.balances[target] = (state.balances[target] || 0) + qty;
    }

    return { state };
}

4. 设定初始状态

创建一个 token-initState.json 文件,定义初始状态,主要是设置 balances.

{
    "owner": "-aVC26iRLJmi7uq4iKtRllLA2DJ5EoABSZV5EwNkwdU",
    "canEvolve": true,
    "balances": {
        "-aVC26iRLJmi7uq4iKtRllLA2DJ5EoABSZV5EwNkwdU": 1000
    }
}

5. 部署脚本

创建一个 ES6 module,创建一个 javascript

const run = async () => {
 //
}

run();

创建 warp 实例,forLocal 表示 local 环境,如果是主网就改成 forMainnet

import { DeployPlugin } from "warp-contracts-plugin-deploy";
const warp = WarpSdk.WarpFactory.forLocal().use(new DeployPlugin());

读取 wallet1 和 wallet2

import fs from "fs";
import * as path from "path";

import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const wallet1 = JSON.parse(
    fs.readFileSync(
        path.join(__dirname, "../jwk.json").toString(),
        "utf-8"
    )
);

const wallet2 = JSON.parse(
    fs.readFileSync(
        path.join(__dirname, "../jwk-2.json").toString(),
        "utf-8"
    )
);

const walletAddress1 = await warp.arweave.wallets.jwkToAddress(wallet1);
const walletAddress2 = await warp.arweave.wallets.jwkToAddress(wallet2);

console.log("address 1 " + walletAddress1);
console.log("address 2 " + walletAddress2);

引入合约 code 和 initState

const contractSrc = fs.readFileSync(
    path.join(__dirname, "./contracts/token.js"),
    "utf8"
);
const initialState = fs.readFileSync(
    path.join(__dirname, "./contracts/token-initState.json"),
    "utf8"
);

部署合约,交易 id 就是合约 id

const { contractTxId } = await warp.deploy({
    wallet: wallet1,
    initState: initialState,
    src: contractSrc,
});
console.log(`contractTxId: `, contractTxId);

如果是主网,要这样写

// for mainnet
import { ArweaveSigner } from "warp-contracts-plugin-deploy";
const { contractTxId } = await warp.deploy({
    wallet: new ArweaveSigner(wallet1),
    initState: initialState,
    src: contractSrc,
});
console.log(`contractTxId: `, contractTxId);

6. 合约交互脚本

用 contractTxId 作参数,把 contract 实例创建出来。本地调试可以把 mineArLocalBlocks 和 waitForConfirmation 开启,以便于直接观察状态变化。

let contract = warp
    .contract(contractTxId)
    .connect(wallet1)
    .setEvaluationOptions({
        mineArLocalBlocks: true,
        waitForConfirmation: true,
    });

发起一笔交易,写入合约状态

let res = await contract.writeInteraction(
    {
        function: "transfer",
        target: walletAddress2,
        qty: 10,
    },
    { strict: true }
);
console.log(`res: %s`, res);

查询最新区块高度

let block = await warp.arweave.blocks.getCurrent();
console.log(`current block: %s`, block.height);

如果是主网,这样查询

// get current block of mainnet
const arweaveWrapper = new WarpSdk.ArweaveWrapper(warp);
let block = await arweaveWrapper.warpGwBlock();
get current block from arlocal

查看合约在当前区块下的状态

const { sortKey, cachedValue } = await contract.readState(block.height);
console.log(`sortKey value: %s`, sortKey);
console.log(`cached value: %s`, JSON.stringify(cachedValue));

把上面的脚本整合起来运行一下

$ node writeContract.js 
address 1 -aVC26iRLJmi7uq4iKtRllLA2DJ5EoABSZV5EwNkwdU
address 2 xmq-uGvTmH-sl8ELVym0OizK8Ps5B3QQFjMNY8uEG5w
contractTxId:  O5UElo74Nt70AdOhhgx0zvRtiUP5D2r3nKcX0Ngaf_w
2023-12-28T18:22:34.382Z INFO [HandlerBasedContract] Write interaction [
  {
    input: {
      function: 'transfer',
      target: 'xmq-uGvTmH-sl8ELVym0OizK8Ps5B3QQFjMNY8uEG5w',
      qty: 10
    },
    options: { strict: true }
  }
]
2023-12-28T18:22:34.430Z INFO [ContractDefinitionLoader] Contract definition loaded in: 40ms []
2023-12-28T18:22:34.450Z INFO [HandlerBasedContract] Call contract input [
  {
    function: 'transfer',
    target: 'xmq-uGvTmH-sl8ELVym0OizK8Ps5B3QQFjMNY8uEG5w',
    qty: 10
  }
]
2023-12-28T18:22:34.463Z INFO [ContractDefinitionLoader] Contract definition loaded in: 9ms []
2023-12-28T18:22:34.471Z INFO [HandlerBasedContract] effectiveCaller [ '-aVC26iRLJmi7uq4iKtRllLA2DJ5EoABSZV5EwNkwdU' ]
2023-12-28T18:22:34.471Z INFO [CacheableStateEvaluator] No missing interactions O5UElo74Nt70AdOhhgx0zvRtiUP5D2r3nKcX0Ngaf_w []
2023-12-28T18:22:34.472Z INFO [HandlerBasedContract] Current state [
  {
    owner: '-aVC26iRLJmi7uq4iKtRllLA2DJ5EoABSZV5EwNkwdU',
    canEvolve: true,
    balances: { '-aVC26iRLJmi7uq4iKtRllLA2DJ5EoABSZV5EwNkwdU': 1000 }
  }
]
2023-12-28T18:22:35.739Z INFO [HandlerBasedContract] Waiting for confirmation of [ '91WJeeSJPeAGtoy-Ebxrs284_fSFdeRbAoji7nICMAg' ]
2023-12-28T18:22:35.754Z INFO [HandlerBasedContract] Transaction 91WJeeSJPeAGtoy-Ebxrs284_fSFdeRbAoji7nICMAg confirmed [ { status: 200, confirmed: 'Pending' } ]
2023-12-28T18:22:35.755Z INFO [HandlerBasedContract] Transaction confirmed after [ '16ms' ]
res: {
  originalTxId: '91WJeeSJPeAGtoy-Ebxrs284_fSFdeRbAoji7nICMAg',
  interactionTx: [Transaction]
}
writeTx block: {
  height: 7,
  block: 'bdgcd6ztl16ojp5zb13j98453e82wqn3vxrhrwna7hku0d8i9ce6sdku2i7xhp63'
}
current block: 7
2023-12-28T18:22:36.167Z INFO [HandlerBasedContract] Read state for [
  {
    contractTxId: 'O5UElo74Nt70AdOhhgx0zvRtiUP5D2r3nKcX0Ngaf_w',
    sortKeyOrBlockHeight: 7
  }
]
2023-12-28T18:22:36.182Z INFO [ContractDefinitionLoader] Contract definition loaded in: 12ms []
2023-12-28T18:22:36.195Z INFO [HandlerBasedContract] Execution Context [
  {
    srcTxId: 'psZuy21rSCNyrBW1y4HMPaTjTyFms8DcHU-Mlk4YZrg',
    missingInteractions: 1,
    cachedSortKey: '000000000000,0000000000000,0000000000000000000000000000000000000000000000000000000000000000'
  }
]
2023-12-28T18:22:36.197Z INFO [HandlerBasedContract] Benchmark [
  {
    'Gateway communication  ': '27ms',
    'Contract evaluation    ': '2ms',
    'Total:                 ': '29ms'
  }
]
sortKey value: 000000000007,0000000000000,f4fbc9e57672bcb120c1486e597e5c465ad7fd43db656f9623be0e1261cc2011
cached value: {"state":{"owner":"-aVC26iRLJmi7uq4iKtRllLA2DJ5EoABSZV5EwNkwdU","canEvolve":true,"balances":{"-aVC26iRLJmi7uq4iKtRllLA2DJ5EoABSZV5EwNkwdU":990,"xmq-uGvTmH-sl8ELVym0OizK8Ps5B3QQFjMNY8uEG5w":10}},"validity":{"91WJeeSJPeAGtoy-Ebxrs284_fSFdeRbAoji7nICMAg":true},"errorMessages":{}}

https://academy.warp.cc/docs/docs-intro