@effectstream/sm 0.100.14 → 0.100.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # @effectstream/sm
2
+
3
+ The state-machine DSL inside an EffectStream node. Define a typed grammar
4
+ of commands, register one generator per command, and `Stm` parses each
5
+ incoming batcher input, dispatches it to the right handler, and yields
6
+ SQL updates through the runtime.
7
+
8
+ - State-machine DSL: define a typed grammar, register one generator per command.
9
+ - Parses each batcher input, dispatches to the handler, yields SQL through the runtime.
10
+ - `@effectstream/sm/builtin` ships common on-chain event primitives (ERC-20/721/1155, Cardano, Midnight, ...).
11
+ - DSL is directly testable in a pure-TS unit test, without a database.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ bun add @effectstream/sm
17
+ # or
18
+ npm install @effectstream/sm
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ This package pairs with [`@effectstream/runtime`](https://www.npmjs.com/package/@effectstream/runtime),
24
+ which drives a per-block loop that calls `stm.processInput(...)` for every
25
+ batcher subunit, collects the SQL yielded by your generators, and commits
26
+ it inside a per-block postgres transaction. You author the DSL here;
27
+ the runtime executes it.
28
+
29
+ The DSL is also directly testable in a pure-TS unit test (parse + dispatch
30
+ without a database) — see
31
+ [`primitives/src/evm-erc20/erc20-primitive.test.ts`](https://github.com/effectstream/effectstream/blob/main/packages/node-sdk/sm/primitives/src/evm-erc20/erc20-primitive.test.ts).
32
+
33
+ ```typescript
34
+ import { Stm } from "@effectstream/sm";
35
+ import { World } from "@effectstream/coroutine";
36
+ import { Type } from "@sinclair/typebox";
37
+ import { join, leave } from "./queries.ts"; // pgtyped queries
38
+
39
+ const grammar = {
40
+ join: [["user", Type.String()]] as const,
41
+ leave: [["user", Type.String()]] as const,
42
+ } as const;
43
+
44
+ const stm = new Stm(grammar);
45
+
46
+ stm.addStateTransition("join", function* ({ parsedInput, msTimestamp }) {
47
+ yield* World.resolve(join, { user: parsedInput.user, ts: msTimestamp });
48
+ });
49
+
50
+ stm.addStateTransition("leave", function* ({ parsedInput }) {
51
+ yield* World.resolve(leave, { user: parsedInput.user });
52
+ });
53
+ ```
54
+
55
+ The runtime calls `stm.processInput(input)` for every batcher subunit; the
56
+ DSL parses against `grammar`, finds the right handler, and the generator
57
+ yields `World.resolve(...)` so the runtime can execute the pgtyped queries.
58
+
59
+ ## Inside EffectStream
60
+
61
+ `Stm` is the central piece a node author writes. The runtime's per-block
62
+ loop wires each user input through the corresponding `Stm` instance,
63
+ collects yielded SQL, and commits it inside the per-block transaction.
64
+ The built-in primitives package (`@effectstream/sm/builtin`) covers
65
+ common on-chain events — ERC-20/721/1155 transfers, Cardano transfers,
66
+ Midnight events, etc. — so you don't re-implement them.
67
+
68
+ ## Key exports
69
+
70
+ - `Stm<Grammar, Events>`: the state machine. `.addStateTransition(prefix, handler)`, `.processInput(input)`, `.grammar`, `.fullJsonGrammar`, `.keyedJsonGrammar`.
71
+ - `ParamToData<Params>` derives the typed argument shape from a grammar entry.
72
+ - `BaseStfInput`: input shape passed to every handler. Includes `msTimestamp`, `blockHeight`, etc.
73
+ - `delegate-wallet` helpers — account delegation primitives reused by built-ins.
74
+
75
+ `MessageListener<Events, Params>` is exported as the handler type but is
76
+ inferred at call sites rather than imported directly.
77
+
78
+ Subpath exports:
79
+
80
+ - `@effectstream/sm/builtin`: `PrimitiveTypeERC20`, `PrimitiveTypeERC721`, `PrimitiveTypeERC1155`, `PrimitiveTypeCardanoTransfer`, `PrimitiveTypeMidnightGeneric`, and 20+ more chain-specific event tags.
81
+ - `@effectstream/sm/grammar`: the underlying grammar/parsing utilities, also re-exported from `@effectstream/concise`.
82
+
83
+ ## Examples
84
+
85
+ - [`primitives/src/evm-erc20/erc20-primitive.test.ts`](https://github.com/effectstream/effectstream/blob/main/packages/node-sdk/sm/primitives/src/evm-erc20/erc20-primitive.test.ts) — a real primitive's behavior unit-tested.
86
+ - Game logic in
87
+ [`templates/dice/packages/node/`](https://github.com/effectstream/effectstream/tree/main/templates/dice/packages/node)
88
+ shows the full `new Stm(...).addStateTransition(...)` pattern.
89
+
90
+ Runnable: [`test/examples.test.ts`](./test/examples.test.ts).
91
+
92
+ ## Links
93
+
94
+ - Docs: https://effectstream.github.io/docs/packages/node/sm
95
+ - Source: https://github.com/effectstream/effectstream/tree/main/packages/node-sdk/sm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectstream/sm",
3
- "version": "0.100.14",
3
+ "version": "0.100.16",
4
4
  "license": "MIT",
5
5
  "homepage": "https://effectstream.github.io/docs/",
6
6
  "repository": {
@@ -17,14 +17,14 @@
17
17
  "./grammar": "./primitives/src/grammar.ts"
18
18
  },
19
19
  "dependencies": {
20
- "@effectstream/concise": "0.100.14",
21
- "@effectstream/config": "0.100.14",
22
- "@effectstream/coroutine": "0.100.14",
23
- "@effectstream/crypto": "0.100.14",
24
- "@effectstream/db": "0.100.14",
25
- "@effectstream/event-client": "0.100.14",
26
- "@effectstream/log": "0.100.14",
27
- "@effectstream/utils": "0.100.14",
20
+ "@effectstream/concise": "0.100.16",
21
+ "@effectstream/config": "0.100.16",
22
+ "@effectstream/coroutine": "0.100.16",
23
+ "@effectstream/crypto": "0.100.16",
24
+ "@effectstream/db": "0.100.16",
25
+ "@effectstream/event-client": "0.100.16",
26
+ "@effectstream/log": "0.100.16",
27
+ "@effectstream/utils": "0.100.16",
28
28
  "pg": "8.17.2",
29
29
  "pg-tx": "^1.0.1",
30
30
  "@sinclair/typebox": "0.34.41",
@@ -63,14 +63,23 @@ export class CardanoMintBurnPrimitive extends Primitive<
63
63
  }
64
64
 
65
65
  override getConfig(): ProtocolPrimitiveMap[ConfigSyncProtocolType.CARDANO_UTXORPC_PARALLEL] {
66
+ // UTxO-RPC's `mints_asset` predicate is interpreted by Dolos as
67
+ // "creates new supply" — it matches only positive-quantity mints and
68
+ // silently filters burns. To catch burns we also match `moves_asset`,
69
+ // which fires when the asset appears in any input/output of the tx
70
+ // (true for burns, since the asset is in the input UTxO being spent).
71
+ // Downstream consumers see both mint and burn payloads through the
72
+ // same primitive; sign-of-quantity discriminates.
73
+ const matchPolicy = (pid: string): UtxorpcTxPredicate => ({
74
+ any_of: [
75
+ { match: { cardano: { mints_asset: { policy_id: pid } } } },
76
+ { match: { cardano: { moves_asset: { policy_id: pid } } } },
77
+ ],
78
+ });
66
79
  const predicate: UtxorpcTxPredicate =
67
80
  this.policyIds.length === 1
68
- ? { match: { cardano: { mints_asset: { policy_id: this.policyIds[0] } } } }
69
- : {
70
- any_of: this.policyIds.map((pid) => ({
71
- match: { cardano: { mints_asset: { policy_id: pid } } },
72
- })),
73
- };
81
+ ? matchPolicy(this.policyIds[0])
82
+ : { any_of: this.policyIds.map(matchPolicy) };
74
83
 
75
84
  return {
76
85
  name: this.instanceName,
@@ -1,4 +1,5 @@
1
- import { getEvmEvent } from "@effectstream/config";
1
+ import { getEvmEvent, getReadNamespaces } from "@effectstream/config";
2
+ import type { SecurityNamespace } from "@effectstream/config";
2
3
  import {
3
4
  type AddressAndType,
4
5
  AddressType,
@@ -60,12 +61,14 @@ export class EffectstreamL2Primitive extends Primitive<
60
61
  readonly contractAddress: EvmAddress;
61
62
  override grammar = [];
62
63
  readonly effectstreamL2Grammar: GrammarDefinition;
64
+ readonly securityNamespace: SecurityNamespace | undefined;
63
65
 
64
66
  constructor(config: {
65
67
  instanceName: string;
66
68
  startBlockHeight: number;
67
69
  contractAddress: EvmAddress;
68
70
  effectstreamL2Grammar: GrammarDefinition;
71
+ securityNamespace: SecurityNamespace | undefined;
69
72
  }) {
70
73
  super({ ...config, stateMachinePrefix: undefined });
71
74
  this.contractAddress = Value.Decode(
@@ -73,6 +76,7 @@ export class EffectstreamL2Primitive extends Primitive<
73
76
  config.contractAddress,
74
77
  );
75
78
  this.effectstreamL2Grammar = config.effectstreamL2Grammar;
79
+ this.securityNamespace = config.securityNamespace;
76
80
  }
77
81
 
78
82
  override *getPayload(
@@ -370,23 +374,34 @@ export class EffectstreamL2Primitive extends Primitive<
370
374
  input: conciseInput,
371
375
  target: batcherTarget,
372
376
  } = parsed;
373
- // TODO: We need to setup & configure the namespace.
374
- const message = createMessageForBatcher(
375
- null,
376
- millisecondTimestamp,
377
- userAddress,
378
- addressType,
379
- conciseInput,
380
- batcherTarget,
381
- );
382
- // We yield the promise to the generator caller.
383
- // Sync Generators cannot resolve promises.
384
- const validSignature = yield* verifySignature(
385
- addressType,
386
- userAddress,
387
- message,
388
- userSignature,
377
+ // Re-verify the batched signature against each acceptable namespace
378
+ // (matches what the batcher checked when admitting the input). The
379
+ // frontend signs with the game's configured security namespace; if we
380
+ // don't try it here, every batched input is silently dropped.
381
+ // Namespace is injected at construction by the runtime — see
382
+ // processPrimitives in @effectstream/runtime.
383
+ const acceptableNamespaces = getReadNamespaces(
384
+ this.securityNamespace,
385
+ effectstream_block_height,
389
386
  );
387
+ let validSignature = false;
388
+ for (const ns of acceptableNamespaces) {
389
+ const message = createMessageForBatcher(
390
+ ns,
391
+ millisecondTimestamp,
392
+ userAddress,
393
+ addressType,
394
+ conciseInput,
395
+ batcherTarget,
396
+ );
397
+ validSignature = yield* verifySignature(
398
+ addressType,
399
+ userAddress,
400
+ message,
401
+ userSignature,
402
+ );
403
+ if (validSignature) break;
404
+ }
390
405
 
391
406
  const cryptoManager = CryptoManager.getCryptoManager(addressType);
392
407
  const signerAddress = cryptoManager.decodeAddress(userAddress);
@@ -0,0 +1,46 @@
1
+ // Examples for the README. We exercise Stm in isolation — define a
2
+ // grammar, register handlers, and check `.processInput` dispatches to
3
+ // the right one. The runtime side (executing yielded SQL) is the job of
4
+ // @effectstream/runtime, not this package.
5
+
6
+ import { test, expect } from "bun:test";
7
+ import { Type } from "@sinclair/typebox";
8
+ import { Stm } from "../Stm.ts";
9
+
10
+ test("README: Stm dispatches inputs to the registered handler", () => {
11
+ const grammar = {
12
+ join: [["user", Type.String()]] as const,
13
+ leave: [["user", Type.String()]] as const,
14
+ } as const;
15
+
16
+ const stm = new Stm(grammar);
17
+ const calls: string[] = [];
18
+
19
+ stm.addStateTransition("join", function* ({ parsedInput }) {
20
+ calls.push(`join:${parsedInput.user}`);
21
+ // Empty generator — no DB writes.
22
+ });
23
+ stm.addStateTransition("leave", function* ({ parsedInput }) {
24
+ calls.push(`leave:${parsedInput.user}`);
25
+ });
26
+
27
+ // Build the on-wire concise input.
28
+ const input = JSON.stringify(["join", "alice"]);
29
+ const gen = stm.processInput({
30
+ conciseInput: input,
31
+ blockHeight: 1,
32
+ msTimestamp: 0,
33
+ } as any);
34
+
35
+ // Drain the generator.
36
+ while (!gen.next().done) {}
37
+
38
+ expect(calls).toEqual(["join:alice"]);
39
+ });
40
+
41
+ test("README: duplicate prefix registration throws", () => {
42
+ const grammar = { join: [["user", Type.String()]] as const } as const;
43
+ const stm = new Stm(grammar);
44
+ stm.addStateTransition("join", function* () {});
45
+ expect(() => stm.addStateTransition("join", function* () {})).toThrow();
46
+ });