Skip to content

Commit 7a48d4e

Browse files
Merge branch 'deser-lib'
2 parents ab96af0 + c9d7a38 commit 7a48d4e

File tree

14 files changed

+734
-3
lines changed

14 files changed

+734
-3
lines changed

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
/modules/sdk-api/ @BitGo/custody-experience @BitGo/wallet-platform
7676
/modules/sdk-core/ @BitGo/wallet-platform @BitGo/hsm
7777
/modules/sdk-lib-mpc/ @BitGo/wallet-platform @BitGo/hsm
78+
/modules/deser-lib/ @BitGo/wallet-platform @BitGo/hsm
7879
/modules/sdk-rpc-wrapper @BitGo/ethalt-team
7980
/modules/sdk-test/ @BitGo/custody-experience @BitGo/wallet-platform
8081
/modules/sdk-unified-wallet @BitGo/ethalt-team

modules/deser-lib/.eslintignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.idea
3+
public
4+
dist
5+

modules/deser-lib/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
.idea/
3+
dist/
4+
yarn-error.log

modules/deser-lib/.mocharc.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"require": ["ts-node/register", "should"],
3+
"timeout": "20000",
4+
"reporter": "min",
5+
"reporter-option": ["cdn=true", "json=false"],
6+
"exit": true,
7+
"spec": ["test/unit/**/*.ts"],
8+
"extension": [".js", ".ts"]
9+
}

modules/deser-lib/.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.nyc_output/
2+
dist/

modules/deser-lib/.prettierrc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
printWidth: 120
2+
singleQuote: true
3+
trailingComma: 'es5'

modules/deser-lib/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Deser Lib
2+
3+
This library will be used to centralize all the serialization and de-serialization schemes used in the bitgojs modules.

modules/deser-lib/package.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@bitgo/deser-lib",
3+
"version": "1.0.0",
4+
"description": "BitGo serialization and deseralization library",
5+
"main": "./dist/src/index.js",
6+
"types": "./dist/src/index.d.ts",
7+
"scripts": {
8+
"test": "yarn unit-test",
9+
"unit-test": "nyc -- mocha --recursive test",
10+
"build": "yarn tsc --build --incremental --verbose .",
11+
"fmt": "prettier --write .",
12+
"check-fmt": "prettier --check .",
13+
"clean": "rm -r ./dist",
14+
"lint": "eslint --quiet .",
15+
"prepare": "npm run build"
16+
},
17+
"repository": {
18+
"type": "git",
19+
"url": "git+https://github.com/BitGo/BitGoJS.git",
20+
"directory": "modules/deser-lib"
21+
},
22+
"author": "BitGo SDK Team <[email protected]>",
23+
"license": "MIT",
24+
"bugs": {
25+
"url": "https://github.com/bitgo/bitgojs/issues"
26+
},
27+
"homepage": "https://github.com/bitgo/bitgojs#readme",
28+
"nyc": {
29+
"extension": [
30+
".ts"
31+
]
32+
},
33+
"lint-staged": {
34+
"*.{js,ts}": [
35+
"yarn prettier --write",
36+
"yarn eslint --fix"
37+
]
38+
},
39+
"publishConfig": {
40+
"access": "public"
41+
},
42+
"dependencies": {
43+
"cbor": "^9.0.1"
44+
}
45+
}

modules/deser-lib/src/cbor.ts

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { decodeFirstSync, encodeCanonical } from 'cbor';
2+
3+
/**
4+
* Return a string describing value as a type.
5+
* @param value - Any javascript value to type.
6+
* @returns String describing value type.
7+
*/
8+
function getType(value: unknown): string {
9+
if (value === null || value === undefined) {
10+
return 'null';
11+
}
12+
if (value instanceof Array) {
13+
const types = value.map(getType);
14+
if (!types.slice(1).every((value) => value === types[0])) {
15+
throw new Error('Array elements are not of the same type');
16+
}
17+
return JSON.stringify([types[0]]);
18+
}
19+
if (value instanceof Object) {
20+
const properties = Object.getOwnPropertyNames(value);
21+
properties.sort();
22+
return JSON.stringify(
23+
properties.reduce((acc, name) => {
24+
acc[name] = getType(value[name]);
25+
return acc;
26+
}, {})
27+
);
28+
}
29+
if (typeof value === 'string') {
30+
if (value.startsWith('0x')) {
31+
return 'bytes';
32+
}
33+
return 'string';
34+
}
35+
return JSON.stringify(typeof value);
36+
}
37+
38+
/**
39+
* Compare two buffers for sorting.
40+
* @param a - left buffer to compare to right buffer.
41+
* @param b - right buffer to compare to left buffer.
42+
* @returns Negative if a < b, positive if b > a, 0 if equal.
43+
*/
44+
function bufferCompare(a: Buffer, b: Buffer): number {
45+
let i = 0;
46+
while (i < a.length && i < b.length && a[i] == b[i]) {
47+
i++;
48+
}
49+
if (i === a.length && i === b.length) {
50+
return 0;
51+
}
52+
if (i === a.length || i === b.length) {
53+
return a.length - b.length;
54+
}
55+
return a[i] - b[i];
56+
}
57+
58+
/** A sortable array element. */
59+
type Sortable = {
60+
weight: number;
61+
value?: unknown;
62+
};
63+
64+
/**
65+
* Type check for sortable array element.
66+
* @param value - Value to type check.
67+
* @returns True if value is a sortable array element.
68+
*/
69+
function isSortable(value: unknown): value is Sortable {
70+
return value instanceof Object && 'weight' in value && typeof (value as Sortable).weight === 'number';
71+
}
72+
73+
/**
74+
* Convert number to base 256 and return as a big-endian Buffer.
75+
* @param value - Value to convert.
76+
* @returns Buffer representation of the number.
77+
*/
78+
function numberToBufferBE(value: number): Buffer {
79+
// Normalize value so that negative numbers aren't compared higher
80+
// than positive numbers when accounting for two's complement.
81+
value += Math.pow(2, 52);
82+
const byteCount = Math.floor((value.toString(2).length + 7) / 8);
83+
const buffer = Buffer.alloc(byteCount);
84+
let i = 0;
85+
while (value) {
86+
buffer[i++] = value % 256;
87+
value = Math.floor(value / 256);
88+
}
89+
return buffer.reverse();
90+
}
91+
92+
/**
93+
* Compare two array elements for sorting.
94+
* @param a - left element to compare to right element.
95+
* @param b - right element to compare to left element.
96+
* @returns Negative if a < b, positive if b > a, 0 if equal.
97+
*/
98+
function elementCompare(a: unknown, b: unknown): number {
99+
if (!isSortable(a) || !isSortable(b)) {
100+
throw new Error('Array elements must be sortable');
101+
}
102+
if (a.weight === b.weight) {
103+
if (a.value === undefined && b.value === undefined) {
104+
throw new Error('Array elements must be sortable');
105+
}
106+
const aVal = transform(a.value);
107+
const bVal = transform(b.value);
108+
if (
109+
(!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') ||
110+
(!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number')
111+
) {
112+
throw new Error('Array element value cannot be compared');
113+
}
114+
let aBuf, bBuf;
115+
if (typeof aVal === 'number') {
116+
aBuf = numberToBufferBE(aVal);
117+
} else {
118+
aBuf = Buffer.from(aVal);
119+
}
120+
if (typeof bVal === 'number') {
121+
bBuf = numberToBufferBE(bVal);
122+
} else {
123+
bBuf = Buffer.from(bVal);
124+
}
125+
return bufferCompare(aBuf, bBuf);
126+
}
127+
return a.weight - b.weight;
128+
}
129+
130+
/**
131+
* Transform value into its canonical, serializable form.
132+
* @param value - Value to transform.
133+
* @returns Canonical, serializable form of value.
134+
*/
135+
export function transform<T>(value: T): T | Buffer {
136+
if (value === null || value === undefined) {
137+
return value;
138+
}
139+
if (typeof value === 'string') {
140+
// Transform hex strings to buffers.
141+
if (value.startsWith('0x')) {
142+
if (!value.match(/^0x([0-9a-fA-F]{2})*$/)) {
143+
throw new Error('0x prefixed string contains non-hex characters.');
144+
}
145+
return Buffer.from(value.slice(2), 'hex');
146+
} else if (value.startsWith('\\0x')) {
147+
return value.slice(1) as unknown as T;
148+
}
149+
} else if (value instanceof Array) {
150+
// Enforce array elements are same type.
151+
getType(value);
152+
value = [...value] as unknown as T;
153+
(value as unknown as Array<unknown>).sort(elementCompare);
154+
return (value as unknown as Array<unknown>).map(transform) as unknown as T;
155+
} else if (value instanceof Object) {
156+
const properties = Object.getOwnPropertyNames(value);
157+
properties.sort();
158+
return properties.reduce((acc, name) => {
159+
acc[name] = transform(value[name]);
160+
return acc;
161+
}, {}) as unknown as T;
162+
}
163+
return value;
164+
}
165+
166+
/**
167+
* Untransform value into its human readable form.
168+
* @param value - Value to untransform.
169+
* @returns Untransformed, human readable form of value.
170+
*/
171+
export function untransform<T>(value: T): T | string {
172+
if (Buffer.isBuffer(value)) {
173+
return '0x' + value.toString('hex');
174+
} else if (typeof value === 'string') {
175+
if (value.startsWith('0x')) {
176+
return '\\' + value;
177+
}
178+
} else if (value instanceof Array && value.length > 1) {
179+
for (let i = 1; i < value.length; i++) {
180+
if (value[i - 1].weight > value[i].weight) {
181+
throw new Error('Array elements are not in canonical order');
182+
}
183+
}
184+
return value.map(untransform) as unknown as T;
185+
} else if (value instanceof Object) {
186+
const properties = Object.getOwnPropertyNames(value);
187+
for (let i = 1; i < properties.length; i++) {
188+
if (properties[i - 1].localeCompare(properties[i]) > 0) {
189+
throw new Error('Object properties are not in caonical order');
190+
}
191+
}
192+
return properties.reduce((acc, name) => {
193+
acc[name] = untransform(value[name]);
194+
return acc;
195+
}, {}) as unknown as T;
196+
}
197+
return value;
198+
}
199+
200+
/**
201+
* Serialize a value.
202+
* @param value - Value to serialize.
203+
* @returns Buffer representing serialized value.
204+
*/
205+
export function serialize<T>(value: T): Buffer {
206+
return encodeCanonical(transform(value));
207+
}
208+
209+
/**
210+
* Deserialize a value.
211+
* @param value - Buffer to deserialize.
212+
* @returns Deserialized value.
213+
*/
214+
export function deserialize(value: Buffer): unknown {
215+
return untransform(decodeFirstSync(value));
216+
}

modules/deser-lib/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as Cbor from './cbor';

0 commit comments

Comments
 (0)