@effectstream/concise
Package:
@effectstream/concise· Source
Type-safe, compact message schemas for EffectStream — the wire format the batcher uses to pack many small user inputs into one on-chain transaction. Define a grammar of allowed commands; the package generates, parses, and validates inputs against it with TypeBox.
Install
bun add @effectstream/concise
# or
npm install @effectstream/concise
Standalone usage
Building and parsing concise inputs
Most of the in-repo usage centers on the grammar API: define a tuple
shape per command, then encode (generateRawStmInput /
generateStmInput) and decode (parseStmInput) values against it.
import {
generateStmInput,
parseStmInput,
toKeyedJsonGrammar,
} from "@effectstream/concise";
import { Type } from "@sinclair/typebox";
const grammar = {
join: [["user", Type.String()]] as const,
leave: [["user", Type.String()]] as const,
} as const;
const keyed = toKeyedJsonGrammar(grammar);
const tuple = generateStmInput(grammar, "join", { user: "alice" });
// tuple === ["join", "alice"]
const parsed = parseStmInput(JSON.stringify(tuple), grammar, keyed);
// parsed.prefix === "join", parsed.data.user === "alice"
Batcher message construction
The package also ships primitives for building the exact message a user's wallet signs before posting to the batcher. These are intended for client SDKs that submit to the batcher HTTP endpoint (and as a reference encoder for tests).
import {
createBatcherSubunit,
createMessageForBatcher,
hashBatchSubunit,
} from "@effectstream/concise";
import { AddressType } from "@effectstream/utils";
const message = createMessageForBatcher(
null,
String(Date.now()) as `${number}`,
"0x1234567890123456789012345678901234567890",
AddressType.EVM,
"join|alice",
);
// const signature = await wallet.signMessage(message);
const subunit = createBatcherSubunit(
String(Date.now()) as `${number}`,
"0x1234567890123456789012345678901234567890",
AddressType.EVM,
/* signature */ "0x…",
"join|alice",
);
const hash = hashBatchSubunit(subunit);
Inside EffectStream
@effectstream/concise sits between user-facing wallets and the batcher:
clients build messages with the helpers above, sign them through
@effectstream/wallets, and POST them to the batcher HTTP endpoint
(@effectstream/batcher-sdk). The batcher then runs the same encoding to
pack accepted subunits into the on-chain transaction the state machine
later reads.
Key exports
Grammar / schema (heavily used across the runtime + state machine):
generateRawStmInput(grammarEntry, prefix, data)— the high-volume encoder (~46cross-package call sites in this repo).buildBatchData(maxSize, inputs)— pack as many subunits as fit under a byte budget (used in the batcher's submission path;~35sites).BatcherGrammar,BuiltinGrammar— built-in command sets.generateStmInput(grammar, command, data)— typed value → JSON tuple.parseStmInput(rawJson, grammar, keyed)— parse and validate.toFullJsonGrammar(...),toKeyedJsonGrammar(...)— derive TypeBox schemas from a grammar map.extractBatches(inputData)— inverse ofbuildBatchData.extractDelegateWallet(...)— pull the delegated wallet out of an account-delegation input.accountMessages,accountPayload_— helpers for the standard account-linking commands.
Batcher message construction (intended for external client SDKs and tests; the in-repo batcher uses a lower-level path):
createMessageForBatcher(namespace, ts, address, addressType, input, target?)— canonical string the wallet signs.createBatcherSubunit(ts, address, addressType, signature, input)— pack a signed input into a subunit shape.hashBatchSubunit(input)—0x-prefixed keccak256 over the subunit.
Also exported: KeyedBatcherGrammar, parseRawStmInput, usesPrefix.
Examples
Runnable: src/batcher.test.ts,
src/delegate.test.ts, and
test/examples.test.ts.
End-to-end batcher flow:
e2e/evm/sync/batcher.test.ts.