@qubic.ts/contracts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/package.json +17 -0
  2. package/scripts/generate-artifacts.ts +178 -0
  3. package/src/codec/entry-input.test.ts +198 -0
  4. package/src/codec/entry-input.ts +959 -0
  5. package/src/codec/index.ts +8 -0
  6. package/src/generated/contracts/CCF.registry.json +741 -0
  7. package/src/generated/contracts/CCF.types.ts +307 -0
  8. package/src/generated/contracts/GQMPROP.registry.json +518 -0
  9. package/src/generated/contracts/GQMPROP.types.ts +238 -0
  10. package/src/generated/contracts/MLM.registry.json +8 -0
  11. package/src/generated/contracts/MLM.types.ts +42 -0
  12. package/src/generated/contracts/MSVAULT.registry.json +1162 -0
  13. package/src/generated/contracts/MSVAULT.types.ts +598 -0
  14. package/src/generated/contracts/NOST.registry.json +1131 -0
  15. package/src/generated/contracts/NOST.types.ts +515 -0
  16. package/src/generated/contracts/QBAY.registry.json +1492 -0
  17. package/src/generated/contracts/QBAY.types.ts +681 -0
  18. package/src/generated/contracts/QBOND.registry.json +734 -0
  19. package/src/generated/contracts/QBOND.types.ts +397 -0
  20. package/src/generated/contracts/QDRAW.registry.json +112 -0
  21. package/src/generated/contracts/QDRAW.types.ts +110 -0
  22. package/src/generated/contracts/QDUEL.registry.json +466 -0
  23. package/src/generated/contracts/QDUEL.types.ts +265 -0
  24. package/src/generated/contracts/QEARN.registry.json +458 -0
  25. package/src/generated/contracts/QEARN.types.ts +265 -0
  26. package/src/generated/contracts/QIP.registry.json +483 -0
  27. package/src/generated/contracts/QIP.types.ts +194 -0
  28. package/src/generated/contracts/QRAFFLE.registry.json +916 -0
  29. package/src/generated/contracts/QRAFFLE.types.ts +446 -0
  30. package/src/generated/contracts/QRP.registry.json +139 -0
  31. package/src/generated/contracts/QRP.types.ts +144 -0
  32. package/src/generated/contracts/QRWA.registry.json +765 -0
  33. package/src/generated/contracts/QRWA.types.ts +402 -0
  34. package/src/generated/contracts/QSWAP.registry.json +941 -0
  35. package/src/generated/contracts/QSWAP.types.ts +479 -0
  36. package/src/generated/contracts/QTF.registry.json +480 -0
  37. package/src/generated/contracts/QTF.types.ts +346 -0
  38. package/src/generated/contracts/QUOTTERY.registry.json +530 -0
  39. package/src/generated/contracts/QUOTTERY.types.ts +262 -0
  40. package/src/generated/contracts/QUTIL.registry.json +1378 -0
  41. package/src/generated/contracts/QUTIL.types.ts +612 -0
  42. package/src/generated/contracts/QVAULT.registry.json +527 -0
  43. package/src/generated/contracts/QVAULT.types.ts +309 -0
  44. package/src/generated/contracts/QX.registry.json +610 -0
  45. package/src/generated/contracts/QX.types.ts +323 -0
  46. package/src/generated/contracts/RANDOM.registry.json +51 -0
  47. package/src/generated/contracts/RANDOM.types.ts +65 -0
  48. package/src/generated/contracts/RL.registry.json +490 -0
  49. package/src/generated/contracts/RL.types.ts +304 -0
  50. package/src/generated/contracts/SWATCH.registry.json +8 -0
  51. package/src/generated/contracts/SWATCH.types.ts +42 -0
  52. package/src/generated/core-registry.codecs.ts +6622 -0
  53. package/src/generated/core-registry.source.json +14342 -0
  54. package/src/generated/core-registry.ts +14349 -0
  55. package/src/generated/core-registry.types.ts +100 -0
  56. package/src/generator/contract-codecs.fixture.test.ts +17 -0
  57. package/src/generator/contract-codecs.test.ts +115 -0
  58. package/src/generator/contract-codecs.ts +416 -0
  59. package/src/generator/index.ts +14 -0
  60. package/src/generator/per-contract-files.test.ts +70 -0
  61. package/src/generator/per-contract-files.ts +122 -0
  62. package/src/generator/registry-runtime.fixture.test.ts +17 -0
  63. package/src/generator/registry-runtime.test.ts +55 -0
  64. package/src/generator/registry-runtime.ts +28 -0
  65. package/src/generator/registry-types.fixture.test.ts +17 -0
  66. package/src/generator/registry-types.test.ts +55 -0
  67. package/src/generator/registry-types.ts +75 -0
  68. package/src/index.test.ts +29 -0
  69. package/src/index.ts +49 -0
  70. package/src/registry/index.ts +17 -0
  71. package/src/registry/io-layout.fixture.test.ts +24 -0
  72. package/src/registry/io-layout.test.ts +93 -0
  73. package/src/registry/io-layout.ts +57 -0
  74. package/src/registry/normalize.ts +61 -0
  75. package/src/registry/schema.fixture.test.ts +21 -0
  76. package/src/registry/schema.ts +97 -0
  77. package/src/registry/types.ts +98 -0
  78. package/test/fixtures/io-layout.contracts.json +32 -0
  79. package/test/fixtures/io-layout.layouts.json +14 -0
  80. package/test/fixtures/registry.sample.codecs.ts +100 -0
  81. package/test/fixtures/registry.sample.json +27 -0
  82. package/test/fixtures/registry.sample.runtime.ts +54 -0
  83. package/test/fixtures/registry.sample.types.ts +16 -0
@@ -0,0 +1,959 @@
1
+ import type {
2
+ ContractDefinition,
3
+ ContractEntry,
4
+ ContractEntryKind,
5
+ ContractIoTypeDefinition,
6
+ ContractsRegistry,
7
+ } from "../registry/types.js";
8
+
9
+ type ContractsInput = ContractsRegistry | readonly ContractDefinition[];
10
+
11
+ export type EncodeContractEntryInputOptions = Readonly<{
12
+ registry: ContractsInput;
13
+ contractName: string;
14
+ entryName: string;
15
+ kind?: ContractEntryKind;
16
+ value: unknown;
17
+ allowSequentialLayout?: boolean;
18
+ }>;
19
+
20
+ export type DecodeContractEntryInputOptions = Readonly<{
21
+ registry: ContractsInput;
22
+ contractName: string;
23
+ entryName: string;
24
+ kind?: ContractEntryKind;
25
+ bytes: Uint8Array;
26
+ allowSequentialLayout?: boolean;
27
+ }>;
28
+
29
+ export type EncodedContractEntryInput = Readonly<{
30
+ contractName: string;
31
+ entryName: string;
32
+ kind: ContractEntryKind;
33
+ inputType: number;
34
+ inputTypeName: string;
35
+ bytes: Uint8Array;
36
+ }>;
37
+
38
+ export type DecodedContractEntryInput<T = unknown> = Readonly<{
39
+ contractName: string;
40
+ entryName: string;
41
+ kind: ContractEntryKind;
42
+ inputType: number;
43
+ inputTypeName: string;
44
+ value: T;
45
+ }>;
46
+
47
+ type CodecContext = Readonly<{
48
+ ioTypeByName: ReadonlyMap<string, ContractIoTypeDefinition>;
49
+ allowSequentialLayout: boolean;
50
+ }>;
51
+
52
+ const MAX_SAFE_U64 = 18_446_744_073_709_551_615n;
53
+ const MAX_SAFE_U128 = (1n << 128n) - 1n;
54
+ const MIN_SAFE_S64 = -(1n << 63n);
55
+ const MAX_SAFE_S64 = (1n << 63n) - 1n;
56
+
57
+ export function encodeContractEntryInputData(
58
+ options: EncodeContractEntryInputOptions,
59
+ ): EncodedContractEntryInput {
60
+ const contract = resolveContract(options.registry, options.contractName);
61
+ const entry = resolveEntry(contract, options.entryName, options.kind);
62
+ const inputTypeName = resolveEntryInputTypeName(entry);
63
+ const context = createCodecContext(contract, options.allowSequentialLayout ?? false);
64
+
65
+ const ioType = context.ioTypeByName.get(inputTypeName);
66
+ const encoded = ioType
67
+ ? encodeByTypeDefinition(ioType, options.value, context, [inputTypeName])
68
+ : encodeWithoutTypeDefinition(entry, inputTypeName, options.value);
69
+
70
+ return {
71
+ contractName: contract.name,
72
+ entryName: entry.name,
73
+ kind: entry.kind,
74
+ inputType: entry.inputType,
75
+ inputTypeName,
76
+ bytes: encoded,
77
+ };
78
+ }
79
+
80
+ export function decodeContractEntryInputData<T = unknown>(
81
+ options: DecodeContractEntryInputOptions,
82
+ ): DecodedContractEntryInput<T> {
83
+ const contract = resolveContract(options.registry, options.contractName);
84
+ const entry = resolveEntry(contract, options.entryName, options.kind);
85
+ const inputTypeName = resolveEntryInputTypeName(entry);
86
+ const context = createCodecContext(contract, options.allowSequentialLayout ?? false);
87
+ const ioType = context.ioTypeByName.get(inputTypeName);
88
+
89
+ if (!ioType) {
90
+ return {
91
+ contractName: contract.name,
92
+ entryName: entry.name,
93
+ kind: entry.kind,
94
+ inputType: entry.inputType,
95
+ inputTypeName,
96
+ value: options.bytes.slice() as T,
97
+ };
98
+ }
99
+
100
+ const decoded = decodeByTypeDefinition(ioType, options.bytes, context, [inputTypeName]);
101
+ if (decoded.byteLength !== options.bytes.length) {
102
+ throw new Error(
103
+ `Input bytes contain trailing data for ${contract.name}.${entry.name}: consumed ${decoded.byteLength} of ${options.bytes.length}`,
104
+ );
105
+ }
106
+
107
+ return {
108
+ contractName: contract.name,
109
+ entryName: entry.name,
110
+ kind: entry.kind,
111
+ inputType: entry.inputType,
112
+ inputTypeName,
113
+ value: decoded.value as T,
114
+ };
115
+ }
116
+
117
+ function resolveContract(registry: ContractsInput, contractName: string): ContractDefinition {
118
+ const contracts = isContractsRegistry(registry) ? registry.contracts : registry;
119
+ const contract = contracts.find((item) => item.name === contractName);
120
+ if (!contract) {
121
+ throw new Error(`Unknown contract: ${contractName}`);
122
+ }
123
+ return contract;
124
+ }
125
+
126
+ function isContractsRegistry(value: ContractsInput): value is ContractsRegistry {
127
+ return !Array.isArray(value);
128
+ }
129
+
130
+ function resolveEntry(
131
+ contract: ContractDefinition,
132
+ entryName: string,
133
+ kind?: ContractEntryKind,
134
+ ): ContractEntry {
135
+ const matches = contract.entries.filter(
136
+ (entry) => entry.name === entryName && (kind ? entry.kind === kind : true),
137
+ );
138
+ if (matches.length === 0) {
139
+ throw new Error(`Unknown contract entry: ${contract.name}.${entryName}`);
140
+ }
141
+ if (matches.length > 1) {
142
+ throw new Error(
143
+ `Ambiguous contract entry: ${contract.name}.${entryName}. Specify kind ("function" | "procedure").`,
144
+ );
145
+ }
146
+ return matches[0] as ContractEntry;
147
+ }
148
+
149
+ function resolveEntryInputTypeName(entry: ContractEntry): string {
150
+ return entry.inputTypeName ?? `${entry.name}_input`;
151
+ }
152
+
153
+ function createCodecContext(
154
+ contract: ContractDefinition,
155
+ allowSequentialLayout: boolean,
156
+ ): CodecContext {
157
+ const ioTypeByName = new Map((contract.ioTypes ?? []).map((typeDef) => [typeDef.name, typeDef]));
158
+ return {
159
+ ioTypeByName,
160
+ allowSequentialLayout,
161
+ };
162
+ }
163
+
164
+ function encodeWithoutTypeDefinition(
165
+ entry: ContractEntry,
166
+ inputTypeName: string,
167
+ value: unknown,
168
+ ): Uint8Array {
169
+ if (value instanceof Uint8Array) {
170
+ if (entry.inputSize !== undefined && value.length !== entry.inputSize) {
171
+ throw new Error(
172
+ `Input byte size mismatch for ${entry.name}: expected ${entry.inputSize}, received ${value.length}`,
173
+ );
174
+ }
175
+ return value.slice();
176
+ }
177
+
178
+ throw new Error(
179
+ `No io type definition found for ${inputTypeName}. Provide raw Uint8Array input bytes or regenerate with --io-extractor castxml.`,
180
+ );
181
+ }
182
+
183
+ function encodeByTypeDefinition(
184
+ typeDef: ContractIoTypeDefinition,
185
+ value: unknown,
186
+ context: CodecContext,
187
+ path: readonly string[],
188
+ ): Uint8Array {
189
+ if (typeDef.kind === "alias") {
190
+ const nestedTypeName = resolveIoTypeName(typeDef.target, context.ioTypeByName);
191
+ if (nestedTypeName) {
192
+ const nested = context.ioTypeByName.get(nestedTypeName) as ContractIoTypeDefinition;
193
+ const encoded = encodeByTypeDefinition(nested, value, context, [...path, nestedTypeName]);
194
+ if (typeDef.byteSize !== undefined && encoded.length !== typeDef.byteSize) {
195
+ throw new Error(
196
+ `Encoded alias byte size mismatch at ${path.join(".")}: expected ${typeDef.byteSize}, received ${encoded.length}`,
197
+ );
198
+ }
199
+ return encoded;
200
+ }
201
+
202
+ return encodeByTypeExpression(typeDef.target, value, context, path, typeDef.byteSize);
203
+ }
204
+
205
+ return encodeStruct(typeDef, value, context, path);
206
+ }
207
+
208
+ function encodeStruct(
209
+ typeDef: Extract<ContractIoTypeDefinition, { kind: "struct" }>,
210
+ value: unknown,
211
+ context: CodecContext,
212
+ path: readonly string[],
213
+ ): Uint8Array {
214
+ const objectValue = asRecord(value, path);
215
+
216
+ const hasOffsets = typeDef.fields.every((field) => field.byteOffset !== undefined);
217
+ if (hasOffsets) {
218
+ if (typeDef.byteSize === undefined) {
219
+ throw new Error(
220
+ `Missing byteSize metadata for ${path.join(".")}. Regenerate with --io-extractor castxml.`,
221
+ );
222
+ }
223
+
224
+ const out = new Uint8Array(typeDef.byteSize);
225
+ for (const field of typeDef.fields) {
226
+ const fieldValue = objectValue[field.name];
227
+ const fieldPath = [...path, field.name];
228
+ const fieldBytes = encodeByTypeExpression(field.type, fieldValue, context, fieldPath);
229
+ const fieldOffset = field.byteOffset as number;
230
+ if (fieldOffset + fieldBytes.length > out.length) {
231
+ throw new Error(`Encoded field exceeds struct size at ${fieldPath.join(".")}`);
232
+ }
233
+ out.set(fieldBytes, fieldOffset);
234
+ }
235
+ return out;
236
+ }
237
+
238
+ if (!context.allowSequentialLayout) {
239
+ throw new Error(
240
+ `Missing byteOffset metadata for ${path.join(".")}. Regenerate with --io-extractor castxml or set allowSequentialLayout=true.`,
241
+ );
242
+ }
243
+
244
+ const fieldBytes: Uint8Array[] = [];
245
+ for (const field of typeDef.fields) {
246
+ const fieldValue = objectValue[field.name];
247
+ fieldBytes.push(encodeByTypeExpression(field.type, fieldValue, context, [...path, field.name]));
248
+ }
249
+ return concatBytes(fieldBytes);
250
+ }
251
+
252
+ function encodeByTypeExpression(
253
+ typeExpression: string,
254
+ value: unknown,
255
+ context: CodecContext,
256
+ path: readonly string[],
257
+ expectedByteSize?: number,
258
+ ): Uint8Array {
259
+ const normalizedType = normalizeTypeExpression(typeExpression);
260
+
261
+ const resolvedTypeName = resolveIoTypeName(normalizedType, context.ioTypeByName);
262
+ if (resolvedTypeName) {
263
+ const typeDef = context.ioTypeByName.get(resolvedTypeName) as ContractIoTypeDefinition;
264
+ const encoded = encodeByTypeDefinition(typeDef, value, context, [...path, resolvedTypeName]);
265
+ if (expectedByteSize !== undefined && encoded.length !== expectedByteSize) {
266
+ throw new Error(
267
+ `Encoded value size mismatch at ${path.join(".")}: expected ${expectedByteSize}, received ${encoded.length}`,
268
+ );
269
+ }
270
+ return encoded;
271
+ }
272
+
273
+ const arraySpec = parseArrayType(normalizedType);
274
+ if (arraySpec) {
275
+ const arrayValue = asArray(value, path);
276
+ if (arrayValue.length !== arraySpec.length) {
277
+ throw new Error(
278
+ `Invalid array length at ${path.join(".")}: expected ${arraySpec.length}, received ${arrayValue.length}`,
279
+ );
280
+ }
281
+ const encodedItems = arrayValue.map((item, index) =>
282
+ encodeByTypeExpression(arraySpec.elementType, item, context, [...path, String(index)]),
283
+ );
284
+ const out = concatBytes(encodedItems);
285
+ if (expectedByteSize !== undefined && out.length !== expectedByteSize) {
286
+ throw new Error(
287
+ `Encoded value size mismatch at ${path.join(".")}: expected ${expectedByteSize}, received ${out.length}`,
288
+ );
289
+ }
290
+ return out;
291
+ }
292
+
293
+ const primitive = encodePrimitiveValue(normalizedType, value, path, expectedByteSize);
294
+ if (primitive) return primitive;
295
+
296
+ if (
297
+ value instanceof Uint8Array &&
298
+ expectedByteSize !== undefined &&
299
+ value.length === expectedByteSize
300
+ ) {
301
+ return value.slice();
302
+ }
303
+
304
+ throw new Error(`Unsupported type expression at ${path.join(".")}: ${typeExpression}`);
305
+ }
306
+
307
+ function decodeByTypeDefinition(
308
+ typeDef: ContractIoTypeDefinition,
309
+ bytes: Uint8Array,
310
+ context: CodecContext,
311
+ path: readonly string[],
312
+ ): Readonly<{ value: unknown; byteLength: number }> {
313
+ if (typeDef.kind === "alias") {
314
+ const nestedTypeName = resolveIoTypeName(typeDef.target, context.ioTypeByName);
315
+ if (nestedTypeName) {
316
+ const nested = context.ioTypeByName.get(nestedTypeName) as ContractIoTypeDefinition;
317
+ return decodeByTypeDefinition(nested, bytes, context, [...path, nestedTypeName]);
318
+ }
319
+
320
+ return decodeByTypeExpression(typeDef.target, bytes, context, path, typeDef.byteSize);
321
+ }
322
+
323
+ return decodeStruct(typeDef, bytes, context, path);
324
+ }
325
+
326
+ function decodeStruct(
327
+ typeDef: Extract<ContractIoTypeDefinition, { kind: "struct" }>,
328
+ bytes: Uint8Array,
329
+ context: CodecContext,
330
+ path: readonly string[],
331
+ ): Readonly<{ value: unknown; byteLength: number }> {
332
+ const hasOffsets = typeDef.fields.every((field) => field.byteOffset !== undefined);
333
+ const objectValue: Record<string, unknown> = {};
334
+
335
+ if (hasOffsets) {
336
+ if (typeDef.byteSize === undefined) {
337
+ throw new Error(
338
+ `Missing byteSize metadata for ${path.join(".")}. Regenerate with --io-extractor castxml.`,
339
+ );
340
+ }
341
+ if (bytes.length < typeDef.byteSize) {
342
+ throw new Error(
343
+ `Input bytes are shorter than expected for ${path.join(".")}: expected at least ${typeDef.byteSize}, received ${bytes.length}`,
344
+ );
345
+ }
346
+
347
+ for (const field of typeDef.fields) {
348
+ const fieldPath = [...path, field.name];
349
+ const fieldOffset = field.byteOffset as number;
350
+ const fieldSize = resolveTypeByteLength(field.type, context, new Set());
351
+ if (fieldSize === undefined) {
352
+ throw new Error(`Cannot resolve field size at ${fieldPath.join(".")} (${field.type})`);
353
+ }
354
+
355
+ const slice = bytes.subarray(fieldOffset, fieldOffset + fieldSize);
356
+ const decoded = decodeByTypeExpression(field.type, slice, context, fieldPath, fieldSize);
357
+ objectValue[field.name] = decoded.value;
358
+ }
359
+
360
+ return {
361
+ value: objectValue,
362
+ byteLength: typeDef.byteSize,
363
+ };
364
+ }
365
+
366
+ if (!context.allowSequentialLayout) {
367
+ throw new Error(
368
+ `Missing byteOffset metadata for ${path.join(".")}. Regenerate with --io-extractor castxml or set allowSequentialLayout=true.`,
369
+ );
370
+ }
371
+
372
+ let cursor = 0;
373
+ for (const field of typeDef.fields) {
374
+ const fieldSize = resolveTypeByteLength(field.type, context, new Set());
375
+ if (fieldSize === undefined) {
376
+ throw new Error(
377
+ `Cannot resolve field size at ${[...path, field.name].join(".")} (${field.type})`,
378
+ );
379
+ }
380
+ const slice = bytes.subarray(cursor, cursor + fieldSize);
381
+ const decoded = decodeByTypeExpression(
382
+ field.type,
383
+ slice,
384
+ context,
385
+ [...path, field.name],
386
+ fieldSize,
387
+ );
388
+ objectValue[field.name] = decoded.value;
389
+ cursor += fieldSize;
390
+ }
391
+
392
+ return {
393
+ value: objectValue,
394
+ byteLength: cursor,
395
+ };
396
+ }
397
+
398
+ function decodeByTypeExpression(
399
+ typeExpression: string,
400
+ bytes: Uint8Array,
401
+ context: CodecContext,
402
+ path: readonly string[],
403
+ expectedByteSize?: number,
404
+ ): Readonly<{ value: unknown; byteLength: number }> {
405
+ const normalizedType = normalizeTypeExpression(typeExpression);
406
+
407
+ const resolvedTypeName = resolveIoTypeName(normalizedType, context.ioTypeByName);
408
+ if (resolvedTypeName) {
409
+ const typeDef = context.ioTypeByName.get(resolvedTypeName) as ContractIoTypeDefinition;
410
+ const decoded = decodeByTypeDefinition(typeDef, bytes, context, [...path, resolvedTypeName]);
411
+ if (expectedByteSize !== undefined && decoded.byteLength !== expectedByteSize) {
412
+ throw new Error(
413
+ `Decoded byte size mismatch at ${path.join(".")}: expected ${expectedByteSize}, received ${decoded.byteLength}`,
414
+ );
415
+ }
416
+ return decoded;
417
+ }
418
+
419
+ const arraySpec = parseArrayType(normalizedType);
420
+ if (arraySpec) {
421
+ const values: unknown[] = [];
422
+ const elementSize = resolveTypeByteLength(arraySpec.elementType, context, new Set());
423
+ if (elementSize === undefined) {
424
+ throw new Error(
425
+ `Cannot resolve array element size at ${path.join(".")} (${arraySpec.elementType})`,
426
+ );
427
+ }
428
+
429
+ let cursor = 0;
430
+ for (let i = 0; i < arraySpec.length; i++) {
431
+ const slice = bytes.subarray(cursor, cursor + elementSize);
432
+ const decoded = decodeByTypeExpression(
433
+ arraySpec.elementType,
434
+ slice,
435
+ context,
436
+ [...path, String(i)],
437
+ elementSize,
438
+ );
439
+ values.push(decoded.value);
440
+ cursor += elementSize;
441
+ }
442
+
443
+ if (expectedByteSize !== undefined && cursor !== expectedByteSize) {
444
+ throw new Error(
445
+ `Decoded byte size mismatch at ${path.join(".")}: expected ${expectedByteSize}, received ${cursor}`,
446
+ );
447
+ }
448
+
449
+ return { value: values, byteLength: cursor };
450
+ }
451
+
452
+ const primitive = decodePrimitiveValue(normalizedType, bytes, path, expectedByteSize);
453
+ if (primitive) return primitive;
454
+
455
+ if (expectedByteSize !== undefined && bytes.length >= expectedByteSize) {
456
+ return {
457
+ value: bytes.subarray(0, expectedByteSize),
458
+ byteLength: expectedByteSize,
459
+ };
460
+ }
461
+
462
+ throw new Error(`Unsupported type expression at ${path.join(".")}: ${typeExpression}`);
463
+ }
464
+
465
+ function resolveTypeByteLength(
466
+ typeExpression: string,
467
+ context: CodecContext,
468
+ seen: Set<string>,
469
+ ): number | undefined {
470
+ const normalizedType = normalizeTypeExpression(typeExpression);
471
+ if (seen.has(normalizedType)) {
472
+ throw new Error(`Recursive type definition detected: ${normalizedType}`);
473
+ }
474
+
475
+ const primitiveSize = getPrimitiveByteLength(normalizedType);
476
+ if (primitiveSize !== undefined) return primitiveSize;
477
+
478
+ const arraySpec = parseArrayType(normalizedType);
479
+ if (arraySpec) {
480
+ const elementSize = resolveTypeByteLength(arraySpec.elementType, context, seen);
481
+ return elementSize !== undefined ? elementSize * arraySpec.length : undefined;
482
+ }
483
+
484
+ const resolvedTypeName = resolveIoTypeName(normalizedType, context.ioTypeByName);
485
+ if (!resolvedTypeName) return undefined;
486
+ const typeDef = context.ioTypeByName.get(resolvedTypeName);
487
+ if (!typeDef) return undefined;
488
+
489
+ seen.add(resolvedTypeName);
490
+ if (typeDef.byteSize !== undefined) {
491
+ seen.delete(resolvedTypeName);
492
+ return typeDef.byteSize;
493
+ }
494
+
495
+ if (typeDef.kind === "alias") {
496
+ const size = resolveTypeByteLength(typeDef.target, context, seen);
497
+ seen.delete(resolvedTypeName);
498
+ return size;
499
+ }
500
+
501
+ if (!context.allowSequentialLayout) {
502
+ seen.delete(resolvedTypeName);
503
+ return undefined;
504
+ }
505
+
506
+ let total = 0;
507
+ for (const field of typeDef.fields) {
508
+ const fieldSize = resolveTypeByteLength(field.type, context, seen);
509
+ if (fieldSize === undefined) {
510
+ seen.delete(resolvedTypeName);
511
+ return undefined;
512
+ }
513
+ total += fieldSize;
514
+ }
515
+ seen.delete(resolvedTypeName);
516
+ return total;
517
+ }
518
+
519
+ function normalizeTypeExpression(value: string): string {
520
+ return value
521
+ .trim()
522
+ .replace(/\bQPI::/g, "")
523
+ .replace(/^::+/, "")
524
+ .replace(/\bint8_t\b/g, "sint8")
525
+ .replace(/\buint8_t\b/g, "uint8")
526
+ .replace(/\bint16_t\b/g, "sint16")
527
+ .replace(/\buint16_t\b/g, "uint16")
528
+ .replace(/\bint32_t\b/g, "sint32")
529
+ .replace(/\buint32_t\b/g, "uint32")
530
+ .replace(/\bint64_t\b/g, "sint64")
531
+ .replace(/\buint64_t\b/g, "uint64")
532
+ .replace(/\bconst\b/g, "")
533
+ .replace(/\bvolatile\b/g, "")
534
+ .replace(/\bstruct\b/g, "")
535
+ .replace(/\bclass\b/g, "")
536
+ .replace(/\s*&+/g, "")
537
+ .replace(/\s*\*+/g, "")
538
+ .replace(/\s+/g, " ")
539
+ .replace(/\s*<\s*/g, "<")
540
+ .replace(/\s*>\s*/g, ">")
541
+ .replace(/\s*,\s*/g, ", ")
542
+ .trim();
543
+ }
544
+
545
+ function resolveIoTypeName(
546
+ typeExpression: string,
547
+ ioTypeByName: ReadonlyMap<string, ContractIoTypeDefinition>,
548
+ ): string | undefined {
549
+ for (const candidate of getTypeLookupCandidates(typeExpression)) {
550
+ if (ioTypeByName.has(candidate)) return candidate;
551
+ }
552
+ return undefined;
553
+ }
554
+
555
+ function getTypeLookupCandidates(value: string): readonly string[] {
556
+ const candidates: string[] = [];
557
+ const seen = new Set<string>();
558
+
559
+ const push = (candidate: string | undefined): void => {
560
+ const normalized = normalizeTypeExpression(candidate ?? "");
561
+ if (!normalized || seen.has(normalized)) return;
562
+ seen.add(normalized);
563
+ candidates.push(normalized);
564
+ };
565
+
566
+ const normalized = normalizeTypeExpression(value);
567
+ push(normalized);
568
+ push(extractUnqualifiedTypeName(normalized));
569
+
570
+ const templateBase = extractTemplateBaseName(normalized);
571
+ if (templateBase) {
572
+ push(templateBase);
573
+ push(extractUnqualifiedTypeName(templateBase));
574
+ }
575
+
576
+ return candidates;
577
+ }
578
+
579
+ function extractTemplateBaseName(value: string): string | undefined {
580
+ const index = value.indexOf("<");
581
+ if (index <= 0) return undefined;
582
+ return value.slice(0, index).trim();
583
+ }
584
+
585
+ function extractUnqualifiedTypeName(value: string): string {
586
+ const trimmed = value.trim();
587
+ const segments = trimmed.split("::").filter((segment) => segment.length > 0);
588
+ return segments[segments.length - 1] ?? trimmed;
589
+ }
590
+
591
+ function parseArrayType(value: string): Readonly<{ elementType: string; length: number }> | null {
592
+ const genericMatch = value.match(/^Array<(.*)>$/);
593
+ if (genericMatch) {
594
+ const args = splitTopLevelGenericArgs(genericMatch[1] ?? "");
595
+ const elementType = args[0]?.trim();
596
+ const lengthRaw = args[1]?.trim();
597
+ const length = lengthRaw ? Number.parseInt(lengthRaw, 10) : Number.NaN;
598
+ if (elementType && Number.isFinite(length) && length >= 0) {
599
+ return { elementType, length };
600
+ }
601
+ }
602
+
603
+ const cArrayMatch = value.match(/^(.*)\[(\d+)\]$/);
604
+ if (!cArrayMatch) return null;
605
+ const elementType = cArrayMatch[1]?.trim();
606
+ const length = Number.parseInt(cArrayMatch[2] ?? "", 10);
607
+ if (!elementType || !Number.isFinite(length) || length < 0) return null;
608
+ return { elementType, length };
609
+ }
610
+
611
+ function splitTopLevelGenericArgs(value: string): readonly string[] {
612
+ const out: string[] = [];
613
+ let depth = 0;
614
+ let current = "";
615
+
616
+ for (const char of value) {
617
+ if (char === "<") depth += 1;
618
+ if (char === ">") depth = Math.max(0, depth - 1);
619
+ if (char === "," && depth === 0) {
620
+ out.push(current);
621
+ current = "";
622
+ continue;
623
+ }
624
+ current += char;
625
+ }
626
+
627
+ if (current.trim().length > 0) out.push(current);
628
+ return out;
629
+ }
630
+
631
+ function encodePrimitiveValue(
632
+ typeName: string,
633
+ value: unknown,
634
+ path: readonly string[],
635
+ expectedByteSize?: number,
636
+ ): Uint8Array | null {
637
+ switch (typeName) {
638
+ case "NoData":
639
+ validateNoData(value, path);
640
+ return withExpectedSize(new Uint8Array(1), expectedByteSize, path);
641
+ case "bit":
642
+ case "bool":
643
+ return withExpectedSize(Uint8Array.of(asBoolByte(value, path)), expectedByteSize, path);
644
+ case "sint8":
645
+ return withExpectedSize(
646
+ Uint8Array.of(asInt(value, -0x80, 0x7f, path) & 0xff),
647
+ expectedByteSize,
648
+ path,
649
+ );
650
+ case "uint8":
651
+ return withExpectedSize(Uint8Array.of(asInt(value, 0, 0xff, path)), expectedByteSize, path);
652
+ case "sint16":
653
+ return withExpectedSize(
654
+ writeInt16(asInt(value, -0x8000, 0x7fff, path)),
655
+ expectedByteSize,
656
+ path,
657
+ );
658
+ case "uint16":
659
+ return withExpectedSize(writeUint16(asInt(value, 0, 0xffff, path)), expectedByteSize, path);
660
+ case "sint32":
661
+ return withExpectedSize(
662
+ writeInt32(asInt(value, -0x8000_0000, 0x7fff_ffff, path)),
663
+ expectedByteSize,
664
+ path,
665
+ );
666
+ case "uint32":
667
+ return withExpectedSize(
668
+ writeUint32(asInt(value, 0, 0xffff_ffff, path)),
669
+ expectedByteSize,
670
+ path,
671
+ );
672
+ case "sint64":
673
+ return withExpectedSize(
674
+ writeBigInt64(asBigInt(value, MIN_SAFE_S64, MAX_SAFE_S64, path)),
675
+ expectedByteSize,
676
+ path,
677
+ );
678
+ case "uint64":
679
+ return withExpectedSize(
680
+ writeBigUint64(asBigInt(value, 0n, MAX_SAFE_U64, path)),
681
+ expectedByteSize,
682
+ path,
683
+ );
684
+ case "uint128":
685
+ return withExpectedSize(
686
+ writeBigUint128(asBigInt(value, 0n, MAX_SAFE_U128, path)),
687
+ expectedByteSize,
688
+ path,
689
+ );
690
+ case "id":
691
+ case "m256i":
692
+ return withExpectedSize(encodeId32(value, path), expectedByteSize, path);
693
+ default:
694
+ return null;
695
+ }
696
+ }
697
+
698
+ function decodePrimitiveValue(
699
+ typeName: string,
700
+ bytes: Uint8Array,
701
+ path: readonly string[],
702
+ expectedByteSize?: number,
703
+ ): Readonly<{ value: unknown; byteLength: number }> | null {
704
+ const size = getPrimitiveByteLength(typeName);
705
+ if (size === undefined) return null;
706
+
707
+ const expected = expectedByteSize ?? size;
708
+ if (bytes.length < expected) {
709
+ throw new Error(
710
+ `Not enough bytes at ${path.join(".")} for ${typeName}: expected ${expected}, received ${bytes.length}`,
711
+ );
712
+ }
713
+ const view = bytes.subarray(0, size);
714
+
715
+ switch (typeName) {
716
+ case "NoData":
717
+ return { value: {}, byteLength: expected };
718
+ case "bit":
719
+ case "bool":
720
+ return { value: view[0] !== 0, byteLength: expected };
721
+ case "sint8":
722
+ return {
723
+ value: new DataView(view.buffer, view.byteOffset, 1).getInt8(0),
724
+ byteLength: expected,
725
+ };
726
+ case "uint8":
727
+ return { value: view[0], byteLength: expected };
728
+ case "sint16":
729
+ return {
730
+ value: new DataView(view.buffer, view.byteOffset, 2).getInt16(0, true),
731
+ byteLength: expected,
732
+ };
733
+ case "uint16":
734
+ return {
735
+ value: new DataView(view.buffer, view.byteOffset, 2).getUint16(0, true),
736
+ byteLength: expected,
737
+ };
738
+ case "sint32":
739
+ return {
740
+ value: new DataView(view.buffer, view.byteOffset, 4).getInt32(0, true),
741
+ byteLength: expected,
742
+ };
743
+ case "uint32":
744
+ return {
745
+ value: new DataView(view.buffer, view.byteOffset, 4).getUint32(0, true),
746
+ byteLength: expected,
747
+ };
748
+ case "sint64":
749
+ return {
750
+ value: new DataView(view.buffer, view.byteOffset, 8).getBigInt64(0, true),
751
+ byteLength: expected,
752
+ };
753
+ case "uint64":
754
+ return {
755
+ value: new DataView(view.buffer, view.byteOffset, 8).getBigUint64(0, true),
756
+ byteLength: expected,
757
+ };
758
+ case "uint128":
759
+ return { value: readBigUint128(view), byteLength: expected };
760
+ case "id":
761
+ case "m256i":
762
+ return { value: decodeId32(view), byteLength: expected };
763
+ default:
764
+ return null;
765
+ }
766
+ }
767
+
768
+ function getPrimitiveByteLength(typeName: string): number | undefined {
769
+ switch (typeName) {
770
+ case "NoData":
771
+ case "bit":
772
+ case "bool":
773
+ case "sint8":
774
+ case "uint8":
775
+ return 1;
776
+ case "sint16":
777
+ case "uint16":
778
+ return 2;
779
+ case "sint32":
780
+ case "uint32":
781
+ return 4;
782
+ case "sint64":
783
+ case "uint64":
784
+ return 8;
785
+ case "uint128":
786
+ return 16;
787
+ case "id":
788
+ case "m256i":
789
+ return 32;
790
+ default:
791
+ return undefined;
792
+ }
793
+ }
794
+
795
+ function asRecord(value: unknown, path: readonly string[]): Record<string, unknown> {
796
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
797
+ throw new Error(`Expected object at ${path.join(".")}`);
798
+ }
799
+ return value as Record<string, unknown>;
800
+ }
801
+
802
+ function asArray(value: unknown, path: readonly string[]): readonly unknown[] {
803
+ if (!Array.isArray(value)) {
804
+ throw new Error(`Expected array at ${path.join(".")}`);
805
+ }
806
+ return value;
807
+ }
808
+
809
+ function validateNoData(value: unknown, path: readonly string[]): void {
810
+ if (value === undefined || value === null) return;
811
+ if (
812
+ typeof value === "object" &&
813
+ !Array.isArray(value) &&
814
+ Object.keys(value as object).length === 0
815
+ ) {
816
+ return;
817
+ }
818
+ throw new Error(`Expected empty object for NoData at ${path.join(".")}`);
819
+ }
820
+
821
+ function asBoolByte(value: unknown, path: readonly string[]): number {
822
+ if (typeof value === "boolean") return value ? 1 : 0;
823
+ if (typeof value === "number" && (value === 0 || value === 1)) return value;
824
+ throw new Error(`Expected boolean (or 0/1) at ${path.join(".")}`);
825
+ }
826
+
827
+ function asInt(value: unknown, min: number, max: number, path: readonly string[]): number {
828
+ if (typeof value !== "number" || !Number.isInteger(value)) {
829
+ throw new Error(`Expected integer number at ${path.join(".")}`);
830
+ }
831
+ if (value < min || value > max) {
832
+ throw new Error(
833
+ `Integer out of range at ${path.join(".")}: expected ${min}..${max}, received ${value}`,
834
+ );
835
+ }
836
+ return value;
837
+ }
838
+
839
+ function asBigInt(value: unknown, min: bigint, max: bigint, path: readonly string[]): bigint {
840
+ const out =
841
+ typeof value === "bigint"
842
+ ? value
843
+ : typeof value === "number" && Number.isInteger(value)
844
+ ? BigInt(value)
845
+ : null;
846
+
847
+ if (out === null) {
848
+ throw new Error(`Expected bigint (or integer number) at ${path.join(".")}`);
849
+ }
850
+ if (out < min || out > max) {
851
+ throw new Error(`BigInt out of range at ${path.join(".")}`);
852
+ }
853
+ return out;
854
+ }
855
+
856
+ function writeInt16(value: number): Uint8Array {
857
+ const out = new Uint8Array(2);
858
+ new DataView(out.buffer).setInt16(0, value, true);
859
+ return out;
860
+ }
861
+
862
+ function writeUint16(value: number): Uint8Array {
863
+ const out = new Uint8Array(2);
864
+ new DataView(out.buffer).setUint16(0, value, true);
865
+ return out;
866
+ }
867
+
868
+ function writeInt32(value: number): Uint8Array {
869
+ const out = new Uint8Array(4);
870
+ new DataView(out.buffer).setInt32(0, value, true);
871
+ return out;
872
+ }
873
+
874
+ function writeUint32(value: number): Uint8Array {
875
+ const out = new Uint8Array(4);
876
+ new DataView(out.buffer).setUint32(0, value, true);
877
+ return out;
878
+ }
879
+
880
+ function writeBigInt64(value: bigint): Uint8Array {
881
+ const out = new Uint8Array(8);
882
+ new DataView(out.buffer).setBigInt64(0, value, true);
883
+ return out;
884
+ }
885
+
886
+ function writeBigUint64(value: bigint): Uint8Array {
887
+ const out = new Uint8Array(8);
888
+ new DataView(out.buffer).setBigUint64(0, value, true);
889
+ return out;
890
+ }
891
+
892
+ function writeBigUint128(value: bigint): Uint8Array {
893
+ const out = new Uint8Array(16);
894
+ let current = value;
895
+ for (let i = 0; i < out.length; i++) {
896
+ out[i] = Number(current & 0xffn);
897
+ current >>= 8n;
898
+ }
899
+ return out;
900
+ }
901
+
902
+ function readBigUint128(value: Uint8Array): bigint {
903
+ let out = 0n;
904
+ for (let i = value.length - 1; i >= 0; i--) {
905
+ out = (out << 8n) | BigInt(value[i] ?? 0);
906
+ }
907
+ return out;
908
+ }
909
+
910
+ function encodeId32(value: unknown, path: readonly string[]): Uint8Array {
911
+ if (value instanceof Uint8Array) {
912
+ if (value.length !== 32) {
913
+ throw new Error(`Expected 32-byte id at ${path.join(".")}`);
914
+ }
915
+ return value.slice();
916
+ }
917
+
918
+ if (typeof value === "string") {
919
+ const hex = value.startsWith("0x") ? value.slice(2) : value;
920
+ if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
921
+ throw new Error(`Expected 32-byte id hex string at ${path.join(".")}`);
922
+ }
923
+ const out = new Uint8Array(32);
924
+ for (let i = 0; i < 32; i++) {
925
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
926
+ }
927
+ return out;
928
+ }
929
+
930
+ throw new Error(`Expected 32-byte id (hex string or Uint8Array) at ${path.join(".")}`);
931
+ }
932
+
933
+ function decodeId32(value: Uint8Array): string {
934
+ return `0x${[...value].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
935
+ }
936
+
937
+ function withExpectedSize(
938
+ value: Uint8Array,
939
+ expectedByteSize: number | undefined,
940
+ path: readonly string[],
941
+ ): Uint8Array {
942
+ if (expectedByteSize !== undefined && value.length !== expectedByteSize) {
943
+ throw new Error(
944
+ `Encoded value size mismatch at ${path.join(".")}: expected ${expectedByteSize}, received ${value.length}`,
945
+ );
946
+ }
947
+ return value;
948
+ }
949
+
950
+ function concatBytes(chunks: readonly Uint8Array[]): Uint8Array {
951
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
952
+ const out = new Uint8Array(total);
953
+ let offset = 0;
954
+ for (const chunk of chunks) {
955
+ out.set(chunk, offset);
956
+ offset += chunk.length;
957
+ }
958
+ return out;
959
+ }