@sig-net/midnight 0.23.0 → 0.24.0-rc.10

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 (62) hide show
  1. package/README.md +52 -24
  2. package/dist/abi-serde.d.ts +198 -57
  3. package/dist/abi-serde.d.ts.map +1 -1
  4. package/dist/abi-serde.js +478 -367
  5. package/dist/abi-serde.js.map +1 -1
  6. package/dist/compact-descriptors.d.ts +25 -8
  7. package/dist/compact-descriptors.d.ts.map +1 -1
  8. package/dist/compact-descriptors.js +42 -25
  9. package/dist/compact-descriptors.js.map +1 -1
  10. package/dist/constants.d.ts +3 -22
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +3 -25
  13. package/dist/constants.js.map +1 -1
  14. package/dist/ecdsa-attestation.d.ts +44 -14
  15. package/dist/ecdsa-attestation.d.ts.map +1 -1
  16. package/dist/ecdsa-attestation.js +50 -15
  17. package/dist/ecdsa-attestation.js.map +1 -1
  18. package/dist/index.d.ts +6 -6
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +5 -5
  21. package/dist/index.js.map +1 -1
  22. package/dist/managed/compiler/contract-info.json +1808 -48
  23. package/dist/managed/compiler/contract-manifest.json +8 -8
  24. package/dist/managed/contract/index.d.ts +424 -5
  25. package/dist/managed/contract/index.js +1311 -171
  26. package/dist/managed/contract/index.js.map +1 -1
  27. package/dist/mpc-output-cache.js +2 -2
  28. package/dist/mpc-output-cache.js.map +1 -1
  29. package/dist/signature-requests-state-reader.d.ts +6 -13
  30. package/dist/signature-requests-state-reader.d.ts.map +1 -1
  31. package/dist/signature-requests-state-reader.js +10 -16
  32. package/dist/signature-requests-state-reader.js.map +1 -1
  33. package/dist/signet-contract-events.d.ts +52 -12
  34. package/dist/signet-contract-events.d.ts.map +1 -1
  35. package/dist/signet-contract-events.js +89 -34
  36. package/dist/signet-contract-events.js.map +1 -1
  37. package/dist/signet-evtype2tx-record-decoding.d.ts.map +1 -1
  38. package/dist/signet-evtype2tx-record-decoding.js +7 -6
  39. package/dist/signet-evtype2tx-record-decoding.js.map +1 -1
  40. package/dist/signet-evtype2tx-requests.d.ts +17 -12
  41. package/dist/signet-evtype2tx-requests.d.ts.map +1 -1
  42. package/dist/signet-evtype2tx-requests.js +92 -16
  43. package/dist/signet-evtype2tx-requests.js.map +1 -1
  44. package/dist/signet-request-id.d.ts +5 -2
  45. package/dist/signet-request-id.d.ts.map +1 -1
  46. package/dist/signet-request-id.js +27 -9
  47. package/dist/signet-request-id.js.map +1 -1
  48. package/dist/signet-request-response-reader.d.ts +8 -0
  49. package/dist/signet-request-response-reader.d.ts.map +1 -1
  50. package/dist/signet-request-response-reader.js +8 -7
  51. package/dist/signet-request-response-reader.js.map +1 -1
  52. package/dist/signet-requests.d.ts +48 -19
  53. package/dist/signet-requests.d.ts.map +1 -1
  54. package/dist/signet-requests.js +19 -8
  55. package/dist/signet-requests.js.map +1 -1
  56. package/dist/testing.d.ts +1 -1
  57. package/dist/testing.d.ts.map +1 -1
  58. package/dist/testing.js +3 -8
  59. package/dist/testing.js.map +1 -1
  60. package/package.json +2 -2
  61. package/src/Signet.compact +299 -101
  62. package/src/circuits.compact +92 -12
package/dist/abi-serde.js CHANGED
@@ -1,54 +1,4 @@
1
- // The two schema-driven conversions every Signet participant performs on the
2
- // respond path, named after the protocol fields that drive them:
3
- //
4
- // EVM call result --deserializeEvmOutput--> decoded values
5
- // (outputDeserializationSchema)
6
- // decoded values --serializeRespondOutput--> RespondBidirectionalEvent bytes
7
- // (respondSerializationSchema)
8
- //
9
- // Run by fakenet and the real MPC, back to back, after the
10
- // destination-chain transaction confirms, and by Signet clients to
11
- // independently recompute the respond bytes the MPC attested or to display
12
- // a decoded result.
13
- //
14
- // The respond output is UNBOUNDED: this module never pads or clamps it. Any
15
- // fixed event-field width is the caller's concern. Schemas are the ABI-style
16
- // JSON carried on chain (NUL-padded fixed-width bytes). Both functions
17
- // accept the schema in any form: already-parsed fields, a JSON string, or
18
- // the raw on-chain bytes. An EMPTY output schema (an unset all-NUL field, a
19
- // blank string, or `[]`) decodes to no values: a plain transfer has no call
20
- // output. A respond schema must be non-empty, and its declared capacities
21
- // are bounded ({@link MAX_RESPOND_PACKED_BYTES}): schemas are
22
- // requester-authored on-chain data, so the responder never honours a schema
23
- // demanding a giant allocation.
24
- //
25
- // Decode-side type grammar is left FULLY to the ABI library (ethers): this
26
- // module checks only the schema's shape. Respond-side types are restricted
27
- // to the Compact-carrier vocabulary below, strictly enforced.
28
- //
29
- // The respond byte layout is Compact's builtin serialize<T, N> /
30
- // deserialize<T, N> (via @sig-net/midnight-serde, pinned against compiled
31
- // circuits), so a consumer contract reads the payload with ONE
32
- // deserialize<T, N> call. Per-type mapping (Compact struct field on the
33
- // right):
34
- // bool 1 byte Boolean
35
- // uint8..uint248 bits / 8 bytes LE Uint<bits>
36
- // (whole-byte widths only: multiples of 8, others are rejected)
37
- // uint256, field 32 bytes LE, below Fr Field
38
- // address 32 bytes LE (numeric) Field
39
- // bytes1..bytes32 N raw bytes Bytes<N>
40
- // string, bytes 8-byte LE length + payload zero-padded to maxBytes
41
- // struct { len: Uint<64>; data: Bytes<maxBytes>; }
42
- // T[] 8-byte LE count + maxItems elements at T's width
43
- // struct { len: Uint<64>; items: Vector<maxItems, T>; }
44
- // intN rejected: Compact has no signed integers
45
- //
46
- // RANGE TRAP: uint256, address and field all map to Compact `Field`, whose
47
- // values must lie strictly below the BLS12-381 Fr modulus (just under
48
- // 2^255). An EVM uint256 at or above Fr cannot be respond-serialised, and
49
- // `serializeRespondOutput` throws at respond time. Schema authors who need
50
- // the full 256-bit range carry the value as bytes32.
51
- import { compactSerialize, compactSerializedSize, } from "@sig-net/midnight-serde";
1
+ import { compactSerialize } from "@sig-net/midnight-serde";
52
2
  import { ethers } from "ethers";
53
3
  // ---------------------------------------------------------------------------
54
4
  // 1. EVM call result -> decoded values (outputDeserializationSchema)
@@ -58,10 +8,10 @@ import { ethers } from "ethers";
58
8
  * into named values, driven by the request's outputDeserializationSchema.
59
9
  * The decode-side counterpart of {@link serializeRespondOutput}. Type
60
10
  * validation is FULLY delegated to ethers: this function checks the schema's
61
- * shape only. An empty schema decodes to an empty object (a plain transfer
62
- * has no output), matching the MPC's empty-schema handling. Returns a plain
63
- * object (never an ethers `Result`), so it survives JSON round-trips and
64
- * structural comparison.
11
+ * shape only, so it decodes ABI types the respond side refuses. An empty
12
+ * schema decodes to an empty object. Returns a plain object (never an
13
+ * ethers `Result`), so it survives JSON round-trips and structural
14
+ * comparison.
65
15
  *
66
16
  * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
67
17
  * @param callResult - The ABI-encoded return data (hex string or bytes).
@@ -73,86 +23,411 @@ export function deserializeEvmOutput(schema, callResult) {
73
23
  return {};
74
24
  }
75
25
  const decoded = ethers.AbiCoder.defaultAbiCoder().decode(fields.map((field) => field.type), callResult);
76
- const output = {};
77
- fields.forEach((field, i) => {
78
- output[field.name] = toPlainValue(decoded[i], field.name);
79
- });
80
- return output;
26
+ return Object.fromEntries(fields.map((field, i) => [
27
+ field.name,
28
+ toPlainValue(decoded[i], field.name),
29
+ ]));
81
30
  }
82
31
  // ---------------------------------------------------------------------------
83
- // 2. decoded values -> respond bytes (respondSerializationSchema)
32
+ // 2. decoded values -> respond bytes (Borsh schema derived from the output schema)
84
33
  // ---------------------------------------------------------------------------
85
34
  /**
86
- * Ceiling on a respond schema's total packed byte width. Schemas are
87
- * requester-authored on-chain data, so unbounded maxBytes/maxItems would let
88
- * a hostile request demand giant allocations from the responder. A real
89
- * respond payload must fit its consumer contract's fixed `deserialize<T, N>`
90
- * width, orders of magnitude below this.
35
+ * The ABI output types the MPC attests, and what each maps to. The respond
36
+ * bytes are the Borsh serialisation of a struct with one member per output
37
+ * field, in schema order, typed by this mapping:
38
+ *
39
+ * - {@link EvmOutputTypeKind.Bool}: ABI `bool`, Borsh `bool`, Compact `Boolean` (1 byte).
40
+ * - {@link EvmOutputTypeKind.Uint256}: ABI `uint256`, Borsh `[u8; 32]`
41
+ * holding the value LITTLE-endian, the byte order of every Borsh and
42
+ * Compact integer, Compact `Bytes<32>` (32 bytes). A circuit that wants a
43
+ * number narrows it with `checkedTruncationU128`, which aborts above
44
+ * 2^128 - 1. The ABI wire word is big-endian: the MPC reverses it once,
45
+ * here, so no circuit has to.
46
+ * - {@link EvmOutputTypeKind.Address}: ABI `address`, Borsh `[u8; 20]`
47
+ * holding the address bytes, Compact `Bytes<20>` (20 bytes).
48
+ * - {@link EvmOutputTypeKind.FixedBytes}: ABI `bytes1` to `bytes32`, Borsh
49
+ * `[u8; N]`, Compact `Bytes<N>` (N bytes).
50
+ *
51
+ * Every other ABI type is unsupported: the MPC drops a request whose output
52
+ * schema names one, and {@link deriveRespondSchema} throws on it.
91
53
  */
92
- export const MAX_RESPOND_PACKED_BYTES = 65536;
54
+ export var EvmOutputTypeKind;
55
+ (function (EvmOutputTypeKind) {
56
+ EvmOutputTypeKind["Bool"] = "bool";
57
+ EvmOutputTypeKind["Uint256"] = "uint256";
58
+ EvmOutputTypeKind["Address"] = "address";
59
+ EvmOutputTypeKind["FixedBytes"] = "bytesN";
60
+ })(EvmOutputTypeKind || (EvmOutputTypeKind = {}));
61
+ /** The byte width of an ABI word, and of the Borsh array a `uint256` output maps to. */
62
+ const ABI_WORD_BYTES = 32;
63
+ /** The byte width of an EVM address, and of the Borsh array an `address` output maps to. */
64
+ const EVM_ADDRESS_BYTES = 20;
93
65
  /**
94
- * Encode decoded output values into the respond payload, driven by the
95
- * request's respondSerializationSchema: the bytes a consumer contract reads
96
- * with `deserialize<T, N>` and the MPC attests. The respond-side counterpart
97
- * of {@link deserializeEvmOutput}.
66
+ * Classify an ABI type string into the supported output subset. The match is
67
+ * on the raw string, so `uint` is unsupported although the ABI library reads
68
+ * it as `uint256`: the MPC matches the same raw string.
98
69
  *
99
- * The result is the PACKED value, unpadded and unbounded: its length follows
100
- * entirely from the schema, and padding to a fixed container width is the
101
- * caller's concern. Strict: values are range-checked, dynamic payloads must
102
- * fit their capacity with no silent truncation, and signed integer types are
103
- * rejected.
70
+ * @param type - The ABI type string of an output schema field.
71
+ * @returns The field's kind and respond byte width, or `undefined` for an unsupported type.
72
+ */
73
+ function classifyEvmOutputType(type) {
74
+ if (type === "bool")
75
+ return { kind: EvmOutputTypeKind.Bool, bytes: 1 };
76
+ if (type === "uint256")
77
+ return { kind: EvmOutputTypeKind.Uint256, bytes: ABI_WORD_BYTES };
78
+ if (type === "address")
79
+ return { kind: EvmOutputTypeKind.Address, bytes: EVM_ADDRESS_BYTES };
80
+ const fixedBytes = /^bytes([1-9]|[12][0-9]|3[0-2])$/.exec(type);
81
+ if (fixedBytes !== null) {
82
+ return { kind: EvmOutputTypeKind.FixedBytes, bytes: Number(fixedBytes[1]) };
83
+ }
84
+ return undefined;
85
+ }
86
+ /**
87
+ * The fields of an outputDeserializationSchema whose ABI type is outside the
88
+ * supported subset ({@link EvmOutputTypeKind}). The check itself never
89
+ * throws on an unsupported type: an empty result means the MPC attests the
90
+ * request, a non-empty one means it drops it.
104
91
  *
105
- * @param schema - The respondSerializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
106
- * @param output - Decoded values keyed by field name (from {@link deserializeEvmOutput} or any source using the same forms).
107
- * @returns The packed respond bytes.
108
- * @throws {Error} If the schema is empty or malformed, packs to more than
109
- * {@link MAX_RESPOND_PACKED_BYTES} bytes, a value falls outside its declared
110
- * range, or a dynamic payload exceeds its capacity.
92
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
93
+ * @returns The unsupported fields in schema order, empty when every field is supported.
94
+ * @throws {Error} If the schema is not an array of uniquely named fields (its shape, not its types).
111
95
  */
112
- export function serializeRespondOutput(schema, output) {
113
- const fields = normalizeRespondSchema(schema);
114
- // Size the descriptor BEFORE any value work: the ceiling check is what
115
- // keeps a hostile capacity from ever reaching an allocation.
116
- const descriptor = respondSchemaToCompactType(fields);
117
- const packedWidth = compactSerializedSize(descriptor);
118
- if (packedWidth > MAX_RESPOND_PACKED_BYTES) {
119
- throw new Error(`respond schema packs to ${String(packedWidth)} bytes, above the ` +
120
- `${String(MAX_RESPOND_PACKED_BYTES)}-byte ceiling`);
96
+ export function unsupportedEvmOutputFields(schema) {
97
+ return parseSchemaShape(schema).filter((field) => classifyEvmOutputType(field.type) === undefined);
98
+ }
99
+ /**
100
+ * Parse an output schema and classify every field, refusing the schema as a
101
+ * whole when any field is unsupported.
102
+ *
103
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
104
+ * @returns The classified fields in schema order.
105
+ * @throws {Error} If the schema is malformed or names an unsupported ABI type.
106
+ */
107
+ function classifiedEvmOutputFields(schema) {
108
+ const fields = parseSchemaShape(schema);
109
+ const unsupported = fields.filter((field) => classifyEvmOutputType(field.type) === undefined);
110
+ if (unsupported.length > 0) {
111
+ throw new Error(`respond output: unsupported ABI output type${unsupported.length > 1 ? "s" : ""} ` +
112
+ unsupported.map((field) => `'${field.name}' (${field.type})`).join(", ") +
113
+ ": the MPC attests bool, uint256, address and bytes1 to bytes32 only");
121
114
  }
122
- const value = {};
123
- for (const field of fields) {
124
- const raw = output[field.name];
125
- if (raw === undefined) {
115
+ return fields.flatMap((field) => {
116
+ const classified = classifyEvmOutputType(field.type);
117
+ return classified === undefined ? [] : [{ ...field, ...classified }];
118
+ });
119
+ }
120
+ /**
121
+ * The Borsh schema the respond bytes of an executed call are serialised
122
+ * with: a struct with one member per output field, in schema order, each
123
+ * typed by {@link EvmOutputTypeKind}. An empty output schema derives an
124
+ * empty struct, which serialises to zero bytes. Borsh writes struct members
125
+ * in the struct object's key order, which equals schema order because field
126
+ * names are Solidity identifiers (see {@link parseSchemaShape}).
127
+ *
128
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
129
+ * @returns The derived Borsh struct schema.
130
+ * @throws {Error} If the schema is malformed or names an unsupported ABI type.
131
+ */
132
+ export function deriveRespondSchema(schema) {
133
+ return {
134
+ struct: Object.fromEntries(classifiedEvmOutputFields(schema).map((field) => [
135
+ field.name,
136
+ field.kind === EvmOutputTypeKind.Bool
137
+ ? "bool"
138
+ : { array: { type: "u8", len: field.bytes } },
139
+ ])),
140
+ };
141
+ }
142
+ /**
143
+ * The byte width of the respond bytes an output schema derives: the sum of
144
+ * its fields' widths under {@link EvmOutputTypeKind}, what a settle circuit
145
+ * declares as its `Bytes<N>` output argument.
146
+ *
147
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
148
+ * @returns The exact serialised width, 0 for an empty schema.
149
+ * @throws {Error} If the schema is malformed or names an unsupported ABI type.
150
+ */
151
+ export function respondOutputWidth(schema) {
152
+ return classifiedEvmOutputFields(schema).reduce((width, field) => width + field.bytes, 0);
153
+ }
154
+ /**
155
+ * The 32-byte LITTLE-endian encoding of a `uint256` output value, as ethers
156
+ * decodes it (bigint) or as indexer JSON renders it (a number or a decimal
157
+ * string). This is the ABI wire word reversed: the Borsh integer byte order,
158
+ * which a Compact `Uint<128>` reads directly.
159
+ *
160
+ * @param value - The decoded value.
161
+ * @param name - The field name, for the error message.
162
+ * @returns The value's 32 little-endian bytes.
163
+ * @throws {RangeError} If the value is not an integer in `0 <= value < 2^256`.
164
+ */
165
+ function uint256Word(value, name) {
166
+ if (typeof value !== "bigint" && typeof value !== "number" && typeof value !== "string") {
167
+ throw new TypeError(`respond output: '${name}' (uint256) expects an integer`);
168
+ }
169
+ if (typeof value === "number" && !Number.isSafeInteger(value)) {
170
+ throw new RangeError(`respond output: '${name}' (uint256) is not a safe integer`);
171
+ }
172
+ if (typeof value === "string" && !/^\s*\d+\s*$/.test(value)) {
173
+ throw new RangeError(`respond output: '${name}' (uint256) is not a decimal integer string`);
174
+ }
175
+ const integer = BigInt(value);
176
+ if (integer < 0n || integer >= 1n << 256n) {
177
+ throw new RangeError(`respond output: '${name}' (uint256) ${String(integer)} is outside uint256`);
178
+ }
179
+ return Array.from(ethers.getBytes(ethers.toBeHex(integer, ABI_WORD_BYTES))).reverse();
180
+ }
181
+ /**
182
+ * The bytes of an `address` or `bytesN` output value, as ethers decodes it
183
+ * (a hex string, checksummed for an address) or as bytes. Byte order is the
184
+ * wire's: these are byte strings, not numbers.
185
+ *
186
+ * @param value - The decoded value.
187
+ * @param field - The field, for the error message.
188
+ * @returns The value's bytes, length-checked by Borsh against the schema.
189
+ * @throws {TypeError} If the value is neither a hex string nor bytes.
190
+ */
191
+ function byteString(value, field) {
192
+ if (value instanceof Uint8Array)
193
+ return Array.from(value);
194
+ if (typeof value === "string")
195
+ return Array.from(ethers.getBytes(value));
196
+ throw new TypeError(`respond output: '${field.name}' (${field.type}) expects hex or bytes`);
197
+ }
198
+ /**
199
+ * Serialise ABI-decoded output into the respond bytes the MPC attests: the
200
+ * Borsh encoding of {@link deriveRespondSchema}'s struct over the output's
201
+ * values. A `uint256` is carried whole as 32 little-endian bytes, so no
202
+ * value is narrowed off chain: a circuit that wants a number narrows the
203
+ * `Bytes<32>` with `checkedTruncationU128`.
204
+ *
205
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
206
+ * @param output - The decoded values keyed by field name, as {@link deserializeEvmOutput} returns them.
207
+ * @returns The unpadded respond bytes, {@link respondOutputWidth} long.
208
+ * @throws {Error} If the schema is malformed or names an unsupported type, a
209
+ * field is missing from `output`, or a value is outside its type.
210
+ */
211
+ export function serializeRespondOutput(schema, output) {
212
+ const values = Object.fromEntries(classifiedEvmOutputFields(schema).map((field) => {
213
+ const value = output[field.name];
214
+ if (value === undefined) {
126
215
  throw new Error(`respond output: missing value for '${field.name}'`);
127
216
  }
128
- value[field.name] = toCompactValue(raw, field);
129
- }
130
- return compactSerialize(descriptor, value);
217
+ switch (field.kind) {
218
+ case EvmOutputTypeKind.Bool:
219
+ if (typeof value !== "boolean") {
220
+ throw new TypeError(`respond output: '${field.name}' (bool) expects a boolean`);
221
+ }
222
+ return [field.name, value];
223
+ case EvmOutputTypeKind.Uint256:
224
+ return [field.name, uint256Word(value, field.name)];
225
+ case EvmOutputTypeKind.Address:
226
+ case EvmOutputTypeKind.FixedBytes:
227
+ return [field.name, byteString(value, field)];
228
+ }
229
+ }));
230
+ return compactSerialize(deriveRespondSchema(schema), values);
131
231
  }
132
- // ===========================================================================
133
- // Helpers from here down. The two functions above are the whole public
134
- // surface: everything below serves them.
135
- // ===========================================================================
136
232
  // ---------------------------------------------------------------------------
137
- // Field-kind guards
233
+ // 3. executed EVM transaction -> attested respond bytes
138
234
  // ---------------------------------------------------------------------------
139
- function isAbiDynamicField(field) {
140
- return field.type === "string" || field.type === "bytes";
235
+ /**
236
+ * Whether an EVM transaction is a contract call, decided over its input
237
+ * bytes exactly as the MPC decides it (`is_contract_call` in its
238
+ * `chain-ethereum/src/event_parsing.rs`): more than two bytes of input, so a
239
+ * one- or two-byte input is NOT a contract call. The answer selects the
240
+ * branch of {@link executedEvmRespondOutput}.
241
+ *
242
+ * Pass the signed transaction's `data`, or, from the request record alone,
243
+ * `assembleCalldata(request.txParams.calldata)`, which yields the same bytes
244
+ * (`"0x"` for a request without calldata).
245
+ *
246
+ * @param input - The transaction's input bytes (hex string or bytes).
247
+ * @returns True when the MPC treats the transaction as a contract call.
248
+ * @throws {Error} If `input` is neither bytes nor valid 0x hex.
249
+ */
250
+ export function isEvmContractCall(input) {
251
+ return ethers.getBytes(input).length > 2;
141
252
  }
142
- function isAbiArrayField(field) {
143
- return field.type.endsWith("[]");
253
+ /**
254
+ * How a `debug_traceTransaction` (callTracer) read of a transaction's top
255
+ * call frame came back: the MPC's `TraceOutput`. Carried by
256
+ * {@link EvmTraceOutput}.
257
+ */
258
+ export var EvmTraceOutputKind;
259
+ (function (EvmTraceOutputKind) {
260
+ /** The transaction was not traced: the MPC traces contract calls only. */
261
+ EvmTraceOutputKind["NotTraced"] = "NotTraced";
262
+ /** The top call frame carries a string `output` field: the return data, possibly empty (`0x`). */
263
+ EvmTraceOutputKind["Output"] = "Output";
264
+ /** The top call frame carries no string `output` field. */
265
+ EvmTraceOutputKind["NoReturnData"] = "NoReturnData";
266
+ })(EvmTraceOutputKind || (EvmTraceOutputKind = {}));
267
+ /**
268
+ * Refuse return data that is not canonical ABI before it is decoded: whole
269
+ * 32-byte words, at least one per declared field, a `bool` word of 0 or 1,
270
+ * an `address` word with zero high bytes and a `bytesN` word zero-padded
271
+ * after its N bytes. Words past the declared fields are not checked. The ABI
272
+ * library decodes such data leniently (a `bool` word of 2 reads as true), so
273
+ * the check sits here, where the MPC decides whether to attest.
274
+ *
275
+ * @param fields - The schema's classified fields, in order.
276
+ * @param returnData - The traced return data, non-empty.
277
+ * @throws {Error} If the data is not whole words, holds fewer words than
278
+ * fields, or a field's word is not canonical for its type.
279
+ */
280
+ function checkCanonicalReturnData(fields, returnData) {
281
+ if (returnData.length % ABI_WORD_BYTES !== 0) {
282
+ throw new Error(`respond output: return data of ${String(returnData.length)} bytes is not whole ABI words`);
283
+ }
284
+ const words = returnData.length / ABI_WORD_BYTES;
285
+ if (words < fields.length) {
286
+ throw new Error(`respond output: return data holds ${String(words)} word${words === 1 ? "" : "s"} but the output schema declares ${String(fields.length)} fields`);
287
+ }
288
+ const isZero = (bytes) => bytes.every((byte) => byte === 0);
289
+ fields.forEach((field, i) => {
290
+ const word = returnData.subarray(i * ABI_WORD_BYTES, (i + 1) * ABI_WORD_BYTES);
291
+ const last = word[ABI_WORD_BYTES - 1];
292
+ const canonical = field.kind === EvmOutputTypeKind.Bool
293
+ ? isZero(word.subarray(0, ABI_WORD_BYTES - 1)) && (last === 0 || last === 1)
294
+ : field.kind === EvmOutputTypeKind.Address
295
+ ? isZero(word.subarray(0, ABI_WORD_BYTES - EVM_ADDRESS_BYTES))
296
+ : field.kind === EvmOutputTypeKind.FixedBytes
297
+ ? isZero(word.subarray(field.bytes))
298
+ : true;
299
+ if (!canonical) {
300
+ throw new Error(`respond output: '${field.name}' (${field.type}) word 0x${ethers.hexlify(word).slice(2)} is not canonical ABI`);
301
+ }
302
+ });
303
+ }
304
+ /**
305
+ * The exact respond output the MPC attests for an EVM transaction that
306
+ * executed (the payload of an `OutputKind.executed` attestation), derived
307
+ * from the request's outputDeserializationSchema alone. The schema and the
308
+ * execution's return data MUST agree: an empty schema declares that the
309
+ * execution returns nothing, a non-empty schema that it returns data.
310
+ *
311
+ * - Not a contract call (a plain transfer): there is no return data, so the
312
+ * schema must be empty and the output is EMPTY.
313
+ * - A contract call whose trace carries no return data, or empty return
314
+ * data (`0x`): the schema must be empty and the output is EMPTY.
315
+ * - A contract call that returned data: the schema must be non-empty, and
316
+ * the output is the return data decoded by {@link deserializeEvmOutput}
317
+ * and serialised by {@link serializeRespondOutput}.
318
+ *
319
+ * The schema's types are checked on every path, a plain transfer included,
320
+ * so a request the MPC should have dropped never attests anything. A failed
321
+ * or unviable execution is attested over an empty output instead, which
322
+ * needs no call. An empty output under `executed` is therefore ordinary for
323
+ * an empty schema: its settle circuit verifies at width 0 and must route on
324
+ * the verified `outputKind`, never on the width.
325
+ *
326
+ * @param schema - The request's outputDeserializationSchema (a request record's field satisfies this).
327
+ * @param isContractCall - Whether the transaction is a contract call, as {@link isEvmContractCall} decides it.
328
+ * @param trace - The transaction's traced return data (`NotTraced` for an untraced plain transfer).
329
+ * @returns The respond bytes, unpadded, empty for an empty schema.
330
+ * @throws {Error} Exactly where the MPC refuses to attest an execution: a
331
+ * malformed schema or an unsupported output type, a contract call not
332
+ * traced, return data under an empty schema, no or empty return data
333
+ * under a non-empty schema, return data that is not canonical ABI
334
+ * ({@link checkCanonicalReturnData}) or that the schema cannot decode, and
335
+ * every {@link serializeRespondOutput} rejection.
336
+ */
337
+ export function executedEvmRespondOutput(schema, isContractCall, trace) {
338
+ const fields = classifiedEvmOutputFields(schema);
339
+ const expectsOutput = fields.length > 0;
340
+ if (!isContractCall) {
341
+ if (expectsOutput) {
342
+ throw new Error("respond output: a plain transfer returns nothing, but the output schema declares return values");
343
+ }
344
+ return new Uint8Array(0);
345
+ }
346
+ if (trace.kind === EvmTraceOutputKind.NotTraced) {
347
+ throw new Error("respond output: a contract call's output needs its trace");
348
+ }
349
+ const returnData = trace.kind === EvmTraceOutputKind.Output
350
+ ? ethers.getBytes(trace.returnData)
351
+ : new Uint8Array(0);
352
+ if (returnData.length === 0) {
353
+ if (expectsOutput) {
354
+ throw new Error("respond output: the contract call returned no data, but the output schema declares return values");
355
+ }
356
+ return new Uint8Array(0);
357
+ }
358
+ if (!expectsOutput) {
359
+ throw new Error("respond output: the contract call returned data, but the output schema declares no return values");
360
+ }
361
+ checkCanonicalReturnData(fields, returnData);
362
+ return serializeRespondOutput(schema, deserializeEvmOutput(schema, returnData));
363
+ }
364
+ /**
365
+ * Whether a JSON value is an object. A type guard, as `Array.isArray` does
366
+ * not narrow a readonly array out of a union.
367
+ *
368
+ * @param value - The JSON value.
369
+ * @returns True for a JSON object, false for an array, a scalar or null.
370
+ */
371
+ function isJsonObject(value) {
372
+ return typeof value === "object" && value !== null && !Array.isArray(value);
144
373
  }
145
- // ---------------------------------------------------------------------------
146
- // Decode-side value flattening
147
- // ---------------------------------------------------------------------------
148
374
  /**
149
- * Flatten ethers `Result` arrays into plain arrays, pass scalars through.
375
+ * A JSON object's own property when it is a string.
376
+ *
377
+ * @param object - The JSON object.
378
+ * @param key - The property name.
379
+ * @returns The string value, or `undefined` when absent or of another JSON kind.
380
+ */
381
+ function ownString(object, key) {
382
+ const value = Object.hasOwn(object, key) ? object[key] : undefined;
383
+ return typeof value === "string" ? value : undefined;
384
+ }
385
+ /**
386
+ * The {@link EvmTraceOutput} of a `debug_traceTransaction` callTracer top
387
+ * call frame, read exactly as the MPC reads it (`trace_output_to_bytes` in
388
+ * its `chain-ethereum/src/rpc.rs`):
389
+ *
390
+ * - The frame must be a JSON object with a `type` field (of any value).
391
+ * - A non-empty string `error` refuses the frame, naming a non-empty string
392
+ * `revertReason` beside it. Any other `error` is ignored.
393
+ * - A string `output` is {@link EvmTraceOutputKind.Output}, its hex decoded
394
+ * after one optional lowercase `0x` prefix (`0x` and `""` are empty return
395
+ * data). An absent or non-string `output` is
396
+ * {@link EvmTraceOutputKind.NoReturnData}.
150
397
  *
151
- * @param value - A decoded ABI value, possibly a nested `Result`.
152
- * @param label - Field path, used in error messages.
153
- * @returns The value with every `Result` replaced by a plain array.
154
- * @throws {Error} If the value is of a kind the respond side cannot carry.
398
+ * @param frame - The JSON-RPC `result` of `debug_traceTransaction` with the callTracer.
399
+ * @returns The frame's traced return data, for {@link executedEvmRespondOutput}.
400
+ * @throws {Error} If the frame is not an object, has no `type`, reports an
401
+ * `error`, or carries an `output` that is not even-length hex.
155
402
  */
403
+ export function evmTraceOutputFromCallFrame(frame) {
404
+ if (!isJsonObject(frame)) {
405
+ throw new Error(`debug_traceTransaction result is not a call frame: ${JSON.stringify(frame)}`);
406
+ }
407
+ if (!Object.hasOwn(frame, "type")) {
408
+ throw new Error(`debug_traceTransaction result has no call frame \`type\`: ${JSON.stringify(frame)}`);
409
+ }
410
+ const error = ownString(frame, "error");
411
+ if (error !== undefined && error !== "") {
412
+ const revertReason = ownString(frame, "revertReason");
413
+ throw new Error(revertReason !== undefined && revertReason !== ""
414
+ ? `debug_traceTransaction reports the call reverted: ${error} (${revertReason})`
415
+ : `debug_traceTransaction reports the call errored: ${error}`);
416
+ }
417
+ const output = ownString(frame, "output");
418
+ if (output === undefined) {
419
+ return { kind: EvmTraceOutputKind.NoReturnData };
420
+ }
421
+ const digits = output.startsWith("0x") ? output.slice(2) : output;
422
+ if (!/^[0-9a-fA-F]*$/.test(digits) || digits.length % 2 !== 0) {
423
+ throw new Error(`debug_traceTransaction call frame output is not hex: "${output}"`);
424
+ }
425
+ return { kind: EvmTraceOutputKind.Output, returnData: ethers.getBytes(`0x${digits}`) };
426
+ }
427
+ // ===========================================================================
428
+ // Helpers from here down. The exports above are the whole public surface:
429
+ // everything below serves them.
430
+ // ===========================================================================
156
431
  function toPlainValue(value, label) {
157
432
  if (value instanceof ethers.Result) {
158
433
  return value.toArray().map((v, i) => toPlainValue(v, `${label}[${String(i)}]`));
@@ -172,44 +447,97 @@ function toPlainValue(value, label) {
172
447
  // ---------------------------------------------------------------------------
173
448
  // Schema parsing + validation
174
449
  // ---------------------------------------------------------------------------
175
- const ADDRESS_BOUND = 1n << 160n;
176
- const MAX_UINT_BITS = 248;
177
- /** Length-prefix width of the dynamic string/bytes/array convention (Uint<64>). */
178
- const DYN_LEN_BYTES = 8;
179
450
  /**
180
- * Parse schema text as JSON with the parse failure named. Blank text (an
181
- * unset all-NUL on-chain schema field) is the empty schema.
451
+ * The canonical text of an output schema: `JSON.stringify` of its fields
452
+ * reduced to `{name, type}` in schema order, with no whitespace. A schema's
453
+ * on-chain bytes must be exactly this text followed by NUL padding, so one
454
+ * schema has exactly one byte form. Write a contract's schema literal with
455
+ * this function's output.
456
+ *
457
+ * @param fields - The schema's fields in order.
458
+ * @returns The canonical JSON text, `[]` for an empty schema.
459
+ */
460
+ export function canonicalSchemaText(fields) {
461
+ return JSON.stringify(fields.map(({ name, type }) => ({ name, type })));
462
+ }
463
+ /**
464
+ * Parse a schema's bytes, requiring the canonical form: the bytes before the
465
+ * first NUL are exactly {@link canonicalSchemaText} of the fields they
466
+ * encode, every byte from the first NUL on is NUL, and an empty schema is
467
+ * `[]` or no bytes at all (an unset all-NUL field).
182
468
  *
183
- * @param text - The schema text, NUL-trimmed.
184
- * @returns The parsed JSON value, `[]` for blank text.
185
- * @throws {Error} If non-blank text is not valid JSON.
469
+ * @param bytes - The schema text's UTF-8 bytes, NUL-padded or not.
470
+ * @returns The schema's fields.
471
+ * @throws {Error} If the padding holds a non-NUL byte, the text is not JSON,
472
+ * the fields fail {@link checkSchemaFields}, or the text is not canonical.
186
473
  */
187
- function parseSchemaJson(text) {
188
- if (text.trim() === "") {
474
+ function parseCanonicalSchema(bytes) {
475
+ const nul = bytes.indexOf(0);
476
+ const body = nul === -1 ? bytes : bytes.subarray(0, nul);
477
+ if (nul !== -1 && bytes.subarray(nul).some((byte) => byte !== 0)) {
478
+ throw new Error("schema: every byte from the first NUL on must be NUL");
479
+ }
480
+ if (body.length === 0) {
189
481
  return [];
190
482
  }
483
+ let parsed;
191
484
  try {
192
- return JSON.parse(text);
485
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body));
193
486
  }
194
487
  catch (error) {
195
488
  throw new Error(`schema is not valid JSON (${String(error)})`, { cause: error });
196
489
  }
490
+ const fields = checkSchemaFields(parsed);
491
+ const canonical = new TextEncoder().encode(canonicalSchemaText(fields));
492
+ if (canonical.length !== body.length || canonical.some((byte, i) => byte !== body[i])) {
493
+ throw new Error(`schema is not canonical: expected exactly ${canonicalSchemaText(fields)}`);
494
+ }
495
+ return fields;
197
496
  }
198
497
  /**
199
- * Parse a schema in any input form and check its SHAPE only: an array of
200
- * fields with non-empty, unique names and non-empty type strings. An empty
201
- * schema is valid here: emptiness policy belongs to the callers
202
- * ({@link deserializeEvmOutput} decodes nothing, {@link serializeRespondOutput}
203
- * rejects).
498
+ * A Solidity identifier, the only form a field name takes. A name outside
499
+ * this grammar can start with a digit, and a JavaScript object orders
500
+ * integer-like keys first, which would move the derived Borsh struct's
501
+ * members out of schema order.
502
+ */
503
+ const SOLIDITY_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
504
+ /**
505
+ * The one identifier no field may use: assigning it on a JavaScript object
506
+ * reaches the prototype accessor, so an output object cannot carry it.
507
+ */
508
+ const PROTOTYPE_ACCESSOR_NAME = "__proto__";
509
+ /**
510
+ * Parse a schema in any input form and check its SHAPE only: text and bytes
511
+ * must be the canonical form ({@link parseCanonicalSchema}), and every form
512
+ * passes {@link checkSchemaFields}. Whether a type is in the attested subset
513
+ * is decided later, by {@link classifyEvmOutputType}. An empty schema is
514
+ * valid here: {@link deserializeEvmOutput} decodes nothing from it and
515
+ * {@link executedEvmRespondOutput} attests an empty output.
204
516
  *
205
- * @param schema - The schema as JSON text, packed bytes, or a field array.
206
- * @returns The schema's fields, names and type strings unvalidated beyond shape.
207
- * @throws {Error} If the schema is not an array of uniquely named fields.
517
+ * @param schema - The schema as canonical JSON text, NUL-padded bytes, or a field array.
518
+ * @returns The schema's fields, type strings as written.
519
+ * @throws {Error} If the text or bytes are not canonical, or the fields fail {@link checkSchemaFields}.
208
520
  */
209
521
  function parseSchemaShape(schema) {
210
- const parsed = typeof schema === "string" || schema instanceof Uint8Array
211
- ? parseSchemaJson(schemaText(schema))
212
- : schema;
522
+ if (typeof schema === "string") {
523
+ return parseCanonicalSchema(new TextEncoder().encode(schema));
524
+ }
525
+ if (schema instanceof Uint8Array) {
526
+ return parseCanonicalSchema(schema);
527
+ }
528
+ return checkSchemaFields(schema);
529
+ }
530
+ /**
531
+ * Check a parsed schema value's shape: an array of fields with unique
532
+ * Solidity-identifier names and ABI type strings `ethers.ParamType` accepts.
533
+ *
534
+ * @param parsed - The schema as parsed JSON or a field array.
535
+ * @returns The schema's fields, type strings as written.
536
+ * @throws {Error} If the value is not an array of fields, a name is not a
537
+ * Solidity identifier or is `__proto__`, a name repeats, or a type is
538
+ * missing or not an ABI type.
539
+ */
540
+ function checkSchemaFields(parsed) {
213
541
  if (!Array.isArray(parsed)) {
214
542
  throw new Error("schema must be a JSON array of fields");
215
543
  }
@@ -218,10 +546,16 @@ function parseSchemaShape(schema) {
218
546
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
219
547
  throw new Error(`schema field ${String(i)} is not an object`);
220
548
  }
221
- const { name, type, maxBytes, maxItems } = raw;
549
+ const { name, type } = raw;
222
550
  if (typeof name !== "string" || name.length === 0) {
223
551
  throw new Error(`schema field ${String(i)} needs a non-empty name`);
224
552
  }
553
+ if (!SOLIDITY_IDENTIFIER.test(name)) {
554
+ throw new Error(`schema: field name '${name}' is not a Solidity identifier`);
555
+ }
556
+ if (name === PROTOTYPE_ACCESSOR_NAME) {
557
+ throw new Error(`schema: field name '${name}' is refused`);
558
+ }
225
559
  if (seen.has(name)) {
226
560
  throw new Error(`schema: duplicate field name '${name}'`);
227
561
  }
@@ -229,236 +563,13 @@ function parseSchemaShape(schema) {
229
563
  if (typeof type !== "string" || type.length === 0) {
230
564
  throw new Error(`schema: '${name}' needs a type`);
231
565
  }
232
- return { name, type, maxBytes, maxItems };
233
- });
234
- }
235
- /**
236
- * Enforce the respond-side Compact-carrier vocabulary on a shape-checked
237
- * schema: every type needs a Compact carrier (no signed ints, uint widths of
238
- * at most 248 bits or exactly 256, bytesN at most 32) and every dynamic field
239
- * needs its fixed capacity.
240
- *
241
- * @param schema - The schema to normalize.
242
- * @returns The schema with every field proven to have a Compact carrier.
243
- * @throws {Error} If a type has no carrier or a dynamic field omits its capacity.
244
- */
245
- function normalizeRespondSchema(schema) {
246
- const fields = parseSchemaShape(schema);
247
- if (fields.length === 0) {
248
- throw new Error("respond schema is empty: a respond serialization schema needs at least one field");
249
- }
250
- return fields.map(({ name, type, maxBytes, maxItems }) => {
251
- if (type === "string" || type === "bytes") {
252
- assertCapacity(maxBytes, `'${name}' (${type}) maxBytes`);
253
- return { name, type, maxBytes };
254
- }
255
- if (type.endsWith("[]")) {
256
- classifyFixedType(type.slice(0, -2), name);
257
- assertCapacity(maxItems, `'${name}' (${type}) maxItems`);
258
- return { name, type: type, maxItems };
259
- }
260
- classifyFixedType(type, name);
261
- return { name, type: type };
262
- });
263
- }
264
- /**
265
- * Cut a NUL-padded on-chain schema at the first NUL and decode to text.
266
- *
267
- * @param schema - Schema text, or the NUL-padded bytes read from the ledger.
268
- * @returns The schema text with the padding removed.
269
- */
270
- function schemaText(schema) {
271
- const raw = typeof schema === "string" ? schema : new TextDecoder().decode(schema);
272
- const nul = raw.indexOf("\0");
273
- return nul === -1 ? raw : raw.slice(0, nul);
274
- }
275
- function assertCapacity(value, label) {
276
- if (value === undefined) {
277
- throw new Error(`schema: ${label} is required: Compact types are fixed-size`);
278
- }
279
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
280
- throw new Error(`schema: ${label} must be a positive integer`);
281
- }
282
- }
283
- /**
284
- * Classify a fixed-width respond type into its Compact carrier, validating
285
- * the respond-side vocabulary in one place: both the schema check
286
- * ({@link normalizeRespondSchema}) and the descriptor build
287
- * ({@link respondSchemaToCompactType}) call this, so the grammar cannot
288
- * drift between them. Respond-side uint widths are restricted to whole-byte
289
- * widths (multiples of 8 from 8 to 248, packing to bits / 8 bytes), plus
290
- * uint256 which maps to Field. Throws with the offending field named.
291
- *
292
- * @param type - The fixed-width ABI type name.
293
- * @param fieldName - The field the type belongs to, used in error messages.
294
- * @returns The Compact descriptor that carries the type.
295
- * @throws {Error} If the type has no respond-side Compact carrier.
296
- */
297
- function classifyFixedType(type, fieldName) {
298
- if (type === "bool")
299
- return { kind: "boolean" };
300
- if (type === "field" || type === "uint256" || type === "address") {
301
- return { kind: "field" };
302
- }
303
- if (/^int\d+$/.test(type)) {
304
- throw new Error(`schema: '${fieldName}' (${type}) is unsupported: Compact has no signed integers`);
305
- }
306
- const uintMatch = /^uint([1-9]\d*)$/.exec(type);
307
- if (uintMatch) {
308
- const bits = Number(uintMatch[1]);
309
- const wholeByteWidth = bits >= 8 && bits <= MAX_UINT_BITS && bits % 8 === 0;
310
- if (!wholeByteWidth) {
311
- throw new Error(`schema: '${fieldName}' (${type}) has no respond carrier: uint widths ` +
312
- `must be multiples of 8 from 8 to ${String(MAX_UINT_BITS)}, or uint256 (maps to Field)`);
313
- }
314
- return { kind: "uint", bits };
315
- }
316
- const bytesMatch = /^bytes([1-9]\d*)$/.exec(type);
317
- if (bytesMatch) {
318
- const n = Number(bytesMatch[1]);
319
- if (n < 1 || n > 32) {
320
- throw new Error(`schema: '${fieldName}' (${type}) is not a valid bytesN type`);
321
- }
322
- return { kind: "bytes", length: n };
323
- }
324
- throw new Error(`schema: '${fieldName}' has unsupported type '${type}'`);
325
- }
326
- // ---------------------------------------------------------------------------
327
- // Schema -> CompactType descriptor + value coercion
328
- // ---------------------------------------------------------------------------
329
- function respondSchemaToCompactType(fields) {
330
- return {
331
- kind: "struct",
332
- fields: fields.map((field) => {
333
- if (isAbiDynamicField(field)) {
334
- return {
335
- name: field.name,
336
- type: {
337
- kind: "struct",
338
- fields: [
339
- { name: "len", type: { kind: "uint", bits: DYN_LEN_BYTES * 8 } },
340
- { name: "data", type: { kind: "bytes", length: field.maxBytes } },
341
- ],
342
- },
343
- };
344
- }
345
- if (isAbiArrayField(field)) {
346
- return {
347
- name: field.name,
348
- type: {
349
- kind: "struct",
350
- fields: [
351
- { name: "len", type: { kind: "uint", bits: DYN_LEN_BYTES * 8 } },
352
- {
353
- name: "items",
354
- type: {
355
- kind: "vector",
356
- length: field.maxItems,
357
- element: classifyFixedType(field.type.slice(0, -2), field.name),
358
- },
359
- },
360
- ],
361
- },
362
- };
363
- }
364
- return { name: field.name, type: classifyFixedType(field.type, field.name) };
365
- }),
366
- };
367
- }
368
- function toCompactValue(value, field) {
369
- const { name } = field;
370
- if (isAbiDynamicField(field)) {
371
- const payload = field.type === "string"
372
- ? new TextEncoder().encode(asString(value, name))
373
- : asBytes(value, name);
374
- if (payload.length > field.maxBytes) {
375
- throw new Error(`'${name}': payload is ${String(payload.length)} bytes, maxBytes is ${String(field.maxBytes)}`);
376
- }
377
- const data = new Uint8Array(field.maxBytes);
378
- data.set(payload);
379
- return { len: BigInt(payload.length), data };
380
- }
381
- if (isAbiArrayField(field)) {
382
- if (!Array.isArray(value)) {
383
- throw new Error(`'${name}' (${field.type}) expects an array`);
384
- }
385
- if (value.length > field.maxItems) {
386
- throw new Error(`'${name}': ${String(value.length)} elements, maxItems is ${String(field.maxItems)}`);
387
- }
388
- const elementType = field.type.slice(0, -2);
389
- const items = value.map((element, i) => fixedCompactValue(element, elementType, `${name}[${String(i)}]`));
390
- // Unused capacity encodes as zero values of the element type.
391
- while (items.length < field.maxItems)
392
- items.push(zeroOf(elementType));
393
- return { len: BigInt(value.length), items };
394
- }
395
- return fixedCompactValue(value, field.type, name);
396
- }
397
- function fixedCompactValue(value, type, label) {
398
- if (type === "bool") {
399
- if (typeof value !== "boolean") {
400
- throw new Error(`'${label}' (bool) expects a boolean`);
401
- }
402
- return value;
403
- }
404
- if (/^bytes\d+$/.test(type)) {
405
- const raw = asBytes(value, label);
406
- const expected = Number(type.slice(5));
407
- if (raw.length !== expected) {
408
- throw new Error(`'${label}' (${type}) expects exactly ${String(expected)} bytes, got ${String(raw.length)}`);
409
- }
410
- return raw;
411
- }
412
- // Numeric carriers: uintN, uint256/field and address.
413
- const n = asBigint(value, label);
414
- if (type === "address" && n >= ADDRESS_BOUND) {
415
- throw new Error(`'${label}': value ${String(n)} exceeds an address`);
416
- }
417
- // Uint width and Field modulus bounds are enforced by @sig-net/midnight-serde.
418
- return n;
419
- }
420
- function zeroOf(type) {
421
- if (type === "bool")
422
- return false;
423
- if (/^bytes\d+$/.test(type))
424
- return new Uint8Array(Number(type.slice(5)));
425
- return 0n;
426
- }
427
- // ---------------------------------------------------------------------------
428
- // Value-form coercions
429
- // ---------------------------------------------------------------------------
430
- function asBigint(value, label) {
431
- if (typeof value === "bigint")
432
- return value;
433
- if (typeof value === "number" && Number.isSafeInteger(value))
434
- return BigInt(value);
435
- if (typeof value === "string") {
436
566
  try {
437
- return BigInt(value);
567
+ ethers.ParamType.from(type);
438
568
  }
439
- catch {
440
- throw new Error(`'${label}': cannot parse '${value}' as an integer`);
569
+ catch (error) {
570
+ throw new Error(`schema: '${name}' has an invalid ABI type '${type}'`, { cause: error });
441
571
  }
442
- }
443
- throw new Error(`'${label}': expected an integer-like value`);
444
- }
445
- function asBytes(value, label) {
446
- if (value instanceof Uint8Array)
447
- return value;
448
- if (typeof value === "string") {
449
- try {
450
- return ethers.getBytes(value);
451
- }
452
- catch {
453
- throw new Error(`'${label}': not a valid 0x hex byte string: "${value}"`);
454
- }
455
- }
456
- throw new Error(`'${label}': expected bytes (Uint8Array or hex string)`);
457
- }
458
- function asString(value, label) {
459
- if (typeof value !== "string") {
460
- throw new Error(`'${label}': expected a string`);
461
- }
462
- return value;
572
+ return { name, type };
573
+ });
463
574
  }
464
575
  //# sourceMappingURL=abi-serde.js.map