@effectstream/sm 0.103.3 → 0.104.1

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 (34) hide show
  1. package/package.json +10 -9
  2. package/primitives/src/avail-generic/avail-primitive.ts +2 -1
  3. package/primitives/src/bitcoin-address/bitcoin-primitive.ts +2 -2
  4. package/primitives/src/cardano-delayed-asset/delayed-asset-primitive.ts +2 -1
  5. package/primitives/src/cardano-mint-burn/mint-burn-primitive.ts +2 -1
  6. package/primitives/src/cardano-pool-delegation/pool-delegation-primitive.ts +2 -1
  7. package/primitives/src/cardano-projected-nft/projected-nft-primitive.ts +2 -1
  8. package/primitives/src/cardano-transfer/transfer-grammar.ts +16 -1
  9. package/primitives/src/cardano-transfer/transfer-primitive.test.ts +106 -0
  10. package/primitives/src/cardano-transfer/transfer-primitive.ts +19 -5
  11. package/primitives/src/cardano-utils/cardano-helpers.ts +5 -0
  12. package/primitives/src/celestia-generic/celestia-primitive.ts +2 -1
  13. package/primitives/src/evm-effectstream-l2/effectstream-l2-primitive.ts +3 -2
  14. package/primitives/src/evm-erc1155/erc1155-primitive.ts +2 -1
  15. package/primitives/src/evm-erc20/erc20-primitive.ts +2 -1
  16. package/primitives/src/evm-erc721/erc721-primitive.ts +2 -1
  17. package/primitives/src/evm-generic/evm-generic-primitive.ts +2 -1
  18. package/primitives/src/midnight-generic/midnight-generic.test.ts +93 -0
  19. package/primitives/src/midnight-generic/midnight-genetic.ts +49 -13
  20. package/primitives/src/midnight-nullifier-and-commitment/midnight-nullifier-and-commitment.ts +1 -1
  21. package/primitives/src/midnight-token-mint/midnight-token-mint.ts +1 -1
  22. package/primitives/src/midnight-unshielded-create/midnight-unshielded-create.ts +1 -1
  23. package/primitives/src/midnight-unshielded-spend/midnight-unshielded-spend.ts +1 -1
  24. package/primitives/src/midnight-zswap-root/midnight-zswap-root.ts +1 -1
  25. package/primitives/src/near-account-watch/near-account-watch-primitive.ts +2 -1
  26. package/primitives/src/near-generic/near-generic-primitive.ts +2 -1
  27. package/primitives/src/near-intent/near-intent-primitive.ts +2 -1
  28. package/primitives/src/near-nep141/nep141-primitive.ts +2 -1
  29. package/primitives/src/near-nep171/nep171-primitive.ts +2 -1
  30. package/primitives/src/near-nep245/nep245-primitive.ts +2 -1
  31. package/primitives/src/solana-account-balance/solana-account-balance-primitive.ts +2 -1
  32. package/primitives/src/solana-program-log/solana-program-log-primitive.ts +2 -1
  33. package/primitives/src/solana-token-account/solana-token-account-primitive.ts +2 -1
  34. package/primitives/src/utxorpc-generic/utxorpc-generic.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectstream/sm",
3
- "version": "0.103.3",
3
+ "version": "0.104.1",
4
4
  "license": "(MIT OR Apache-2.0)",
5
5
  "homepage": "https://effectstream.github.io/docs/",
6
6
  "repository": {
@@ -17,14 +17,15 @@
17
17
  "./grammar": "./primitives/src/grammar.ts"
18
18
  },
19
19
  "dependencies": {
20
- "@effectstream/concise": "0.103.3",
21
- "@effectstream/config": "0.103.3",
22
- "@effectstream/coroutine": "0.103.3",
23
- "@effectstream/crypto": "0.103.3",
24
- "@effectstream/db": "0.103.3",
25
- "@effectstream/event-client": "0.103.3",
26
- "@effectstream/log": "0.103.3",
27
- "@effectstream/utils": "0.103.3",
20
+ "@effectstream/concise": "0.104.1",
21
+ "@effectstream/config": "0.104.1",
22
+ "@effectstream/coroutine": "0.104.1",
23
+ "@effectstream/crypto": "0.104.1",
24
+ "@effectstream/db": "0.104.1",
25
+ "@effectstream/event-client": "0.104.1",
26
+ "@effectstream/log": "0.104.1",
27
+ "@effectstream/utils": "0.104.1",
28
+ "@noble/hashes": "^2.2.0",
28
29
  "pg": "8.21.0",
29
30
  "pg-tx": "^1.0.1",
30
31
  "@sinclair/typebox": "0.34.41",
@@ -1,4 +1,5 @@
1
- import { type JsonObject, Primitive } from "@effectstream/sm";
1
+ import { Primitive } from "../../Primitive.ts";
2
+ import type { JsonObject } from "../../types.ts";
2
3
  import type { StaticDecode } from "@sinclair/typebox";
3
4
  import { type CommandTuple, generateRawStmInput } from "@effectstream/concise";
4
5
  import type {
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  type ParamToData,
@@ -110,4 +111,3 @@ export class BitcoinAddressPrimitive extends Primitive<
110
111
  } as const;
111
112
  }
112
113
  }
113
-
@@ -35,7 +35,8 @@ import type {
35
35
  UtxorpcTxPredicate,
36
36
  } from "@effectstream/config";
37
37
  import type { StateUpdateStream } from "@effectstream/coroutine";
38
- import { type JsonObject, Primitive } from "@effectstream/sm";
38
+ import { Primitive } from "../../Primitive.ts";
39
+ import type { JsonObject } from "../../types.ts";
39
40
  import {
40
41
  type AddressAndType,
41
42
  AddressType,
@@ -29,7 +29,8 @@ import type {
29
29
  UtxorpcTxPredicate,
30
30
  } from "@effectstream/config";
31
31
  import type { StateUpdateStream } from "@effectstream/coroutine";
32
- import { type JsonObject, Primitive } from "@effectstream/sm";
32
+ import { Primitive } from "../../Primitive.ts";
33
+ import type { JsonObject } from "../../types.ts";
33
34
  import {
34
35
  type AddressAndType,
35
36
  AddressType,
@@ -32,7 +32,8 @@ import type {
32
32
  ProtocolPrimitiveMap,
33
33
  } from "@effectstream/config";
34
34
  import type { StateUpdateStream } from "@effectstream/coroutine";
35
- import { type JsonObject, Primitive } from "@effectstream/sm";
35
+ import { Primitive } from "../../Primitive.ts";
36
+ import type { JsonObject } from "../../types.ts";
36
37
  import {
37
38
  type AddressAndType,
38
39
  AddressType,
@@ -43,7 +43,8 @@ import type {
43
43
  ProtocolPrimitiveMap,
44
44
  } from "@effectstream/config";
45
45
  import type { StateUpdateStream } from "@effectstream/coroutine";
46
- import { type JsonObject, Primitive } from "@effectstream/sm";
46
+ import { Primitive } from "../../Primitive.ts";
47
+ import type { JsonObject } from "../../types.ts";
47
48
  import {
48
49
  type AddressAndType,
49
50
  AddressType,
@@ -3,6 +3,21 @@ import { Type } from "@sinclair/typebox";
3
3
  export const transferGrammar = [
4
4
  ["txId", Type.String()],
5
5
  ["metadata", Type.String()],
6
- ["inputCredentials", Type.String()],
6
+ [
7
+ "inputCredentials",
8
+ Type.String({
9
+ deprecated: true,
10
+ description:
11
+ "Deprecated: JSON array of raw verification keys. Use signerKeyHashes.",
12
+ }),
13
+ ],
7
14
  ["outputs", Type.String()],
15
+ // The default lets state machines parse historical four-field tuples.
16
+ [
17
+ "signerKeyHashes",
18
+ Type.String({
19
+ default: "[]",
20
+ description: "JSON array of Cardano verification key hashes.",
21
+ }),
22
+ ],
8
23
  ] as const;
@@ -0,0 +1,106 @@
1
+ import { expect, test } from "bun:test";
2
+ import { parseRawStmInput, toKeyedJsonGrammar } from "@effectstream/concise";
3
+ import { CardanoTransferPrimitive } from "../mod.ts";
4
+ import { PrimitiveRegistry } from "../../PrimitiveRegistry.ts";
5
+ import { transferGrammar } from "./transfer-grammar.ts";
6
+
7
+ const VERIFICATION_KEY = Uint8Array.from({ length: 32 }, (_, index) => index);
8
+ const VERIFICATION_KEY_HEX =
9
+ "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
10
+ const CREDENTIAL_HASH =
11
+ "491112dd01155c07dab485f71b572e0cae759e2cd38b1c0e97554297";
12
+
13
+ function makePrimitive(stateMachinePrefix?: string) {
14
+ PrimitiveRegistry.primitives = {};
15
+ return new CardanoTransferPrimitive({
16
+ instanceName: "CardanoTransfer",
17
+ startBlockHeight: 0,
18
+ stateMachinePrefix,
19
+ predicate: { match: { cardano: {} } },
20
+ } as any);
21
+ }
22
+
23
+ function payloadFor(
24
+ vkeys: Uint8Array[] | undefined,
25
+ stateMachinePrefix?: string,
26
+ ) {
27
+ const witnesses =
28
+ vkeys === undefined
29
+ ? undefined
30
+ : { vkeywitness: vkeys.map((vkey) => ({ vkey })) };
31
+ const tx = {
32
+ hash: new Uint8Array(32),
33
+ outputs: [],
34
+ witnesses,
35
+ };
36
+ const iterator = makePrimitive(stateMachinePrefix).getPayload(
37
+ 0 as any,
38
+ {
39
+ output: { payload: { tx } },
40
+ } as any,
41
+ );
42
+ const result = iterator.next().value as any;
43
+ return result.data[0];
44
+ }
45
+
46
+ test("keeps deprecated inputCredentials as raw verification keys", () => {
47
+ const { accountingPayload } = payloadFor([VERIFICATION_KEY]);
48
+
49
+ expect(accountingPayload.inputCredentials).toEqual([VERIFICATION_KEY_HEX]);
50
+ expect(accountingPayload.inputCredentials[0]).toMatch(/^[0-9a-f]{64}$/);
51
+ });
52
+
53
+ test("emits Cardano credential hashes as signerKeyHashes", () => {
54
+ const { accountingPayload } = payloadFor([VERIFICATION_KEY]);
55
+
56
+ expect(accountingPayload.signerKeyHashes).toEqual([CREDENTIAL_HASH]);
57
+ expect(accountingPayload.signerKeyHashes[0]).toMatch(/^[0-9a-f]{56}$/);
58
+ });
59
+
60
+ test("deduplicates legacy keys and signer hashes in first-seen order", () => {
61
+ const secondVkey = new Uint8Array(32).fill(0xff);
62
+ const { accountingPayload } = payloadFor([
63
+ VERIFICATION_KEY,
64
+ secondVkey,
65
+ VERIFICATION_KEY,
66
+ ]);
67
+
68
+ expect(accountingPayload.inputCredentials).toHaveLength(2);
69
+ expect(accountingPayload.inputCredentials[0]).toBe(VERIFICATION_KEY_HEX);
70
+ expect(accountingPayload.signerKeyHashes).toHaveLength(2);
71
+ expect(accountingPayload.signerKeyHashes[0]).toBe(CREDENTIAL_HASH);
72
+ expect(accountingPayload.signerKeyHashes[1]).toMatch(/^[0-9a-f]{56}$/);
73
+ });
74
+
75
+ test("emits empty legacy and signer lists when witnesses are absent", () => {
76
+ const { accountingPayload } = payloadFor(undefined);
77
+
78
+ expect(accountingPayload.inputCredentials).toEqual([]);
79
+ expect(accountingPayload.signerKeyHashes).toEqual([]);
80
+ });
81
+
82
+ test("appends signerKeyHashes without shifting legacy STM fields", () => {
83
+ const { stateMachinePayload } = payloadFor(
84
+ [VERIFICATION_KEY],
85
+ "cardano-transfer",
86
+ );
87
+
88
+ expect(stateMachinePayload.slice(3, 5)).toEqual([
89
+ JSON.stringify([VERIFICATION_KEY_HEX]),
90
+ "[]",
91
+ ]);
92
+ expect(JSON.parse(stateMachinePayload[5])).toEqual([CREDENTIAL_HASH]);
93
+ });
94
+
95
+ test("defaults signerKeyHashes when parsing historical STM tuples", () => {
96
+ const grammar = { "cardano-transfer": transferGrammar } as const;
97
+ const parsed = parseRawStmInput(
98
+ ["cardano-transfer", "tx-id", "", "[]", "[]"],
99
+ grammar,
100
+ toKeyedJsonGrammar(grammar),
101
+ );
102
+
103
+ expect(parsed.data.inputCredentials).toBe("[]");
104
+ expect(parsed.data.outputs).toBe("[]");
105
+ expect(parsed.data.signerKeyHashes).toBe("[]");
106
+ });
@@ -10,8 +10,10 @@
10
10
  * Extraction: for each matched transaction, `getPayload()` maps `tx.outputs` into
11
11
  * structured objects with `{index, address, coin, assets[]}`. Coin values are extracted
12
12
  * from the protobuf BigInt (either `int` for small values or `bigUInt` for large).
13
- * Input credentials are extracted from `tx.witnesses.vkeywitness[].vkey` — these are
14
- * the public key hashes that signed the transaction, identifying who authorized the spend.
13
+ * `inputCredentials` retains the raw `tx.witnesses.vkeywitness[].vkey` values for
14
+ * backward compatibility, but is deprecated because those values are not credentials.
15
+ * `signerKeyHashes` hashes each verification key with BLAKE2b-224, yielding the
16
+ * Cardano key hashes that identify who authorized the transaction.
15
17
  * Transaction metadata is extracted from `tx.auxiliary.metadata`.
16
18
  *
17
19
  * Predicate: user-provided (typically `has_address` to watch specific addresses).
@@ -29,7 +31,8 @@ import type {
29
31
  UtxorpcTxPredicate,
30
32
  } from "@effectstream/config";
31
33
  import type { StateUpdateStream } from "@effectstream/coroutine";
32
- import { type JsonObject, Primitive } from "@effectstream/sm";
34
+ import { Primitive } from "../../Primitive.ts";
35
+ import type { JsonObject } from "../../types.ts";
33
36
  import {
34
37
  type AddressAndType,
35
38
  AddressType,
@@ -42,6 +45,7 @@ import {
42
45
  addressToHex,
43
46
  assetQuantityToString,
44
47
  metadataToJson,
48
+ verificationKeyToCredentialHex,
45
49
  } from "../cardano-utils/cardano-helpers.ts";
46
50
 
47
51
  export class CardanoTransferPrimitive extends Primitive<
@@ -111,10 +115,18 @@ export class CardanoTransferPrimitive extends Primitive<
111
115
  });
112
116
 
113
117
  const inputCredentials: string[] = [];
118
+ const signerKeyHashes: string[] = [];
114
119
  if (tx.witnesses) {
115
120
  for (const w of tx.witnesses.vkeywitness) {
116
- const hash = uint8ArrayToHexString(w.vkey);
117
- if (!inputCredentials.includes(hash)) inputCredentials.push(hash);
121
+ const verificationKey = uint8ArrayToHexString(w.vkey);
122
+ if (!inputCredentials.includes(verificationKey)) {
123
+ inputCredentials.push(verificationKey);
124
+ }
125
+
126
+ const signerKeyHash = verificationKeyToCredentialHex(w.vkey);
127
+ if (!signerKeyHashes.includes(signerKeyHash)) {
128
+ signerKeyHashes.push(signerKeyHash);
129
+ }
118
130
  }
119
131
  }
120
132
 
@@ -125,6 +137,7 @@ export class CardanoTransferPrimitive extends Primitive<
125
137
  metadata: metadata ? JSON.stringify(metadata) : null,
126
138
  inputCredentials,
127
139
  outputs,
140
+ signerKeyHashes,
128
141
  };
129
142
 
130
143
  const stateMachinePayload: any = this.stateMachinePrefix
@@ -133,6 +146,7 @@ export class CardanoTransferPrimitive extends Primitive<
133
146
  metadata: metadata ? JSON.stringify(metadata) : "",
134
147
  inputCredentials: JSON.stringify(inputCredentials),
135
148
  outputs: JSON.stringify(outputs),
149
+ signerKeyHashes: JSON.stringify(signerKeyHashes),
136
150
  })
137
151
  : null;
138
152
 
@@ -1,3 +1,4 @@
1
+ import { blake2b } from "@noble/hashes/blake2.js";
1
2
  import { uint8ArrayToHexString } from "@effectstream/utils";
2
3
  import type { cardano } from "@utxorpc/spec";
3
4
 
@@ -31,6 +32,10 @@ export function addressToHex(address: Uint8Array): string {
31
32
  return uint8ArrayToHexString(address);
32
33
  }
33
34
 
35
+ export function verificationKeyToCredentialHex(vkey: Uint8Array): string {
36
+ return uint8ArrayToHexString(blake2b(vkey, { dkLen: 28 }));
37
+ }
38
+
34
39
  export function metadataToJson(metadata: cardano.Metadata[]): Record<string, unknown> | null {
35
40
  if (!metadata || metadata.length === 0) return null;
36
41
  const result: Record<string, unknown> = {};
@@ -1,4 +1,5 @@
1
- import { type JsonObject, Primitive } from "@effectstream/sm";
1
+ import { Primitive } from "../../Primitive.ts";
2
+ import type { JsonObject } from "../../types.ts";
2
3
  import type { StaticDecode } from "@sinclair/typebox";
3
4
  import { type CommandTuple, generateRawStmInput } from "@effectstream/concise";
4
5
  import type {
@@ -38,12 +38,13 @@ import {
38
38
  account_createAccount,
39
39
  account_linkAddress,
40
40
  account_unlinkAddress,
41
- } from "@effectstream/sm";
41
+ } from "../../../delegate-wallet.ts";
42
42
  import { BuiltinGrammarPrefix } from "@effectstream/concise";
43
43
 
44
44
  import { effectstreamL2 } from "./effectstream-l2-abi.ts";
45
45
  import { type StaticDecode, type TSchema, Type } from "@sinclair/typebox";
46
- import { type JsonObject, Primitive } from "@effectstream/sm";
46
+ import { Primitive } from "../../Primitive.ts";
47
+ import type { JsonObject } from "../../types.ts";
47
48
  import { Value } from "@sinclair/typebox/value";
48
49
  import type { CommandTuple } from "@effectstream/concise";
49
50
  import { PrimitiveTypeEVMEffectstreamL2 } from "../builtin.ts";
@@ -13,7 +13,8 @@ import {
13
13
  type EffectstreamBlockNumber,
14
14
  TypeboxHelpers,
15
15
  } from "@effectstream/utils";
16
- import { type JsonObject, Primitive } from "@effectstream/sm";
16
+ import { Primitive } from "../../Primitive.ts";
17
+ import type { JsonObject } from "../../types.ts";
17
18
  import {
18
19
  type CommandTuple,
19
20
  generateRawStmInput,
@@ -12,7 +12,8 @@ import {
12
12
  type EffectstreamBlockNumber,
13
13
  TypeboxHelpers,
14
14
  } from "@effectstream/utils";
15
- import { type JsonObject, Primitive } from "@effectstream/sm";
15
+ import { Primitive } from "../../Primitive.ts";
16
+ import type { JsonObject } from "../../types.ts";
16
17
  import { Value } from "@sinclair/typebox/value";
17
18
  import {
18
19
  type CommandTuple,
@@ -13,7 +13,8 @@ import {
13
13
  type EffectstreamBlockNumber,
14
14
  TypeboxHelpers,
15
15
  } from "@effectstream/utils";
16
- import { type JsonObject, Primitive } from "@effectstream/sm";
16
+ import { Primitive } from "../../Primitive.ts";
17
+ import type { JsonObject } from "../../types.ts";
17
18
  import {
18
19
  type CommandTuple,
19
20
  generateRawStmInput,
@@ -12,7 +12,8 @@ import {
12
12
  type EffectstreamBlockNumber,
13
13
  TypeboxHelpers,
14
14
  } from "@effectstream/utils";
15
- import { type JsonObject, Primitive } from "@effectstream/sm";
15
+ import { Primitive } from "../../Primitive.ts";
16
+ import type { JsonObject } from "../../types.ts";
16
17
  import { Value } from "@sinclair/typebox/value";
17
18
  import {
18
19
  type CommandTuple,
@@ -0,0 +1,93 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { StateValue } from "@effectstream/config";
3
+ import { MidnightGenericPrimitive } from "./midnight-genetic.ts";
4
+
5
+ function cell(...bytes: number[]): StateValue {
6
+ return {
7
+ type: () => "cell",
8
+ asCell: () => ({ value: [Uint8Array.from(bytes)] }),
9
+ } as unknown as StateValue;
10
+ }
11
+
12
+ function array(...values: StateValue[]): StateValue {
13
+ return {
14
+ type: () => "array",
15
+ asArray: () => values,
16
+ } as unknown as StateValue;
17
+ }
18
+
19
+ function nullValue(): StateValue {
20
+ return { type: () => "null" } as unknown as StateValue;
21
+ }
22
+
23
+ function map(entries: [number[], StateValue][]): StateValue {
24
+ const value = new Map(
25
+ entries.map(([key, item]) => [
26
+ { value: [Uint8Array.from(key)] },
27
+ item,
28
+ ]),
29
+ );
30
+ return {
31
+ type: () => "map",
32
+ asMap: () => value,
33
+ } as unknown as StateValue;
34
+ }
35
+
36
+ function primitive(name: string, ledgerSchema?: Record<string, any>) {
37
+ return new MidnightGenericPrimitive({
38
+ instanceName: name,
39
+ startBlockHeight: 1,
40
+ contractAddress: "0".repeat(64) as any,
41
+ stateMachinePrefix: name,
42
+ ledgerSchema,
43
+ });
44
+ }
45
+
46
+ describe("MidnightGenericPrimitive ledger schema", () => {
47
+ test("decodes simple, optional, and map fields in declaration order", () => {
48
+ const decoder = primitive("schema-values", {
49
+ round: "uint128",
50
+ bytes: "bytes",
51
+ enabled: "boolean",
52
+ missing: { type: "option", value: "uint64" },
53
+ entries: { type: "map", value: "uint16" },
54
+ });
55
+
56
+ expect(decoder.parseAdditionalLedgerFields(array(
57
+ cell(0x01, 0x02),
58
+ cell(0xab, 0xcd),
59
+ cell(0x01),
60
+ nullValue(),
61
+ map([[[0x0a], cell(0x03)]]),
62
+ ))).toEqual({
63
+ round: "513",
64
+ bytes: "0xabcd",
65
+ enabled: true,
66
+ missing: null,
67
+ entries: { "0x0a": "3" },
68
+ });
69
+ });
70
+
71
+ test("requires either generated contract code or a non-empty schema", () => {
72
+ expect(() => primitive("missing-decoder")).toThrow(/requires either/);
73
+ expect(() => primitive("empty-schema", {})).toThrow(/empty ledger schema/);
74
+ });
75
+
76
+ test("reports declaration-order and field-type mismatches", () => {
77
+ const decoder = primitive("schema-order", {
78
+ entries: { type: "map", value: "uint16" },
79
+ round: "uint128",
80
+ });
81
+ expect(() => decoder.parseAdditionalLedgerFields(array(
82
+ cell(0x01),
83
+ map([]),
84
+ ))).toThrow(/field "entries" expects a map/);
85
+ });
86
+
87
+ test("rejects an incompatible ledger root", () => {
88
+ const decoder = primitive("schema-root", { round: "uint128" });
89
+ expect(() => decoder.parseAdditionalLedgerFields(cell(0x01))).toThrow(
90
+ /Ledger root must be an array/,
91
+ );
92
+ });
93
+ });
@@ -1,4 +1,4 @@
1
- import { Primitive } from "@effectstream/sm";
1
+ import { Primitive } from "../../Primitive.ts";
2
2
  import {
3
3
  type AddressAndType,
4
4
  AddressType,
@@ -33,7 +33,7 @@ export class MidnightGenericPrimitive extends Primitive<
33
33
  readonly internalTypeName = PrimitiveTypeMidnightGeneric;
34
34
  override readonly grammar = midnightGenericGrammar;
35
35
  readonly contractAddress: string;
36
- readonly contract: {
36
+ readonly contract?: {
37
37
  ledger: (data: StateValue) => any;
38
38
  };
39
39
  readonly ledgerSchema?: LedgerSchema;
@@ -53,7 +53,7 @@ export class MidnightGenericPrimitive extends Primitive<
53
53
  startBlockHeight: number;
54
54
  contractAddress: MidnightAddress;
55
55
  stateMachinePrefix: string;
56
- contract: {
56
+ contract?: {
57
57
  ledger: (data: StateValue) => any;
58
58
  };
59
59
  ledgerSchema?: LedgerSchema;
@@ -61,6 +61,16 @@ export class MidnightGenericPrimitive extends Primitive<
61
61
  genesisHash?: string;
62
62
  }) {
63
63
  super(config);
64
+ if (!config.contract && !config.ledgerSchema) {
65
+ throw new Error(
66
+ `Midnight primitive "${config.instanceName}" requires either a generated contract decoder or a ledger schema.`,
67
+ );
68
+ }
69
+ if (config.ledgerSchema && Object.keys(config.ledgerSchema).length === 0) {
70
+ throw new Error(
71
+ `Midnight primitive "${config.instanceName}" received an empty ledger schema.`,
72
+ );
73
+ }
64
74
  this.contractAddress = config.contractAddress;
65
75
  this.contract = config.contract;
66
76
  this.ledgerSchema = config.ledgerSchema;
@@ -76,13 +86,24 @@ export class MidnightGenericPrimitive extends Primitive<
76
86
  parseAdditionalLedgerFields(stateValue: StateValue): Record<string, any> {
77
87
  if (!this.ledgerSchema) return {};
78
88
 
79
- const stateArray: StateValue[] = stateValue.asArray?.() ?? [];
89
+ const stateArray: StateValue[] | undefined = stateValue.asArray?.();
80
90
  const result: Record<string, any> = {};
81
91
  const schemaEntries = Object.entries(this.ledgerSchema);
82
92
 
83
- for (let i = 0; i < schemaEntries.length && i < stateArray.length; i++) {
93
+ if (!stateArray) {
94
+ throw new Error(
95
+ `Ledger root must be an array; received ${stateValue.type()}.`,
96
+ );
97
+ }
98
+ if (stateArray.length < schemaEntries.length) {
99
+ throw new Error(
100
+ `Ledger schema declares ${schemaEntries.length} field(s), but the ledger contains only ${stateArray.length}.`,
101
+ );
102
+ }
103
+
104
+ for (let i = 0; i < schemaEntries.length; i++) {
84
105
  const [fieldName, fieldType] = schemaEntries[i];
85
- result[fieldName] = parseLedgerField(stateArray[i], fieldType);
106
+ result[fieldName] = parseLedgerField(stateArray[i], fieldType, fieldName);
86
107
  }
87
108
 
88
109
  return result;
@@ -156,6 +177,7 @@ export class MidnightGenericPrimitive extends Primitive<
156
177
  // TODO This should be optional
157
178
  scheduledPrefix: this.stateMachinePrefix ?? "",
158
179
  contract: this.contract,
180
+ ledgerSchema: this.ledgerSchema,
159
181
  parseAdditionalLedgerFields: this.ledgerSchema
160
182
  ? this.parseAdditionalLedgerFields.bind(this)
161
183
  : undefined,
@@ -193,30 +215,44 @@ function parseUintCellLittleEndian(chunks: Uint8Array[]): string {
193
215
  return result.toString();
194
216
  }
195
217
 
196
- function parseLedgerField(sv: StateValue, type: LedgerFieldType): any {
218
+ function parseLedgerField(
219
+ sv: StateValue,
220
+ type: LedgerFieldType,
221
+ path: string,
222
+ ): any {
197
223
  if (typeof type === "object" && type.type === "option") {
198
224
  if (sv.type() === "null") return null;
199
- return parseLedgerField(sv, type.value);
225
+ return parseLedgerField(sv, type.value, path);
200
226
  }
201
227
 
202
228
  if (typeof type === "object" && type.type === "map") {
203
- const map = sv.asMap();
204
- if (!map) return {};
229
+ const map = sv.asMap?.();
230
+ if (!map) {
231
+ throw new Error(
232
+ `Ledger field "${path}" expects a map; received ${sv.type()}.`,
233
+ );
234
+ }
205
235
 
206
236
  const result: Record<string, any> = {};
207
237
  for (const key of map.keys()) {
208
238
  const val = map.get(key);
209
239
  if (val !== undefined) {
210
- result[alignedValueToHex(key as { value: Uint8Array[] })] = parseLedgerField(
240
+ const mapKey = alignedValueToHex(key as { value: Uint8Array[] });
241
+ result[mapKey] = parseLedgerField(
211
242
  val,
212
243
  type.value,
244
+ `${path}[${mapKey}]`,
213
245
  );
214
246
  }
215
247
  }
216
248
  return result;
217
249
  }
218
250
 
219
- if (sv.type() !== "cell") return null;
251
+ if (sv.type() !== "cell") {
252
+ throw new Error(
253
+ `Ledger field "${path}" expects ${String(type)}; received ${sv.type()}.`,
254
+ );
255
+ }
220
256
  const cell = sv.asCell();
221
257
 
222
258
  if (
@@ -239,7 +275,7 @@ function parseLedgerField(sv: StateValue, type: LedgerFieldType): any {
239
275
  );
240
276
  }
241
277
 
242
- return null;
278
+ throw new Error(`Unsupported ledger type for field "${path}": ${String(type)}.`);
243
279
  }
244
280
 
245
281
  function isCompactMap(data: object): data is Iterable<[unknown, unknown]> {
@@ -1,4 +1,4 @@
1
- import { Primitive } from "@effectstream/sm";
1
+ import { Primitive } from "../../Primitive.ts";
2
2
  import {
3
3
  type AddressAndType,
4
4
  AddressType,
@@ -1,4 +1,4 @@
1
- import { Primitive } from "@effectstream/sm";
1
+ import { Primitive } from "../../Primitive.ts";
2
2
  import {
3
3
  type AddressAndType,
4
4
  AddressType,
@@ -1,4 +1,4 @@
1
- import { Primitive } from "@effectstream/sm";
1
+ import { Primitive } from "../../Primitive.ts";
2
2
  import {
3
3
  type AddressAndType,
4
4
  AddressType,
@@ -1,4 +1,4 @@
1
- import { Primitive } from "@effectstream/sm";
1
+ import { Primitive } from "../../Primitive.ts";
2
2
  import {
3
3
  type AddressAndType,
4
4
  AddressType,
@@ -1,4 +1,4 @@
1
- import { Primitive } from "@effectstream/sm";
1
+ import { Primitive } from "../../Primitive.ts";
2
2
  import {
3
3
  type AddressAndType,
4
4
  AddressType,
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  generateRawStmInput,
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  generateRawStmInput,
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  generateRawStmInput,
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  generateRawStmInput,
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  generateRawStmInput,
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  generateRawStmInput,
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  generateRawStmInput,
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  generateRawStmInput,
@@ -9,7 +9,8 @@ import {
9
9
  AddressType,
10
10
  type EffectstreamBlockNumber,
11
11
  } from "@effectstream/utils";
12
- import { type JsonObject, Primitive } from "@effectstream/sm";
12
+ import { Primitive } from "../../Primitive.ts";
13
+ import type { JsonObject } from "../../types.ts";
13
14
  import {
14
15
  type CommandTuple,
15
16
  generateRawStmInput,
@@ -6,7 +6,8 @@ import {
6
6
  } from "@effectstream/concise";
7
7
  import type { ConfigSyncProtocolType, FlattenSyncProtocolIOFor, ProtocolPrimitiveMap, UtxorpcTxPredicate } from '@effectstream/config';
8
8
  import type { StateUpdateStream } from "@effectstream/coroutine";
9
- import { type JsonObject, Primitive } from "@effectstream/sm";
9
+ import { Primitive } from "../../Primitive.ts";
10
+ import type { JsonObject } from "../../types.ts";
10
11
  import { type AddressAndType, AddressType, type EffectstreamBlockNumber, uint8ArrayToHexString } from '@effectstream/utils';
11
12
  import { utxorpcGenericGrammar } from './utxorpc-generic-grammar.ts';
12
13
  import { PrimitiveTypeUtxorpcGeneric } from '../builtin.ts';