Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement isomorphic utils for nodejs and browser #7060

Merged
merged 3 commits into from
Aug 30, 2024
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
33 changes: 3 additions & 30 deletions packages/utils/src/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {toBufferLE, toBigIntLE, toBufferBE, toBigIntBE} from "bigint-buffer";
type Endianness = "le" | "be";

const hexByByte: string[] = [];
/**
* @deprecated Use toHex() instead.
*/
export function toHexString(bytes: Uint8Array): string {
let hex = "0x";
for (const byte of bytes) {
Expand Down Expand Up @@ -45,33 +48,3 @@ export function bytesToBigInt(value: Uint8Array, endianness: Endianness = "le"):
}
throw new Error("endianness must be either 'le' or 'be'");
}

export function toHex(buffer: Uint8Array | Parameters<typeof Buffer.from>[0]): string {
if (Buffer.isBuffer(buffer)) {
return "0x" + buffer.toString("hex");
} else if (buffer instanceof Uint8Array) {
return "0x" + Buffer.from(buffer.buffer, buffer.byteOffset, buffer.length).toString("hex");
} else {
return "0x" + Buffer.from(buffer).toString("hex");
}
}

// Shared buffer to convert root to hex
const rootBuf = Buffer.alloc(32);

/**
* Convert a Uint8Array, length 32, to 0x-prefixed hex string
*/
export function toRootHex(root: Uint8Array): string {
if (root.length !== 32) {
throw Error(`Expect root to be 32 bytes, got ${root.length}`);
}

rootBuf.set(root);
return `0x${rootBuf.toString("hex")}`;
}

export function fromHex(hex: string): Uint8Array {
const b = Buffer.from(hex.replace("0x", ""), "hex");
return new Uint8Array(b.buffer, b.byteOffset, b.length);
}
66 changes: 66 additions & 0 deletions packages/utils/src/bytes/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export function toHex(bytes: Uint8Array): string {
const charCodes = new Array<number>(bytes.length * 2 + 2);
charCodes[0] = 48;
charCodes[1] = 120;

for (let i = 0; i < bytes.length; i++) {
const byte = bytes[i];
const first = (byte & 0xf0) >> 4;
const second = byte & 0x0f;

// "0".charCodeAt(0) = 48
// "a".charCodeAt(0) = 97 => delta = 87
charCodes[2 + 2 * i] = first < 10 ? first + 48 : first + 87;
charCodes[2 + 2 * i + 1] = second < 10 ? second + 48 : second + 87;
}
return String.fromCharCode(...charCodes);
}

const rootCharCodes = new Array<number>(32 * 2 + 2);
// "0".charCodeAt(0)
rootCharCodes[0] = 48;
// "x".charCodeAt(0)
rootCharCodes[1] = 120;

/**
* Convert a Uint8Array, length 32, to 0x-prefixed hex string
*/
export function toRootHex(root: Uint8Array): string {
if (root.length !== 32) {
throw Error(`Expect root to be 32 bytes, got ${root.length}`);
}

for (let i = 0; i < root.length; i++) {
const byte = root[i];
const first = (byte & 0xf0) >> 4;
const second = byte & 0x0f;

// "0".charCodeAt(0) = 48
// "a".charCodeAt(0) = 97 => delta = 87
rootCharCodes[2 + 2 * i] = first < 10 ? first + 48 : first + 87;
rootCharCodes[2 + 2 * i + 1] = second < 10 ? second + 48 : second + 87;
}
return String.fromCharCode(...rootCharCodes);
}

export function fromHex(hex: string): Uint8Array {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (typeof hex !== "string") {
throw new Error(`hex argument type ${typeof hex} must be of type string`);
}

if (hex.startsWith("0x")) {
hex = hex.slice(2);
}

if (hex.length % 2 !== 0) {
throw new Error(`hex string length ${hex.length} must be multiple of 2`);
}

const byteLen = hex.length / 2;
const bytes = new Uint8Array(byteLen);
for (let i = 0; i < byteLen; i++) {
const byte = parseInt(hex.slice(i * 2, (i + 1) * 2), 16);
bytes[i] = byte;
}
return bytes;
}
14 changes: 14 additions & 0 deletions packages/utils/src/bytes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {toHex as browserToHex, toRootHex as browserToRootHex, fromHex as browserFromHex} from "./browser.js";
import {toHex as nodeToHex, toRootHex as nodeToRootHex, fromHex as nodeFromHex} from "./nodejs.js";

let toHex = browserToHex;
let toRootHex = browserToRootHex;
let fromHex = browserFromHex;

if (typeof Buffer !== "undefined") {
toHex = nodeToHex;
toRootHex = nodeToRootHex;
fromHex = nodeFromHex;
}

export {toHex, toRootHex, fromHex};
33 changes: 33 additions & 0 deletions packages/utils/src/bytes/nodejs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export function toHex(buffer: Uint8Array | Parameters<typeof Buffer.from>[0]): string {
if (Buffer.isBuffer(buffer)) {
return "0x" + buffer.toString("hex");
} else if (buffer instanceof Uint8Array) {
return "0x" + Buffer.from(buffer.buffer, buffer.byteOffset, buffer.length).toString("hex");
} else {
return "0x" + Buffer.from(buffer).toString("hex");
}
}

// Shared buffer to convert root to hex
let rootBuf: Buffer | undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


/**
* Convert a Uint8Array, length 32, to 0x-prefixed hex string
*/
export function toRootHex(root: Uint8Array): string {
if (root.length !== 32) {
throw Error(`Expect root to be 32 bytes, got ${root.length}`);
}

if (rootBuf === undefined) {
rootBuf = Buffer.alloc(32);
}

rootBuf.set(root);
return `0x${rootBuf.toString("hex")}`;
}

export function fromHex(hex: string): Uint8Array {
const b = Buffer.from(hex.replace("0x", ""), "hex");
return new Uint8Array(b.buffer, b.byteOffset, b.length);
}
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./yaml/index.js";
export * from "./assert.js";
export * from "./base64.js";
export * from "./bytes.js";
export * from "./bytes/index.js";
export * from "./command.js";
export * from "./err.js";
export * from "./errors.js";
Expand Down
38 changes: 35 additions & 3 deletions packages/utils/test/perf/bytes.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {itBench} from "@dapplion/benchmark";
import {toHex, toRootHex} from "../../src/bytes.js";
import {toHex, toRootHex} from "../../src/bytes/nodejs.js";
import {toHex as browserToHex, toRootHex as browserToRootHex} from "../../src/bytes/browser.js";
import {toHexString} from "../../src/bytes.js";

describe("bytes utils", function () {
const runsFactor = 1000;
const blockRoot = new Uint8Array(Array.from({length: 32}, (_, i) => i));

itBench({
id: "block root to RootHex using toHex",
id: "nodejs block root to RootHex using toHex",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
toHex(blockRoot);
Expand All @@ -16,12 +18,42 @@ describe("bytes utils", function () {
});

itBench({
id: "block root to RootHex using toRootHex",
id: "nodejs block root to RootHex using toRootHex",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
toRootHex(blockRoot);
}
},
runsFactor,
});

itBench({
id: "browser block root to RootHex using the deprecated toHexString",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
toHexString(blockRoot);
}
},
runsFactor,
});

itBench({
id: "browser block root to RootHex using toHex",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
browserToHex(blockRoot);
}
},
runsFactor,
});

itBench({
id: "browser block root to RootHex using toRootHex",
fn: () => {
for (let i = 0; i < runsFactor; i++) {
browserToRootHex(blockRoot);
}
},
runsFactor,
});
});
23 changes: 21 additions & 2 deletions packages/utils/test/unit/bytes.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {describe, it, expect} from "vitest";
import {intToBytes, bytesToInt, toHex, fromHex, toHexString} from "../../src/index.js";
import {intToBytes, bytesToInt, toHex, fromHex, toHexString, toRootHex} from "../../src/index.js";

describe("intToBytes", () => {
const zeroedArray = (length: number): number[] => Array.from({length}, () => 0);
Expand Down Expand Up @@ -48,7 +48,7 @@ describe("bytesToInt", () => {
});

describe("toHex", () => {
const testCases: {input: Buffer | Uint8Array | string; output: string}[] = [
const testCases: {input: Uint8Array; output: string}[] = [
{input: Buffer.from("Hello, World!", "utf-8"), output: "0x48656c6c6f2c20576f726c6421"},
{input: new Uint8Array([72, 101, 108, 108, 111]), output: "0x48656c6c6f"},
{input: Buffer.from([72, 101, 108, 108, 111]), output: "0x48656c6c6f"},
Expand All @@ -61,6 +61,25 @@ describe("toHex", () => {
}
});

describe("toRootHex", () => {
const testCases: {input: Uint8Array; output: string}[] = [
{
input: new Uint8Array(Array.from({length: 32}, (_, i) => i)),
output: "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
},
{
input: new Uint8Array(Array.from({length: 32}, () => 0)),
output: "0x0000000000000000000000000000000000000000000000000000000000000000",
},
];

for (const {input, output} of testCases) {
it(`should convert root to hex string ${output}`, () => {
expect(toRootHex(input)).toBe(output);
});
}
});

describe("fromHex", () => {
const testCases: {input: string; output: Buffer | Uint8Array}[] = [
{
Expand Down
Loading