Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Create a my-service/ directory with a git repo
  2. Add the as-lan SDK as a git submodule
  3. Download template files from the fibonacci example and patch paths
  4. 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:

  1. Compiles AssemblyScript to WASM (debug + release targets)
  2. Converts the release WASM to a JAM PVM binary (.pvm) using wasm-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:

  1. Add the SDK as a git submodule:

    git submodule add https://github.com/tomusdrw/as-lan.git sdk
    
  2. Add dependencies to package.json:

    {
      "devDependencies": {
        "@fluffylabs/as-lan": "file:./sdk",
        "assemblyscript": "^0.28.9",
        "ecalli": "file:./sdk/sdk-ecalli-mocks"
      }
    }
    
  3. Build the ecalli mocks before first use:

    cd sdk/sdk-ecalli-mocks && npm install && npm run build && cd ../..
    
  4. 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) -> u64 and accumulate(ptr: u32, len: u32) -> u64 for a regular service
  • is_authorized(ptr: u32, len: u32) -> u64 for 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 packed u64.
  • readFromMemory(ptr, len) — Read raw bytes from WASM linear memory.
  • ptrAndLen(data) — Pack a raw Uint8Array into a u64 return 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: u32
  • serviceId: ServiceId (u32)
  • payload: BytesBlob
  • workPackageHash: 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".

TypeDescription
SlotBlock slot number (u32)
ServiceIdService identifier (u32)
CoreIndexCore index (u16)
CodeHash32-byte blake2b hash
PayloadHash32-byte blake2b payload hash
WorkPackageHash32-byte work package hash
HeaderHash32-byte blake2b header hash
StateRootHash32-byte blake2b state root hash
MmrPeakHash32-byte keccak256 MMR peak hash
BytesBlobVariable-length byte array with .toPtrAndLen()
WorkOutputAlias for BytesBlob
WorkPayloadAlias for BytesBlob
AuthOutputAlias 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
Bytes32Fixed-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: Logger accepts string messages, 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, use LogMsg instead (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 a BytesBlob as 0x-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 raw Uint8Array
  • .hex(data) — append Uint8Array as 0x-prefixed hex
  • .u32(v), .u64(v), .i32(v) — append numbers as decimal ASCII

Terminal methods:

  • .finish() — copy buffer into a new Uint8Array and 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 parsing
  • BytesBlob — 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):

IDFunctionDescription
0gas()Returns remaining gas
1fetch(dest_ptr, offset, length, kind, param1, param2)Fetch context data
2lookup(service, hash_ptr, out_ptr, offset, length)Look up preimage by hash
3read(service, key_ptr, key_len, out_ptr, offset, length)Read from storage
4write(key_ptr, key_len, value_ptr, value_len)Write to storage
5info(service, out_ptr, offset, length)Get service account info
100log(level, target_ptr, target_len, msg_ptr, msg_len)JIP-1 debug log (prefer Logger)

Refine (available during refinement):

IDFunctionDescription
6historical_lookup(service, hash_ptr, out_ptr, offset, length)Historical preimage lookup
7export_(segment_ptr, segment_len)Export a data segment
8machine(code_ptr, code_len, entrypoint)Create inner PVM machine
9peek(machine_id, dest_ptr, source, length)Read inner machine memory
10poke(machine_id, source_ptr, dest, length)Write inner machine memory
11pages(machine_id, start_page, page_count, access_type)Set inner machine page access
12invoke(machine_id, io_ptr, out_r8)Run inner machine (r7=exit reason, r8 written to out_r8)
13expunge(machine_id)Destroy inner machine

Accumulate (available during accumulation):

IDFunctionDescription
14bless(manager, auth_queue_ptr, delegator, registrar, auto_accum_ptr, count)Set privileged config
15assign(core, auth_queue_ptr, assigners)Assign core
16designate(validators_ptr)Set next epoch validators
17checkpoint()Commit state, return remaining gas
18new_service(code_hash_ptr, code_len, gas, allowance, gratis_storage, id)Create service
19upgrade(code_hash_ptr, gas, allowance)Upgrade service code
20transfer(dest, amount, gas_fee, memo_ptr)Transfer funds
21eject(service, prev_code_hash_ptr)Remove service
22query(hash_ptr, length, out_r8)Query preimage status (r8 written to out_r8)
23solicit(hash_ptr, length)Request preimage availability
24forget(hash_ptr, length)Cancel preimage solicitation
25yield_result(hash_ptr)Provide accumulation result hash
26provide(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):

EcalliDefault
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):

EcalliDefault
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):

EcalliDefault
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.