🧙‍♂️

WAO - Wizard AO SDKの紹介

に公開

原文: wao

WAO SDKは、洗練された構文拡張とシームレスなメッセージパイプ機能によって、Arweave/AO開発を効率化し、快適なコーディング体験を提供します。GraphQL操作も非常に簡単に行えます。

さらに、aoconnectの代替としてそのまま使用できる機能を備えており、AOユニットをメモリ上でエミュレートすることで、Luaスクリプトのテストをメインネットより1000倍速く実行可能です。arlocalao-localnet を使用したテストと比べても100倍速くなっています。

Quick Start

WAOは現在も積極的に開発が進められています。使用にあたってはご自身の判断でお願いします。

インストール

yarn add wao

テスト用の aoconnect 代替としてそのまま使える

aoconnect をWAO connectに置き換えることで、すべての処理がメモリ上で実行され、待ち時間ゼロでテストが1000倍高速になります。APIは完全に同一のため、コードの他の部分を変更する必要はありません。

//import { spawn, message, dryrun, assign, result } from "@permaweb/aoconnect"
import { connect, acc } from "wao/test";
const { spawn, message, dryrun, assign, result } = connect();

Setting up a Project

テスト用のAOプロジェクトを手動でセットアップするのはとても簡単です。

mkdir wao-test && cd wao-test
yarn init && yarn add wao

package.jsontesttest-only のコマンドを追加してください。

{
  "scripts": {
    "test": "node --experimental-wasm-memory64",
    "test-only": "node --experimental-wasm-memory64 --test-only"
  }
}

testtest.js ファイルを作成してください。

mkdir test && touch test/test.js

テストを書く

test.js にシンプルなテストを書きます。

import assert from "assert";
import { describe, it } from "node:test";
import { connect, acc } from "wao/test";
const { spawn, message, dryrun } = connect();
const signer = acc[0].signer;
const src_data = `
Handlers.add("Hello", "Hello", function (msg)
  msg.reply({ Data = "Hello, World!" })
end)
`;
describe("WAO", function () {
  it("should spawn a process and send messages", async () => {
    const pid = await spawn({
      signer,
      module: "Do_Uc2Sju_ffp6Ev0AnLVdPtot15rvMjP-a9VVaA5fM",
      scheduler: "_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA",
    });

    // on mainnet, you need to wait here till the process becomes available.
    // WAO automatically handles it. No need with in-memory tests.
    // await wait({ pid })

    await message({
      process: pid,
      tags: [{ name: "Action", value: "Eval" }],
      data: src_data,
      signer,
    });
    const res = await dryrun({
      process: pid,
      tags: [{ name: "Action", value: "Hello" }],
      signer,
    });
    assert.equal(res.Messages[0].Data, "Hello, World!");
  });
});

テストごとにランダムなArweaveウォレットを生成すると時間がかかり、テストの実行速度が低下する点に注意してください。そのため、Wao connectではあらかじめ生成されたアカウントをテスト用に提供しており、テストを何千回も実行する場合でも大幅な時間短縮が可能です。

  • acc[0] = { jwk, addr, signer }

テストを実行します。

yarn test test/test.js

WAO SDKを使用する

WAOには洗練されたシンタックスシュガーが備わっており、AOプロジェクトの開発が格段に楽しくなります。

同じテストは以下のように記述できます。

import assert from "assert";
import { describe, it } from "node:test";
import { AO, acc } from "wao/test";

const src_data = `
Handlers.add("Hello", "Hello", function (msg)
  msg.reply({ Data = "Hello, World!" })
end)
`;
describe("WAO", function () {
  it("should spawn a process and send messages", async () => {
    const ao = await new AO().init(acc[0]);
    const { p } = await ao.deploy({ src_data });
    assert.equal(await p.d("Hello", false), "Hello, World!");
  });
});

AO クラスはインメモリテスト専用ではなく、本番コードにも使用できます。異なるパスからインポートするだけで利用可能です。

import { AR, AO, GQL } from "wao";

出力の絞り込み(Cherry-Picking Outputs)

複数のメッセージがspawnされるような処理結果から、特定のデータだけを取り出したい場面はよくあります。そのためには、すべての戻りメッセージを走査し、さらに各メッセージのタグやデータをチェックする必要があり、かなりのコード量になります。
AO にはこれを簡略化するための get パラメータが用意されています。

以下のLuaハンドラーを例に見てみましょう。

local json = require('json')

Handlers.add("Hello", "Hello", function (msg)
  msg.reply({ Data = json.encode({ Name = "Bob" })})
end)

Handlers.add("Hello2", "Hello2", function (msg)
  msg.reply({ Data = "Hello, World!", Name = "Bob", Age = "30" })
end)

Handlers.add("Hello3", "Hello3", function (msg)
  msg.reply({ Profile = json.encode({ Name = "Bob", Age = "30" })})
end)

// by default it extracts JSON decoded Data
const out = await p.d("Hello");
assert.deepEqual(out, { Name: "Bob" });

// equivalent
const out2 = await p.d("Hello", { get: true });
assert.deepEqual(out2, { Name: "Bob" });

// get string Data
const out3 = await p.d("Hello2", { get: false });
assert.equal(out3, "Hello, World!");

// get a tag
const out4 = await p.d("Hello2", { get: "Age" });
assert.equal(out4, "30");

// get multiple tags
const out5 = await p.d("Hello2", {
  get: { obj: { firstname: "Name", age: "Age" } },
});
assert.deepEqual(out5, { firstname: "Bob", age: "30" });

// shortcut if keys don't include name, data, from, json
const out6 = await p.d("Hello2", { get: { firstname: "Name", age: "Age" } });
assert.deepEqual(out6, { firstname: "Bob", age: "30" });

// await p.d("Hello2", { get: { name: "Name", age: "Age" } }) doesn't work

// handle tag as json
const out7 = await p.d("Hello3", {
  get: { prof: { name: "Profile", json: true } },
});
assert.deepEqual(out7, { prof: { Name: "Bob", Age: "30" } });

メッセージの成功判定(Determining Message Success)

メッセージが成功したかどうかを判定するには、多くの場合、非同期で送られる一連のメッセージを追跡し、最終的に得られたタグやデータを検証する必要があります。
これは実際には非常に複雑な処理であり、多くのコードを書くことになります。

幸いにも、AO にはこの作業を大幅に簡素化する check パラメータが用意されています。check はメッセージを追跡し、指定された条件が満たされたかどうかをLazy Evaluation(遅延評価)で判断します。

// check if Data exists
await p.m("Hello2", { check: true })

// check if Data is a certain value
await p.m("Hello2", { check: "Hello, World! })

// check if a tag exists
await p.m("Hello2", { check: "Name" })

// check if tags are certain values
await p.m("Hello2", { check: { Name: "Bob", Age: "30" } })

// it throws an Error if the conditions are not met
try{
  await p.m("Hello2", { check: { Name: "Bob", Age: "20" } })
}catch(e){
  console.log("something went wrong!")
}

// check if Name is Bob and Age exists, then get Age
const age = await p.m("Hello2", { check: { Name: "Bob", Age: true }, get : "Age" })
assert.equal(age, "30", "Bob is not 30 yo!")

receive() を使った非同期メッセージの追跡

AOS2では便利な関数 receive() が導入されており、別のプロセスにメッセージを送信し、その返信を同じハンドラー内で受け取ることができます。

Handlers.add("Hello3", "Hello3", function (msg)
  msg.reply({ Data = "How old are you?" })
  local age = Send({
    Target = msg.To, Action = "Get-Age", Name = msg.Who
  }).receive().Data
  msg.reply({ Data = "got your age!", Name = msg.Who, Age = age })
end)

Target プロセスからの返信によってトリガーされる別のメッセージの一部として、2番目の返信が返されるため、arconnectresult 関数だけでは最終的な返信を取得することはできません。
そのためには、プロセスの results を繰り返し確認したり、メッセージチェーンをたどって問題の原因を調べる必要があります。

AOget および check は、こうした複雑な処理をバックグラウンドで短絡的に(short-circuit)かつ遅延評価で自動的に処理してくれます。適切な timeout(ミリ秒)を指定することを推奨します。

const age = await p.m(
  "Hello3",
  { Who: "Bob", To: DB_PROCESS_ID }, // second argument can be tags
  { get: "Age", check: "got your age!", timeout: 5000 }
);
assert.equal(age, "30");

複雑なAO開発をより簡単にするための強力なテクニックが、まだまだたくさん用意されています。

詳細はこの後のAPIリファレンスセクションをご覧ください!

ログ出力(Logging)

WAOはAOSのコアモジュールコードをホットパッチしており、ao.log を使うと自動的にJSの console.log に転送され、ログがそのままターミナルに表示されます。Luaのテーブルは自動的にJSONオブジェクトに変換されます。

この仕組みは本番コードには一切影響せず、テスト時のみモジュールに対してホットパッチが適用されます。これにより、複雑なデバッグ作業も非常に簡単になります。

Handlers.add("Hello4", "Hello4", function (msg)
  ao.log("Hello, Wordl!") -- will be displayed in the terminal
  ao.log({ Hello = "World!" }) -- will be auto-converted to JSON

  -- passing multiple values
  ao.log("Hi", 3, true, [ 1, 2, 3 ], { Hello = "World!" })
end)

エラーハンドラー内でエラーが発生した場合でもログを取得できるため、原因の特定が非常に容易になります。

Wasmメモリのフォーク(Fork Wasm Memory)

Wasmメモリを新しいプロセスにフォーク(複製)することができます。これはテスト用のチェックポイントを作成する際に便利です。

この機能はインメモリテスト環境でのみ動作します。

const src_counter = `
local count = 0
Handlers.add("Add", "Add", function (msg)
  count = count + tonumber(msg.Plus)
end)
Handlers.add("Get", "Get", function (msg)
  msg.reply({ Data = tostring(count) })
end)
`;
const ao = await new AO().init(acc[0]);
const { p, pid } = await ao.deploy({ boot: true, src_data: src_counter });
await p.m("Add", { Plus: 3 });
assert.equal(await p.d("Get"), "3");

const ao2 = await new AO().init(acc[0]);
// pass the exisiting wasm memory to a new process
const { p: p2 } = await ao2.spwn({ memory: ao.mem.env[pid].memory });
assert.equal(await p2.d("Get"), "3");
await p2.m("Add", { Plus: 2 });
assert.equal(await p2.d("Get"), "5");

また、メインネット上のプロセスのメモリをCUエンドポイント(GET /state/{pid})から取得し、それをテスト用にフォークすることも可能です。

WeaveDrive

WeaveDrive 拡張機能はWAO上で完全にエミュレートされています。AO から attest および avail 関数を使用することができます。

import { blueprint, AO, acc } from "wao/test";
const attestor = acc[0];
const handler = `
apm.install('@rakis/WeaveDrive')
Drive = require('@rakis/WeaveDrive')
Handlers.add("Get", "Get", function (msg)
  msg.reply({ Data = Drive.getData(msg.id) })
end)`;

describe("WeaveDrive", () => {
  it("should load Arweave tx data", async () => {
    const ao = await new AO().init(attestor);

    const { p } = await ao.deploy({
      tags: { Extension: "WeaveDrive", Attestor: attestor.addr },
      loads: [await blueprint("apm"), handler],
    });

    const { id } = await ao.ar.post({ data: "Hello" });
    await ao.attest({ id });

    assert.equal(await p.d("Get", { id }), "Hello");
  });
});

ローカル永続サーバー(Local Persistent Server)

ローカルで永続ストレージ付きのWAOサーバーを起動することができ、これによりフロントエンドアプリなどの外部コンポーネントと接続することが可能になります。

npx wao
npx wao --port 5000 --db .custom_cache_dir --reset

この場合、各ユニットのポートは以下のように割り当てられます:

  • AR ⇒ 5000
  • MU ⇒ 5002
  • SU ⇒ 5003
  • CU ⇒ 5004

WAOユニットにはWAO SDKまたはAOConnectを使って接続できますが、WAO SDKを使用した場合は以下のタグが自動的に設定されます:

  • AOS2.0.1 モジュール: Do_Uc2Sju_ffp6Ev0AnLVdPtot15rvMjP-a9VVaA5fM
  • スケジューラー: _GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA
  • オーソリティ: eNaLJLsMiWCSWvQKNbk_YT-9ydeWl9lrWwXxLVp9kcg
import { describe, it } from "node:test";
import assert from "assert";
import { AO } from "wao";

const src_data = `
Handlers.add("Hello", "Hello", function (msg)
  msg.reply({ Data = "Hello, World!" })
end)`;

describe("WAO Server", () => {
  it("should connect with WAO SDK", async () => {
    const ao = await new AO(4000).init(YOUR_JWK);
    const { p } = await ao.deploy({ src_data });
    assert.equal(await p.d("Hello"), "Hello, World!");
  });
});

AOConnectでは、

import { describe, it } from "node:test";
import assert from "assert";
import { connect, createDataItemSigner } from "@permaweb/aoconnect";
const { spawn, message, dryrun, assign, result } = connect({
  MU_URL: `http://localhost:4002`,
  CU_URL: `http://localhost:4003`,
  GATEWAY_URL: `http://localhost:4000`,
});

const src_data = `
Handlers.add("Hello", "Hello", function (msg)
  msg.reply({ Data = "Hello, World!" })
end)`;

describe("WAO Server", () => {
  it("should connect with WAO SDK", async () => {
    const pid = await spawn({
      module: "Do_Uc2Sju_ffp6Ev0AnLVdPtot15rvMjP-a9VVaA5fM",
      scheduler: "_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA",
      tags: [
        {
          name: "Authority",
          value: "eNaLJLsMiWCSWvQKNbk_YT-9ydeWl9lrWwXxLVp9kcg",
        },
      ],
      signer: createDataItemSigner(YOUR_JWK),
    });

    // wait till the process becomes available

    const mid = await message({
      process: pid,
      tags: [{ name: "Action", value: "Eval" }],
      data: src_data,
      signer: createDataItemSigner(acc[0].jwk),
    });

    console.log(await result({ process: pid, message: mid }));

    const res = await dryrun({
      process: pid,
      data: "",
      tags: [{ name: "Action", value: "Hello" }],
      anchor: "1234",
    });

    assert.equal(res.Messages[0].Data, "Hello, World!");
  });
});

Connecting with the AOS terminal,

aos \
  --gateway-url http://localhost:4000 \
  --cu-url http://localhost:4004 \
  --mu-url http://localhost:4002 \
  --tag-name Authority \
  --tag-value eNaLJLsMiWCSWvQKNbk_YT-9ydeWl9lrWwXxLVp9kcg

HyperBEAM

HyperBEAMとの統合は、現時点では非常に実験的な段階にあります。

WAOをクローンし、依存関係をインストールしてください。

git clone https://github.com/weavedb/wao.git && cd wao && yarn

CUは http://localhost:4004 で起動している必要があります。

Hyperbeamのセットアップガイド に従って、最新版を正しくビルド済みであることを確認してください。

ただし、ここではまだ rebar3 shell を実行しないでください。

代わりに、WAOのルートディレクトリから以下のコマンドでHyperBEAMノードを起動します。

yarn hb PATH_TO_HB_DIR

HyperBEAMは http://localhost:10001 で起動するはずです。

WAOサーバーをローカルCUとして使用しながら、HyperBEAMリクエストのテストが可能です。

import assert from "assert";
import { after, describe, it } from "node:test";
import { acc } from "wao/test";
import { HB } from "wao";

const data = `
local count = 0
Handlers.add("Inc", "Inc", function (msg)
  count = count + 1
  msg.reply({ Data = "Count: "..tostring(count) })
end)

Handlers.add("Get", "Get", function (msg)
  msg.reply({ Data = "Count: "..tostring(count) })
end)
`;
describe("Hyperbeam", function () {
  it("should interact with a hyperbeam node", async () => {
    const hb = await new HB({ url: "http://localhost:10001" }).init(acc[0].jwk);
    const metrics = await hb.metrics();
    const info = await hb.info();
    const process = await hb.process();
    const slot = await hb.schedule({ process, data });
    const r = await hb.compute({ process, slot });
    const slot2 = await hb.schedule({ process, action: "Inc" });
    const r2 = await hb.compute({ process, slot: slot2 });
    assert.equal(r2.Messages[0].Data, "Count: 1");
    const r3 = await hb.dryrun({ process, action: "Get" });
    assert.equal(r3.Messages[0].Data, "Count: 1");
  });
});

API Reference

AO

インスタンス化(Instantiate)

AOはARと同様の方法で初期化できます。

import { AO } from "wao";
const ao = await new AO().init(jwk || arweaveWallet);

ARの設定を渡したい場合は ar を使用してください。ao.ar が自動的に利用可能になります。

const ao = await new AO({ ar: { port: 4000 } }).init(jwk || arweaveWallet);
const addr = ao.ar.addr;
await ao.ar.post({ data, tags });

AOのコア機能

deploy

プロセスをspawnし、Luaソースを取得してスクリプトを評価します。src にはLuaスクリプトのArweaveトランザクションIDを指定します。

const { err, res, pid, p } = await ao.deploy({ data, tags, src, fills });

src の代わりに、Luaスクリプトそのものを src_data として直接渡すことも可能です。

const { err, res, pid, p } = await ao.deploy({ data, tags, src_data, fills });

boot オプションを使用すると、Eval アクションの代わりに On-Boot タグを用いてプロセスを初期化できます。
このとき true を設定すれば src_data を使用しますし、既存スクリプトのtxidを指定することも可能です。true を使う場合は、data を未定義にしておく必要があり、src_dataspawn の中で data を埋めます。

const { err, res, pid, p } = await ao.deploy({
  boot: true,
  tags,
  src_data,
  fills,
});

fills は、src で指定されたLuaソーススクリプト内のプレースホルダーを置き換えるために使用されます。

local replace_me = '<REPLACE_ME>'
local replace_me_again = '<REPLACE_ME_AGAIN>'
local replace_me_with_hello_again = '<REPLACE_ME>'
const fills = { REPLACE_ME: "hello", REPLACE_ME_AGAIN: "world" };

最終的には、以下のようなLuaスクリプトが生成されます。

local replace_me = 'hello'
local replace_me_again = 'world'
local replace_me_with_hello_again = 'hello'

複数のスクリプトがある場合は、loads を使用し、それぞれの src および fills を渡してください。

await ao.deploy({
  tags,
  loads: [
    { src, fills },
    { src: src2, fills: fills2 },
  ],
});

loads には文字列データの配列を渡すことも可能です。

const num = `num = 0`;
const inc = `Handlers.add("Inc", "Inc", function () num = num + 1 end)`;
const get = `Handlers.add("Get", "Get", function (m) m.reply({ Data = num }) end)`;

const { p } = await ao.deploy({ tags, loads: [num, inc, get] });
await p.m("Inc");
assert.equal(await p.d("Get"), 1);
msg

messageを送信します。

const { err, mid, res, out } = await ao.msg({
  pid,
  data,
  act,
  tags,
  check,
  get,
  mode,
  limit,
});

check は、res 内の Messages に含まれる Tags をチェックすることでメッセージ呼び出しの成功可否を判定します。

check または getfrom を使用する場合は、modegql に設定する必要があります。mode のデフォルトは aoconnect であり、これは aoconnect.results 関数を使って結果を追跡しますが、どのプロセスからの結果かを判別することができません。
一方 gql モードは、AOやArweaveのメインネットと併用した場合、ブロックファイナリティの遅延により一部の結果を取得できないことがあります。
limit は、check に対して何件のトランザクションや結果を取得するかを指定します。

const check = { Status: "Success" }; // Statusタグが"Success"であれば成功
const check2 = { Status: true }; // Statusタグが存在すれば成功

文字列またはブール値を指定した場合は、Tags ではなく Data フィールドをチェックします。

const check3 = "Success"; // Dataが"Success"なら成功
const check4 = true; // Dataが存在すれば成功
const check5 = /ok/; // Dataが"ok"を含んでいれば成功
const check6 = (n) => +n > 10; // Dataが10より大きければ成功
const check7 = { json: { a: 3 } }; // DataがJSONでaが3なら成功
const check8 = { json: { a: 3, b: 4 }, eq: true }; // JSONが完全一致なら成功
const check9 = { data: true, tags: { Status: true, Balance: (n) => +n > 10 } };
const check10 = { data: true, from: PID }; // メッセージ送信元のプロセスを指定

配列を使用すれば、複数の条件を同時にチェックできます。

const check11 = ["Success", { Age: "30" }]; // DataがSuccessかつAgeタグが30
const check12 = [
  { data: "Success", from: PID },
  { Age: "30", from: PID2 },
];

get を使えば、指定したデータを out 経由で取得できます。

const get = "ID"; // returns the value of "ID" tag
const get2 = { name: "Profile", json: true }; // "Profile" tag with JSON.parse()
const get3 = { data: true, json: true }; // returns Data field with JSON.parse()
const get4 = true; // => { data: true, json: true }
const get5 = false; // => { data: true, json: false }
const get6 = { obj: { age: "Age", who: "Name" } }; // => { age: 30, who: "Bob" }
const get7 = { age: "Age", who: "Name" }; // same as get6
const get8 = { name: "Profile", json: true, from: PID }; // specify sender process
const get9 = { age: { name: "Age", from: PID }, who: "Name" }; // another example
const get10 = {
  data: true,
  json: true,
  match: (val, index, res) => val.Age < 10,
};

checkget は、非同期メッセージを追跡しながらタグやデータを遅延評価(Lazy Evaluation)します。条件が満たされた時点で、それ以上のメッセージは追跡しません。
AOS 2.0 で追加された receive() を使用すると、result 関数から取得できるのは receive() 関数までにspawnされたメッセージのみです。
しかしWAOは receive() 以降にspawnされたメッセージも自動的に追跡し、check 条件が満たされたかどうかを判断します。

たとえば、次のようなハンドラーを考えてみましょう。

Handlers.add("Hello", "Hello", function (msg)
  msg.reply({ Data = "Hello, World!" })
  local name = Send({ Target = msg.to, Action = "Reply" }).receive().Data
  msg.reply({ Data = "Hello, " .. name .. "!" })
end)

この場合、aoconnectresult 関数では最初の "Hello, World!" は取得できますが、2回目の "Hello, " .. name .. "!" は取得できません。


dry

メッセージをArweaveに書き込まずに実行します(ドライラン)。

const { err, res, out } = await ao.dry({ pid, data, action, tags, check, get });

res

既存の mid(メッセージID)を対象に、msg と同様の処理を行います。

const { err, res, out } = await ao.res({ pid, mid, check, get });

ress

あるプロセスから複数の結果を取得します。メッセージが多い場合はページネーション用に next() が返されます。

const { err, out: msgs, res, next } = await ao.ress({ pid, limit, asc, from });

if (next) {
  const { out: msgs2 } = await next();
}
  • pid:プロセスID
  • limit:取得件数
  • asc:デフォルトでは降順。昇順にする場合は asc=true
  • from:取得開始カーソル

asgn

既存のメッセージを指定プロセスに割り当てます。

const { err, mid, res, out } = await ao.asgn({ pid, mid, check, get });

load

ArweaveからLuaソーススクリプトを取得し、プロセス上で評価(eval)します。

const { err, res, mid } = await ao.load({ src, fills, pid });

eval

Luaスクリプトを指定プロセス上で評価(eval)します。

const { err, res, mid } = await ao.eval({ pid, data });

spwn

プロセスをspawnします。modulescheduler は省略された場合、自動で設定されます。

const { err, res, pid } = await ao.spwn({ module, scheduler, tags, data });

aoconnect の関数群

従来の aoconnect 関数である messagespawnresultassigndryrun も利用可能です。
また、createDataItemSignertoSigner として提供されています。

const signer = ao.toSigner(jwk);
const process = await ao.spawn({ module, scheduler, signer, tags, data });
const message = await ao.message({ process, signer, tags, data });
const result = await ao.result({ process, message });

高度な機能(Advanced Functions)


postModule

data にはWASMバイナリを指定します。overwrite を指定すると、AOインスタンスのデフォルトモジュールを置き換えます。

const { err, id: module } = await ao.postModule({ data, jwk, tags, overwrite });

postScheduler

scheduler を返す Scheduler-Locationjwk のアドレスで投稿します。

const { err, scheduler } = await ao.postScheduler({
  url,
  jwk,
  tags,
  overwrite,
});

attest

WeaveDrive用にArweaveトランザクションを証明(attest)します。

const { err, res, id } = await ao.attest({ id, tags, jwk });

avail

ArweaveトランザクションをWeaveDrive用に利用可能状態にします。

const { err, res, id } = await ao.avail({ ids, tags, jwk });

wait

spwn の後、プロセスが利用可能になるまで待機します。主に deploy の内部処理で使用されます。

const { err } = await ao.wait({ pid });

var

dryrun を使って現在のステートからLua変数を読み取ります。

const { pid } = await ao.deploy({
  src_data: `Table = { String = "Hello", Array = { "str", 3, true } }`,
});

const table = await ao.var({ pid, data: "Table" });

出力から装飾的なタグを除去し、Luaテーブルを自動的にJSONに変換します。変換の無効化は json および pretty オプションで制御可能です。

const table = await ao.var({ pid, data: "Table", json: false, pretty: true });

Process

Process クラスを使えば、さらに簡潔な構文で操作できます。

インスタンス化

const p = ao.p(pid);

or

const { p, pid } = await ao.deploy({ data, tags, src, fills });

msg

最初の引数は Action、2 番目の引数は Tags、3 番目の引数はその他のオプションです。

const { mid, res, out, err } = await p.msg(
  "Action",
  { Tag1: "value1", Tag2: "value2" },
  { get: true, check: { TagA: "valueA" }, jwk }
);

3 番目の引数のデフォルトは { get: true } で、JSON デコードされた Data を返します。

const { mid, out } = await p.msg("Action", { Tag1: "value1", Tag2: "value2" });

3 番目のパラメータがオブジェクトでない場合、デフォルトで get になります。

const { mid, out } = await p.msg("Action", { Tag1: "value1" }, "TagA");

これは次と同じ意味です。

const { mid, out } = await p.msg("Action", { Tag1: "value1" }, "TagA");

タグを渡す必要がない場合、2 番目の引数は省略できます。

const { mid, out } = await p.msg("Action", { check: "success!" }}

m

m では out のみを取得できます。これは最も簡潔な形式です。

const out = await p.m("Action", { Tag1: "value1", Tag2: "value2" });

これはテスト中によく見られるパターンです。aoconnectで同じことを行うには、特に async/await receive() を使用する場合、膨大な量のコードが必要になります。

const { p } = await ao.deploy({ tags, src_data, fills });
const out = await p.m("Action", { Tag1: "value1", Tag2: "value2" }); // get Data
assert.equal(out, EXPECTED_JSON);

dry

const { mid, out } = await p.dry("Action", { Tag1: "value1", Tag2: "value2" });

d

const out = await p.d("Action", { Tag1: "value1", Tag2: "value2" });

res

const { err, res, out } = await p.res({ mid, check, get });

r

const out = await p.r({ mid, check, get });

v

vdryrun を使用して Lua 変数を取得するための var のショートカットです。

const { p } = await ao.deploy({
  src_data: `Table = { String = "Hello", Array = { "str", 3, true } }`,
});
const table = await p.v("Table"); // { String: "Hello", Array: [ "str", 3, true ] }

自動 JSON コンバージョンを無効にし、整形出力を有効にするには、第 2 引数と第 3 引数を使用します。

const table = await p.v("Table", false, true);

関数のパイプ処理(Function Piping)

pipe

多くの関数は { err, res, out, pid, mid, id } の形式で値を返すため、これらを pipe でチェーンすることができます。pipe により複数メッセージの実行が簡単になります。

以下は deploy が内部でどのように pipe を使っているかの例です。いずれかの関数でエラーが発生すると即座に中断されます。

let fns = [
  {
    fn: "spwn",
    args: { module, scheduler, tags, data },
    then: { "args.pid": "pid" },
  },
  { fn: "wait", then: { "args.pid": "pid" } },
  { fn: "load", args: { src, fills }, then: { "args.pid": "pid" } },
];
const { err, res, out, pid } = await this.pipe({ jwk, fns });

bind

AO 以外のインスタンスから関数を使う場合は bind を使用します。

const fns = [{ fn: "post", bind: this.ar, args: { data, tags } }];

then

thenを使うことで、関数間で値を受け渡すことができます。たとえば、前の関数の結果を次の関数の引数として渡すのは一般的な操作です。

const fns = [
  {
    fn: "post",
    bind: ao.ar,
    args: { data, tags },
    then: ({ id, args, out }) => {
      args.tags.TxId = id; // adding TxId tag to `msg` args
      out.txid = id; // `out` will be returned at last with `pipe`
    },
  },
  { fn: "msg", args: { pid, tags } },
];
const {
  out: { txid },
} = await ao.pipe({ fns, jwk });

then が値を返した場合、pipe はその値で即座に終了します。また、err を使ってエラーとして中断することも可能です。

const fns = [
  {
    fn: "msg",
    args: { pid, tags },
    then: ({ inp }) => {
      if (inp.done) return inp.val;
    },
  },
  {
    fn: "msg",
    args: { pid, tags },
    err: ({ inp }) => {
      if (!inp.done) return "something went wrong";
    },
  },
];
const val = await ao.pipe({ jwk, fns });

thenには便利なパラメータがたくさんあります。

  • res : 前の処理からの res(結果)
  • args : 次の関数に渡すための args(引数)
  • out : pipe シーケンス全体の最終的な out(出力)
  • inp : 前の処理からの out(出力)
  • _ : pipe の最後にトップレベルのフィールドとして返したい値をこの _ フィールドに格納することで返却される
  • pid : もし前の関数が pid(例: deploy)を返していれば、それが渡される
  • mid : もし前の関数が mid(例: msg)を返していれば、それが渡される
  • id : もし前の関数が id(例: post)を返していれば、それが渡される

then は簡易的なハッシュマップ(オブジェクト)として記述することも可能です。

let fns = [
  {
    fn: "msg",
    args: { tags },
    then: { "args.mid": "mid", "out.key": "inp.a", "_.val": "inp.b" },
  },
  { fn: "some_func", args: {} }, // args.mid will be set from the previous `then`
];
const {
  out: { key },
  val,
} = await ao.pipe({ jwk, fns });

err

errthen と同じ構造(シグネチャ)を持ちます。err が値を返すと、その値を使って pipeError をスローします。

const fns = [
  {
    fn: "msg",
    args: { pid, tags },
    err: ({ inp }) => {
      if (!inp.done) return "something went wrong!";
    },
  },
];
const val = await ao.pipe({ jwk, fns });

cb

cb は各関数の実行後に、pipe の現在の進行状況を報告するために使用できます。

await ao.pipe({
  jwk,
  fns,
  cb: ({ i, fns, inp }) => {
    console.log(`${i} / ${fns.length} functions executed`);
  },
});

AR

AR は、Arweave の基盤となるストレージレイヤーの操作およびウォレット接続を管理します。

Instantiate

import { AR } from "wao";
const ar = new AR();

hostportprotocol を設定することで、https://arweave.net 以外の特定の gateway にアクセスすることができます。

const ar = new AR({ host: "localhost", port: 4000, protocol: "http" });

ローカルの gateway を使用する場合は、port だけを設定すればよく、他の設定項目は自動的に補完されます。

const ar = new AR({ port: 4000 });

AO クラスは内部で自動的に AR をインスタンス化します。

import { AO } from "wao";
const ao = new AO();
const ar = ao.ar;

ウォレットの設定または生成

AR は、JWK ウォレットまたは ArConnect を使って初期化できます。

const ar = await new AR().init(jwk || arweaveWallet);

新しいウォレットを生成することも可能です。ArLocal を使用している場合は、同時に AR をミントすることもできます。

const { jwk, addr, pub, balance } = await ar.gen("100"); // 100 AR をミント

これら 3 つの方法のいずれかでウォレットを設定すると、別のウォレットでそのインスタンスを使用することはできません。別のウォレットで使用したい場合は、再度初期化する必要があります。これは、ブラウザ側で接続されているアクティブアドレスが知らないうちに変更され、誤ったウォレットでトランザクションを実行してしまうのを防ぐためです。

initgen を呼び出さずにそのまま進むことも可能です。この場合、AR は必要に応じてランダムなウォレットを自動生成し、複数のウォレットの使用も許可されます。これは、AO に対して dryrun を呼び出すだけのケースで便利です。AO では dryrun にも署名が必要ですが、読み取り専用の呼び出しのたびにブラウザ拡張ウォレットを起動してユーザーに負担をかけたくない場合に役立ちます。

ウォレットが設定されると、ar.jwk および ar.addr が利用可能になります。

トークン関連メソッド

toAddr

jwk を対応するアドレスに変換します。

const addr = await ar.toAddr(jwk);
mine

保留中のブロックをマイニングします(ArLocal 専用)。

await ar.mine();
balance | toAR | toWinston

指定したアドレスの現在の残高を AR 単位で取得します。addr を省略すると、自身のアドレス(ar.addr)が使用されます。

const balance_AR = await ar.balance(); // 自分の残高を取得
const balance_Winston = ar.toWinston(balance_AR);
const balance_AR2 = ar.toAR(balance_Winston);
const balance_AR3 = await ar.balance(addr); // 任意のアドレスの残高を取得
transfer

AR トークンを送信します。amount はシンプルにするため AR 単位で指定します(winston ではありません)。

const { id } = await ar.transfer(amount, to);

第 3 引数として送信元の jwk を指定することも可能です。指定しない場合は ar.jwk が使用されます。

const { id } = await ar.transfer(amount, to, jwk);

多くの書き込み系関数では、jwk は最後の引数として、または { data, tags, jwk } のようなオブジェクトのフィールドとして指定できます。

checkWallet

checkWallet は主に内部で使用されますが、init によってウォレットが設定されていれば this.jwk を返します。そうでなければ、使用するためのランダムウォレットを生成して返します。

このようなパターンは多くの箇所で利用されています。init でウォレットが設定されており、ユーザーが渡す jwk が異なる場合、checkWallet は誤ったウォレットによる操作を防ぐためにエラーを出します。一方、init または gen でウォレットが設定されておらず、jwk も渡されない場合は、ランダムウォレットを生成して返します。

some_class_method({ jwk }){
  let err = null
  ;({ err, jwk } = await ar.checkWallet({ jwk }))
  if(!err){
    // do something with the jwk
  }
}

ストレージ関連メソッド

post

データを Arweave に投稿します。

const { err, id } = await ar.post({ data, tags });

tags は配列ではなく、簡潔に書くためにハッシュマップ(オブジェクト)として指定します。

const tags = { "Content-Type": "text/markdown", Type: "blog-post" };

同じタグ名を複数使用する必要がある場合は、値に配列を使うことで対応できます。

const tags = { Name: ["name-tag-1", "name-tag-2"] };
tx

指定したトランザクション ID のトランザクションを取得します。

const tx = await ar.tx(txid);
data

指定したトランザクション ID のデータを取得します。

const data = await ar.data(txid, true); // 文字列として取得する場合は true を指定
bundle

ANS-104 に準拠した DataItem をバンドルします。

const { err, id } = await ar.bundle(dataitems);

dataitems[ [ data, tags ], [ data, tags ], [ data, tags ]です。

const { err, id } = await ar.bundle([
  ["this is text", { "Content-Type": "text/plain" }],
  ["# this is markdown", { "Content-Type": "text/markdown" }],
  [png_image, { "Content-Type": "image/png" }],
]);

GQL

GQLArwave GraphQLの操作を簡素化し、ブロックやトランザクションをクエリします。

インスタンス化

エンドポイント url で GQL クラスをインスタンス化できます。

import { GQL } from "wao";
const gql = new GQL({ url: "https://arweave.net/graphql" }); // the default url

AR クラスは GQL を内部的に自動生成します。

import { AO } from "wao";
const ao = new AO();
const gql = ao.ar.gql;
import { AR } from "wao";
const ar = new AR();
const gql = ar.gql;

Txs

最新のトランザクションを取得します。

const txs = await gql.txs();
asc

トランザクションを昇順で取得します。

const txs = await gql.txs({ asc: true });
first

最初の X 件のトランザクションを取得します。

const txs = await gql.txs({ first: 3 });
after

ページネーションのために、特定のトランザクション以降のトランザクションを取得します。cursor を渡して指定します。

const txs = await gql.txs({ first: 3 });
const txs2 = await gql.txs({ first: 3, after: txs[2].cursor });
next

next を使うことで、より簡単にページネーションを行うことができます。

const { next, data: txs0_2 } = await gql.txs({ first: 3, next: true });
const { next: next2, data: txs3_5 } = await next();
const { next: next3, data: txs6_8 } = await next2();

トランザクションのページネーションが終了している場合、res.nextnull になります。

block

特定のブロック高の範囲内にあるトランザクションを取得します。

const txs = await gql.txs({ block: { min: 0, max: 10 } });

or

const txs = await gql.txs({ block: [0, 10] });

Ymin または max のどちらか一方のみを指定することも可能です。

トランザクション ID 指定

トランザクション ID を指定してトランザクションを取得します。

const txs = await gql.txs({ id: TXID });

または

const txs = await gql.txs({ ids: [TXID1, TXID2, TXID3] });
by Recipients

受信者(recipients)を指定してトランザクションを取得します。

const txs = await gql.txs({ recipient: ADDR });

or

const txs = await gql.txs({ recipients: [ADDR1, ADDR2, ADDR3] });
by Owners

オーナー(owners)を指定してトランザクションを取得します。

const txs = await gql.txs({ owner: ADDR });

or

const txs = await gql.txs({ owners: [ADDR1, ADDR2, ADDR3] });
by Tags

タグに一致するトランザクションを取得します。

const txs = await gql.txs({ tags: { Name: "Bob", Age: "30" } });
fields

返却されるフィールドを選択することができます。

const txs = await gql.txs({ fields: ["id", "recipient"] });

ネストされたオブジェクトに対しては、次のように指定できます。

const txs = await gql.txs({ fields: ["id", { owner: ["address", "key"] }] });

また、ハッシュマップ形式でもフィールドを指定可能です。

const txs = await gql.txs({
  fields: { id: true, owner: { address: true, key: true } },
});

false を指定すると、それ以外のフィールドが返されます。

const txs = await gql.txs({
  fields: { id: true, { block: { previous: false } } }
})

たとえば、上記の例では block 内の previous を除外し、idtimestampheight が返されます。

GraphQL クエリにおいて、トランザクションで利用可能な全フィールドは以下の通りです。

const tx_fields = `{
  id 
  anchor 
  signature 
  recipient 
  owner { address key } 
  fee { winston ar } 
  quantity { winston ar } 
  data { size type } 
  tags { name value } 
  block { id timestamp height previous } 
  parent { id }
  bundledIn { id }
}`;

Blocks

最新のブロックを取得します。

const blocks = await gql.blocks();
asc

ブロックを昇順で取得します。

const blocks = await gql.blocks({ asc: true });
first

最初の X 件のブロックを取得します。

const blocks = await gql.blocks({ first: 3 });
after

特定のブロック以降のブロックを取得してページネーションを行うには、cursor を渡して指定します。

const blocks = await gql.blocks({ first: 3 });
const blocks2 = await gql.blocks({ first: 3, after: blocks[2].cursor });
by Block IDs

ブロック ID によってブロックを取得します。

const blocks = await gql.blocks({ id: BLCID });

or

const blocks = await gql.blocks({ ids: [BLKID1, BLKID2, BLKID3] });
height

ブロック高の範囲によってブロックを取得します。

const blocks = await gql.blocks({ height: { min: 0, max: 10 } });

または

const blocks = await gql.blocks({ height: [0, 10] });
next

next を使うことで、より簡単にページネーションを行うことができます。

const { next, data: blocks0_2 } = await gql.blocks({ first: 3, next: true });
const { next: next2, data: blocks3_5 } = await next();
const { next: next3, data: blocks6_8 } = await next2();

ページネーション可能なブロックがもう存在しない場合、res.nextnull になります。

fields
const blocks = await gql.blocks({
  fields: ["id", "timestamp", "height", "previous"],
});

GraphQL クエリにおいて、ブロックで利用可能な全フィールドは以下の通りです。

const block_fields = `{ id timestamp height previous }`;

ArMem

ArMem は「Arweave in memory(インメモリ上の Arweave)」の略で、Arweave ノードおよび AO ユニットをメモリ上でエミュレートするためのクラスです。これは WAO のテストフレームワーク内で内部的に使用されます。ArMem をインスタンス化し、他のクラス間で受け渡すことで複数のエミュレータを制御できます。

インスタンス化

wao/test から WAO の connectAO をインスタンス化すると、ArMem は自動的に内部でインスタンス化されます。

import { connect } from "wao/test";
const { spawn, message, dryrun, assign, result, mem } = connect(); // aoconnect APIs
import { AO } from "wao/test";
const ao = new AO(); // ao.mem

ArMem を自分でインスタンス化し、それを他のクラスに渡すこともできます。

import { ArMem, AO, AR, connect } = "wao/test"
const mem = new ArMem()
const { spawn, message, dryrun, assign, result } = connect(mem)
const ao = new AO({ mem })
const ar = new AR({ mem })

同じ ArMem インスタンスを渡さない場合、2 つの AO インスタンスは異なる環境を持つことになります。

import { AO } = "wao/test"
const ao = new AO() // ao.mem
const ao2 = new AO() // ao2.mem

ao.memao2.mem は接続されておらず、異なるネットワーク上に存在しています。

import { AO } = "wao/test"
const ao = new AO()
const ao2 = new AO({ mem: ao.mem })

これにより、2 つのインスタンスが接続されます。

HB

HB は、Hyperbeam ノードとのインタラクションを管理します。

インスタンス化

import { HB } = "wao"
const hb = await new HB({ url: "http://localhost:10000" }).init(jwk)

metrics

ノードのメトリクスを取得します。

const metrics = await hb.metrics();

info

ノード情報を取得します。

const info = await hb.info();
const operator = info.address;

messages

あるプロセス上のメッセージを取得します。次のメッセージは message.next を使って取得できます。

const msgs = await hb.messages({ target, from, to })
for(const item of msgs.edges){
  const { cursor: slot, node: { assignment, message } } = item
}
if(msgs.next) const msgs2 = await msg.next()
  • target : プロセス ID
  • from | to : スロット番号を指定します。HB ではメッセージは txid ではなく整数(スロット)で識別されます。0 はプロセスを生成した Type=Process メッセージです。to は上限値として含まれます。両方とも省略可能です。

process

プロセスを生成(spawn)する操作に相当します。

const pid = await hb.process({ tags, data });

schedule

メッセージを送信する操作に相当します。

const slot = await hb.schedule({ tags, data, process: pid, action });

compute

結果を取得する操作に相当します。

const res = await hb.compute({ process: pid, slot });
const { Messages, Spawns, Assignments, Output } = res;

dryrun

const res = await hb.dryrun({ tags, data, process: pid, action });
const { Messages, Spawns, Assignments, Output } = res;

get

署名付きの HTTP リクエストを GET メソッドで HyperBEAM に送信します。

const res = await hb.get({ device, path });

post

署名付きの HTTP リクエストを POST メソッドで HyperBEAM に送信します。

const res = await hb.post({ device, path, tags });

request

署名付きの HTTP リクエストを HyperBEAM に送信します。

const res = await hb.request({ device, path, tags, method });
WeaveDB

Discussion