Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/snap-networks-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add helpers `serialize`, `deserialize`, and `Serializable` for round-tripping `BigNumber`, `bigint`, `Uint8Array`, and `undefined` through snap state ([#197](https://github.com/MetaMask/internal-snaps/pull/197))
- Add `InFlightCoalescer`, exported from a new `./dedupe` entry point, which coalesces concurrent async operations by key so callers share one in-flight run ([#149](https://github.com/MetaMask/internal-snaps/pull/149))
- Add origin permission helpers ([#193](https://github.com/MetaMask/internal-snaps/pull/193))
- `createOriginPermissions` for building origin-to-method maps
Expand Down
5 changes: 4 additions & 1 deletion packages/snap-networks-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,16 @@
"@metamask/remote-feature-flag-controller": "^5.0.0",
"@metamask/snaps-sdk": "^11.2.0",
"@metamask/superstruct": "^3.4.1",
"@metamask/utils": "^11.11.0"
"@metamask/utils": "^11.11.0",
"bignumber.js": "^9.3.1",
"lodash": "^4.17.21"
},
"devDependencies": {
"@metamask/auto-changelog": "^6.1.1",
"@metamask/messenger": "^2.0.0",
"@ts-bridge/cli": "^0.6.4",
"@types/jest": "^30.0.0",
"@types/lodash": "^4.17.15",
"deepmerge": "^4.2.2",
"jest": "30.0.3",
"ts-jest": "^29.4.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/snap-networks-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type {
LogMethodDecorator,
LoggerDecorators,
} from './logger';
export { serialize, deserialize } from './serialization/serialization';
export type { Serializable } from './serialization/types';
export {
createOriginPermissions,
DEFAULT_PROD_ORIGINS,
Expand Down
197 changes: 197 additions & 0 deletions packages/snap-networks-utils/src/serialization/serialization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { BigNumber } from 'bignumber.js';

import { deserialize, serialize } from './serialization';

describe('serialize', () => {
it('serializes primitive values', () => {
expect(serialize('test')).toBe('test');
expect(serialize(42)).toBe(42);
expect(serialize(true)).toBe(true);
expect(serialize(null)).toBeNull();
expect(serialize(undefined)).toStrictEqual({ __type: 'undefined' });
});

it('serializes special types', () => {
expect(serialize(BigInt(9007199254740991))).toStrictEqual({
__type: 'bigint',
value: '9007199254740991',
});
expect(serialize(new BigNumber('123456789.123456789'))).toStrictEqual({
__type: 'BigNumber',
value: '123456789.123456789',
});
});

it('serializes arrays with mixed types', () => {
const input = [undefined, new BigNumber('123'), BigInt('456')];
const result = serialize(input);
expect(result).toStrictEqual([
{ __type: 'undefined' },
{ __type: 'BigNumber', value: '123' },
{ __type: 'bigint', value: '456' },
]);
});

it('serializes objects with nested structures', () => {
const input = {
nested: {
bigNumber: new BigNumber('123.456'),
bigint: BigInt('9007199254740991'),
undefined,
},
array: [new BigNumber('789.012'), BigInt('9007199254740992')],
};

const result = serialize(input);

expect(result).toStrictEqual({
nested: {
bigNumber: { __type: 'BigNumber', value: '123.456' },
bigint: { __type: 'bigint', value: '9007199254740991' },
undefined: { __type: 'undefined' },
},
array: [
{ __type: 'BigNumber', value: '789.012' },
{ __type: 'bigint', value: '9007199254740992' },
],
});
});

it('serializes empty objects and arrays', () => {
const input = {
emptyObject: {},
emptyArray: [],
};

const result = serialize(input);

expect(result).toStrictEqual(input);
});

it('serializes deeply nested structures', () => {
const input = {
level1: {
level2: {
level3: {
bigint: BigInt('123'),
undefined,
},
},
},
};

const result = serialize(input);

expect(result).toStrictEqual({
level1: {
level2: {
level3: {
bigint: { __type: 'bigint', value: '123' },
undefined: { __type: 'undefined' },
},
},
},
});
});

it('serializes Uint8Array', () => {
const input = new Uint8Array([1, 2, 3]);
const result = serialize(input);
expect(result).toStrictEqual({
__type: 'Uint8Array',
value: 'AQID',
});
});
});

/* eslint-disable jest/prefer-strict-equal */
describe('deserialize', () => {
it('deserializes primitive values', () => {
expect(deserialize('test')).toBe('test');
expect(deserialize(42)).toBe(42);
expect(deserialize(true)).toBe(true);
expect(deserialize(null)).toBeNull();
});

it('deserializes special serialized types', () => {
expect(deserialize({ __type: 'undefined' })).toBeUndefined();
expect(
deserialize({ __type: 'bigint', value: '9007199254740991' }),
).toStrictEqual(BigInt(9007199254740991));
expect(
deserialize({ __type: 'BigNumber', value: '123456789.123456789' }),
).toStrictEqual(new BigNumber('123456789.123456789'));
});

it('deserializes arrays with mixed types', () => {
expect(
deserialize([
1,
'hello',
true,
null,
{ __type: 'undefined' },
{ __type: 'bigint', value: '9007199254740991' },
{ __type: 'BigNumber', value: '123456789.123456789' },
]),
).toEqual([
1,
'hello',
true,
null,
undefined,
BigInt(9007199254740991),
new BigNumber('123456789.123456789'),
]);
});

it('deserializes objects with nested structures', () => {
const input = {
nested: {
bigNumber: { __type: 'BigNumber', value: '123.456' },
bigint: { __type: 'bigint', value: '9007199254740991' },
undefined: { __type: 'undefined' },
},
array: [
{ __type: 'BigNumber', value: '789.012' },
{ __type: 'bigint', value: '9007199254740992' },
],
};

const result = deserialize(input);

expect(result).toEqual({
nested: {
bigNumber: new BigNumber('123.456'),
bigint: BigInt('9007199254740991'),
undefined,
},
array: [new BigNumber('789.012'), BigInt('9007199254740992')],
});
});

it('handles non-undefined falsy values correctly', () => {
const input = {
zero: 0,
emptyString: '',
falseValue: false,
nullValue: null,
};

const result = deserialize(input);

expect(result).toStrictEqual({
zero: 0,
emptyString: '',
falseValue: false,
nullValue: null,
});
});

it('deserializes Uint8Array', () => {
const input = { __type: 'Uint8Array', value: 'AQID' };
const result = deserialize(input);
expect(result).toStrictEqual(new Uint8Array([1, 2, 3]));
});
});
/* eslint-enable jest/prefer-strict-equal */
85 changes: 85 additions & 0 deletions packages/snap-networks-utils/src/serialization/serialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Json } from '@metamask/snaps-sdk';
import { BigNumber } from 'bignumber.js';
import { cloneDeepWith } from 'lodash';

import type { Serializable } from './types';

/**
* Serializes the passed value to a JSON object so it can be stored in JSON-serializable storage like the snap state and interface context.
* It transforms non-JSON-serializable values into a specific JSON-serializable representation that can be deserialized later.
*
* @param value - The value to serialize.
* @returns The serialized value.
*/
export const serialize = (value: Serializable): Json =>
cloneDeepWith(value, (val: unknown) => {
if (val === undefined) {
return {
__type: 'undefined',
};
}

if (val instanceof BigNumber) {
return {
__type: 'BigNumber',
value: val.toString(),
};
}

if (typeof val === 'bigint') {
return {
__type: 'bigint',
value: val.toString(),
};
}

if (val instanceof Uint8Array) {
const binaryString = Array.from(val, (byte) =>
String.fromCharCode(byte),
).join('');
return {
__type: 'Uint8Array',
value: btoa(binaryString),
};
}

// Return undefined to let lodash handle the cloning of other values
return undefined;
});

/**
* Deserializes the passed value from a JSON object back to its original values.
* It transforms the JSON-serializable representation of non-JSON-serializable values back into their original values.
*
* @param serializedValue - The value to deserialize.
* @returns The deserialized value.
*/
export const deserialize = (serializedValue: Json): Serializable =>
JSON.parse(JSON.stringify(serializedValue), (_key, value) => {
if (!value) {
return value;
}

if (value.__type === 'undefined') {
return undefined;
}

if (value.__type === 'BigNumber') {
return new BigNumber(value.value);
}

if (value.__type === 'bigint') {
return BigInt(value.value);
}

if (value.__type === 'Uint8Array') {
const binaryString = atob(value.value);
const bytes = new Uint8Array(binaryString.length);
for (let index = 0; index < binaryString.length; index++) {
bytes[index] = binaryString.charCodeAt(index);
}
return bytes;
}

return value;
});
17 changes: 17 additions & 0 deletions packages/snap-networks-utils/src/serialization/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Json } from '@metamask/snaps-sdk';
import type { BigNumber } from 'bignumber.js';

/**
* A primitive value that can be serialized to JSON using the `serialize` function.
*/
export type Serializable =
| Json
| undefined
| null
| bigint
| BigNumber
| Uint8Array
| Serializable[]
| {
[prop: string]: Serializable;
};
3 changes: 3 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3314,8 +3314,11 @@ __metadata:
"@metamask/utils": "npm:^11.11.0"
"@ts-bridge/cli": "npm:^0.6.4"
"@types/jest": "npm:^30.0.0"
"@types/lodash": "npm:^4.17.15"
bignumber.js: "npm:^9.3.1"
deepmerge: "npm:^4.2.2"
jest: "npm:30.0.3"
lodash: "npm:^4.17.21"
ts-jest: "npm:^29.4.1"
tsx: "npm:^4.20.5"
typedoc: "npm:^0.25.13"
Expand Down