@effectstream/sm 0.100.14 → 0.100.15

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,90 @@
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
+ ## Install
9
+
10
+ ```bash
11
+ bun add @effectstream/sm
12
+ # or
13
+ npm install @effectstream/sm
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ This package pairs with [`@effectstream/runtime`](https://www.npmjs.com/package/@effectstream/runtime),
19
+ which drives a per-block loop that calls `stm.processInput(...)` for every
20
+ batcher subunit, collects the SQL yielded by your generators, and commits
21
+ it inside a per-block postgres transaction. You author the DSL here;
22
+ the runtime executes it.
23
+
24
+ The DSL is also directly testable in a pure-TS unit test (parse + dispatch
25
+ without a database) — see
26
+ [`primitives/src/evm-erc20/erc20-primitive.test.ts`](https://github.com/PaimaStudios/paima-engine/blob/main/packages/node-sdk/sm/primitives/src/evm-erc20/erc20-primitive.test.ts).
27
+
28
+ ```typescript
29
+ import { Stm } from "@effectstream/sm";
30
+ import { World } from "@effectstream/coroutine";
31
+ import { Type } from "@sinclair/typebox";
32
+ import { join, leave } from "./queries.ts"; // pgtyped queries
33
+
34
+ const grammar = {
35
+ join: [["user", Type.String()]] as const,
36
+ leave: [["user", Type.String()]] as const,
37
+ } as const;
38
+
39
+ const stm = new Stm(grammar);
40
+
41
+ stm.addStateTransition("join", function* ({ parsedInput, msTimestamp }) {
42
+ yield* World.resolve(join, { user: parsedInput.user, ts: msTimestamp });
43
+ });
44
+
45
+ stm.addStateTransition("leave", function* ({ parsedInput }) {
46
+ yield* World.resolve(leave, { user: parsedInput.user });
47
+ });
48
+ ```
49
+
50
+ The runtime calls `stm.processInput(input)` for every batcher subunit; the
51
+ DSL parses against `grammar`, finds the right handler, and the generator
52
+ yields `World.resolve(...)` so the runtime can execute the pgtyped queries.
53
+
54
+ ## Inside EffectStream
55
+
56
+ `Stm` is the central piece a node author writes. The runtime's per-block
57
+ loop wires each user input through the corresponding `Stm` instance,
58
+ collects yielded SQL, and commits it inside the per-block transaction.
59
+ The built-in primitives package (`@effectstream/sm/builtin`) covers
60
+ common on-chain events — ERC-20/721/1155 transfers, Cardano transfers,
61
+ Midnight events, etc. — so you don't re-implement them.
62
+
63
+ ## Key exports
64
+
65
+ - `Stm<Grammar, Events>` — the state machine. `.addStateTransition(prefix, handler)`, `.processInput(input)`, `.grammar`, `.fullJsonGrammar`, `.keyedJsonGrammar`.
66
+ - `ParamToData<Params>` — derives the typed argument shape from a grammar entry.
67
+ - `BaseStfInput` — the input shape passed to every handler (includes `msTimestamp`, `blockHeight`, etc.).
68
+ - `delegate-wallet` helpers — account delegation primitives reused by built-ins.
69
+
70
+ `MessageListener<Events, Params>` is exported as the handler type but is
71
+ inferred at call sites rather than imported directly.
72
+
73
+ Subpath exports:
74
+
75
+ - `@effectstream/sm/builtin` — `PrimitiveTypeERC20`, `PrimitiveTypeERC721`, `PrimitiveTypeERC1155`, `PrimitiveTypeCardanoTransfer`, `PrimitiveTypeMidnightGeneric`, and many more (20+ chain-specific event tags).
76
+ - `@effectstream/sm/grammar` — the underlying grammar/parsing utilities (also re-exported from `@effectstream/concise`).
77
+
78
+ ## Examples
79
+
80
+ - [`primitives/src/evm-erc20/erc20-primitive.test.ts`](https://github.com/PaimaStudios/paima-engine/blob/main/packages/node-sdk/sm/primitives/src/evm-erc20/erc20-primitive.test.ts) — a real primitive's behavior unit-tested.
81
+ - Game logic in
82
+ [`templates/dice/packages/node/`](https://github.com/PaimaStudios/paima-engine/tree/main/templates/dice/packages/node)
83
+ shows the full `new Stm(...).addStateTransition(...)` pattern.
84
+
85
+ Runnable: [`test/examples.test.ts`](./test/examples.test.ts).
86
+
87
+ ## Links
88
+
89
+ - Docs: https://effectstream.github.io/docs/packages/node/sm
90
+ - Source: https://github.com/PaimaStudios/paima-engine/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.15",
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.15",
21
+ "@effectstream/config": "0.100.15",
22
+ "@effectstream/coroutine": "0.100.15",
23
+ "@effectstream/crypto": "0.100.15",
24
+ "@effectstream/db": "0.100.15",
25
+ "@effectstream/event-client": "0.100.15",
26
+ "@effectstream/log": "0.100.15",
27
+ "@effectstream/utils": "0.100.15",
28
28
  "pg": "8.17.2",
29
29
  "pg-tx": "^1.0.1",
30
30
  "@sinclair/typebox": "0.34.41",
@@ -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
+ });