@d9-network/ink 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@d9-network/ink",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.cts",
9
+ "publishConfig": {
10
+ "exports": {
11
+ ".": {
12
+ "require": "./dist/index.cjs",
13
+ "import": "./dist/index.mjs"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "access": "public"
18
+ },
19
+ "exports": {
20
+ ".": "./src/index.ts",
21
+ "./package.json": "./package.json"
22
+ },
23
+ "scripts": {
24
+ "test": "bun test",
25
+ "build": "tsdown",
26
+ "typecheck": "tsc --noEmit -p tsconfig.json"
27
+ },
28
+ "dependencies": {
29
+ "@noble/hashes": "^2.0.1",
30
+ "@polkadot-api/ink-contracts": "^0.4.4",
31
+ "@polkadot-api/substrate-bindings": "^0.16.5",
32
+ "@polkadot-labs/hdkd-helpers": "^0.0.11",
33
+ "@subsquid/scale-codec": "^4.0.1",
34
+ "polkadot-api": "^1.23.1"
35
+ },
36
+ "devDependencies": {
37
+ "@polkadot-api/descriptors": "file:../spec/.papi/descriptors",
38
+ "@polkadot-api/legacy-provider": "^0.3.6",
39
+ "@polkadot-api/signer": "^0.2.11",
40
+ "@polkadot-labs/hdkd": "^0.0.11",
41
+ "@types/bun": "latest",
42
+ "@types/node": "latest",
43
+ "tsdown": "^0.18.1",
44
+ "typescript": "~5.9.3"
45
+ }
46
+ }
@@ -0,0 +1,573 @@
1
+ /**
2
+ * Auto-build SCALE decoders from ink metadata type definitions.
3
+ *
4
+ * This module provides a way to automatically construct decoders for ink contract
5
+ * message return types without manually specifying codecs for each message.
6
+ */
7
+
8
+ import {
9
+ u8,
10
+ u16,
11
+ u32,
12
+ u64,
13
+ u128,
14
+ i8,
15
+ i16,
16
+ i32,
17
+ i64,
18
+ i128,
19
+ bool,
20
+ str,
21
+ Bytes,
22
+ Vector,
23
+ Tuple,
24
+ Struct,
25
+ Variant,
26
+ _void,
27
+ Option,
28
+ AccountId,
29
+ type Codec,
30
+ } from "@polkadot-api/substrate-bindings";
31
+ import type { InkMetadata } from "@polkadot-api/ink-contracts";
32
+ import { blake2b } from "@noble/hashes/blake2.js";
33
+
34
+ // Use 'any' for dynamic codec building to avoid TypeScript strict type issues
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ type AnyCodec = Codec<any>;
37
+
38
+ /**
39
+ * Type definition from ink metadata
40
+ */
41
+ interface TypeDef {
42
+ primitive?: string;
43
+ composite?: {
44
+ fields: Array<{
45
+ name?: string;
46
+ type: number;
47
+ typeName?: string;
48
+ }>;
49
+ };
50
+ variant?: {
51
+ variants: Array<{
52
+ name: string;
53
+ index: number;
54
+ fields: Array<{
55
+ name?: string;
56
+ type: number;
57
+ typeName?: string;
58
+ }>;
59
+ }>;
60
+ };
61
+ sequence?: {
62
+ type: number;
63
+ };
64
+ array?: {
65
+ len: number;
66
+ type: number;
67
+ };
68
+ tuple?: number[];
69
+ }
70
+
71
+ interface TypeEntry {
72
+ id: number;
73
+ type: {
74
+ def: TypeDef;
75
+ path?: string[];
76
+ params?: Array<{
77
+ name: string;
78
+ type?: number;
79
+ }>;
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Cache for built codecs to avoid rebuilding the same type
85
+ */
86
+ type CodecCache = Map<number, AnyCodec>;
87
+
88
+ /**
89
+ * Build a SCALE codec from ink metadata type definition
90
+ */
91
+ function buildCodecFromType(
92
+ typeId: number,
93
+ types: TypeEntry[],
94
+ cache: CodecCache,
95
+ ): AnyCodec {
96
+ // Check cache first
97
+ const cached = cache.get(typeId);
98
+ if (cached) {
99
+ return cached;
100
+ }
101
+
102
+ const typeEntry = types.find((t) => t.id === typeId);
103
+ if (!typeEntry) {
104
+ throw new Error(`Type ${typeId} not found in metadata`);
105
+ }
106
+
107
+ const def = typeEntry.type.def;
108
+ const path = typeEntry.type.path;
109
+
110
+ // Handle primitive types
111
+ if (def.primitive) {
112
+ const codec = buildPrimitiveCodec(def.primitive);
113
+ cache.set(typeId, codec);
114
+ return codec;
115
+ }
116
+
117
+ // Handle special path types (AccountId, Hash, etc.)
118
+ if (path && path.length > 0) {
119
+ const specialCodec = buildSpecialTypeCodec(path, typeEntry, types, cache);
120
+ if (specialCodec) {
121
+ cache.set(typeId, specialCodec);
122
+ return specialCodec;
123
+ }
124
+ }
125
+
126
+ // Handle tuple
127
+ if (def.tuple) {
128
+ const codec = buildTupleCodec(def.tuple, types, cache);
129
+ cache.set(typeId, codec);
130
+ return codec;
131
+ }
132
+
133
+ // Handle sequence (Vec<T>)
134
+ if (def.sequence) {
135
+ const innerCodec = buildCodecFromType(def.sequence.type, types, cache);
136
+ const codec = Vector(innerCodec);
137
+ cache.set(typeId, codec);
138
+ return codec;
139
+ }
140
+
141
+ // Handle array [T; N]
142
+ if (def.array) {
143
+ const innerCodec = buildCodecFromType(def.array.type, types, cache);
144
+ // For fixed-size byte arrays, use Bytes
145
+ if (def.array.type === findPrimitiveTypeId(types, "u8")) {
146
+ const codec = Bytes(def.array.len);
147
+ cache.set(typeId, codec);
148
+ return codec;
149
+ }
150
+ // For other arrays, use Vector with fixed length validation
151
+ const codec = Vector(innerCodec, def.array.len);
152
+ cache.set(typeId, codec);
153
+ return codec;
154
+ }
155
+
156
+ // Handle composite (struct)
157
+ if (def.composite) {
158
+ const codec = buildCompositeCodec(def.composite, types, cache);
159
+ cache.set(typeId, codec);
160
+ return codec;
161
+ }
162
+
163
+ // Handle variant (enum)
164
+ if (def.variant) {
165
+ const codec = buildVariantCodec(def.variant, path, types, cache);
166
+ cache.set(typeId, codec);
167
+ return codec;
168
+ }
169
+
170
+ throw new Error(
171
+ `Unknown type definition for type ${typeId}: ${JSON.stringify(def)}`,
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Build codec for primitive types
177
+ */
178
+ function buildPrimitiveCodec(primitive: string): AnyCodec {
179
+ switch (primitive) {
180
+ case "u8":
181
+ return u8;
182
+ case "u16":
183
+ return u16;
184
+ case "u32":
185
+ return u32;
186
+ case "u64":
187
+ return u64;
188
+ case "u128":
189
+ return u128;
190
+ case "i8":
191
+ return i8;
192
+ case "i16":
193
+ return i16;
194
+ case "i32":
195
+ return i32;
196
+ case "i64":
197
+ return i64;
198
+ case "i128":
199
+ return i128;
200
+ case "bool":
201
+ return bool;
202
+ case "str":
203
+ return str;
204
+ default:
205
+ throw new Error(`Unknown primitive type: ${primitive}`);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Build codec for special types based on path
211
+ */
212
+ function buildSpecialTypeCodec(
213
+ path: string[],
214
+ typeEntry: TypeEntry,
215
+ types: TypeEntry[],
216
+ cache: CodecCache,
217
+ ): AnyCodec | null {
218
+ const fullPath = path.join("::");
219
+
220
+ // AccountId type
221
+ if (fullPath.includes("AccountId")) {
222
+ return AccountId();
223
+ }
224
+
225
+ // Option type
226
+ if (path[0] === "Option") {
227
+ const params = typeEntry.type.params;
228
+ if (params && params.length > 0 && params[0]?.type !== undefined) {
229
+ const innerCodec = buildCodecFromType(params[0].type, types, cache);
230
+ return Option(innerCodec);
231
+ }
232
+ }
233
+
234
+ // Result type - we need special handling
235
+ if (path[0] === "Result") {
236
+ const params = typeEntry.type.params;
237
+ if (params && params.length >= 2) {
238
+ const okTypeId = params[0]?.type;
239
+ const errTypeId = params[1]?.type;
240
+ if (okTypeId !== undefined && errTypeId !== undefined) {
241
+ const okCodec = buildCodecFromType(okTypeId, types, cache);
242
+ const errCodec = buildCodecFromType(errTypeId, types, cache);
243
+ // Build a proper Result variant
244
+ return Variant(
245
+ {
246
+ Ok: okCodec,
247
+ Err: errCodec,
248
+ },
249
+ [0, 1],
250
+ );
251
+ }
252
+ }
253
+ }
254
+
255
+ return null;
256
+ }
257
+
258
+ /**
259
+ * Build codec for tuple types
260
+ */
261
+ function buildTupleCodec(
262
+ tupleTypes: number[],
263
+ types: TypeEntry[],
264
+ cache: CodecCache,
265
+ ): AnyCodec {
266
+ if (tupleTypes.length === 0) {
267
+ return _void;
268
+ }
269
+
270
+ const innerCodecs = tupleTypes.map((t) => buildCodecFromType(t, types, cache));
271
+
272
+ // Handle different tuple sizes
273
+ switch (innerCodecs.length) {
274
+ case 1:
275
+ return Tuple(innerCodecs[0]!);
276
+ case 2:
277
+ return Tuple(innerCodecs[0]!, innerCodecs[1]!);
278
+ case 3:
279
+ return Tuple(innerCodecs[0]!, innerCodecs[1]!, innerCodecs[2]!);
280
+ case 4:
281
+ return Tuple(
282
+ innerCodecs[0]!,
283
+ innerCodecs[1]!,
284
+ innerCodecs[2]!,
285
+ innerCodecs[3]!,
286
+ );
287
+ default:
288
+ // For larger tuples, use dynamic tuple (cast to any)
289
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
290
+ return (Tuple as any)(...innerCodecs);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Build codec for composite (struct) types
296
+ */
297
+ function buildCompositeCodec(
298
+ composite: NonNullable<TypeDef["composite"]>,
299
+ types: TypeEntry[],
300
+ cache: CodecCache,
301
+ ): AnyCodec {
302
+ const fields = composite.fields;
303
+
304
+ // Single unnamed field - unwrap it
305
+ if (fields.length === 1 && !fields[0]?.name) {
306
+ return buildCodecFromType(fields[0]!.type, types, cache);
307
+ }
308
+
309
+ // Multiple fields - build a struct
310
+ const structDef: Record<string, AnyCodec> = {};
311
+ for (const field of fields) {
312
+ const fieldName = field.name || `field${fields.indexOf(field)}`;
313
+ structDef[fieldName] = buildCodecFromType(field.type, types, cache);
314
+ }
315
+
316
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
317
+ return Struct(structDef as any);
318
+ }
319
+
320
+ /**
321
+ * Build codec for variant (enum) types
322
+ */
323
+ function buildVariantCodec(
324
+ variant: NonNullable<TypeDef["variant"]>,
325
+ path: string[] | undefined,
326
+ types: TypeEntry[],
327
+ cache: CodecCache,
328
+ ): AnyCodec {
329
+ const variants = variant.variants;
330
+
331
+ // Check if this is a LangError type (ink specific)
332
+ const isLangError = path?.includes("LangError");
333
+
334
+ // Build variant definition
335
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
336
+ const variantDef: Record<string, any> = {};
337
+ const indices: number[] = [];
338
+
339
+ // For LangError, add a placeholder for index 0 if missing
340
+ if (isLangError && !variants.some((v) => v.index === 0)) {
341
+ variantDef["_Placeholder"] = _void;
342
+ indices.push(0);
343
+ }
344
+
345
+ for (const v of variants) {
346
+ let fieldCodec: AnyCodec;
347
+
348
+ // Handle variants with no fields or undefined fields
349
+ const fields = v.fields ?? [];
350
+
351
+ if (fields.length === 0) {
352
+ fieldCodec = _void;
353
+ } else if (fields.length === 1 && !fields[0]?.name) {
354
+ // Single unnamed field
355
+ fieldCodec = buildCodecFromType(fields[0]!.type, types, cache);
356
+ } else {
357
+ // Multiple or named fields - build struct
358
+ const structDef: Record<string, AnyCodec> = {};
359
+ for (const field of fields) {
360
+ const fieldName = field.name || `field${fields.indexOf(field)}`;
361
+ structDef[fieldName] = buildCodecFromType(field.type, types, cache);
362
+ }
363
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
364
+ fieldCodec = Struct(structDef as any);
365
+ }
366
+
367
+ variantDef[v.name] = fieldCodec;
368
+ indices.push(v.index);
369
+ }
370
+
371
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
372
+ return Variant(variantDef as any, indices as any);
373
+ }
374
+
375
+ /**
376
+ * Find the type ID for a primitive type
377
+ */
378
+ function findPrimitiveTypeId(types: TypeEntry[], primitive: string): number {
379
+ const entry = types.find((t) => t.type.def.primitive === primitive);
380
+ return entry?.id ?? -1;
381
+ }
382
+
383
+ /**
384
+ * Extract the inner type from Result<T, LangError>
385
+ * Returns the type ID of T
386
+ */
387
+ function extractResultInnerType(typeEntry: TypeEntry): number | null {
388
+ const path = typeEntry.type.path;
389
+ if (!path || path[0] !== "Result") {
390
+ return null;
391
+ }
392
+
393
+ const params = typeEntry.type.params;
394
+ if (!params || params.length < 1) {
395
+ return null;
396
+ }
397
+
398
+ return params[0]?.type ?? null;
399
+ }
400
+
401
+ /**
402
+ * Build a decoder for a message's return type from ink metadata.
403
+ * This handles the Result<T, LangError> wrapper and returns a decoder for T.
404
+ *
405
+ * @param metadata - The ink contract metadata
406
+ * @param messageLabel - The message label (e.g., "PSP22::balance_of")
407
+ * @returns A decoder function that decodes the inner value bytes
408
+ */
409
+ export function buildMessageDecoder(
410
+ metadata: InkMetadata,
411
+ messageLabel: string,
412
+ ): (data: Uint8Array) => unknown {
413
+ const types = metadata.types as TypeEntry[];
414
+ const message = metadata.spec.messages.find((m) => m.label === messageLabel);
415
+
416
+ if (!message) {
417
+ throw new Error(`Message "${messageLabel}" not found in metadata`);
418
+ }
419
+
420
+ const returnTypeId = message.returnType.type;
421
+ const returnTypeEntry = types.find((t) => t.id === returnTypeId);
422
+
423
+ if (!returnTypeEntry) {
424
+ throw new Error(
425
+ `Return type ${returnTypeId} not found for message "${messageLabel}"`,
426
+ );
427
+ }
428
+
429
+ const cache: CodecCache = new Map();
430
+
431
+ // Check if it's a Result type and extract inner type
432
+ const innerTypeId = extractResultInnerType(returnTypeEntry);
433
+
434
+ if (innerTypeId !== null) {
435
+ // Build decoder for the inner type (T in Result<T, LangError>)
436
+ const innerCodec = buildCodecFromType(innerTypeId, types, cache);
437
+ return (data: Uint8Array) => innerCodec.dec(data);
438
+ }
439
+
440
+ // Not a Result type, build decoder for the full return type
441
+ const codec = buildCodecFromType(returnTypeId, types, cache);
442
+ return (data: Uint8Array) => codec.dec(data);
443
+ }
444
+
445
+ /**
446
+ * Build decoders for all messages in the metadata.
447
+ * Returns a Map of message label -> decoder function.
448
+ */
449
+ export function buildAllMessageDecoders(
450
+ metadata: InkMetadata,
451
+ ): Map<string, (data: Uint8Array) => unknown> {
452
+ const decoders = new Map<string, (data: Uint8Array) => unknown>();
453
+
454
+ for (const message of metadata.spec.messages as Array<{ label: string }>) {
455
+ try {
456
+ const decoder = buildMessageDecoder(metadata, message.label);
457
+ decoders.set(message.label, decoder);
458
+ } catch (error) {
459
+ console.warn(
460
+ `Failed to build decoder for message "${message.label}":`,
461
+ error,
462
+ );
463
+ }
464
+ }
465
+
466
+ return decoders;
467
+ }
468
+
469
+ /**
470
+ * Create a codec registry compatible with ResponseDecoder type
471
+ */
472
+ export function createCodecRegistry(
473
+ metadata: InkMetadata,
474
+ ): Map<string, { dec: (data: Uint8Array) => unknown }> {
475
+ const decoders = buildAllMessageDecoders(metadata);
476
+ const registry = new Map<string, { dec: (data: Uint8Array) => unknown }>();
477
+
478
+ for (const [label, decoder] of decoders) {
479
+ registry.set(label, { dec: decoder });
480
+ }
481
+
482
+ return registry;
483
+ }
484
+
485
+ /**
486
+ * Build a SCALE decoder for a contract event from ink metadata
487
+ *
488
+ * @param metadata - The ink contract metadata
489
+ * @param eventLabel - The event label (e.g., "Transfer", "Approval")
490
+ * @returns A decoder function that decodes the event data bytes
491
+ */
492
+ export function buildEventDecoder(
493
+ metadata: InkMetadata,
494
+ eventLabel: string,
495
+ ): (data: Uint8Array) => unknown {
496
+ const types = metadata.types as TypeEntry[];
497
+ const events = metadata.spec.events as Array<{
498
+ label: string;
499
+ args: Array<{
500
+ label: string;
501
+ type: { type: number };
502
+ }>;
503
+ }>;
504
+
505
+ const event = events.find((e) => e.label === eventLabel);
506
+
507
+ if (!event) {
508
+ throw new Error(`Event "${eventLabel}" not found in metadata`);
509
+ }
510
+
511
+ const cache: CodecCache = new Map();
512
+
513
+ // Build struct codec from event args
514
+ const structDef: Record<string, AnyCodec> = {};
515
+
516
+ for (const arg of event.args) {
517
+ const fieldName = arg.label;
518
+ const fieldCodec = buildCodecFromType(arg.type.type, types, cache);
519
+ structDef[fieldName] = fieldCodec;
520
+ }
521
+
522
+ // If event has no args, return void decoder
523
+ if (event.args.length === 0) {
524
+ return () => undefined;
525
+ }
526
+
527
+ // If event has single unnamed arg, unwrap it
528
+ if (event.args.length === 1) {
529
+ const argCodec = structDef[event.args[0]!.label];
530
+ return (data: Uint8Array) => argCodec!.dec(data);
531
+ }
532
+
533
+ // Multiple args - return struct
534
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
535
+ const codec = Struct(structDef as any);
536
+ return (data: Uint8Array) => codec.dec(data);
537
+ }
538
+
539
+ /**
540
+ * Build decoders for all events in the metadata
541
+ *
542
+ * @param metadata - The ink contract metadata
543
+ * @returns Map of event label -> decoder function
544
+ */
545
+ export function buildAllEventDecoders(
546
+ metadata: InkMetadata,
547
+ ): Map<string, (data: Uint8Array) => unknown> {
548
+ const decoders = new Map<string, (data: Uint8Array) => unknown>();
549
+
550
+ const events = metadata.spec.events as Array<{ label: string }>;
551
+
552
+ for (const event of events) {
553
+ try {
554
+ const decoder = buildEventDecoder(metadata, event.label);
555
+ decoders.set(event.label, decoder);
556
+ } catch (error) {
557
+ console.warn(`Failed to build decoder for event "${event.label}":`, error);
558
+ }
559
+ }
560
+
561
+ return decoders;
562
+ }
563
+
564
+ /**
565
+ * Get event signature (topic[0]) for filtering
566
+ * Events in ink! use blake2_256 hash of event label as topic[0]
567
+ *
568
+ * @param eventLabel - The event label (e.g., "Transfer")
569
+ * @returns Event signature as Uint8Array (32 bytes)
570
+ */
571
+ export function getEventSignature(eventLabel: string): Uint8Array {
572
+ return blake2b(new TextEncoder().encode(eventLabel), { dkLen: 32 });
573
+ }