@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.
|
|
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.
|
|
21
|
-
"@effectstream/config": "0.100.
|
|
22
|
-
"@effectstream/coroutine": "0.100.
|
|
23
|
-
"@effectstream/crypto": "0.100.
|
|
24
|
-
"@effectstream/db": "0.100.
|
|
25
|
-
"@effectstream/event-client": "0.100.
|
|
26
|
-
"@effectstream/log": "0.100.
|
|
27
|
-
"@effectstream/utils": "0.100.
|
|
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
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
});
|