@wormhole-foundation/sdk-base 0.1.0-beta.3

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.
@@ -0,0 +1,140 @@
1
+ //TODO:
2
+ // * make FixedItem recursive
3
+ // * implement a swtich layout item that maps different values (versions) to different sublayouts
4
+ // * implement a method that determines the total size of a layout, if all items have known size
5
+ // * implement a method that determines the offsets of items in a layout (if all preceding items
6
+ // have known, fixed size (i.e. no arrays))
7
+ // * leverage the above to implement deserialization of just a set of fields of a layout
8
+ // * implement a method that takes several layouts and a serialized piece of data and quickly
9
+ // determines which layouts this payload conforms to (might be 0 or even all!). Should leverage
10
+ // the above methods and fixed values in the layout to quickly exclude candidates.
11
+ // * implement a method that allows "raw" serialization and deserialization" i.e. that skips all the
12
+ // custom conversions (should only be used for testing!) or even just partitions i.e. slices
13
+ // the encoded Uint8Array
14
+
15
+ export type PrimitiveType = number | bigint | Uint8Array;
16
+ export const isPrimitiveType = (x: any): x is PrimitiveType =>
17
+ typeof x === "number" || typeof x === "bigint" || x instanceof Uint8Array;
18
+
19
+ export type BinaryLiterals = "uint" | "bytes" | "array" | "object";
20
+
21
+ //Why only a max value of 2**(6*8)?
22
+ //quote from here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#description
23
+ //"In a similar sense, numbers around the magnitude of Number.MAX_SAFE_INTEGER will suffer from
24
+ // loss of precision and make Number.isInteger return true even when it's not an integer.
25
+ // (The actual threshold varies based on how many bits are needed to represent the decimal — for
26
+ // example, Number.isInteger(4500000000000000.1) is true, but
27
+ // Number.isInteger(4500000000000000.5) is false.)"
28
+ //So we are being conservative and just stay away from threshold.
29
+ export type NumberSize = 1 | 2 | 3 | 4 | 5 | 6;
30
+ export const numberMaxSize = 6;
31
+
32
+ export type UintSizeToPrimitive<Size extends number> =
33
+ Size extends NumberSize ? number : bigint;
34
+
35
+ export type FixedConversion<FromType extends PrimitiveType, ToType> = {
36
+ readonly to: ToType,
37
+ readonly from: FromType,
38
+ };
39
+
40
+ export type CustomConversion<FromType extends PrimitiveType, ToType> = {
41
+ readonly to: (val: FromType) => ToType,
42
+ readonly from: (val: ToType) => FromType,
43
+ };
44
+
45
+ interface LayoutItemBase<BL extends BinaryLiterals> {
46
+ readonly name: string,
47
+ readonly binary: BL,
48
+ };
49
+
50
+ interface PrimitiveFixedCustom<T extends PrimitiveType> {
51
+ custom: T,
52
+ omit?: boolean
53
+ };
54
+
55
+ interface OptionalToFromCustom<T extends PrimitiveType> {
56
+ custom?: FixedConversion<T, any> | CustomConversion<T, any>
57
+ };
58
+
59
+ //size: number of bytes used to encode the item
60
+ interface UintLayoutItemBase<T extends number | bigint> extends LayoutItemBase<"uint"> {
61
+ size: T extends bigint ? number : NumberSize,
62
+ };
63
+
64
+ export interface PrimitiveFixedUintLayoutItem<T extends number | bigint>
65
+ extends UintLayoutItemBase<T>, PrimitiveFixedCustom<T> {};
66
+
67
+ export interface OptionalToFromUintLayoutItem<T extends number | bigint>
68
+ extends UintLayoutItemBase<T>, OptionalToFromCustom<T> {};
69
+
70
+ export interface FixedPrimitiveBytesLayoutItem
71
+ extends LayoutItemBase<"bytes">, PrimitiveFixedCustom<Uint8Array> {};
72
+
73
+ export interface FixedValueBytesLayoutItem extends LayoutItemBase<"bytes"> {
74
+ readonly custom: FixedConversion<Uint8Array, any>,
75
+ };
76
+
77
+ export interface FixedSizeBytesLayoutItem extends LayoutItemBase<"bytes"> {
78
+ readonly size: number,
79
+ readonly custom?: CustomConversion<Uint8Array, any>,
80
+ };
81
+
82
+ //length size: number of bytes used to encode the preceeding length field which in turn
83
+ // hold either the number of bytes (for bytes) or elements (for array)
84
+ // undefined means it will consume the rest of the data
85
+ export interface LengthPrefixedBytesLayoutItem extends LayoutItemBase<"bytes"> {
86
+ readonly lengthSize?: NumberSize,
87
+ readonly custom?: CustomConversion<Uint8Array, any>,
88
+ };
89
+
90
+ export interface ArrayLayoutItem extends LayoutItemBase<"array"> {
91
+ readonly lengthSize?: NumberSize,
92
+ readonly layout: Layout,
93
+ };
94
+
95
+ export interface ObjectLayoutItem extends LayoutItemBase<"object"> {
96
+ readonly layout: Layout,
97
+ }
98
+
99
+ export type UintLayoutItem = |
100
+ PrimitiveFixedUintLayoutItem<number> |
101
+ OptionalToFromUintLayoutItem<number> |
102
+ PrimitiveFixedUintLayoutItem<bigint> |
103
+ OptionalToFromUintLayoutItem<bigint>;
104
+ export type BytesLayoutItem = |
105
+ FixedPrimitiveBytesLayoutItem |
106
+ FixedValueBytesLayoutItem |
107
+ FixedSizeBytesLayoutItem |
108
+ LengthPrefixedBytesLayoutItem;
109
+ export type LayoutItem = UintLayoutItem | BytesLayoutItem | ArrayLayoutItem | ObjectLayoutItem;
110
+ export type Layout = readonly LayoutItem[];
111
+
112
+ type NameOrOmitted<T extends { name: PropertyKey }> = T extends {omit: true} ? never : T["name"];
113
+
114
+ //the order of the checks matters here!
115
+ // if FixedConversion was tested for first, its ToType would erroneously be inferred to be a
116
+ // the `to` function that actually belongs to a CustomConversion
117
+ // unclear why this happens, seeing how the `from` type wouldn't fit but it happened nonetheless
118
+ export type LayoutToType<L extends Layout> =
119
+ { readonly [I in L[number] as NameOrOmitted<I>]: LayoutItemToType<I> };
120
+
121
+ export type LayoutItemToType<I extends LayoutItem> =
122
+ [I] extends [ArrayLayoutItem]
123
+ ? LayoutToType<I["layout"]>[]
124
+ : [I] extends [ObjectLayoutItem]
125
+ ? LayoutToType<I["layout"]>
126
+ : [I] extends [UintLayoutItem]
127
+ ? I["custom"] extends number | bigint
128
+ ? I["custom"]
129
+ : I["custom"] extends CustomConversion<any, infer ToType>
130
+ ? ToType
131
+ : I["custom"] extends FixedConversion<any, infer ToType>
132
+ ? ToType
133
+ : UintSizeToPrimitive<I["size"]>
134
+ : [I] extends [BytesLayoutItem]
135
+ ? I["custom"] extends CustomConversion<any, infer ToType>
136
+ ? ToType
137
+ : I["custom"] extends FixedConversion<any, infer ToType>
138
+ ? ToType
139
+ : Uint8Array
140
+ : never;
@@ -0,0 +1,191 @@
1
+ import {
2
+ Layout,
3
+ LayoutItem,
4
+ LayoutToType,
5
+ LayoutItemToType,
6
+ FixedSizeBytesLayoutItem,
7
+ LengthPrefixedBytesLayoutItem,
8
+ FixedPrimitiveBytesLayoutItem,
9
+ FixedValueBytesLayoutItem,
10
+ CustomConversion,
11
+ numberMaxSize,
12
+ } from "./layout";
13
+ import { checkUint8ArrayDeeplyEqual, checkUint8ArraySize, checkUintEquals } from "./utils";
14
+
15
+ export function serializeLayout<const L extends Layout>(
16
+ layout: L,
17
+ data: LayoutToType<L>,
18
+ ): Uint8Array;
19
+
20
+ export function serializeLayout<const L extends Layout>(
21
+ layout: L,
22
+ data: LayoutToType<L>,
23
+ encoded: Uint8Array,
24
+ offset?: number,
25
+ ): number;
26
+
27
+ export function serializeLayout<const L extends Layout>(
28
+ layout: L,
29
+ data: LayoutToType<L>,
30
+ encoded?: Uint8Array,
31
+ offset = 0,
32
+ ): Uint8Array | number {
33
+ let ret = encoded ?? new Uint8Array(calcLayoutSize(layout, data));
34
+ for (let i = 0; i < layout.length; ++i)
35
+ offset = serializeLayoutItem(layout[i], data[layout[i].name as keyof typeof data], ret, offset);
36
+
37
+ return encoded === undefined ? ret : offset;
38
+ }
39
+
40
+ const calcLayoutSize = (
41
+ layout: Layout,
42
+ data: LayoutToType<typeof layout>
43
+ ): number =>
44
+ layout.reduce((acc: number, item: LayoutItem) => {
45
+ switch (item.binary) {
46
+ case "object": {
47
+ return acc + calcLayoutSize(item.layout, data[item.name] as LayoutItemToType<typeof item>)
48
+ }
49
+ case "array": {
50
+ if (item.lengthSize !== undefined)
51
+ acc += item.lengthSize;
52
+
53
+ const narrowedData = data[item.name] as LayoutItemToType<typeof item>;
54
+ for (let i = 0; i < narrowedData.length; ++i)
55
+ acc += calcLayoutSize(item.layout, narrowedData[i]);
56
+
57
+ return acc;
58
+ }
59
+ case "bytes": {
60
+ if (item.custom !== undefined) {
61
+ if (item.custom instanceof Uint8Array)
62
+ return acc + item.custom.length;
63
+ else if (item.custom.from instanceof Uint8Array)
64
+ return acc + item.custom.from.length;
65
+ }
66
+
67
+ item = item as FixedSizeBytesLayoutItem | LengthPrefixedBytesLayoutItem;
68
+
69
+ if ("size" in item && item.size !== undefined)
70
+ return acc + item.size;
71
+
72
+ if (item.lengthSize !== undefined)
73
+ acc += item.lengthSize;
74
+
75
+ return acc + (
76
+ (item.custom !== undefined)
77
+ ? item.custom.from(data[item.name])
78
+ : (data[item.name] as LayoutItemToType<typeof item>)
79
+ ).length;
80
+ }
81
+ case "uint": {
82
+ return acc + item.size;
83
+ }
84
+ }
85
+ },
86
+ 0
87
+ );
88
+
89
+ //Wormhole uses big endian by default for all uints
90
+ //endianess can be easily added to UintLayout items if necessary
91
+ export function serializeUint(
92
+ encoded: Uint8Array,
93
+ offset: number,
94
+ val: number | bigint,
95
+ bytes: number,
96
+ ): number {
97
+ if (val < 0 || (typeof val === "number" && !Number.isInteger(val)))
98
+ throw new Error(`Value ${val} is not an unsigned integer`);
99
+
100
+ if (bytes > numberMaxSize && typeof val === "number" && val >= 2**(numberMaxSize * 8))
101
+ throw new Error(`Value ${val} is too large to be safely converted into an integer`);
102
+
103
+ if (val >= 2n ** BigInt(bytes * 8))
104
+ throw new Error(`Value ${val} is too large for ${bytes} bytes`);
105
+
106
+ //big endian byte order
107
+ for (let i = 0; i < bytes; ++i)
108
+ encoded[offset + i] = Number((BigInt(val) >> BigInt(8*(bytes-i-1)) & 0xffn));
109
+
110
+ return offset + bytes;
111
+ }
112
+
113
+ function serializeLayoutItem(
114
+ item: LayoutItem,
115
+ data: any,
116
+ encoded: Uint8Array,
117
+ offset: number
118
+ ): number {
119
+ try {
120
+ switch (item.binary) {
121
+ case "object": {
122
+ for (const i of item.layout)
123
+ offset = serializeLayoutItem(i, data[i.name], encoded, offset);
124
+
125
+ break;
126
+ }
127
+ case "array": {
128
+ if (item.lengthSize !== undefined)
129
+ offset = serializeUint(encoded, offset, data.length, item.lengthSize);
130
+
131
+ for (let i = 0; i < data.length; ++i)
132
+ offset = serializeLayout(item.layout, data[i], encoded, offset);
133
+
134
+ break;
135
+ }
136
+ case "bytes": {
137
+ const value = (() => {
138
+ if (item.custom !== undefined) {
139
+ if (item.custom instanceof Uint8Array) {
140
+ if (!(item as { omit?: boolean })?.omit)
141
+ checkUint8ArrayDeeplyEqual(item.custom, data);
142
+ return item.custom;
143
+ }
144
+ if (item.custom.from instanceof Uint8Array)
145
+ //no proper way to deeply check equality of item.custom.to and data in JS
146
+ return item.custom.from;
147
+ }
148
+
149
+ item = item as
150
+ Exclude<typeof item, FixedPrimitiveBytesLayoutItem | FixedValueBytesLayoutItem>;
151
+ const ret = item.custom !== undefined ? item.custom.from(data) : data;
152
+ if ("size" in item && item.size !== undefined)
153
+ checkUint8ArraySize(ret, item.size);
154
+ else if (item.lengthSize !== undefined)
155
+ offset = serializeUint(encoded, offset, ret.length, item.lengthSize);
156
+
157
+ return ret;
158
+ })();
159
+
160
+ encoded.set(value, offset);
161
+ offset += value.length;
162
+ break;
163
+ }
164
+ case "uint": {
165
+ const value = (() => {
166
+ if (item.custom !== undefined) {
167
+ if (typeof item.custom == "number" || typeof item.custom === "bigint") {
168
+ if (!(item as { omit?: boolean })?.omit)
169
+ checkUintEquals(item.custom, data);
170
+ return item.custom;
171
+ }
172
+ if (typeof item.custom.from == "number" || typeof item.custom.from === "bigint")
173
+ //no proper way to deeply check equality of item.custom.to and data in JS
174
+ return item.custom.from;
175
+ }
176
+
177
+ type narrowedCustom = CustomConversion<number, any> | CustomConversion<bigint, any>;
178
+ return item.custom !== undefined ? (item.custom as narrowedCustom).from(data) : data;
179
+ })();
180
+
181
+ offset = serializeUint(encoded, offset, value, item.size);
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ catch (e) {
187
+ (e as Error).message = `when serializing item '${item.name}': ${(e as Error).message}`;
188
+ throw e;
189
+ }
190
+ return offset;
191
+ };
@@ -0,0 +1,23 @@
1
+ export const checkUint8ArraySize = (custom: Uint8Array, size: number): void => {
2
+ if (custom.length !== size)
3
+ throw new Error(
4
+ `binary size mismatch: layout size: ${custom.length}, data size: ${size}`
5
+ );
6
+ }
7
+
8
+ export const checkUintEquals = (custom: number | bigint, data: number | bigint): void => {
9
+ if (custom != data)
10
+ throw new Error(
11
+ `value mismatch: (constant) layout value: ${custom}, data value: ${data}`
12
+ );
13
+ }
14
+
15
+ export const checkUint8ArrayDeeplyEqual = (custom: Uint8Array, data: Uint8Array): void => {
16
+ checkUint8ArraySize(custom, data.length);
17
+
18
+ for (let i = 0; i < custom.length; ++i)
19
+ if (custom[i] !== data[i])
20
+ throw new Error(
21
+ `binary data mismatch: layout value: ${custom}, data value: ${data}`
22
+ );
23
+ }