as-lan
AssemblyScript SDK for building JAM services.
Overview
as-lan provides everything you need to write JAM services in AssemblyScript:
- A service framework that handles host ABI glue so you only write business logic
- Type-safe wrappers for JAM primitives (slots, service IDs, code hashes, …)
- A scaffold script that sets up a ready-to-build project in seconds
Next Steps
- Quick Start — scaffold a new service in one command
- SDK API — types, service framework, and utilities reference
- Testing — test framework and configurable ecalli mocks
Quick Start
Scaffold a New Service
Create a new JAM service project with a single command:
curl -sL https://todr.me/as-lan/start.sh | bash -s my-service
cd my-service
npm run build
This will:
- Create a
my-service/directory with a git repo - Add the as-lan SDK as a git submodule
- Download template files from the fibonacci example and patch paths
- Run
npm install
What You Get
my-service/
├── assembly/
│ ├── index.ts # Entry point — re-exports refine & accumulate
│ ├── fibonacci.ts # Example service logic (fibonacci computation)
│ ├── index.test.ts # Unit tests
│ ├── test-run.ts # Test runner entry point
│ └── tsconfig.json # AssemblyScript path mappings
├── bin/
│ └── test.js # Test runner (node)
├── sdk/ # as-lan SDK (git submodule)
│ ├── sdk-ecalli-mocks/ # Configurable ecalli stubs for testing
│ └── pvm-adapter.wat # Adapter mapping WASM imports to PVM ecalli host calls
├── asconfig.json
└── package.json
The ecalli host call stubs used for testing live in sdk/sdk-ecalli-mocks/ and are shared across all services. There is no per-service ecalli/ directory — just a dependency in package.json:
"ecalli": "file:./sdk/sdk-ecalli-mocks"
Implement Your Service
Edit assembly/fibonacci.ts (rename it to match your service). You need to implement refine and accumulate functions (or is_authorized for an authorizer service).
Each function takes (ptr: u32, len: u32) raw memory arguments and returns a packed u64 result. The SDK provides helpers for parsing arguments, and results are returned by calling .toPtrAndLen() on a BytesBlob:
import { Logger, Optional, RefineArgs, AccumulateArgs, encodeOptionalCodeHash } from "@fluffylabs/as-lan";
import { CodeHash } from "@fluffylabs/as-lan";
const logger = Logger.create("my-service");
export function accumulate(ptr: u32, len: u32): u64 {
const result = AccumulateArgs.parse(ptr, len);
if (result.isError) {
logger.warn(`Failed to parse accumulate args: ${result.error}`);
return 0;
}
const args = result.okay!;
logger.info(`accumulate called for service ${args.serviceId} at slot ${args.slot}`);
// TODO: implement your accumulate logic here
return encodeOptionalCodeHash(Optional.none<CodeHash>()).toPtrAndLen();
}
export function refine(ptr: u32, len: u32): u64 {
const result = RefineArgs.parse(ptr, len);
if (result.isError) {
logger.warn(`Failed to parse refine args: ${result.error}`);
return 0;
}
const args = result.okay!;
logger.info(`refine called for service ${args.serviceId}`);
// TODO: implement your refine logic here — for now, echo payload back
return args.payload.toPtrAndLen();
}
Build & Test
You need wasm-pvm installed (cargo install wasm-pvm-cli@0.7.0) to produce PVM binaries.
npm run build # compile WASM (debug + release) and PVM binary
npm test # compile test target and run tests
The build pipeline:
- Compiles AssemblyScript to WASM (debug + release targets)
- Converts the release WASM to a JAM PVM binary (
.pvm) usingwasm-pvm
The resulting build/release.pvm is the JAM SPI binary ready for deployment.
See the Testing guide for details on writing tests and configuring ecalli mocks.
Manual Setup (without the script)
If you prefer to set things up yourself:
-
Add the SDK as a git submodule:
git submodule add https://github.com/tomusdrw/as-lan.git sdk -
Add dependencies to
package.json:{ "devDependencies": { "@fluffylabs/as-lan": "file:./sdk", "assemblyscript": "^0.28.9", "ecalli": "file:./sdk/sdk-ecalli-mocks" } } -
Build the ecalli mocks before first use:
cd sdk/sdk-ecalli-mocks && npm install && npm run build && cd ../.. -
Follow the patterns in the scaffolded project for
assembly/index.ts,asconfig.json, etc.
See the SDK API reference for the full list of available types and utilities.
SDK API
Calling Convention
Your service WASM module must export one of:
refine(ptr: u32, len: u32) -> u64andaccumulate(ptr: u32, len: u32) -> u64for a regular serviceis_authorized(ptr: u32, len: u32) -> u64for an authorizer service
Arguments are passed as a pointer + length into linear memory. The return value is a packed u64 where len = result >> 32 and ptr = result & 0xffffffff. Use .toPtrAndLen() on a BytesBlob to produce this value.
Service Helpers
The SDK provides helpers for parsing arguments and encoding results:
ctx.parseArgs(ptr, len)— Parse raw refine/accumulate arguments. Panics on invalid host-provided entry-point data.ctx.yieldHash(hash)— Encode an optional accumulate result hash and return the packedu64.readFromMemory(ptr, len)— Read raw bytes from WASM linear memory.ptrAndLen(data)— Pack a rawUint8Arrayinto au64return value.
To return a result, call .toPtrAndLen() on a BytesBlob (or use ptrAndLen() for a raw Uint8Array).
Entry Point Pattern
// assembly/index.ts
export { refine, accumulate } from "./service";
// assembly/service.ts
import { AccumulateContext, Logger, RefineContext } from "@fluffylabs/as-lan";
import { CodeHash } from "@fluffylabs/as-lan";
const logger = Logger.create("my-service");
export function accumulate(ptr: u32, len: u32): u64 {
const ctx = AccumulateContext.create();
const args = ctx.parseArgs(ptr, len);
logger.info(`accumulate called for service ${args.serviceId}`);
return ctx.yieldHash(null);
}
export function refine(ptr: u32, len: u32): u64 {
const ctx = RefineContext.create();
const args = ctx.parseArgs(ptr, len);
logger.info(`refine called for service ${args.serviceId}`);
return args.payload.toPtrAndLen();
}
Parsed Argument Types
RefineArgs (fields available after successful parse):
coreIndex: CoreIndex(u16)itemIndex: u32serviceId: ServiceId(u32)payload: BytesBlobworkPackageHash: WorkPackageHash(Bytes32)
AccumulateArgs (fields available after successful parse):
slot: Slot(u32)serviceId: ServiceId(u32)argsLength: u32
Types
All types are imported from "@fluffylabs/as-lan".
| Type | Description |
|---|---|
Slot | Block slot number (u32) |
ServiceId | Service identifier (u32) |
CoreIndex | Core index (u16) |
CodeHash | 32-byte blake2b hash |
PayloadHash | 32-byte blake2b payload hash |
WorkPackageHash | 32-byte work package hash |
HeaderHash | 32-byte blake2b header hash |
StateRootHash | 32-byte blake2b state root hash |
MmrPeakHash | 32-byte keccak256 MMR peak hash |
BytesBlob | Variable-length byte array with .toPtrAndLen() |
WorkOutput | Alias for BytesBlob |
WorkPayload | Alias for BytesBlob |
AuthOutput | Alias for BytesBlob |
Optional<T> | Option type with .isSome and .val (nullable T) |
OptionalN<T> | Option type for non-nullable T |
Result<Ok, Err> | Result type with .isOkay, .okay, .isError, .error |
ResultN<Ok, Err> | Result type for non-nullable Ok |
Bytes32 | Fixed-size 32-byte wrapper with hex parsing |
Utilities
Logger
JIP-1 structured logger. Methods: fatal, warn, info, debug, trace.
debug and trace are compiled out at optimization level 3 (release builds).
import { Logger } from "@fluffylabs/as-lan";
const logger = Logger.create("my-service");
logger.info("processing work item");
logger.debug(`payload length: ${payload.length}`);
Binary size note:
Loggeracceptsstringmessages, so using template literals (`value: ${n}`) pulls in AssemblyScript’s string concatenation, UTF-8 encoding, and number-to-string machinery. This can add ~1.3 KiB to the WASM output. If binary size is a concern, useLogMsginstead (see below).
LogMsg (lightweight logger)
A buffer-based logger that writes directly to a fixed-size byte buffer,
bypassing AssemblyScript’s String machinery entirely. It uses a builder
pattern to append text and numbers, then sends the raw bytes to the host.
Using LogMsg instead of Logger can reduce WASM output by 5KB and PVM
output by 8KB for a typical service. Note that for large services the
trade-off between code size and readability & debuggability might not be worth it.
import { LogMsg } from "@fluffylabs/as-lan";
const logger = LogMsg.create("my-service");
logger.str("processing item ").u32(itemId).info();
logger.str("result: ").u64(value).str(" bytes").debug();
Builder methods (all return LogMsg for chaining):
.str(s)— append an ASCII string.u32(v)— append an unsigned 32-bit number as decimal.u64(v)— append an unsigned 64-bit number as decimal.i32(v)— append a signed 32-bit number as decimal.blob(data)— append aBytesBlobas0x-prefixed hex (no String allocation)
Terminal methods (send the message and reset the buffer):
.fatal(),.warn(),.info(),.debug(),.trace()
debug and trace are compiled out at optimization level 3, same as Logger.
ByteBuf (byte-buffer builder)
A lightweight Uint8Array builder that avoids String allocations. Used
internally by LogMsg and useful for constructing binary output (e.g. auth
traces) from string fragments and raw byte slices.
import { ByteBuf, ptrAndLen } from "@fluffylabs/as-lan";
const result = ByteBuf.create(64)
.str("Auth=<")
.bytes(token.raw)
.str(">")
.finish(); // → Uint8Array
return ptrAndLen(result);
Builder methods (all return ByteBuf for chaining):
.str(s)— append an ASCII string.bytes(data)— append rawUint8Array.hex(data)— appendUint8Arrayas0x-prefixed hex.u32(v),.u64(v),.i32(v)— append numbers as decimal ASCII
Terminal methods:
.finish()— copy buffer into a newUint8Arrayand reset.reset()— discard contents without producing output
The buffer is heap-allocated at a fixed capacity; writes beyond the capacity are silently truncated.
Decoder
Binary protocol decoder for reading host-provided data.
import { Decoder } from "@fluffylabs/as-lan";
const decoder = Decoder.fromBlob(data);
const value = decoder.varU64();
const hash = decoder.bytes32();
const blob = decoder.bytesVarLen();
Key methods: u8, u16, u32, u64, varU64, bytes32, bytesFixLen, bytesVarLen, object, optional, sequenceFixLen, sequenceVarLen, skip, isFinished, isError.
Byte Types
Bytes32— Fixed-size 32-byte array with hex string parsingBytesBlob— Variable-length byte array wrapper with.toPtrAndLen()for returning results
Host Calls (ecalli)
Declared host functions available to services. Import from "@fluffylabs/as-lan" or
from a specific group ("@fluffylabs/as-lan/ecalli/general", .../refine, .../accumulate).
General (available in all contexts):
| ID | Function | Description |
|---|---|---|
| 0 | gas() | Returns remaining gas |
| 1 | fetch(dest_ptr, offset, length, kind, param1, param2) | Fetch context data |
| 2 | lookup(service, hash_ptr, out_ptr, offset, length) | Look up preimage by hash |
| 3 | read(service, key_ptr, key_len, out_ptr, offset, length) | Read from storage |
| 4 | write(key_ptr, key_len, value_ptr, value_len) | Write to storage |
| 5 | info(service, out_ptr, offset, length) | Get service account info |
| 100 | log(level, target_ptr, target_len, msg_ptr, msg_len) | JIP-1 debug log (prefer Logger) |
Refine (available during refinement):
| ID | Function | Description |
|---|---|---|
| 6 | historical_lookup(service, hash_ptr, out_ptr, offset, length) | Historical preimage lookup |
| 7 | export_(segment_ptr, segment_len) | Export a data segment |
| 8 | machine(code_ptr, code_len, entrypoint) | Create inner PVM machine |
| 9 | peek(machine_id, dest_ptr, source, length) | Read inner machine memory |
| 10 | poke(machine_id, source_ptr, dest, length) | Write inner machine memory |
| 11 | pages(machine_id, start_page, page_count, access_type) | Set inner machine page access |
| 12 | invoke(machine_id, io_ptr, out_r8) | Run inner machine (r7=exit reason, r8 written to out_r8) |
| 13 | expunge(machine_id) | Destroy inner machine |
Accumulate (available during accumulation):
| ID | Function | Description |
|---|---|---|
| 14 | bless(manager, auth_queue_ptr, delegator, registrar, auto_accum_ptr, count) | Set privileged config |
| 15 | assign(core, auth_queue_ptr, assigners) | Assign core |
| 16 | designate(validators_ptr) | Set next epoch validators |
| 17 | checkpoint() | Commit state, return remaining gas |
| 18 | new_service(code_hash_ptr, code_len, gas, allowance, gratis_storage, id) | Create service |
| 19 | upgrade(code_hash_ptr, gas, allowance) | Upgrade service code |
| 20 | transfer(dest, amount, gas_fee, memo_ptr) | Transfer funds |
| 21 | eject(service, prev_code_hash_ptr) | Remove service |
| 22 | query(hash_ptr, length, out_r8) | Query preimage status (r8 written to out_r8) |
| 23 | solicit(hash_ptr, length) | Request preimage availability |
| 24 | forget(hash_ptr, length) | Cancel preimage solicitation |
| 25 | yield_result(hash_ptr) | Provide accumulation result hash |
| 26 | provide(service, preimage_ptr, preimage_len) | Supply solicited preimage |
All functions return i64. Error sentinels are defined in EcalliResult:
NONE (-1), WHAT (-2), OOB (-3), WHO (-4), FULL (-5), CORE (-6), CASH (-7), LOW (-8), HUH (-9).
Test Utilities
The SDK exports test helpers for writing AssemblyScript tests:
import { test, Assert } from "@fluffylabs/as-lan/test";
const allTests = [
test("my test", (): Assert => {
const a = Assert.create();
a.isEqual(1 + 1, 2, "basic math");
return a;
}),
];
See the Testing guide for the full test framework, including
configurable ecalli mocks (TestGas, TestFetch, TestLookup, TestStorage).
Testing
The SDK provides a test framework for writing and running AssemblyScript tests against your JAM service, with configurable ecalli host call mocks.
Architecture
Testing involves two layers that work together:
┌─────────────────────────────────────┐
│ AssemblyScript test code (WASM) │
│ │
│ ┌───────────┐ ┌────────────────┐ │
│ │ Your test │ │ SDK test utils │ │
│ │ assertions│ │ TestGas, etc. │ │
│ └───────────┘ └───────┬────────┘ │
│ │ │
│ @external("ecalli", ...) │
└─────────────────────────┼───────────┘
│ WASM imports
┌─────────────────────────┼───────────┐
│ sdk-ecalli-mocks (Node.js) │
│ │
│ Stub implementations of ecalli │
│ host calls + configuration state │
└─────────────────────────────────────┘
-
sdk-ecalli-mocks/— A TypeScript (Node.js) package that provides stub implementations of all 27 ecalli host calls (general 0-5 + 100, refine 6-13, accumulate 14-26). These stubs satisfy the WASM imports at test time and hold configurable state (gas value, storage map, preimage data, etc.). -
sdk/test/test-ecalli/— AssemblyScript wrapper classes (TestGas,TestFetch,TestLookup,TestStorage,TestEcalli) that bridge to the JS-side stubs via@external("ecalli", ...)WASM imports. These give your AS test code a high-level API for configuring stub behavior.
Writing Tests
Test structure
Tests use the test() helper and Assert class from the SDK:
import { Assert, Test, test } from "@fluffylabs/as-lan/test";
export const TESTS: Test[] = [
test("my feature works", () => {
const assert = Assert.create();
assert.isEqual(1 + 1, 2, "basic math");
return assert;
}),
];
Each test function returns an Assert instance. Use assert.isEqual(actual, expected, msg)
to add assertions — any failure is recorded and reported after the test completes.
Test runner
Each service needs a test-run.ts entry point that registers test suites:
import { TestSuite, runTestSuites } from "@fluffylabs/as-lan/test";
import * as myTests from "./index.test";
export function runAllTests(): void {
runTestSuites([TestSuite.create(myTests.TESTS, "my-service.ts")]);
}
And a bin/test.js that boots the WASM and runs:
import { setMemory } from "ecalli";
import { memory, runAllTests } from "../build/test.js";
setMemory(memory);
runAllTests();
Build and run
npm test # compiles test target and runs bin/test.js
This compiles your test-run entry point to WASM (with the test target from
asconfig.json), then executes it in Node.js with the ecalli stubs providing
host call implementations.
Configuring Ecalli Mocks
By default the stubs provide sensible test values (e.g. gas() returns
1_000_000, lookup() returns "test-preimage", read()/write() use an
in-memory Map). You can override these from within your AS test code.
TestGas
Set the value returned by the gas() ecalli:
import { TestGas } from "@fluffylabs/as-lan/test";
TestGas.set(500); // gas() will now return 500
TestFetch
Set fixed data returned by the fetch() ecalli (overrides the default
kind-dependent pattern):
import { TestFetch } from "@fluffylabs/as-lan/test";
const data = new Uint8Array(4);
data[0] = 0xde; data[1] = 0xad; data[2] = 0xbe; data[3] = 0xef;
TestFetch.setData(data);
TestLookup
Set the preimage returned by the lookup() ecalli:
import { TestLookup } from "@fluffylabs/as-lan/test";
const preimage = new Uint8Array(3);
preimage[0] = 1; preimage[1] = 2; preimage[2] = 3;
TestLookup.setPreimage(preimage);
TestStorage
Pre-populate or delete entries in the read()/write() stub storage:
import { BytesBlob } from "@fluffylabs/as-lan";
import { TestStorage } from "@fluffylabs/as-lan/test";
// Pre-populate a key
const key = BytesBlob.wrap(String.UTF8.encode("counter"));
const value = BytesBlob.wrap(new Uint8Array(8));
TestStorage.set(key, value);
// Delete a key
TestStorage.set(key, null);
TestEcalli
Reset all configuration to defaults and clear storage:
import { TestEcalli } from "@fluffylabs/as-lan/test";
TestEcalli.reset();
Default Stub Behavior
General (0-5, 100):
| Ecalli | Default |
|---|---|
gas() | Returns 1_000_000 |
fetch() | Writes a 16-byte kind-dependent pattern, returns 16 |
lookup() | Writes "test-preimage" (13 bytes), returns 13 |
read() | Reads from in-memory Map; returns NONE (-1) if key missing |
write() | Writes to in-memory Map; returns previous value length or NONE |
info() | Returns a 96-byte structure (code_hash=0xAA..., balance=1000) |
log() | Prints [LEVEL] target: message to console |
Refine (6-13):
| Ecalli | Default |
|---|---|
historical_lookup() | Writes "test-historical" (15 bytes), returns 15 |
export_() | Returns incrementing segment index (0, 1, 2, …) |
machine() | Returns incrementing machine ID (0, 1, 2, …) |
peek() | Returns OK (0) |
poke() | Returns OK (0) |
pages() | Returns OK (0) |
invoke() | Returns HALT (0), writes r8 = 0 |
expunge() | Returns OK (0) |
Accumulate (14-26):
| Ecalli | Default |
|---|---|
bless() | Returns OK (0) |
assign() | Returns OK (0) |
designate() | Returns OK (0) |
checkpoint() | Returns remaining gas (delegates to gas() mock) |
new_service() | Returns incrementing service ID (256, 257, …) |
upgrade() | Returns OK (0) |
transfer() | Returns OK (0) |
eject() | Returns OK (0) |
query() | Returns NONE (-1), writes r8 = 0 |
solicit() | Returns OK (0) |
forget() | Returns OK (0) |
yield_result() | Returns OK (0) |
provide() | Returns OK (0) |
See the fibonacci and ecalli-test examples for usage examples.