@effectstream/sm 0.101.1 → 0.103.0

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.
Files changed (26) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/package.json +10 -10
  4. package/primitives/Primitive.ts +4 -1
  5. package/primitives/src/builtin.ts +24 -3
  6. package/primitives/src/grammar.ts +8 -0
  7. package/primitives/src/midnight-nullifier-and-commitment/midnight-nullifier-and-commitment-grammar.ts +37 -0
  8. package/primitives/src/{midnight-nullifier/midnight-nullifier.ts → midnight-nullifier-and-commitment/midnight-nullifier-and-commitment.ts} +37 -14
  9. package/primitives/src/midnight-token-mint/midnight-token-mint-grammar.ts +12 -4
  10. package/primitives/src/midnight-token-mint/midnight-token-mint-ivm.ts +105 -0
  11. package/primitives/src/midnight-token-mint/midnight-token-mint.test.ts +150 -0
  12. package/primitives/src/midnight-token-mint/midnight-token-mint.ts +80 -23
  13. package/primitives/src/midnight-unshielded-create/midnight-unshielded-create-grammar.ts +14 -1
  14. package/primitives/src/midnight-unshielded-create/midnight-unshielded-create.ts +4 -1
  15. package/primitives/src/midnight-unshielded-spend/midnight-unshielded-spend-grammar.ts +14 -1
  16. package/primitives/src/midnight-unshielded-spend/midnight-unshielded-spend.ts +4 -1
  17. package/primitives/src/mod.ts +17 -4
  18. package/primitives/src/solana-account-balance/solana-account-balance-grammar.ts +7 -0
  19. package/primitives/src/solana-account-balance/solana-account-balance-primitive.ts +108 -0
  20. package/primitives/src/solana-program-log/solana-program-log-grammar.ts +7 -0
  21. package/primitives/src/solana-program-log/solana-program-log-primitive.test.ts +128 -0
  22. package/primitives/src/solana-program-log/solana-program-log-primitive.ts +108 -0
  23. package/primitives/src/solana-token-account/solana-token-account-grammar.ts +13 -0
  24. package/primitives/src/solana-token-account/solana-token-account-primitive.test.ts +117 -0
  25. package/primitives/src/solana-token-account/solana-token-account-primitive.ts +156 -0
  26. package/primitives/src/midnight-nullifier/midnight-nullifier-grammar.ts +0 -8
@@ -0,0 +1,150 @@
1
+ import { test, expect } from "bun:test";
2
+ import { run } from "effection";
3
+ import type {
4
+ ConfigSyncProtocolType,
5
+ FlattenSyncProtocolIOFor,
6
+ } from "@effectstream/config";
7
+ // Import via mod.ts (not the primitive file directly) to avoid the
8
+ // primitive ⇄ @effectstream/sm ⇄ mod.ts init cycle — same as erc20-primitive.test.ts.
9
+ import { MidnightTokenMintPrimitive } from "../mod.ts";
10
+ import { PrimitiveRegistry } from "../../PrimitiveRegistry.ts";
11
+
12
+ // Minimal view strategy — the IVM only calls strategy.createView(...).
13
+ const STRATEGY = {
14
+ migrationSuffix: "",
15
+ createView: (name: string, sql: string) => `CREATE VIEW ${name} AS ${sql};`,
16
+ } as any;
17
+
18
+ const MOCK_TX_DATA = {
19
+ output: {
20
+ payload: {
21
+ contractAddress: "0200abcd",
22
+ domainSep: "ab".repeat(32),
23
+ rawTokenType: "cd".repeat(32),
24
+ kind: "shielded",
25
+ amount: "18446744073709551615", // u64 max — exceeds MAX_SAFE_INTEGER
26
+ txHash: "ef".repeat(32),
27
+ entryPoint: "mint",
28
+ },
29
+ },
30
+ } as unknown as FlattenSyncProtocolIOFor<
31
+ ConfigSyncProtocolType.MIDNIGHT_PARALLEL
32
+ >;
33
+
34
+ function cleanup() {
35
+ // The base constructor registers `this` by instanceName and throws on dupes.
36
+ PrimitiveRegistry.primitives = {};
37
+ }
38
+
39
+ async function getPayloadOnce(p: MidnightTokenMintPrimitive) {
40
+ let item: any;
41
+ await run(function* () {
42
+ const result = p.getPayload(123, MOCK_TX_DATA).next().value;
43
+ if (!result || !("isBatched" in result)) {
44
+ throw new Error("No payload generated");
45
+ }
46
+ item = result.data[0];
47
+ });
48
+ return item;
49
+ }
50
+
51
+ test("MidnightTokenMint - persist defaults to true and owns its table", () => {
52
+ cleanup();
53
+ const p = new MidnightTokenMintPrimitive({
54
+ instanceName: "tok",
55
+ startBlockHeight: 1,
56
+ stateMachinePrefix: undefined,
57
+ });
58
+ expect(p.persist).toBe(true);
59
+
60
+ const ddl = p.getDynamicTables("tok", STRATEGY);
61
+ expect(ddl).toBeDefined();
62
+ // Owned intermediate table + the trigger on primitive_accounting.
63
+ expect(ddl).toContain("primitives.midnight_token_mint_intermediate_tok");
64
+ expect(ddl).toContain("trigger_update_midnight_token_mint_tok");
65
+ expect(ddl).toContain("effectstream.primitive_accounting");
66
+ expect(p.getViewPrefix()).toContain("midnight_token_mint_view_");
67
+ });
68
+
69
+ test("MidnightTokenMint - persist:false disables the owned table", () => {
70
+ cleanup();
71
+ const p = new MidnightTokenMintPrimitive({
72
+ instanceName: "tok",
73
+ startBlockHeight: 1,
74
+ stateMachinePrefix: undefined,
75
+ persist: false,
76
+ });
77
+ expect(p.persist).toBe(false);
78
+ // No DDL emitted → createDynamicTables skips it → no table/trigger.
79
+ expect(p.getDynamicTables("tok", STRATEGY)).toBeUndefined();
80
+ });
81
+
82
+ test("MidnightTokenMint - hyphenated instance names are sanitized, not rejected", () => {
83
+ cleanup();
84
+ // "Midnight-TokenMint" is the repo's own naming convention. Throwing here
85
+ // killed the sync node at startup ("Invalid name: Midnight-TokenMint").
86
+ const p = new MidnightTokenMintPrimitive({
87
+ instanceName: "Midnight-TokenMint",
88
+ startBlockHeight: 1,
89
+ stateMachinePrefix: "midnightTokenMintState",
90
+ });
91
+
92
+ const ddl = p.getDynamicTables("Midnight-TokenMint", STRATEGY)!;
93
+ expect(ddl).toContain(
94
+ "primitives.midnight_token_mint_view_midnighttokenmint",
95
+ );
96
+ expect(ddl).toContain(
97
+ "primitives.midnight_token_mint_intermediate_midnighttokenmint",
98
+ );
99
+ // No hyphen may survive into an SQL identifier.
100
+ expect(ddl).not.toContain("midnight_token_mint_view_midnight-tokenmint");
101
+ });
102
+
103
+ test("MidnightTokenMint - a quote in the name is escaped in the trigger literal", () => {
104
+ cleanup();
105
+ // The name is stripped for identifiers but compared as a string literal in
106
+ // the trigger body, where a bare `'` would emit unparseable DDL.
107
+ const p = new MidnightTokenMintPrimitive({
108
+ instanceName: "Bob's-Mints",
109
+ startBlockHeight: 1,
110
+ stateMachinePrefix: undefined,
111
+ });
112
+
113
+ const ddl = p.getDynamicTables("Bob's-Mints", STRATEGY)!;
114
+ expect(ddl).toContain("NEW.primitive_name = 'Bob''s-Mints'");
115
+ expect(ddl).toContain("midnight_token_mint_view_bobsmints");
116
+ });
117
+
118
+ test("MidnightTokenMint - owning a table does not suppress the STM input", async () => {
119
+ cleanup();
120
+ const p = new MidnightTokenMintPrimitive({
121
+ instanceName: "tok",
122
+ startBlockHeight: 1,
123
+ stateMachinePrefix: "midnightTokenMintState",
124
+ });
125
+
126
+ // Both paths are live at once: owned table DDL *and* an STM payload.
127
+ expect(p.getDynamicTables("tok", STRATEGY)).toBeDefined();
128
+ expect(p.getConfig().scheduledPrefix).toBe("midnightTokenMintState");
129
+
130
+ const item = await getPayloadOnce(p);
131
+ expect(Array.isArray(item.stateMachinePayload)).toBe(true);
132
+ expect(item.stateMachinePayload[0]).toBe("midnightTokenMintState");
133
+ // Flat fields, in grammar order, so the STM handler reads them by name.
134
+ expect(item.accountingPayload.rawTokenType).toBe("cd".repeat(32));
135
+ expect(item.accountingPayload.amount).toBe("18446744073709551615");
136
+ });
137
+
138
+ test("MidnightTokenMint - no prefix means accounting only (opt-out is explicit)", async () => {
139
+ cleanup();
140
+ const p = new MidnightTokenMintPrimitive({
141
+ instanceName: "tok",
142
+ startBlockHeight: 1,
143
+ stateMachinePrefix: undefined,
144
+ });
145
+
146
+ const item = await getPayloadOnce(p);
147
+ expect(item.stateMachinePayload).toBe(null);
148
+ // The accounting row (and therefore the owned table) is still written.
149
+ expect(item.accountingPayload.rawTokenType).toBe("cd".repeat(32));
150
+ });
@@ -18,31 +18,50 @@ import type {
18
18
  import type { SyncStateUpdateStream } from "@effectstream/coroutine";
19
19
  import { PrimitiveTypeMidnightTokenMint } from "../builtin.ts";
20
20
  import { midnightTokenMintGrammar } from "./midnight-token-mint-grammar.ts";
21
+ import {
22
+ MIDNIGHT_TOKEN_MINT_INTERMEDIATE_PREFIX,
23
+ MIDNIGHT_TOKEN_MINT_VIEW_PREFIX,
24
+ midnightTokenMintIvm,
25
+ } from "./midnight-token-mint-ivm.ts";
21
26
 
22
27
  /**
23
28
  * MidnightTokenMintPrimitive
24
29
  *
25
- * Watches custom token *mints* on the Midnight ledger. A contract call's
26
- * effects record minted tokens as `domain_sep → amount` maps (shielded and
27
- * unshielded), and the resulting token type ("color") is derived as
28
- * `rawTokenType(domain_sep, contract_address)` — so this primitive is the
29
- * only way for a consumer to map a wallet-visible token id back to the
30
- * contract that minted it. The fetcher deserializes each regular
31
- * transaction's raw bytes with ledger-v8, walks the contract calls, and
32
- * emits one state-machine input per `(tx, call, domainSep, kind)` — only
33
- * for effects that actually applied on chain (failed segments roll back).
30
+ * Watches custom token *mints* on the Midnight ledger and maps a token id
31
+ * ("color") back to the contract that minted it — the mapping wallets cannot
32
+ * provide. A contract call's effects record mints as `domain_sep → amount`
33
+ * maps (shielded and unshielded); the token type is
34
+ * `rawTokenType(domain_sep, contract_address)`. The fetcher deserializes each
35
+ * regular transaction with ledger-v8, walks the contract calls, and emits one
36
+ * event per `(tx, call, domainSep, kind)`.
34
37
  *
35
- * The mint nonce is NOT part of token identity (it only randomizes coin
36
- * commitments) and is never public for shielded mints, so it is not part
37
- * of the payload.
38
+ * The primitive OWNS its registry table: `dynamicTables` declares
39
+ * `primitives.midnight_token_mint_view_<instance>` (maintained by a trigger on
40
+ * effectstream.primitive_accounting), so a consumer gets the registry with
41
+ * only an `.addPrimitive(...)` line — no state-machine handler, grammar entry,
42
+ * or migration. Set `persist: false` in config to skip the owned table on a
43
+ * fresh database (the accounting row is still written; useful when a consumer
44
+ * wants to handle the data itself or skip it) — see the flag's own docs for why
45
+ * it does not tear down a table an earlier run already created.
38
46
  *
39
- * Usage:
47
+ * The owned table does NOT replace the state machine: the two paths are
48
+ * independent and run together. Whenever `stateMachinePrefix` is set the
49
+ * primitive still emits an STM input for every mint, exactly as it would
50
+ * without an owned table — the registry is the read model, the STM handler is
51
+ * where app-specific logic lives. Pass `stateMachinePrefix: undefined` to opt
52
+ * out explicitly (the flag is a required config key so the choice is never
53
+ * made by omission).
54
+ *
55
+ * Usage (STM path, in addition to the owned view):
56
+ * grammar: { myPrefix: builtinGrammars.midnightTokenMint }
40
57
  * stm.addStateTransition("myPrefix", function* (data) {
41
- * const { payload } = data.parsedInput;
42
- * // payload = { txHash, contractAddress, domainSep, rawTokenType,
43
- * // kind: "shielded" | "unshielded", amount, entryPoint }
58
+ * const { rawTokenType, kind, contractAddress, domainSep, amount, txHash }
59
+ * = data.parsedInput;
44
60
  * // amount is a decimal string (u64 mints can exceed MAX_SAFE_INTEGER)
45
61
  * });
62
+ *
63
+ * The mint nonce is NOT part of token identity (it only randomizes coin
64
+ * commitments) and is never public for shielded mints, so it is not recorded.
46
65
  */
47
66
  export class MidnightTokenMintPrimitive extends Primitive<
48
67
  ConfigSyncProtocolType.MIDNIGHT_PARALLEL,
@@ -51,20 +70,42 @@ export class MidnightTokenMintPrimitive extends Primitive<
51
70
  readonly internalTypeName = PrimitiveTypeMidnightTokenMint;
52
71
  override readonly grammar = midnightTokenMintGrammar;
53
72
 
54
- override dynamicTables = undefined;
73
+ /**
74
+ * When true (default) the primitive owns + populates its registry table.
75
+ *
76
+ * Only affects whether the DDL is emitted, and the DDL is applied once per
77
+ * instance behind a migration row with no down path — so flipping this to
78
+ * `false` on a database that already ran with `true` leaves the table and its
79
+ * trigger in place, still accumulating. It skips the table on a fresh
80
+ * database; it is not a runtime off switch.
81
+ */
82
+ readonly persist: boolean;
83
+
84
+ // Owned table — gated by `persist`. When disabled, no DDL is emitted, so no
85
+ // table/trigger is created and nothing is consolidated (accounting only).
86
+ override dynamicTables = (
87
+ name: string,
88
+ strategy: Parameters<typeof midnightTokenMintIvm>[1],
89
+ ): string | undefined =>
90
+ this.persist ? midnightTokenMintIvm(name, strategy) : undefined;
91
+
55
92
  override getIntermediatePrefix(): string[] {
56
- return [];
93
+ return [MIDNIGHT_TOKEN_MINT_INTERMEDIATE_PREFIX];
57
94
  }
58
95
  override getViewPrefix(): string[] {
59
- return [];
96
+ return [MIDNIGHT_TOKEN_MINT_VIEW_PREFIX];
60
97
  }
61
98
 
62
99
  constructor(config: {
63
100
  instanceName: string;
64
101
  startBlockHeight: number;
65
- stateMachinePrefix: string;
102
+ // Required key (may be `undefined`) like every other primitive: owning a
103
+ // table is not a reason to skip the STM, so opting out has to be deliberate.
104
+ stateMachinePrefix: string | undefined;
105
+ persist?: boolean;
66
106
  }) {
67
107
  super(config);
108
+ this.persist = config.persist ?? true;
68
109
  }
69
110
 
70
111
  override *getPayload(
@@ -82,10 +123,26 @@ export class MidnightTokenMintPrimitive extends Primitive<
82
123
  accountingPayload: ParamToData<typeof midnightTokenMintGrammar>;
83
124
  }[];
84
125
  }> {
85
- const payload = primitiveTransactionData.output.payload;
126
+ const p = primitiveTransactionData.output.payload as {
127
+ contractAddress: string;
128
+ domainSep: string;
129
+ rawTokenType: string;
130
+ kind: string;
131
+ amount: string;
132
+ txHash: string;
133
+ entryPoint?: string;
134
+ };
86
135
 
136
+ // Flat accounting payload so the owned-table trigger reads columns via
137
+ // payload->>'field' (matches ERC20/NEP141).
87
138
  const accountingPayload: ParamToData<typeof this.grammar> = {
88
- payload,
139
+ contractAddress: p.contractAddress,
140
+ domainSep: p.domainSep,
141
+ rawTokenType: p.rawTokenType,
142
+ kind: p.kind,
143
+ amount: p.amount,
144
+ txHash: p.txHash,
145
+ entryPoint: p.entryPoint ?? "",
89
146
  };
90
147
 
91
148
  const stateMachinePayload:
@@ -120,7 +177,7 @@ export class MidnightTokenMintPrimitive extends Primitive<
120
177
  name: this.instanceName,
121
178
  type: this.internalTypeName,
122
179
  startBlockHeight: this.startBlockHeight,
123
- scheduledPrefix: this.stateMachinePrefix ?? "",
180
+ scheduledPrefix: this.stateMachinePrefix,
124
181
  } as const;
125
182
  }
126
183
  }
@@ -3,6 +3,19 @@ import { Type } from "@sinclair/typebox";
3
3
  export const midnightUnshieldedCreateGrammar = [
4
4
  [
5
5
  "payload",
6
- Type.Any(),
6
+ Type.Object({
7
+ /** Bech32m owner address of the created UTXO */
8
+ owner: Type.String(),
9
+ /** Hex-encoded hash of the creating intent */
10
+ intentHash: Type.String(),
11
+ /** Output index within the creating intent's offer */
12
+ outputIndex: Type.Number(),
13
+ /** u128 value as a decimal string */
14
+ value: Type.String(),
15
+ /** Hex-encoded serialized token type */
16
+ tokenType: Type.String(),
17
+ /** Hash of the creating transaction */
18
+ txHash: Type.String(),
19
+ }),
7
20
  ],
8
21
  ] as const;
@@ -77,7 +77,10 @@ export class MidnightUnshieldedCreatePrimitive extends Primitive<
77
77
  accountingPayload: ParamToData<typeof midnightUnshieldedCreateGrammar>;
78
78
  }[];
79
79
  }> {
80
- const payload = primitiveTransactionData.output.payload;
80
+ // The sync-protocol payload map types Midnight payloads as Record<string, any>;
81
+ // the fetcher's fetchUnshieldedCreates builds exactly the grammar's payload shape.
82
+ const payload = primitiveTransactionData.output
83
+ .payload as ParamToData<typeof this.grammar>["payload"];
81
84
 
82
85
  const accountingPayload: ParamToData<typeof this.grammar> = {
83
86
  payload,
@@ -3,6 +3,19 @@ import { Type } from "@sinclair/typebox";
3
3
  export const midnightUnshieldedSpendGrammar = [
4
4
  [
5
5
  "payload",
6
- Type.Any(),
6
+ Type.Object({
7
+ /** Bech32m owner address of the spent UTXO */
8
+ owner: Type.String(),
9
+ /** Hex-encoded hash of the intent that CREATED the spent UTXO */
10
+ intentHash: Type.String(),
11
+ /** Output index within the creating intent's offer */
12
+ outputIndex: Type.Number(),
13
+ /** u128 value as a decimal string */
14
+ value: Type.String(),
15
+ /** Hex-encoded serialized token type */
16
+ tokenType: Type.String(),
17
+ /** Hash of the spending transaction */
18
+ txHash: Type.String(),
19
+ }),
7
20
  ],
8
21
  ] as const;
@@ -71,7 +71,10 @@ export class MidnightUnshieldedSpendPrimitive extends Primitive<
71
71
  accountingPayload: ParamToData<typeof midnightUnshieldedSpendGrammar>;
72
72
  }[];
73
73
  }> {
74
- const payload = primitiveTransactionData.output.payload;
74
+ // The sync-protocol payload map types Midnight payloads as Record<string, any>;
75
+ // the fetcher's fetchUnshieldedSpends builds exactly the grammar's payload shape.
76
+ const payload = primitiveTransactionData.output
77
+ .payload as ParamToData<typeof this.grammar>["payload"];
75
78
 
76
79
  const accountingPayload: ParamToData<typeof this.grammar> = {
77
80
  payload,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PrimitiveTypeMidnightGeneric,
3
- PrimitiveTypeMidnightNullifier,
3
+ PrimitiveTypeMidnightNullifierAndCommitment,
4
4
  PrimitiveTypeMidnightUnshieldedSpend,
5
5
  PrimitiveTypeMidnightUnshieldedCreate,
6
6
  PrimitiveTypeMidnightZswapRoot,
@@ -19,6 +19,9 @@ import {
19
19
  PrimitiveTypeNEARIntent,
20
20
  PrimitiveTypeNEARGeneric,
21
21
  PrimitiveTypeNEARAccountWatch,
22
+ PrimitiveTypeSolanaProgramLog,
23
+ PrimitiveTypeSolanaAccountBalance,
24
+ PrimitiveTypeSolanaTokenAccount,
22
25
  PrimitiveTypeCardanoMintBurn,
23
26
  PrimitiveTypeCardanoTransfer,
24
27
  PrimitiveTypeCardanoPoolDelegation,
@@ -28,7 +31,8 @@ import {
28
31
  } from "./builtin.ts";
29
32
 
30
33
  import { MidnightGenericPrimitive } from "./midnight-generic/midnight-genetic.ts";
31
- import { MidnightNullifierPrimitive } from "./midnight-nullifier/midnight-nullifier.ts";
34
+ import { MidnightNullifierAndCommitmentPrimitive } from "./midnight-nullifier-and-commitment/midnight-nullifier-and-commitment.ts";
35
+ export type { MidnightZswapCapture } from "./midnight-nullifier-and-commitment/midnight-nullifier-and-commitment.ts";
32
36
  import { MidnightUnshieldedSpendPrimitive } from "./midnight-unshielded-spend/midnight-unshielded-spend.ts";
33
37
  import { MidnightUnshieldedCreatePrimitive } from "./midnight-unshielded-create/midnight-unshielded-create.ts";
34
38
  import { MidnightZswapRootPrimitive } from "./midnight-zswap-root/midnight-zswap-root.ts";
@@ -47,6 +51,9 @@ import { Nep245Primitive } from "./near-nep245/nep245-primitive.ts";
47
51
  import { NearIntentPrimitive } from "./near-intent/near-intent-primitive.ts";
48
52
  import { NearGenericPrimitive } from "./near-generic/near-generic-primitive.ts";
49
53
  import { NearAccountWatchPrimitive } from "./near-account-watch/near-account-watch-primitive.ts";
54
+ import { SolanaProgramLogPrimitive } from "./solana-program-log/solana-program-log-primitive.ts";
55
+ import { SolanaAccountBalancePrimitive } from "./solana-account-balance/solana-account-balance-primitive.ts";
56
+ import { SolanaTokenAccountPrimitive } from "./solana-token-account/solana-token-account-primitive.ts";
50
57
  import { CardanoMintBurnPrimitive } from "./cardano-mint-burn/mint-burn-primitive.ts";
51
58
  import { CardanoTransferPrimitive } from "./cardano-transfer/transfer-primitive.ts";
52
59
  import { CardanoPoolDelegationPrimitive } from "./cardano-pool-delegation/pool-delegation-primitive.ts";
@@ -56,7 +63,7 @@ import { CardanoProjectedNftPrimitive } from "./cardano-projected-nft/projected-
56
63
 
57
64
  const builtInPrimitivesMap = {
58
65
  [PrimitiveTypeMidnightGeneric]: MidnightGenericPrimitive,
59
- [PrimitiveTypeMidnightNullifier]: MidnightNullifierPrimitive,
66
+ [PrimitiveTypeMidnightNullifierAndCommitment]: MidnightNullifierAndCommitmentPrimitive,
60
67
  [PrimitiveTypeMidnightUnshieldedSpend]: MidnightUnshieldedSpendPrimitive,
61
68
  [PrimitiveTypeMidnightUnshieldedCreate]: MidnightUnshieldedCreatePrimitive,
62
69
  [PrimitiveTypeMidnightZswapRoot]: MidnightZswapRootPrimitive,
@@ -75,6 +82,9 @@ const builtInPrimitivesMap = {
75
82
  [PrimitiveTypeNEARIntent]: NearIntentPrimitive,
76
83
  [PrimitiveTypeNEARGeneric]: NearGenericPrimitive,
77
84
  [PrimitiveTypeNEARAccountWatch]: NearAccountWatchPrimitive,
85
+ [PrimitiveTypeSolanaProgramLog]: SolanaProgramLogPrimitive,
86
+ [PrimitiveTypeSolanaAccountBalance]: SolanaAccountBalancePrimitive,
87
+ [PrimitiveTypeSolanaTokenAccount]: SolanaTokenAccountPrimitive,
78
88
  [PrimitiveTypeCardanoMintBurn]: CardanoMintBurnPrimitive,
79
89
  [PrimitiveTypeCardanoTransfer]: CardanoTransferPrimitive,
80
90
  [PrimitiveTypeCardanoPoolDelegation]: CardanoPoolDelegationPrimitive,
@@ -89,7 +99,7 @@ export {
89
99
 
90
100
  // Built-in Primitives
91
101
  MidnightGenericPrimitive,
92
- MidnightNullifierPrimitive,
102
+ MidnightNullifierAndCommitmentPrimitive,
93
103
  MidnightUnshieldedSpendPrimitive,
94
104
  MidnightUnshieldedCreatePrimitive,
95
105
  MidnightZswapRootPrimitive,
@@ -108,6 +118,9 @@ export {
108
118
  NearIntentPrimitive,
109
119
  NearGenericPrimitive,
110
120
  NearAccountWatchPrimitive,
121
+ SolanaProgramLogPrimitive,
122
+ SolanaAccountBalancePrimitive,
123
+ SolanaTokenAccountPrimitive,
111
124
  CardanoMintBurnPrimitive,
112
125
  CardanoTransferPrimitive,
113
126
  CardanoPoolDelegationPrimitive,
@@ -0,0 +1,7 @@
1
+ import { Type } from "@sinclair/typebox";
2
+
3
+ export const solanaAccountBalanceGrammar = [
4
+ ["address", Type.String()],
5
+ ["lamports", Type.Number()],
6
+ ["slot", Type.Number()],
7
+ ] as const;
@@ -0,0 +1,108 @@
1
+ import type { StaticDecode } from "@sinclair/typebox";
2
+ import type {
3
+ ConfigSyncProtocolType,
4
+ FlattenSyncProtocolIOFor,
5
+ ProtocolPrimitiveMap,
6
+ } from "@effectstream/config";
7
+ import {
8
+ type AddressAndType,
9
+ AddressType,
10
+ type EffectstreamBlockNumber,
11
+ } from "@effectstream/utils";
12
+ import { type JsonObject, Primitive } from "@effectstream/sm";
13
+ import {
14
+ type CommandTuple,
15
+ generateRawStmInput,
16
+ type ParamToData,
17
+ } from "@effectstream/concise";
18
+ import type { StateUpdateStream } from "@effectstream/coroutine";
19
+ import { solanaAccountBalanceGrammar } from "./solana-account-balance-grammar.ts";
20
+ import { PrimitiveTypeSolanaAccountBalance } from "../builtin.ts";
21
+
22
+ /**
23
+ * Built-in primitive: tracks the lamport balance of a watched `address` per
24
+ * slot (derived from a transaction's postBalances by SolanaFetcher.readPrimitives).
25
+ */
26
+ export class SolanaAccountBalancePrimitive extends Primitive<
27
+ ConfigSyncProtocolType.SOLANA_RPC_PARALLEL,
28
+ typeof solanaAccountBalanceGrammar
29
+ > {
30
+ readonly internalTypeName = PrimitiveTypeSolanaAccountBalance;
31
+ override grammar = solanaAccountBalanceGrammar;
32
+ readonly address: string;
33
+
34
+ constructor(config: {
35
+ instanceName: string;
36
+ startBlockHeight: number;
37
+ address: string;
38
+ stateMachinePrefix: string | undefined;
39
+ }) {
40
+ super(config);
41
+ this.address = config.address;
42
+ }
43
+
44
+ override *getPayload(
45
+ _: EffectstreamBlockNumber,
46
+ primitiveTransactionData: FlattenSyncProtocolIOFor<
47
+ ConfigSyncProtocolType.SOLANA_RPC_PARALLEL
48
+ >,
49
+ ): StateUpdateStream<{
50
+ isBatched: boolean;
51
+ data: {
52
+ fromAddressAndType: AddressAndType;
53
+ stateMachinePayload:
54
+ | StaticDecode<CommandTuple<string, typeof solanaAccountBalanceGrammar>>
55
+ | null;
56
+ accountingPayload: JsonObject;
57
+ }[];
58
+ }> {
59
+ const payload = primitiveTransactionData.output.payload as {
60
+ address?: string;
61
+ lamports?: number;
62
+ slot?: number;
63
+ };
64
+ const address = String(payload.address ?? this.address);
65
+ const lamports = Number(payload.lamports ?? 0);
66
+ const slot = Number(payload.slot ?? 0);
67
+
68
+ const accountingPayload: ParamToData<typeof solanaAccountBalanceGrammar> = {
69
+ address,
70
+ lamports,
71
+ slot,
72
+ };
73
+
74
+ const stateMachinePayload = this.stateMachinePrefix
75
+ ? generateRawStmInput(
76
+ this.grammar,
77
+ this.stateMachinePrefix,
78
+ accountingPayload,
79
+ )
80
+ : null;
81
+
82
+ return {
83
+ isBatched: false,
84
+ data: [
85
+ {
86
+ fromAddressAndType: {
87
+ type: AddressType.SOLANA,
88
+ address,
89
+ },
90
+ accountingPayload,
91
+ stateMachinePayload,
92
+ },
93
+ ],
94
+ };
95
+ }
96
+
97
+ override getConfig(): ProtocolPrimitiveMap[
98
+ ConfigSyncProtocolType.SOLANA_RPC_PARALLEL
99
+ ] {
100
+ return {
101
+ name: this.instanceName,
102
+ type: this.internalTypeName,
103
+ startBlockHeight: this.startBlockHeight,
104
+ address: this.address,
105
+ scheduledPrefix: this.stateMachinePrefix,
106
+ } as ProtocolPrimitiveMap[ConfigSyncProtocolType.SOLANA_RPC_PARALLEL];
107
+ }
108
+ }
@@ -0,0 +1,7 @@
1
+ import { Type } from "@sinclair/typebox";
2
+
3
+ export const solanaProgramLogGrammar = [
4
+ ["slot", Type.Number()],
5
+ ["programId", Type.String()],
6
+ ["logMessages", Type.Array(Type.String())],
7
+ ] as const;