@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.
- package/README.md +52 -24
- package/dist/abi-serde.d.ts +198 -57
- package/dist/abi-serde.d.ts.map +1 -1
- package/dist/abi-serde.js +478 -367
- package/dist/abi-serde.js.map +1 -1
- package/dist/compact-descriptors.d.ts +25 -8
- package/dist/compact-descriptors.d.ts.map +1 -1
- package/dist/compact-descriptors.js +42 -25
- package/dist/compact-descriptors.js.map +1 -1
- package/dist/constants.d.ts +3 -22
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -25
- package/dist/constants.js.map +1 -1
- package/dist/ecdsa-attestation.d.ts +44 -14
- package/dist/ecdsa-attestation.d.ts.map +1 -1
- package/dist/ecdsa-attestation.js +50 -15
- package/dist/ecdsa-attestation.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/managed/compiler/contract-info.json +1808 -48
- package/dist/managed/compiler/contract-manifest.json +8 -8
- package/dist/managed/contract/index.d.ts +424 -5
- package/dist/managed/contract/index.js +1311 -171
- package/dist/managed/contract/index.js.map +1 -1
- package/dist/mpc-output-cache.js +2 -2
- package/dist/mpc-output-cache.js.map +1 -1
- package/dist/signature-requests-state-reader.d.ts +6 -13
- package/dist/signature-requests-state-reader.d.ts.map +1 -1
- package/dist/signature-requests-state-reader.js +10 -16
- package/dist/signature-requests-state-reader.js.map +1 -1
- package/dist/signet-contract-events.d.ts +52 -12
- package/dist/signet-contract-events.d.ts.map +1 -1
- package/dist/signet-contract-events.js +89 -34
- package/dist/signet-contract-events.js.map +1 -1
- package/dist/signet-evtype2tx-record-decoding.d.ts.map +1 -1
- package/dist/signet-evtype2tx-record-decoding.js +7 -6
- package/dist/signet-evtype2tx-record-decoding.js.map +1 -1
- package/dist/signet-evtype2tx-requests.d.ts +17 -12
- package/dist/signet-evtype2tx-requests.d.ts.map +1 -1
- package/dist/signet-evtype2tx-requests.js +92 -16
- package/dist/signet-evtype2tx-requests.js.map +1 -1
- package/dist/signet-request-id.d.ts +5 -2
- package/dist/signet-request-id.d.ts.map +1 -1
- package/dist/signet-request-id.js +27 -9
- package/dist/signet-request-id.js.map +1 -1
- package/dist/signet-request-response-reader.d.ts +8 -0
- package/dist/signet-request-response-reader.d.ts.map +1 -1
- package/dist/signet-request-response-reader.js +8 -7
- package/dist/signet-request-response-reader.js.map +1 -1
- package/dist/signet-requests.d.ts +48 -19
- package/dist/signet-requests.d.ts.map +1 -1
- package/dist/signet-requests.js +19 -8
- package/dist/signet-requests.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -8
- package/dist/testing.js.map +1 -1
- package/package.json +2 -2
- package/src/Signet.compact +299 -101
- package/src/circuits.compact +92 -12
package/dist/abi-serde.js
CHANGED
|
@@ -1,54 +1,4 @@
|
|
|
1
|
-
|
|
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
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 (
|
|
32
|
+
// 2. decoded values -> respond bytes (Borsh schema derived from the output schema)
|
|
84
33
|
// ---------------------------------------------------------------------------
|
|
85
34
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
|
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
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
106
|
-
* @
|
|
107
|
-
* @
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
//
|
|
233
|
+
// 3. executed EVM transaction -> attested respond bytes
|
|
138
234
|
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
*
|
|
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
|
|
152
|
-
* @
|
|
153
|
-
* @
|
|
154
|
-
*
|
|
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
|
-
*
|
|
181
|
-
*
|
|
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
|
|
184
|
-
* @returns The
|
|
185
|
-
* @throws {Error} If non-
|
|
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
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
|
|
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,
|
|
206
|
-
* @returns The schema's fields,
|
|
207
|
-
* @throws {Error} If the
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
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
|
-
|
|
567
|
+
ethers.ParamType.from(type);
|
|
438
568
|
}
|
|
439
|
-
catch {
|
|
440
|
-
throw new Error(`'${
|
|
569
|
+
catch (error) {
|
|
570
|
+
throw new Error(`schema: '${name}' has an invalid ABI type '${type}'`, { cause: error });
|
|
441
571
|
}
|
|
442
|
-
|
|
443
|
-
|
|
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
|