Skip to content

Commit

Permalink
Merge branch 'deser-lib'
Browse files Browse the repository at this point in the history
  • Loading branch information
johnoliverdriscoll committed Jan 19, 2024
2 parents ab96af0 + c9d7a38 commit 7a48d4e
Show file tree
Hide file tree
Showing 14 changed files with 734 additions and 3 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
/modules/sdk-api/ @BitGo/custody-experience @BitGo/wallet-platform
/modules/sdk-core/ @BitGo/wallet-platform @BitGo/hsm
/modules/sdk-lib-mpc/ @BitGo/wallet-platform @BitGo/hsm
/modules/deser-lib/ @BitGo/wallet-platform @BitGo/hsm
/modules/sdk-rpc-wrapper @BitGo/ethalt-team
/modules/sdk-test/ @BitGo/custody-experience @BitGo/wallet-platform
/modules/sdk-unified-wallet @BitGo/ethalt-team
Expand Down
5 changes: 5 additions & 0 deletions modules/deser-lib/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.idea
public
dist

4 changes: 4 additions & 0 deletions modules/deser-lib/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
.idea/
dist/
yarn-error.log
9 changes: 9 additions & 0 deletions modules/deser-lib/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"require": ["ts-node/register", "should"],
"timeout": "20000",
"reporter": "min",
"reporter-option": ["cdn=true", "json=false"],
"exit": true,
"spec": ["test/unit/**/*.ts"],
"extension": [".js", ".ts"]
}
2 changes: 2 additions & 0 deletions modules/deser-lib/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.nyc_output/
dist/
3 changes: 3 additions & 0 deletions modules/deser-lib/.prettierrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
printWidth: 120
singleQuote: true
trailingComma: 'es5'
3 changes: 3 additions & 0 deletions modules/deser-lib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Deser Lib

This library will be used to centralize all the serialization and de-serialization schemes used in the bitgojs modules.
45 changes: 45 additions & 0 deletions modules/deser-lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@bitgo/deser-lib",
"version": "1.0.0",
"description": "BitGo serialization and deseralization library",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"scripts": {
"test": "yarn unit-test",
"unit-test": "nyc -- mocha --recursive test",
"build": "yarn tsc --build --incremental --verbose .",
"fmt": "prettier --write .",
"check-fmt": "prettier --check .",
"clean": "rm -r ./dist",
"lint": "eslint --quiet .",
"prepare": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/BitGo/BitGoJS.git",
"directory": "modules/deser-lib"
},
"author": "BitGo SDK Team <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/bitgo/bitgojs/issues"
},
"homepage": "https://github.com/bitgo/bitgojs#readme",
"nyc": {
"extension": [
".ts"
]
},
"lint-staged": {
"*.{js,ts}": [
"yarn prettier --write",
"yarn eslint --fix"
]
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"cbor": "^9.0.1"
}
}
216 changes: 216 additions & 0 deletions modules/deser-lib/src/cbor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { decodeFirstSync, encodeCanonical } from 'cbor';

/**
* Return a string describing value as a type.
* @param value - Any javascript value to type.
* @returns String describing value type.
*/
function getType(value: unknown): string {
if (value === null || value === undefined) {
return 'null';
}
if (value instanceof Array) {
const types = value.map(getType);
if (!types.slice(1).every((value) => value === types[0])) {
throw new Error('Array elements are not of the same type');
}
return JSON.stringify([types[0]]);
}
if (value instanceof Object) {
const properties = Object.getOwnPropertyNames(value);
properties.sort();
return JSON.stringify(
properties.reduce((acc, name) => {
acc[name] = getType(value[name]);
return acc;
}, {})
);
}
if (typeof value === 'string') {
if (value.startsWith('0x')) {
return 'bytes';
}
return 'string';
}
return JSON.stringify(typeof value);
}

/**
* Compare two buffers for sorting.
* @param a - left buffer to compare to right buffer.
* @param b - right buffer to compare to left buffer.
* @returns Negative if a < b, positive if b > a, 0 if equal.
*/
function bufferCompare(a: Buffer, b: Buffer): number {
let i = 0;
while (i < a.length && i < b.length && a[i] == b[i]) {
i++;
}
if (i === a.length && i === b.length) {
return 0;
}
if (i === a.length || i === b.length) {
return a.length - b.length;
}
return a[i] - b[i];
}

/** A sortable array element. */
type Sortable = {
weight: number;
value?: unknown;
};

/**
* Type check for sortable array element.
* @param value - Value to type check.
* @returns True if value is a sortable array element.
*/
function isSortable(value: unknown): value is Sortable {
return value instanceof Object && 'weight' in value && typeof (value as Sortable).weight === 'number';
}

/**
* Convert number to base 256 and return as a big-endian Buffer.
* @param value - Value to convert.
* @returns Buffer representation of the number.
*/
function numberToBufferBE(value: number): Buffer {
// Normalize value so that negative numbers aren't compared higher
// than positive numbers when accounting for two's complement.
value += Math.pow(2, 52);
const byteCount = Math.floor((value.toString(2).length + 7) / 8);
const buffer = Buffer.alloc(byteCount);
let i = 0;
while (value) {
buffer[i++] = value % 256;
value = Math.floor(value / 256);
}
return buffer.reverse();
}

/**
* Compare two array elements for sorting.
* @param a - left element to compare to right element.
* @param b - right element to compare to left element.
* @returns Negative if a < b, positive if b > a, 0 if equal.
*/
function elementCompare(a: unknown, b: unknown): number {
if (!isSortable(a) || !isSortable(b)) {
throw new Error('Array elements must be sortable');
}
if (a.weight === b.weight) {
if (a.value === undefined && b.value === undefined) {
throw new Error('Array elements must be sortable');
}
const aVal = transform(a.value);
const bVal = transform(b.value);
if (
(!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') ||
(!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number')
) {
throw new Error('Array element value cannot be compared');
}
let aBuf, bBuf;
if (typeof aVal === 'number') {
aBuf = numberToBufferBE(aVal);
} else {
aBuf = Buffer.from(aVal);
}
if (typeof bVal === 'number') {
bBuf = numberToBufferBE(bVal);
} else {
bBuf = Buffer.from(bVal);
}
return bufferCompare(aBuf, bBuf);
}
return a.weight - b.weight;
}

/**
* Transform value into its canonical, serializable form.
* @param value - Value to transform.
* @returns Canonical, serializable form of value.
*/
export function transform<T>(value: T): T | Buffer {
if (value === null || value === undefined) {
return value;
}
if (typeof value === 'string') {
// Transform hex strings to buffers.
if (value.startsWith('0x')) {
if (!value.match(/^0x([0-9a-fA-F]{2})*$/)) {
throw new Error('0x prefixed string contains non-hex characters.');
}
return Buffer.from(value.slice(2), 'hex');
} else if (value.startsWith('\\0x')) {
return value.slice(1) as unknown as T;
}
} else if (value instanceof Array) {
// Enforce array elements are same type.
getType(value);
value = [...value] as unknown as T;
(value as unknown as Array<unknown>).sort(elementCompare);
return (value as unknown as Array<unknown>).map(transform) as unknown as T;
} else if (value instanceof Object) {
const properties = Object.getOwnPropertyNames(value);
properties.sort();
return properties.reduce((acc, name) => {
acc[name] = transform(value[name]);
return acc;
}, {}) as unknown as T;
}
return value;
}

/**
* Untransform value into its human readable form.
* @param value - Value to untransform.
* @returns Untransformed, human readable form of value.
*/
export function untransform<T>(value: T): T | string {
if (Buffer.isBuffer(value)) {
return '0x' + value.toString('hex');
} else if (typeof value === 'string') {
if (value.startsWith('0x')) {
return '\\' + value;
}
} else if (value instanceof Array && value.length > 1) {
for (let i = 1; i < value.length; i++) {
if (value[i - 1].weight > value[i].weight) {
throw new Error('Array elements are not in canonical order');
}
}
return value.map(untransform) as unknown as T;
} else if (value instanceof Object) {
const properties = Object.getOwnPropertyNames(value);
for (let i = 1; i < properties.length; i++) {
if (properties[i - 1].localeCompare(properties[i]) > 0) {
throw new Error('Object properties are not in caonical order');
}
}
return properties.reduce((acc, name) => {
acc[name] = untransform(value[name]);
return acc;
}, {}) as unknown as T;
}
return value;
}

/**
* Serialize a value.
* @param value - Value to serialize.
* @returns Buffer representing serialized value.
*/
export function serialize<T>(value: T): Buffer {
return encodeCanonical(transform(value));
}

/**
* Deserialize a value.
* @param value - Buffer to deserialize.
* @returns Deserialized value.
*/
export function deserialize(value: Buffer): unknown {
return untransform(decodeFirstSync(value));
}
1 change: 1 addition & 0 deletions modules/deser-lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as Cbor from './cbor';
Loading

0 comments on commit 7a48d4e

Please sign in to comment.