@haneullabs/bcs 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 (63) hide show
  1. package/CHANGELOG.md +388 -0
  2. package/README.md +358 -0
  3. package/dist/cjs/bcs-type.d.ts +127 -0
  4. package/dist/cjs/bcs-type.js +386 -0
  5. package/dist/cjs/bcs-type.js.map +7 -0
  6. package/dist/cjs/bcs.d.ts +175 -0
  7. package/dist/cjs/bcs.js +406 -0
  8. package/dist/cjs/bcs.js.map +7 -0
  9. package/dist/cjs/index.d.ts +22 -0
  10. package/dist/cjs/index.js +59 -0
  11. package/dist/cjs/index.js.map +7 -0
  12. package/dist/cjs/package.json +5 -0
  13. package/dist/cjs/reader.d.ts +92 -0
  14. package/dist/cjs/reader.js +136 -0
  15. package/dist/cjs/reader.js.map +7 -0
  16. package/dist/cjs/types.d.ts +28 -0
  17. package/dist/cjs/types.js +17 -0
  18. package/dist/cjs/types.js.map +7 -0
  19. package/dist/cjs/uleb.d.ts +5 -0
  20. package/dist/cjs/uleb.js +66 -0
  21. package/dist/cjs/uleb.js.map +7 -0
  22. package/dist/cjs/utils.d.ts +18 -0
  23. package/dist/cjs/utils.js +74 -0
  24. package/dist/cjs/utils.js.map +7 -0
  25. package/dist/cjs/writer.d.ts +117 -0
  26. package/dist/cjs/writer.js +196 -0
  27. package/dist/cjs/writer.js.map +7 -0
  28. package/dist/esm/bcs-type.d.ts +127 -0
  29. package/dist/esm/bcs-type.js +366 -0
  30. package/dist/esm/bcs-type.js.map +7 -0
  31. package/dist/esm/bcs.d.ts +175 -0
  32. package/dist/esm/bcs.js +397 -0
  33. package/dist/esm/bcs.js.map +7 -0
  34. package/dist/esm/index.d.ts +22 -0
  35. package/dist/esm/index.js +46 -0
  36. package/dist/esm/index.js.map +7 -0
  37. package/dist/esm/package.json +5 -0
  38. package/dist/esm/reader.d.ts +92 -0
  39. package/dist/esm/reader.js +116 -0
  40. package/dist/esm/reader.js.map +7 -0
  41. package/dist/esm/types.d.ts +28 -0
  42. package/dist/esm/types.js +1 -0
  43. package/dist/esm/types.js.map +7 -0
  44. package/dist/esm/uleb.d.ts +5 -0
  45. package/dist/esm/uleb.js +46 -0
  46. package/dist/esm/uleb.js.map +7 -0
  47. package/dist/esm/utils.d.ts +18 -0
  48. package/dist/esm/utils.js +54 -0
  49. package/dist/esm/utils.js.map +7 -0
  50. package/dist/esm/writer.d.ts +117 -0
  51. package/dist/esm/writer.js +176 -0
  52. package/dist/esm/writer.js.map +7 -0
  53. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  54. package/dist/tsconfig.tsbuildinfo +1 -0
  55. package/package.json +73 -0
  56. package/src/bcs-type.ts +531 -0
  57. package/src/bcs.ts +543 -0
  58. package/src/index.ts +82 -0
  59. package/src/reader.ts +156 -0
  60. package/src/types.ts +52 -0
  61. package/src/uleb.ts +61 -0
  62. package/src/utils.ts +75 -0
  63. package/src/writer.ts +222 -0
@@ -0,0 +1,531 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { fromBase58, fromBase64, toBase58, toBase64, fromHex, toHex } from '@haneullabs/utils';
5
+ import { BcsReader } from './reader.js';
6
+ import { ulebEncode } from './uleb.js';
7
+ import type { BcsWriterOptions } from './writer.js';
8
+ import { BcsWriter } from './writer.js';
9
+ import type { EnumInputShape, EnumOutputShape, JoinString } from './types.js';
10
+
11
+ export interface BcsTypeOptions<T, Input = T, Name extends string = string> {
12
+ name?: Name;
13
+ validate?: (value: Input) => void;
14
+ }
15
+
16
+ export class BcsType<T, Input = T, const Name extends string = string> {
17
+ $inferType!: T;
18
+ $inferInput!: Input;
19
+ name: Name;
20
+ read: (reader: BcsReader) => T;
21
+ serializedSize: (value: Input, options?: BcsWriterOptions) => number | null;
22
+ validate: (value: Input) => void;
23
+ #write: (value: Input, writer: BcsWriter) => void;
24
+ #serialize: (value: Input, options?: BcsWriterOptions) => Uint8Array<ArrayBuffer>;
25
+
26
+ constructor(
27
+ options: {
28
+ name: Name;
29
+ read: (reader: BcsReader) => T;
30
+ write: (value: Input, writer: BcsWriter) => void;
31
+ serialize?: (value: Input, options?: BcsWriterOptions) => Uint8Array<ArrayBuffer>;
32
+ serializedSize?: (value: Input) => number | null;
33
+ validate?: (value: Input) => void;
34
+ } & BcsTypeOptions<T, Input, Name>,
35
+ ) {
36
+ this.name = options.name;
37
+ this.read = options.read;
38
+ this.serializedSize = options.serializedSize ?? (() => null);
39
+ this.#write = options.write;
40
+ this.#serialize =
41
+ options.serialize ??
42
+ ((value, options) => {
43
+ const writer = new BcsWriter({
44
+ initialSize: this.serializedSize(value) ?? undefined,
45
+ ...options,
46
+ });
47
+ this.#write(value, writer);
48
+ return writer.toBytes();
49
+ });
50
+
51
+ this.validate = options.validate ?? (() => {});
52
+ }
53
+
54
+ write(value: Input, writer: BcsWriter) {
55
+ this.validate(value);
56
+ this.#write(value, writer);
57
+ }
58
+
59
+ serialize(value: Input, options?: BcsWriterOptions) {
60
+ this.validate(value);
61
+ return new SerializedBcs(this, this.#serialize(value, options));
62
+ }
63
+
64
+ parse(bytes: Uint8Array): T {
65
+ const reader = new BcsReader(bytes);
66
+ return this.read(reader);
67
+ }
68
+
69
+ fromHex(hex: string) {
70
+ return this.parse(fromHex(hex));
71
+ }
72
+
73
+ fromBase58(b64: string) {
74
+ return this.parse(fromBase58(b64));
75
+ }
76
+
77
+ fromBase64(b64: string) {
78
+ return this.parse(fromBase64(b64));
79
+ }
80
+
81
+ transform<T2 = T, Input2 = Input, NewName extends string = Name>({
82
+ name,
83
+ input,
84
+ output,
85
+ validate,
86
+ }: {
87
+ input?: (val: Input2) => Input;
88
+ output?: (value: T) => T2;
89
+ } & BcsTypeOptions<T2, Input2, NewName>) {
90
+ return new BcsType<T2, Input2, NewName>({
91
+ name: (name ?? this.name) as NewName,
92
+ read: (reader) => (output ? output(this.read(reader)) : (this.read(reader) as never)),
93
+ write: (value, writer) => this.#write(input ? input(value) : (value as never), writer),
94
+ serializedSize: (value) => this.serializedSize(input ? input(value) : (value as never)),
95
+ serialize: (value, options) =>
96
+ this.#serialize(input ? input(value) : (value as never), options),
97
+ validate: (value) => {
98
+ validate?.(value);
99
+ this.validate(input ? input(value) : (value as never));
100
+ },
101
+ });
102
+ }
103
+ }
104
+
105
+ const SERIALIZED_BCS_BRAND = Symbol.for('@haneullabs/serialized-bcs') as never;
106
+ export function isSerializedBcs(obj: unknown): obj is SerializedBcs<unknown> {
107
+ return !!obj && typeof obj === 'object' && (obj as any)[SERIALIZED_BCS_BRAND] === true;
108
+ }
109
+
110
+ export class SerializedBcs<T, Input = T> {
111
+ #schema: BcsType<T, Input>;
112
+ #bytes: Uint8Array<ArrayBuffer>;
113
+
114
+ // Used to brand SerializedBcs so that they can be identified, even between multiple copies
115
+ // of the @haneullabs/bcs package are installed
116
+ get [SERIALIZED_BCS_BRAND]() {
117
+ return true;
118
+ }
119
+
120
+ constructor(schema: BcsType<T, Input>, bytes: Uint8Array<ArrayBuffer>) {
121
+ this.#schema = schema;
122
+ this.#bytes = bytes;
123
+ }
124
+
125
+ toBytes() {
126
+ return this.#bytes;
127
+ }
128
+
129
+ toHex() {
130
+ return toHex(this.#bytes);
131
+ }
132
+
133
+ toBase64() {
134
+ return toBase64(this.#bytes);
135
+ }
136
+
137
+ toBase58() {
138
+ return toBase58(this.#bytes);
139
+ }
140
+
141
+ parse() {
142
+ return this.#schema.parse(this.#bytes);
143
+ }
144
+ }
145
+
146
+ export function fixedSizeBcsType<T, Input = T, const Name extends string = string>({
147
+ size,
148
+ ...options
149
+ }: {
150
+ name: Name;
151
+ size: number;
152
+ read: (reader: BcsReader) => T;
153
+ write: (value: Input, writer: BcsWriter) => void;
154
+ } & BcsTypeOptions<T, Input, Name>) {
155
+ return new BcsType<T, Input, Name>({
156
+ ...options,
157
+ serializedSize: () => size,
158
+ });
159
+ }
160
+
161
+ export function uIntBcsType<const Name extends string = string>({
162
+ readMethod,
163
+ writeMethod,
164
+ ...options
165
+ }: {
166
+ name: Name;
167
+ size: number;
168
+ readMethod: `read${8 | 16 | 32}`;
169
+ writeMethod: `write${8 | 16 | 32}`;
170
+ maxValue: number;
171
+ } & BcsTypeOptions<number, number, Name>) {
172
+ return fixedSizeBcsType<number, number, Name>({
173
+ ...options,
174
+ read: (reader) => reader[readMethod](),
175
+ write: (value, writer) => writer[writeMethod](value),
176
+ validate: (value) => {
177
+ if (value < 0 || value > options.maxValue) {
178
+ throw new TypeError(
179
+ `Invalid ${options.name} value: ${value}. Expected value in range 0-${options.maxValue}`,
180
+ );
181
+ }
182
+ options.validate?.(value);
183
+ },
184
+ });
185
+ }
186
+
187
+ export function bigUIntBcsType<const Name extends string = string>({
188
+ readMethod,
189
+ writeMethod,
190
+ ...options
191
+ }: {
192
+ name: Name;
193
+ size: number;
194
+ readMethod: `read${64 | 128 | 256}`;
195
+ writeMethod: `write${64 | 128 | 256}`;
196
+ maxValue: bigint;
197
+ } & BcsTypeOptions<string, string | number | bigint>) {
198
+ return fixedSizeBcsType<string, string | number | bigint, Name>({
199
+ ...options,
200
+ read: (reader) => reader[readMethod](),
201
+ write: (value, writer) => writer[writeMethod](BigInt(value)),
202
+ validate: (val) => {
203
+ const value = BigInt(val);
204
+ if (value < 0 || value > options.maxValue) {
205
+ throw new TypeError(
206
+ `Invalid ${options.name} value: ${value}. Expected value in range 0-${options.maxValue}`,
207
+ );
208
+ }
209
+ options.validate?.(value);
210
+ },
211
+ });
212
+ }
213
+
214
+ export function dynamicSizeBcsType<T, Input = T, const Name extends string = string>({
215
+ serialize,
216
+ ...options
217
+ }: {
218
+ name: Name;
219
+ read: (reader: BcsReader) => T;
220
+ serialize: (value: Input, options?: BcsWriterOptions) => Uint8Array<ArrayBuffer>;
221
+ } & BcsTypeOptions<T, Input>) {
222
+ const type = new BcsType<T, Input>({
223
+ ...options,
224
+ serialize,
225
+ write: (value, writer) => {
226
+ for (const byte of type.serialize(value).toBytes()) {
227
+ writer.write8(byte);
228
+ }
229
+ },
230
+ });
231
+
232
+ return type;
233
+ }
234
+
235
+ export function stringLikeBcsType<const Name extends string = string>({
236
+ toBytes,
237
+ fromBytes,
238
+ ...options
239
+ }: {
240
+ name: Name;
241
+ toBytes: (value: string) => Uint8Array;
242
+ fromBytes: (bytes: Uint8Array) => string;
243
+ serializedSize?: (value: string) => number | null;
244
+ } & BcsTypeOptions<string, string, Name>) {
245
+ return new BcsType<string, string, Name>({
246
+ ...options,
247
+ read: (reader) => {
248
+ const length = reader.readULEB();
249
+ const bytes = reader.readBytes(length);
250
+
251
+ return fromBytes(bytes);
252
+ },
253
+ write: (hex, writer) => {
254
+ const bytes = toBytes(hex);
255
+ writer.writeULEB(bytes.length);
256
+ for (let i = 0; i < bytes.length; i++) {
257
+ writer.write8(bytes[i]);
258
+ }
259
+ },
260
+ serialize: (value) => {
261
+ const bytes = toBytes(value);
262
+ const size = ulebEncode(bytes.length);
263
+ const result = new Uint8Array(size.length + bytes.length);
264
+ result.set(size, 0);
265
+ result.set(bytes, size.length);
266
+
267
+ return result;
268
+ },
269
+ validate: (value) => {
270
+ if (typeof value !== 'string') {
271
+ throw new TypeError(`Invalid ${options.name} value: ${value}. Expected string`);
272
+ }
273
+ options.validate?.(value);
274
+ },
275
+ });
276
+ }
277
+
278
+ export function lazyBcsType<T, Input>(cb: () => BcsType<T, Input>) {
279
+ let lazyType: BcsType<T, Input> | null = null;
280
+ function getType() {
281
+ if (!lazyType) {
282
+ lazyType = cb();
283
+ }
284
+ return lazyType;
285
+ }
286
+
287
+ return new BcsType<T, Input>({
288
+ name: 'lazy' as never,
289
+ read: (data) => getType().read(data),
290
+ serializedSize: (value) => getType().serializedSize(value),
291
+ write: (value, writer) => getType().write(value, writer),
292
+ serialize: (value, options) => getType().serialize(value, options).toBytes(),
293
+ });
294
+ }
295
+
296
+ export interface BcsStructOptions<
297
+ T extends Record<string, BcsType<any>>,
298
+ Name extends string = string,
299
+ > extends Omit<
300
+ BcsTypeOptions<
301
+ {
302
+ [K in keyof T]: T[K] extends BcsType<infer U, any> ? U : never;
303
+ },
304
+ {
305
+ [K in keyof T]: T[K] extends BcsType<any, infer U> ? U : never;
306
+ },
307
+ Name
308
+ >,
309
+ 'name'
310
+ > {
311
+ name: Name;
312
+ fields: T;
313
+ }
314
+
315
+ export class BcsStruct<
316
+ T extends Record<string, BcsType<any>>,
317
+ const Name extends string = string,
318
+ > extends BcsType<
319
+ {
320
+ [K in keyof T]: T[K] extends BcsType<infer U, any> ? U : never;
321
+ },
322
+ {
323
+ [K in keyof T]: T[K] extends BcsType<any, infer U> ? U : never;
324
+ },
325
+ Name
326
+ > {
327
+ constructor({ name, fields, ...options }: BcsStructOptions<T, Name>) {
328
+ const canonicalOrder = Object.entries(fields);
329
+
330
+ super({
331
+ name,
332
+ serializedSize: (values) => {
333
+ let total = 0;
334
+ for (const [field, type] of canonicalOrder) {
335
+ const size = type.serializedSize(values[field]);
336
+ if (size == null) {
337
+ return null;
338
+ }
339
+
340
+ total += size;
341
+ }
342
+
343
+ return total;
344
+ },
345
+ read: (reader) => {
346
+ const result: Record<string, unknown> = {};
347
+ for (const [field, type] of canonicalOrder) {
348
+ result[field] = type.read(reader);
349
+ }
350
+
351
+ return result as never;
352
+ },
353
+ write: (value, writer) => {
354
+ for (const [field, type] of canonicalOrder) {
355
+ type.write(value[field], writer);
356
+ }
357
+ },
358
+ ...options,
359
+ validate: (value) => {
360
+ options?.validate?.(value);
361
+ if (typeof value !== 'object' || value == null) {
362
+ throw new TypeError(`Expected object, found ${typeof value}`);
363
+ }
364
+ },
365
+ });
366
+ }
367
+ }
368
+
369
+ export interface BcsEnumOptions<
370
+ T extends Record<string, BcsType<any> | null>,
371
+ Name extends string = string,
372
+ > extends Omit<
373
+ BcsTypeOptions<
374
+ EnumOutputShape<{
375
+ [K in keyof T]: T[K] extends BcsType<infer U, any, any> ? U : true;
376
+ }>,
377
+ EnumInputShape<{
378
+ [K in keyof T]: T[K] extends BcsType<any, infer U, any> ? U : boolean | object | null;
379
+ }>,
380
+ Name
381
+ >,
382
+ 'name'
383
+ > {
384
+ name: Name;
385
+ fields: T;
386
+ }
387
+
388
+ export class BcsEnum<
389
+ T extends Record<string, BcsType<any> | null>,
390
+ const Name extends string = string,
391
+ > extends BcsType<
392
+ EnumOutputShape<{
393
+ [K in keyof T]: T[K] extends BcsType<infer U, any> ? U : true;
394
+ }>,
395
+ EnumInputShape<{
396
+ [K in keyof T]: T[K] extends BcsType<any, infer U, any> ? U : boolean | object | null;
397
+ }>,
398
+ Name
399
+ > {
400
+ constructor({ fields, ...options }: BcsEnumOptions<T, Name>) {
401
+ const canonicalOrder = Object.entries(fields as object);
402
+ super({
403
+ read: (reader) => {
404
+ const index = reader.readULEB();
405
+
406
+ const enumEntry = canonicalOrder[index];
407
+ if (!enumEntry) {
408
+ throw new TypeError(`Unknown value ${index} for enum ${options.name}`);
409
+ }
410
+
411
+ const [kind, type] = enumEntry;
412
+
413
+ return {
414
+ [kind]: type?.read(reader) ?? true,
415
+ $kind: kind,
416
+ } as never;
417
+ },
418
+ write: (value, writer) => {
419
+ const [name, val] = Object.entries(value).filter(([name]) =>
420
+ Object.hasOwn(fields, name),
421
+ )[0];
422
+
423
+ for (let i = 0; i < canonicalOrder.length; i++) {
424
+ const [optionName, optionType] = canonicalOrder[i];
425
+ if (optionName === name) {
426
+ writer.writeULEB(i);
427
+ optionType?.write(val, writer);
428
+ return;
429
+ }
430
+ }
431
+ },
432
+ ...options,
433
+ validate: (value) => {
434
+ options?.validate?.(value);
435
+ if (typeof value !== 'object' || value == null) {
436
+ throw new TypeError(`Expected object, found ${typeof value}`);
437
+ }
438
+
439
+ const keys = Object.keys(value).filter(
440
+ (k) => value[k] !== undefined && Object.hasOwn(fields, k),
441
+ );
442
+
443
+ if (keys.length !== 1) {
444
+ throw new TypeError(
445
+ `Expected object with one key, but found ${keys.length} for type ${options.name}}`,
446
+ );
447
+ }
448
+
449
+ const [variant] = keys;
450
+
451
+ if (!Object.hasOwn(fields, variant)) {
452
+ throw new TypeError(`Invalid enum variant ${variant}`);
453
+ }
454
+ },
455
+ });
456
+ }
457
+ }
458
+
459
+ export interface BcsTupleOptions<
460
+ T extends readonly BcsType<any>[],
461
+ Name extends string,
462
+ > extends Omit<
463
+ BcsTypeOptions<
464
+ {
465
+ -readonly [K in keyof T]: T[K] extends BcsType<infer T, any> ? T : never;
466
+ },
467
+ {
468
+ [K in keyof T]: T[K] extends BcsType<any, infer T> ? T : never;
469
+ },
470
+ Name
471
+ >,
472
+ 'name'
473
+ > {
474
+ name?: Name;
475
+ fields: T;
476
+ }
477
+
478
+ export class BcsTuple<
479
+ const T extends readonly BcsType<any>[],
480
+ const Name extends string =
481
+ `(${JoinString<{ [K in keyof T]: T[K] extends BcsType<any, any, infer T> ? T : never }, ', '>})`,
482
+ > extends BcsType<
483
+ {
484
+ -readonly [K in keyof T]: T[K] extends BcsType<infer T, any> ? T : never;
485
+ },
486
+ {
487
+ [K in keyof T]: T[K] extends BcsType<any, infer T> ? T : never;
488
+ },
489
+ Name
490
+ > {
491
+ constructor({ fields, name, ...options }: BcsTupleOptions<T, Name>) {
492
+ super({
493
+ name: name ?? (`(${fields.map((t) => t.name).join(', ')})` as never),
494
+ serializedSize: (values) => {
495
+ let total = 0;
496
+ for (let i = 0; i < fields.length; i++) {
497
+ const size = fields[i].serializedSize(values[i]);
498
+ if (size == null) {
499
+ return null;
500
+ }
501
+
502
+ total += size;
503
+ }
504
+
505
+ return total;
506
+ },
507
+ read: (reader) => {
508
+ const result: unknown[] = [];
509
+ for (const field of fields) {
510
+ result.push(field.read(reader));
511
+ }
512
+ return result as never;
513
+ },
514
+ write: (value, writer) => {
515
+ for (let i = 0; i < fields.length; i++) {
516
+ fields[i].write(value[i], writer);
517
+ }
518
+ },
519
+ ...options,
520
+ validate: (value) => {
521
+ options?.validate?.(value);
522
+ if (!Array.isArray(value)) {
523
+ throw new TypeError(`Expected array, found ${typeof value}`);
524
+ }
525
+ if (value.length !== fields.length) {
526
+ throw new TypeError(`Expected array of length ${fields.length}, found ${value.length}`);
527
+ }
528
+ },
529
+ });
530
+ }
531
+ }