@sig-net/midnight 0.24.0-rc.6 → 0.24.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/abi-serde.js CHANGED
@@ -1,60 +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, and the
3
- // MPC's rule composing them for an executed EVM transaction:
4
- //
5
- // EVM call result --deserializeEvmOutput--> decoded values
6
- // (outputDeserializationSchema)
7
- // decoded values --serializeRespondOutput--> RespondBidirectionalEvent bytes
8
- // (respondSerializationSchema)
9
- // callTracer top frame --evmTraceOutputFromCallFrame--> the trace
10
- // executed EVM transaction --executedEvmRespondOutput--> attested bytes
11
- // (both schemas, isEvmContractCall, the trace)
12
- //
13
- // Run by fakenet and the real MPC after the destination-chain transaction
14
- // confirms, and by Signet clients to independently recompute the respond
15
- // bytes the MPC attested or to display a decoded result. A client
16
- // recomputing an attestation calls executedEvmRespondOutput: the MPC decodes
17
- // and re-packs a contract call's return data, and synthesises the output
18
- // of a plain transfer or a void call from the respond schema alone.
19
- //
20
- // The respond output is UNBOUNDED: this module never pads or clamps it. Any
21
- // fixed event-field width is the caller's concern. Schemas are the ABI-style
22
- // JSON carried on chain (NUL-padded fixed-width bytes). Every function
23
- // accepts the schema in any form: already-parsed fields, a JSON string, or
24
- // the raw on-chain bytes. An EMPTY output schema (an unset all-NUL field, a
25
- // blank string, or `[]`) decodes to no values. A respond schema must be
26
- // non-empty, and its declared capacities are bounded
27
- // ({@link MAX_RESPOND_PACKED_BYTES}): schemas are requester-authored
28
- // on-chain data, so the responder never honours a schema demanding a giant
29
- // allocation.
30
- //
31
- // Decode-side type grammar is left FULLY to the ABI library (ethers): this
32
- // module checks only the schema's shape. Respond-side types are restricted
33
- // to the Compact-carrier vocabulary below, strictly enforced.
34
- //
35
- // The respond byte layout is Compact's builtin serialize<T, N> /
36
- // deserialize<T, N> (via @sig-net/midnight-serde, pinned against compiled
37
- // circuits), so a consumer contract reads the payload with ONE
38
- // deserialize<T, N> call. Per-type mapping (Compact struct field on the
39
- // right):
40
- // bool 1 byte Boolean
41
- // uint8..uint248 bits / 8 bytes LE Uint<bits>
42
- // (whole-byte widths only: multiples of 8, others are rejected)
43
- // uint256, field 32 bytes LE, below Fr Field
44
- // address 32 bytes LE (numeric) Field
45
- // bytes1..bytes32 N raw bytes Bytes<N>
46
- // string, bytes 8-byte LE length + payload zero-padded to maxBytes
47
- // struct { len: Uint<64>; data: Bytes<maxBytes>; }
48
- // T[] 8-byte LE count + maxItems elements at T's width
49
- // struct { len: Uint<64>; items: Vector<maxItems, T>; }
50
- // intN rejected: Compact has no signed integers
51
- //
52
- // RANGE TRAP: uint256, address and field all map to Compact `Field`, whose
53
- // values must lie strictly below the BLS12-381 Fr modulus (just under
54
- // 2^255). An EVM uint256 at or above Fr cannot be respond-serialised, and
55
- // `serializeRespondOutput` throws at respond time. Schema authors who need
56
- // the full 256-bit range carry the value as bytes32.
57
- import { compactSerialize, compactSerializedSize, } from "@sig-net/midnight-serde";
1
+ import { compactSerialize } from "@sig-net/midnight-serde";
58
2
  import { ethers } from "ethers";
59
3
  // ---------------------------------------------------------------------------
60
4
  // 1. EVM call result -> decoded values (outputDeserializationSchema)
@@ -64,10 +8,10 @@ import { ethers } from "ethers";
64
8
  * into named values, driven by the request's outputDeserializationSchema.
65
9
  * The decode-side counterpart of {@link serializeRespondOutput}. Type
66
10
  * validation is FULLY delegated to ethers: this function checks the schema's
67
- * shape only. An empty schema decodes to an empty object. The MPC never
68
- * decodes under an empty schema: {@link executedEvmRespondOutput} carries
69
- * its rule for that case. Returns a plain object (never an ethers
70
- * `Result`), so it survives JSON round-trips and 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.
71
15
  *
72
16
  * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
73
17
  * @param callResult - The ABI-encoded return data (hex string or bytes).
@@ -86,54 +30,204 @@ export function deserializeEvmOutput(schema, callResult) {
86
30
  return output;
87
31
  }
88
32
  // ---------------------------------------------------------------------------
89
- // 2. decoded values -> respond bytes (respondSerializationSchema)
33
+ // 2. decoded values -> respond bytes (Borsh schema derived from the output schema)
90
34
  // ---------------------------------------------------------------------------
91
35
  /**
92
- * Ceiling on a respond schema's total packed byte width. Schemas are
93
- * requester-authored on-chain data, so unbounded maxBytes/maxItems would let
94
- * a hostile request demand giant allocations from the responder. A real
95
- * respond payload must fit its consumer contract's fixed `deserialize<T, N>`
96
- * width, orders of magnitude below this.
36
+ * The ABI output types the MPC attests, and what each maps to. The respond
37
+ * bytes are the Borsh serialisation of a struct with one member per output
38
+ * field, in schema order, typed by this mapping:
39
+ *
40
+ * - {@link EvmOutputTypeKind.Bool}: ABI `bool`, Borsh `bool`, Compact `Boolean` (1 byte).
41
+ * - {@link EvmOutputTypeKind.Uint256}: ABI `uint256`, Borsh `[u8; 32]`
42
+ * holding the value LITTLE-endian, the byte order of every Borsh and
43
+ * Compact integer, Compact `Bytes<32>` (32 bytes). A circuit that wants a
44
+ * number narrows it with `checkedTruncationU128`, which aborts above
45
+ * 2^128 - 1. The ABI wire word is big-endian: the MPC reverses it once,
46
+ * here, so no circuit has to.
47
+ * - {@link EvmOutputTypeKind.Address}: ABI `address`, Borsh `[u8; 20]`
48
+ * holding the address bytes, Compact `Bytes<20>` (20 bytes).
49
+ * - {@link EvmOutputTypeKind.FixedBytes}: ABI `bytes1` to `bytes32`, Borsh
50
+ * `[u8; N]`, Compact `Bytes<N>` (N bytes).
51
+ *
52
+ * Every other ABI type is unsupported: the MPC drops a request whose output
53
+ * schema names one, and {@link deriveRespondSchema} throws on it.
97
54
  */
98
- export const MAX_RESPOND_PACKED_BYTES = 65536;
55
+ export var EvmOutputTypeKind;
56
+ (function (EvmOutputTypeKind) {
57
+ EvmOutputTypeKind["Bool"] = "bool";
58
+ EvmOutputTypeKind["Uint256"] = "uint256";
59
+ EvmOutputTypeKind["Address"] = "address";
60
+ EvmOutputTypeKind["FixedBytes"] = "bytesN";
61
+ })(EvmOutputTypeKind || (EvmOutputTypeKind = {}));
62
+ /** The byte width of an ABI word, and of the Borsh array a `uint256` output maps to. */
63
+ const ABI_WORD_BYTES = 32;
64
+ /** The byte width of an EVM address, and of the Borsh array an `address` output maps to. */
65
+ const EVM_ADDRESS_BYTES = 20;
99
66
  /**
100
- * Encode decoded output values into the respond payload, driven by the
101
- * request's respondSerializationSchema: the bytes a consumer contract reads
102
- * with `deserialize<T, N>` and the MPC attests. The respond-side counterpart
103
- * of {@link deserializeEvmOutput}.
67
+ * Classify an ABI type string into the supported output subset.
104
68
  *
105
- * The result is the PACKED value, unpadded and unbounded: its length follows
106
- * entirely from the schema, and padding to a fixed container width is the
107
- * caller's concern. Strict: values are range-checked, dynamic payloads must
108
- * fit their capacity with no silent truncation, and signed integer types are
109
- * rejected.
69
+ * @param type - The ABI type string of an output schema field.
70
+ * @returns The field's kind and respond byte width, or `undefined` for an unsupported type.
71
+ */
72
+ function classifyEvmOutputType(type) {
73
+ if (type === "bool")
74
+ return { kind: EvmOutputTypeKind.Bool, bytes: 1 };
75
+ if (type === "uint256")
76
+ return { kind: EvmOutputTypeKind.Uint256, bytes: ABI_WORD_BYTES };
77
+ if (type === "address")
78
+ return { kind: EvmOutputTypeKind.Address, bytes: EVM_ADDRESS_BYTES };
79
+ const fixedBytes = /^bytes([1-9]|[12][0-9]|3[0-2])$/.exec(type);
80
+ if (fixedBytes !== null) {
81
+ return { kind: EvmOutputTypeKind.FixedBytes, bytes: Number(fixedBytes[1]) };
82
+ }
83
+ return undefined;
84
+ }
85
+ /**
86
+ * The fields of an outputDeserializationSchema whose ABI type is outside the
87
+ * supported subset ({@link EvmOutputTypeKind}). The check itself never
88
+ * throws on an unsupported type: an empty result means the MPC attests the
89
+ * request, a non-empty one means it drops it.
110
90
  *
111
- * @param schema - The respondSerializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
112
- * @param output - Decoded values keyed by field name (from {@link deserializeEvmOutput} or any source using the same forms).
113
- * @returns The packed respond bytes.
114
- * @throws {Error} If the schema is empty or malformed, packs to more than
115
- * {@link MAX_RESPOND_PACKED_BYTES} bytes, a value falls outside its declared
116
- * range, or a dynamic payload exceeds its capacity.
91
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
92
+ * @returns The unsupported fields in schema order, empty when every field is supported.
93
+ * @throws {Error} If the schema is not an array of uniquely named fields (its shape, not its types).
117
94
  */
118
- export function serializeRespondOutput(schema, output) {
119
- const fields = normalizeRespondSchema(schema);
120
- // Size the descriptor BEFORE any value work: the ceiling check is what
121
- // keeps a hostile capacity from ever reaching an allocation.
122
- const descriptor = respondSchemaToCompactType(fields);
123
- const packedWidth = compactSerializedSize(descriptor);
124
- if (packedWidth > MAX_RESPOND_PACKED_BYTES) {
125
- throw new Error(`respond schema packs to ${String(packedWidth)} bytes, above the ` +
126
- `${String(MAX_RESPOND_PACKED_BYTES)}-byte ceiling`);
95
+ export function unsupportedEvmOutputFields(schema) {
96
+ return parseSchemaShape(schema).filter((field) => classifyEvmOutputType(field.type) === undefined);
97
+ }
98
+ /**
99
+ * Parse an output schema and classify every field, refusing the schema as a
100
+ * whole when any field is unsupported.
101
+ *
102
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
103
+ * @returns The classified fields in schema order.
104
+ * @throws {Error} If the schema is malformed or names an unsupported ABI type.
105
+ */
106
+ function classifiedEvmOutputFields(schema) {
107
+ const fields = parseSchemaShape(schema);
108
+ const unsupported = fields.filter((field) => classifyEvmOutputType(field.type) === undefined);
109
+ if (unsupported.length > 0) {
110
+ throw new Error(`respond output: unsupported ABI output type${unsupported.length > 1 ? "s" : ""} ` +
111
+ unsupported.map((field) => `'${field.name}' (${field.type})`).join(", ") +
112
+ ": the MPC attests bool, uint256, address and bytes1 to bytes32 only");
113
+ }
114
+ return fields.flatMap((field) => {
115
+ const classified = classifyEvmOutputType(field.type);
116
+ return classified === undefined ? [] : [{ ...field, ...classified }];
117
+ });
118
+ }
119
+ /**
120
+ * The Borsh schema the respond bytes of an executed call are serialised
121
+ * with: a struct with one member per output field, in schema order, each
122
+ * typed by {@link EvmOutputTypeKind}. An empty output schema derives an
123
+ * empty struct, which serialises to zero bytes.
124
+ *
125
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
126
+ * @returns The derived Borsh struct schema.
127
+ * @throws {Error} If the schema is malformed or names an unsupported ABI type.
128
+ */
129
+ export function deriveRespondSchema(schema) {
130
+ return {
131
+ struct: Object.fromEntries(classifiedEvmOutputFields(schema).map((field) => [
132
+ field.name,
133
+ field.kind === EvmOutputTypeKind.Bool
134
+ ? "bool"
135
+ : { array: { type: "u8", len: field.bytes } },
136
+ ])),
137
+ };
138
+ }
139
+ /**
140
+ * The byte width of the respond bytes an output schema derives: the sum of
141
+ * its fields' widths under {@link EvmOutputTypeKind}, what a settle circuit
142
+ * declares as its `Bytes<N>` output argument.
143
+ *
144
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
145
+ * @returns The exact serialised width, 0 for an empty schema.
146
+ * @throws {Error} If the schema is malformed or names an unsupported ABI type.
147
+ */
148
+ export function respondOutputWidth(schema) {
149
+ return classifiedEvmOutputFields(schema).reduce((width, field) => width + field.bytes, 0);
150
+ }
151
+ /**
152
+ * The 32-byte LITTLE-endian encoding of a `uint256` output value, as ethers
153
+ * decodes it (bigint) or as indexer JSON renders it (a number or a decimal
154
+ * string). This is the ABI wire word reversed: the Borsh integer byte order,
155
+ * which a Compact `Uint<128>` reads directly.
156
+ *
157
+ * @param value - The decoded value.
158
+ * @param name - The field name, for the error message.
159
+ * @returns The value's 32 little-endian bytes.
160
+ * @throws {RangeError} If the value is not an integer in `0 <= value < 2^256`.
161
+ */
162
+ function uint256Word(value, name) {
163
+ if (typeof value !== "bigint" && typeof value !== "number" && typeof value !== "string") {
164
+ throw new TypeError(`respond output: '${name}' (uint256) expects an integer`);
165
+ }
166
+ if (typeof value === "number" && !Number.isSafeInteger(value)) {
167
+ throw new RangeError(`respond output: '${name}' (uint256) is not a safe integer`);
127
168
  }
128
- const value = {};
129
- for (const field of fields) {
130
- const raw = output[field.name];
131
- if (raw === undefined) {
169
+ if (typeof value === "string" && !/^\s*\d+\s*$/.test(value)) {
170
+ throw new RangeError(`respond output: '${name}' (uint256) is not a decimal integer string`);
171
+ }
172
+ const integer = BigInt(value);
173
+ if (integer < 0n || integer >= 1n << 256n) {
174
+ throw new RangeError(`respond output: '${name}' (uint256) ${String(integer)} is outside uint256`);
175
+ }
176
+ return Array.from(ethers.getBytes(ethers.toBeHex(integer, ABI_WORD_BYTES))).reverse();
177
+ }
178
+ /**
179
+ * The bytes of an `address` or `bytesN` output value, as ethers decodes it
180
+ * (a hex string, checksummed for an address) or as bytes. Byte order is the
181
+ * wire's: these are byte strings, not numbers.
182
+ *
183
+ * @param value - The decoded value.
184
+ * @param field - The field, for the error message.
185
+ * @returns The value's bytes, length-checked by Borsh against the schema.
186
+ * @throws {TypeError} If the value is neither a hex string nor bytes.
187
+ */
188
+ function byteString(value, field) {
189
+ if (value instanceof Uint8Array)
190
+ return Array.from(value);
191
+ if (typeof value === "string")
192
+ return Array.from(ethers.getBytes(value));
193
+ throw new TypeError(`respond output: '${field.name}' (${field.type}) expects hex or bytes`);
194
+ }
195
+ /**
196
+ * Serialise ABI-decoded output into the respond bytes the MPC attests: the
197
+ * Borsh encoding of {@link deriveRespondSchema}'s struct over the output's
198
+ * values. A `uint256` is carried whole as 32 little-endian bytes, so no
199
+ * value is narrowed off chain: a circuit that wants a number narrows the
200
+ * `Bytes<32>` with `checkedTruncationU128`.
201
+ *
202
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
203
+ * @param output - The decoded values keyed by field name, as {@link deserializeEvmOutput} returns them.
204
+ * @returns The unpadded respond bytes, {@link respondOutputWidth} long.
205
+ * @throws {Error} If the schema is malformed or names an unsupported type, a
206
+ * field is missing from `output`, or a value is outside its type.
207
+ */
208
+ export function serializeRespondOutput(schema, output) {
209
+ const values = {};
210
+ for (const field of classifiedEvmOutputFields(schema)) {
211
+ const value = output[field.name];
212
+ if (value === undefined)
132
213
  throw new Error(`respond output: missing value for '${field.name}'`);
214
+ switch (field.kind) {
215
+ case EvmOutputTypeKind.Bool:
216
+ if (typeof value !== "boolean") {
217
+ throw new TypeError(`respond output: '${field.name}' (bool) expects a boolean`);
218
+ }
219
+ values[field.name] = value;
220
+ break;
221
+ case EvmOutputTypeKind.Uint256:
222
+ values[field.name] = uint256Word(value, field.name);
223
+ break;
224
+ case EvmOutputTypeKind.Address:
225
+ case EvmOutputTypeKind.FixedBytes:
226
+ values[field.name] = byteString(value, field);
227
+ break;
133
228
  }
134
- value[field.name] = toCompactValue(raw, field);
135
229
  }
136
- return compactSerialize(descriptor, value);
230
+ return compactSerialize(deriveRespondSchema(schema), values);
137
231
  }
138
232
  // ---------------------------------------------------------------------------
139
233
  // 3. executed EVM transaction -> attested respond bytes
@@ -170,89 +264,62 @@ export var EvmTraceOutputKind;
170
264
  /** The top call frame carries no string `output` field. */
171
265
  EvmTraceOutputKind["NoReturnData"] = "NoReturnData";
172
266
  })(EvmTraceOutputKind || (EvmTraceOutputKind = {}));
173
- /** The string default the MPC synthesises for an execution without call output. */
174
- const NON_FUNCTION_CALL_SUCCESS = "non_function_call_success";
175
- /**
176
- * The per-field values the MPC synthesises for an execution without call
177
- * output (its `default_value_for_kind`): `true` for a bool field,
178
- * {@link NON_FUNCTION_CALL_SUCCESS} for a string field.
179
- *
180
- * @param respondSchema - The respondSerializationSchema.
181
- * @returns The synthesised values keyed by field name.
182
- * @throws {Error} If the schema is one {@link serializeRespondOutput}
183
- * rejects, or a field is of any other kind.
184
- */
185
- function nonFunctionCallDefaults(respondSchema) {
186
- const output = {};
187
- for (const field of normalizeRespondSchema(respondSchema)) {
188
- if (field.type === "bool") {
189
- output[field.name] = true;
190
- }
191
- else if (field.type === "string") {
192
- output[field.name] = NON_FUNCTION_CALL_SUCCESS;
193
- }
194
- else {
195
- throw new Error(`respond output: '${field.name}' (${field.type}) has no non-function-call ` +
196
- "default: only bool and string fields have one");
197
- }
198
- }
199
- return output;
200
- }
201
267
  /**
202
- * The exact serialised respond output the MPC attests for an EVM transaction
203
- * that executed (the payload of an `OutputKind.executed` attestation),
204
- * mirroring the MPC's `build_serialized_output` under the Midnight respond
205
- * format:
268
+ * The exact respond output the MPC attests for an EVM transaction that
269
+ * executed (the payload of an `OutputKind.executed` attestation), derived
270
+ * from the request's outputDeserializationSchema alone. The schema and the
271
+ * execution's return data MUST agree: an empty schema declares that the
272
+ * execution returns nothing, a non-empty schema that it returns data.
206
273
  *
207
- * - Not a contract call: the output schema and the trace are ignored, and
208
- * the respond fields get synthesised defaults (bool `true`, string
209
- * `"non_function_call_success"` in its `maxBytes` buffer, any other kind
210
- * throws).
211
- * - A contract call under an EMPTY output schema (a void call): the same
212
- * defaults when the trace has no or empty return data, a throw when it
213
- * returned data.
214
- * - A contract call under a non-empty output schema: the return data
215
- * decoded by {@link deserializeEvmOutput} and packed by
216
- * {@link serializeRespondOutput}, a throw when the trace has no return
217
- * data.
274
+ * - Not a contract call (a plain transfer): there is no return data, so the
275
+ * schema must be empty and the output is EMPTY.
276
+ * - A contract call whose trace carries no return data, or empty return
277
+ * data (`0x`): the schema must be empty and the output is EMPTY.
278
+ * - A contract call that returned data: the schema must be non-empty, and
279
+ * the output is the return data decoded by {@link deserializeEvmOutput}
280
+ * and serialised by {@link serializeRespondOutput}.
218
281
  *
219
- * A contract call that was not traced always throws. A failed or unviable
220
- * execution is attested over an empty output instead, which needs no call.
282
+ * The schema's types are checked on every path, a plain transfer included,
283
+ * so a request the MPC should have dropped never attests anything. A failed
284
+ * or unviable execution is attested over an empty output instead, which
285
+ * needs no call. An empty output under `executed` is therefore ordinary for
286
+ * an empty schema: its settle circuit verifies at width 0 and must route on
287
+ * the verified `outputKind`, never on the width.
221
288
  *
222
- * @param schemas - The request's two schemas (a request record satisfies this).
289
+ * @param schema - The request's outputDeserializationSchema (a request record's field satisfies this).
223
290
  * @param isContractCall - Whether the transaction is a contract call, as {@link isEvmContractCall} decides it.
224
291
  * @param trace - The transaction's traced return data (`NotTraced` for an untraced plain transfer).
225
- * @returns The packed respond bytes, unpadded.
292
+ * @returns The respond bytes, unpadded, empty for an empty schema.
226
293
  * @throws {Error} Exactly where the MPC refuses to attest an execution: a
227
- * contract call not traced, an empty output schema with return data, a
228
- * non-empty output schema with no or undecodable return data, a malformed
229
- * output schema on a contract call, a respond field without a synthesised
230
- * default, and every {@link serializeRespondOutput} rejection.
294
+ * malformed schema or an unsupported output type, a contract call not
295
+ * traced, return data under an empty schema, no or empty return data
296
+ * under a non-empty schema, return data the schema cannot decode, and
297
+ * every {@link serializeRespondOutput} rejection.
231
298
  */
232
- export function executedEvmRespondOutput(schemas, isContractCall, trace) {
233
- const { outputDeserializationSchema, respondSerializationSchema } = schemas;
299
+ export function executedEvmRespondOutput(schema, isContractCall, trace) {
300
+ const expectsOutput = classifiedEvmOutputFields(schema).length > 0;
234
301
  if (!isContractCall) {
235
- return serializeRespondOutput(respondSerializationSchema, nonFunctionCallDefaults(respondSerializationSchema));
302
+ if (expectsOutput) {
303
+ throw new Error("respond output: a plain transfer returns nothing, but the output schema declares return values");
304
+ }
305
+ return new Uint8Array(0);
236
306
  }
237
- const expectsNoOutput = parseSchemaShape(outputDeserializationSchema).length === 0;
238
- switch (trace.kind) {
239
- case EvmTraceOutputKind.NotTraced:
240
- throw new Error("respond output: a contract call's output needs its trace");
241
- case EvmTraceOutputKind.NoReturnData:
242
- if (!expectsNoOutput) {
243
- throw new Error("respond output: the contract call's trace has no return data for a non-empty output schema");
244
- }
245
- break;
246
- case EvmTraceOutputKind.Output:
247
- if (!expectsNoOutput) {
248
- return serializeRespondOutput(respondSerializationSchema, deserializeEvmOutput(outputDeserializationSchema, trace.returnData));
249
- }
250
- if (ethers.getBytes(trace.returnData).length > 0) {
251
- throw new Error("respond output: the contract call returned data but its output schema declares no return values");
252
- }
253
- break;
307
+ if (trace.kind === EvmTraceOutputKind.NotTraced) {
308
+ throw new Error("respond output: a contract call's output needs its trace");
254
309
  }
255
- return serializeRespondOutput(respondSerializationSchema, nonFunctionCallDefaults(respondSerializationSchema));
310
+ const returnData = trace.kind === EvmTraceOutputKind.Output
311
+ ? ethers.getBytes(trace.returnData)
312
+ : new Uint8Array(0);
313
+ if (returnData.length === 0) {
314
+ if (expectsOutput) {
315
+ throw new Error("respond output: the contract call returned no data, but the output schema declares return values");
316
+ }
317
+ return new Uint8Array(0);
318
+ }
319
+ if (!expectsOutput) {
320
+ throw new Error("respond output: the contract call returned data, but the output schema declares no return values");
321
+ }
322
+ return serializeRespondOutput(schema, deserializeEvmOutput(schema, returnData));
256
323
  }
257
324
  /**
258
325
  * Whether a JSON value is an object. A type guard, as `Array.isArray` does
@@ -321,26 +388,6 @@ export function evmTraceOutputFromCallFrame(frame) {
321
388
  // Helpers from here down. The exports above are the whole public surface:
322
389
  // everything below serves them.
323
390
  // ===========================================================================
324
- // ---------------------------------------------------------------------------
325
- // Field-kind guards
326
- // ---------------------------------------------------------------------------
327
- function isAbiDynamicField(field) {
328
- return field.type === "string" || field.type === "bytes";
329
- }
330
- function isAbiArrayField(field) {
331
- return field.type.endsWith("[]");
332
- }
333
- // ---------------------------------------------------------------------------
334
- // Decode-side value flattening
335
- // ---------------------------------------------------------------------------
336
- /**
337
- * Flatten ethers `Result` arrays into plain arrays, pass scalars through.
338
- *
339
- * @param value - A decoded ABI value, possibly a nested `Result`.
340
- * @param label - Field path, used in error messages.
341
- * @returns The value with every `Result` replaced by a plain array.
342
- * @throws {Error} If the value is of a kind the respond side cannot carry.
343
- */
344
391
  function toPlainValue(value, label) {
345
392
  if (value instanceof ethers.Result) {
346
393
  return value.toArray().map((v, i) => toPlainValue(v, `${label}[${String(i)}]`));
@@ -360,10 +407,6 @@ function toPlainValue(value, label) {
360
407
  // ---------------------------------------------------------------------------
361
408
  // Schema parsing + validation
362
409
  // ---------------------------------------------------------------------------
363
- const ADDRESS_BOUND = 1n << 160n;
364
- const MAX_UINT_BITS = 248;
365
- /** Length-prefix width of the dynamic string/bytes/array convention (Uint<64>). */
366
- const DYN_LEN_BYTES = 8;
367
410
  /**
368
411
  * Parse schema text as JSON with the parse failure named. Blank text (an
369
412
  * unset all-NUL on-chain schema field) is the empty schema.
@@ -386,10 +429,8 @@ function parseSchemaJson(text) {
386
429
  /**
387
430
  * Parse a schema in any input form and check its SHAPE only: an array of
388
431
  * fields with non-empty, unique names and non-empty type strings. An empty
389
- * schema is valid here: emptiness policy belongs to the callers
390
- * ({@link deserializeEvmOutput} decodes nothing, {@link serializeRespondOutput}
391
- * rejects, {@link executedEvmRespondOutput} synthesises defaults for a
392
- * contract call without return data).
432
+ * schema is valid here: {@link deserializeEvmOutput} decodes nothing from
433
+ * it and {@link executedEvmRespondOutput} attests an empty output.
393
434
  *
394
435
  * @param schema - The schema as JSON text, packed bytes, or a field array.
395
436
  * @returns The schema's fields, names and type strings unvalidated beyond shape.
@@ -407,7 +448,7 @@ function parseSchemaShape(schema) {
407
448
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
408
449
  throw new Error(`schema field ${String(i)} is not an object`);
409
450
  }
410
- const { name, type, maxBytes, maxItems } = raw;
451
+ const { name, type } = raw;
411
452
  if (typeof name !== "string" || name.length === 0) {
412
453
  throw new Error(`schema field ${String(i)} needs a non-empty name`);
413
454
  }
@@ -418,36 +459,7 @@ function parseSchemaShape(schema) {
418
459
  if (typeof type !== "string" || type.length === 0) {
419
460
  throw new Error(`schema: '${name}' needs a type`);
420
461
  }
421
- return { name, type, maxBytes, maxItems };
422
- });
423
- }
424
- /**
425
- * Enforce the respond-side Compact-carrier vocabulary on a shape-checked
426
- * schema: every type needs a Compact carrier (no signed ints, uint widths of
427
- * at most 248 bits or exactly 256, bytesN at most 32) and every dynamic field
428
- * needs its fixed capacity.
429
- *
430
- * @param schema - The schema to normalize.
431
- * @returns The schema with every field proven to have a Compact carrier.
432
- * @throws {Error} If a type has no carrier or a dynamic field omits its capacity.
433
- */
434
- function normalizeRespondSchema(schema) {
435
- const fields = parseSchemaShape(schema);
436
- if (fields.length === 0) {
437
- throw new Error("respond schema is empty: a respond serialization schema needs at least one field");
438
- }
439
- return fields.map(({ name, type, maxBytes, maxItems }) => {
440
- if (type === "string" || type === "bytes") {
441
- assertCapacity(maxBytes, `'${name}' (${type}) maxBytes`);
442
- return { name, type, maxBytes };
443
- }
444
- if (type.endsWith("[]")) {
445
- classifyFixedType(type.slice(0, -2), name);
446
- assertCapacity(maxItems, `'${name}' (${type}) maxItems`);
447
- return { name, type: type, maxItems };
448
- }
449
- classifyFixedType(type, name);
450
- return { name, type: type };
462
+ return { name, type };
451
463
  });
452
464
  }
453
465
  /**
@@ -461,193 +473,4 @@ function schemaText(schema) {
461
473
  const nul = raw.indexOf("\0");
462
474
  return nul === -1 ? raw : raw.slice(0, nul);
463
475
  }
464
- function assertCapacity(value, label) {
465
- if (value === undefined) {
466
- throw new Error(`schema: ${label} is required: Compact types are fixed-size`);
467
- }
468
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
469
- throw new Error(`schema: ${label} must be a positive integer`);
470
- }
471
- }
472
- /**
473
- * Classify a fixed-width respond type into its Compact carrier, validating
474
- * the respond-side vocabulary in one place: both the schema check
475
- * ({@link normalizeRespondSchema}) and the descriptor build
476
- * ({@link respondSchemaToCompactType}) call this, so the grammar cannot
477
- * drift between them. Respond-side uint widths are restricted to whole-byte
478
- * widths (multiples of 8 from 8 to 248, packing to bits / 8 bytes), plus
479
- * uint256 which maps to Field. Throws with the offending field named.
480
- *
481
- * @param type - The fixed-width ABI type name.
482
- * @param fieldName - The field the type belongs to, used in error messages.
483
- * @returns The Compact descriptor that carries the type.
484
- * @throws {Error} If the type has no respond-side Compact carrier.
485
- */
486
- function classifyFixedType(type, fieldName) {
487
- if (type === "bool")
488
- return { kind: "boolean" };
489
- if (type === "field" || type === "uint256" || type === "address") {
490
- return { kind: "field" };
491
- }
492
- if (/^int\d+$/.test(type)) {
493
- throw new Error(`schema: '${fieldName}' (${type}) is unsupported: Compact has no signed integers`);
494
- }
495
- const uintMatch = /^uint([1-9]\d*)$/.exec(type);
496
- if (uintMatch) {
497
- const bits = Number(uintMatch[1]);
498
- const wholeByteWidth = bits >= 8 && bits <= MAX_UINT_BITS && bits % 8 === 0;
499
- if (!wholeByteWidth) {
500
- throw new Error(`schema: '${fieldName}' (${type}) has no respond carrier: uint widths ` +
501
- `must be multiples of 8 from 8 to ${String(MAX_UINT_BITS)}, or uint256 (maps to Field)`);
502
- }
503
- return { kind: "uint", bits };
504
- }
505
- const bytesMatch = /^bytes([1-9]\d*)$/.exec(type);
506
- if (bytesMatch) {
507
- const n = Number(bytesMatch[1]);
508
- if (n < 1 || n > 32) {
509
- throw new Error(`schema: '${fieldName}' (${type}) is not a valid bytesN type`);
510
- }
511
- return { kind: "bytes", length: n };
512
- }
513
- throw new Error(`schema: '${fieldName}' has unsupported type '${type}'`);
514
- }
515
- // ---------------------------------------------------------------------------
516
- // Schema -> CompactType descriptor + value coercion
517
- // ---------------------------------------------------------------------------
518
- function respondSchemaToCompactType(fields) {
519
- return {
520
- kind: "struct",
521
- fields: fields.map((field) => {
522
- if (isAbiDynamicField(field)) {
523
- return {
524
- name: field.name,
525
- type: {
526
- kind: "struct",
527
- fields: [
528
- { name: "len", type: { kind: "uint", bits: DYN_LEN_BYTES * 8 } },
529
- { name: "data", type: { kind: "bytes", length: field.maxBytes } },
530
- ],
531
- },
532
- };
533
- }
534
- if (isAbiArrayField(field)) {
535
- return {
536
- name: field.name,
537
- type: {
538
- kind: "struct",
539
- fields: [
540
- { name: "len", type: { kind: "uint", bits: DYN_LEN_BYTES * 8 } },
541
- {
542
- name: "items",
543
- type: {
544
- kind: "vector",
545
- length: field.maxItems,
546
- element: classifyFixedType(field.type.slice(0, -2), field.name),
547
- },
548
- },
549
- ],
550
- },
551
- };
552
- }
553
- return { name: field.name, type: classifyFixedType(field.type, field.name) };
554
- }),
555
- };
556
- }
557
- function toCompactValue(value, field) {
558
- const { name } = field;
559
- if (isAbiDynamicField(field)) {
560
- const payload = field.type === "string"
561
- ? new TextEncoder().encode(asString(value, name))
562
- : asBytes(value, name);
563
- if (payload.length > field.maxBytes) {
564
- throw new Error(`'${name}': payload is ${String(payload.length)} bytes, maxBytes is ${String(field.maxBytes)}`);
565
- }
566
- const data = new Uint8Array(field.maxBytes);
567
- data.set(payload);
568
- return { len: BigInt(payload.length), data };
569
- }
570
- if (isAbiArrayField(field)) {
571
- if (!Array.isArray(value)) {
572
- throw new Error(`'${name}' (${field.type}) expects an array`);
573
- }
574
- if (value.length > field.maxItems) {
575
- throw new Error(`'${name}': ${String(value.length)} elements, maxItems is ${String(field.maxItems)}`);
576
- }
577
- const elementType = field.type.slice(0, -2);
578
- const items = value.map((element, i) => fixedCompactValue(element, elementType, `${name}[${String(i)}]`));
579
- // Unused capacity encodes as zero values of the element type.
580
- while (items.length < field.maxItems)
581
- items.push(zeroOf(elementType));
582
- return { len: BigInt(value.length), items };
583
- }
584
- return fixedCompactValue(value, field.type, name);
585
- }
586
- function fixedCompactValue(value, type, label) {
587
- if (type === "bool") {
588
- if (typeof value !== "boolean") {
589
- throw new Error(`'${label}' (bool) expects a boolean`);
590
- }
591
- return value;
592
- }
593
- if (/^bytes\d+$/.test(type)) {
594
- const raw = asBytes(value, label);
595
- const expected = Number(type.slice(5));
596
- if (raw.length !== expected) {
597
- throw new Error(`'${label}' (${type}) expects exactly ${String(expected)} bytes, got ${String(raw.length)}`);
598
- }
599
- return raw;
600
- }
601
- // Numeric carriers: uintN, uint256/field and address.
602
- const n = asBigint(value, label);
603
- if (type === "address" && n >= ADDRESS_BOUND) {
604
- throw new Error(`'${label}': value ${String(n)} exceeds an address`);
605
- }
606
- // Uint width and Field modulus bounds are enforced by @sig-net/midnight-serde.
607
- return n;
608
- }
609
- function zeroOf(type) {
610
- if (type === "bool")
611
- return false;
612
- if (/^bytes\d+$/.test(type))
613
- return new Uint8Array(Number(type.slice(5)));
614
- return 0n;
615
- }
616
- // ---------------------------------------------------------------------------
617
- // Value-form coercions
618
- // ---------------------------------------------------------------------------
619
- function asBigint(value, label) {
620
- if (typeof value === "bigint")
621
- return value;
622
- if (typeof value === "number" && Number.isSafeInteger(value))
623
- return BigInt(value);
624
- if (typeof value === "string") {
625
- try {
626
- return BigInt(value);
627
- }
628
- catch {
629
- throw new Error(`'${label}': cannot parse '${value}' as an integer`);
630
- }
631
- }
632
- throw new Error(`'${label}': expected an integer-like value`);
633
- }
634
- function asBytes(value, label) {
635
- if (value instanceof Uint8Array)
636
- return value;
637
- if (typeof value === "string") {
638
- try {
639
- return ethers.getBytes(value);
640
- }
641
- catch {
642
- throw new Error(`'${label}': not a valid 0x hex byte string: "${value}"`);
643
- }
644
- }
645
- throw new Error(`'${label}': expected bytes (Uint8Array or hex string)`);
646
- }
647
- function asString(value, label) {
648
- if (typeof value !== "string") {
649
- throw new Error(`'${label}': expected a string`);
650
- }
651
- return value;
652
- }
653
476
  //# sourceMappingURL=abi-serde.js.map