WAO SDK streamlines Arweave/AO development with elegant syntax enhancements and seamless message piping for an enjoyable coding experience. Additionally, it includes a drop-in replacement for aoconnect
, allowing to test lua scripts 1000x faster than the mainnet by emulating AO units in-memory.
yarn add wao
By replacing aoconnect
with WAO connect, everything runs in-memory with zero latency and your tests execute 1000x faster. The APIs are identical. So there's no need to change anything else in your code.
//import { spawn, message, dryrun, assign, result } from "@permaweb/aoconnect"
import { connect } from "wao/test"
const { spawn, message, dryrun, assign, result } = connect()
It's super easy to set up a test AO project manually.
mkdir wao-test && cd wao-test
yarn init && yarn add wao && yarn add mocha chai --dev
Add a test
command to your package.json
, and set module
type to work with ES6.
{
"name": "wao-test",
"version": "1.0.0",
"type": "module",
"dependencies": {
"wao": "^0.1.1"
},
"devDependencies": {
"chai": "^5.1.2",
"mocha": "^10.8.2"
},
"scripts": {
"test": "mocha --node-option=experimental-wasm-memory64"
}
}
Create test
directory and test.js
file.
mkdir test && touch test.js
Write a simple test in test.js
.
import { expect } from "chai"
import { connect } from "wao/test"
const { accounts, spawn, message, dryrun } = connect()
// import { wait } from "wao/utils"
const src_data = `
Handlers.add("Hello", "Hello", function (msg)
msg.reply({ Data = "Hello, World!" })
end)
`
describe("WAO", function () {
this.timeout(0)
describe("Aoconnect Replacement", function () {
it("should spawn a process and send messages", async () => {
const pid = await spawn({ signer: accounts[0].signer })
// 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,
})
expect(res.Messages[0].Data).to.eql("Hello, World!")
})
})
})
Note that generating random Arweave wallets for every test takes time and slows down your test executions, so Wao connect provides pre-generated accounts for your tests, which saves hours if you are to run your tests thousands of times.
accounts[0] = { jwk, addr, signer }
Run the test.
yarn test
WAO comes with elegant syntactic sugar and makes writing AO projects absolute joy.
The same test can be written as follows.
import { expect } from "chai"
import { AO } from "wao/test"
const src_data = `
Handlers.add( "Hello", "Hello", function (msg)
msg.reply({ Data = "Hello, World!" })
end)
`
describe("WAO", function () {
this.timeout(0)
describe("AO Class", function () {
it("should spawn a process send messages", async () => {
const ao = new AO()
const { p } = await ao.deploy({ src_data })
expect(await p.d("Hello")).to.eql("Hello, World!")
})
})
})
The AO
class is not only for tests, but also for production code. You just need to import from a different path.
import { AO } from "wao"
AR
handles operations on the base Arweave Storage layer as well as wallet connections.
- Instantiate
- Set or Generate Wallet
- toAddr
- mine
- balance | toAR | toWinston
- transfer
- checkWallet
- post
- tx
- data
- bundle
const ar = new AR()
host
, port
, and protocol
can be set to access a specific gateway rather than https://arweave.net
.
const ar = new AR({ host: "localhost", port: 4000, protocol: "http" })
In case of local gateways, you can only set port
and the rest will be automatically figured out.
const ar = new AR({ port: 4000 })
You can initialize AR with a wallet JWK or ArConnect.
const ar = await new AR().init(jwk || arweaveWallet)
Or you can generate a new wallet. In case of ArLocal, you can mint AR at the same time.
const { jwk, addr, pub, balance } = await ar.gen("100") // mint 100 AR
Once a wallet is set in one of these 3 ways, you cannot use the instance with another wallet unless you re-initialize it with another wallet. This is to prevent executing transactions with the wrong wallet when the browser connected active address has been changed unknowingly.
You can go on without calling init
or gen
, in this case, AR generates a random wallet when needed, and also using different wallets will be allowed. This is useful, if you are only calling dryrun
with AO, since AO requires a signature for dryrun
too, but you don't want to bother the user by triggering the browser extension wallet for read only calls.
Once a wallet is set, ar.jwk
and ar.addr
will be available.
Convert a jwk to the corresponding address.
const addr = await ar.toAddr(jwk)
Mine pending blocks (only for arlocal).
await ar.mine()
Get the current balance of the specified address in AR. addr
will be ar.addr
if omitted.
const balance_AR = await ar.balance() // get own balance
const balance_Winston = ar.toWinston(balance_AR)
const balance_AR2 = ar.toAR(balance_Winston)
const balance_AR3 = await ar.balance(addr) // specify wallet address
Transfer AR token. amount
is in AR, not in winston for simplicity.
const { id } = await ar.transfer(amount, to)
You can set a jwk to the 3rd parameter as a sender, otherwise the sender is ar.jwk
.
const { id } = await ar.transfer(amount, to, jwk)
For most write functions, jwk
can be specified as the last parameter or a field like { data, tags, jwk }
.
checkWallet
is mostly used internally, but it returns this.jwk
if a wallet has been assigned with init
, or else it generates a random wallet to use. The following pattern is used in many places. With this pattern, if a wallet is set with init
and the jwk
the user is passing is different, checkWallet
produces an error to prevent the wrong wallet. If no wallet has been set with init
or gen
and the jwk
is not passed, it generates and returns a random wallet.
some_class_method({ jwk }){
let err = null
;({ err, jwk } = await ar.checkWallet({ jwk }))
if(!err){
// do domething with the jwk
}
}
Post a data to Arweave.
const { err, id } = await ar.post({ data, tags })
tags
are not an Array but a hash map Object for brevity.
const tags = { "Content-Type": "text/markdown", Type: "blog-post" }
If you must use the same name for multiple tags, the value can be an Array.
const tags = { Name: [ "name-tag-1", "name-tag-2" ] }
Get a transaction.
const tx = await ar.tx(txid)
Get a data.
const data = await ar.data(txid, true) // true if string
Bundle ANS-104 dataitems.
const { err, id } = await ar.bundle(dataitems)
dataitems
are [ [ 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" }]
])
- Instantiate
- deploy
- msg
- dry
- asgn
- load
- eval
- spwn
- aoconnect Functions
- postModule
- postScheduler
- wait
- Function Piping
You can initialize AO in the same way as AR.
const ao = await new AO().init(arweaveWallet || jwk)
If you need to pass AR settings, use ar
. ao.ar
will be automatically available.
const ao = await new AO({ ar: { port: 4000 }}).init(arweaveWallet || jwk)
const addr = ao.ar.addr
await ao.ar.post({ data, tags })
Spawn a process, get a Lua source, and eval the script. src
is an Arweave txid of the Lua script.
const { err, res, pid } = await ao.deploy({ data, tags, src, fills })
fills
replace the Lua source script from src
.
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" }
This will end up in the following lua script.
local replace_me = 'hello'
local replace_me_again = 'world'
local replace_me_with_hello_again = 'hello'
In case you have multiple scripts, use loads
and pass src
and fills
respectively.
await ao.deploy({ tags, loads: [ { src, fills }, { src: src2, fills: fills2 } ] })
Send a message.
const { err, mid, res, out } = await ao.msg({
data, action, tags, check, checkData, get
})
check
determins if the message call is successful by checking through Tags
in Messages
in res
.
const check = { "Status" : "Success" } // succeeds if Status tag is "Success"
const check2 = { "Status" : true } // succeeds if Status tag exists
checkData
checks Data
field instead of Tags
.
const checkData = "Success" // succeeds if Data field is "Success"
const checkData2 = true // succeeds if Data field exists
get
will return specified data via 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()
Dryrun a message without writing to Arweave.
const { err, res, out } = await ao.dry({
data, action, tags, check, checkData, get
})
Assign an existing message to a process.
const { err, mid, res, out } = await ao.asgn({ pid, mid, check, checkData, get })
Get a Lua source script from Arweave and eval it on a process.
const { err, res, mid } = await ao.load({ src, fills, pid })
Eval a Lua script on a process.
const { err, res, mid } = await ao.eval({ pid, data })
Spawn a process. module
and scheduler
are auto-set if omitted.
const { err, res, pid } = await ao.spwn({ module, scheduler, tags, data })
The original aoconnect functions message
| spawn
| result
| assign
| dryrun
are also available.
createDataItemSigner
is available as toSigner
.
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 })
data
should be wasm binary. overwrite
to replace the default module set to the AO instance.
const { err, id: module } = await ao.postModule({ data, jwk, tags, overwrite })
This will post Scheduler-Location
with the jwk
address as the returning scheduler
.
const { err, scheduler } = await ao.postScheduler({ url, jwk, tags, overwrite })
wait
untill the process becomes available after spwn
. This is mostly used internally with deploy
.
const { err } = await ao.wait({ pid })
Most functions return in the format of { err, res, out, pid, mid, id }
, and these function can be chained with pipe
, which makes executing multiple messages a breeze.
For example, following is how deploy
uses pipe
internally. The execusion will be immediately aborted if any of the functions in fns
produces an error.
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 })
If the function comes from other instances rather than AO
, use bind
.
const fns = [{ fn: "post", bind: this.ar, args: { data, tags }}]
You can pass values between functions with then
. For instance, passing the result from the previous functions to the next function's arguments is a common operation.
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 })
If then
returns a value, pipe
will immediately return with that single value. You can also use err
to abort pipe
with an error.
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
has many useful parameters.
res
:res
from the previous resultargs
:args
for the next functionout
:out
the finalout
result from thepipe
sequenceinp
:out
from the previous result_
: if values are assigned to the_
fields,pipe
returns them as top-level fields in the endpid
:pid
will be passed if any previous functions returnspid
( e.g.deploy
)mid
:mid
will be passed if any previous functions returnsmid
( e.g.msg
)id
:id
will be passed if any previous functions returnsid
( e.g.post
)
then
can be a simplified hashmap object.
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 })
cb
can report the current progress of pipe
after every function execution.
await ao.pipe({ jwk, fns, cb: ({ i, fns, inp })=>{
console.log(`${i} / ${fns.length} functions executed`)
}})