diff --git a/UNIQUE_SETUPS.md b/UNIQUE_SETUPS.md index e4af4fb7..dfffe4f3 100644 --- a/UNIQUE_SETUPS.md +++ b/UNIQUE_SETUPS.md @@ -1,6 +1,6 @@ # Unique Setup Steps for Xrpl.js -For when you need to do more than just install `xrpl.js` for it to work (especially for React projects in the browser). +Starting in 3.0 xrpl and all the packages in this repo no longer require custom configurations (ex. polyfills) to run. ### Using xrpl.js from a CDN @@ -13,48 +13,7 @@ Ensure that the full path is provided so the browser can find the sourcemaps. ### Using xrpl.js with `create-react-app` -To use `xrpl.js` with React, you need to install shims for core NodeJS modules. Starting with version 5, Webpack stopped including shims by default, so you must modify your Webpack configuration to add the shims you need. Either you can eject your config and modify it, or you can use a library such as `react-app-rewired`. The example below uses `react-app-rewired`. - -1. Install shims (you can use `yarn` as well): - - ```shell - npm install --save-dev \ - buffer \ - process - ``` - -2. Modify your webpack configuration - - 1. Install `react-app-rewired` - - ```shell - npm install --save-dev react-app-rewired - ``` - - 2. At the project root, add a file named `config-overrides.js` with the following content: - - ```javascript - const webpack = require("webpack"); - - module.exports = function override(config) { - config.plugins = (config.plugins || []).concat([ - new webpack.ProvidePlugin({ - process: "process/browser", - Buffer: ["buffer", "Buffer"], - }), - ]); - - return config; - }; - ``` - - 3. Update package.json scripts section with - - ``` - "start": "react-app-rewired start", - "build": "react-app-rewired build", - "test": "react-app-rewired test", - ``` +Starting in 3.0 xrpl and its related packages no longer require custom configurations (ex. polyfills) to run. This online template uses these steps to run xrpl.js with React in the browser: https://codesandbox.io/s/xrpl-intro-pxgdjr?file=/src/App.js @@ -97,54 +56,7 @@ import './polyfills' ### Using xrpl.js with Vite React -Similar to above, to get xrpl.js to work with Vite you need to set up a couple aliases in the vite.config.ts file. - -1. If it's a fresh project you can use `npm create vite@latest` then choose the React and TypeScript options. - -2. Copy these settings into your `vite.config.ts` file. - -```javascript -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import { NodeGlobalsPolyfillPlugin } from "@esbuild-plugins/node-globals-polyfill"; -import polyfillNode from 'rollup-plugin-polyfill-node' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], - define: { - 'process.env': {} - }, - optimizeDeps: { - esbuildOptions: { - define: { - global: 'globalThis', - }, - plugins: [ - NodeGlobalsPolyfillPlugin({ - process: true, - buffer: true, - }), - ], - }, - }, - build: { - rollupOptions: { - plugins: [ - polyfillNode(), - ] - } - }, -}) -``` - -3. Install the config dependencies and xrpl (e.g. using this command) - -```shell -npm install --save-dev @esbuild-plugins/node-globals-polyfill \ - rollup-plugin-polyfill-node \ - && npm install xrpl -``` +Starting in 3.0 xrpl and all the packages in this repo no longer require custom configurations (ex. polyfills) to run. ### Using xrpl.js with Deno diff --git a/package-lock.json b/package-lock.json index c8f861bc..13846535 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,6 @@ "@typescript-eslint/parser": "^5.28.0", "@xrplf/eslint-config": "^1.9.1", "@xrplf/prettier-config": "^1.9.1", - "buffer": "^6.0.2", "chai": "^4.3.4", "copyfiles": "^2.4.1", "eslint": "^8.18.0", @@ -4223,33 +4222,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -4420,29 +4392,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7836,25 +7785,6 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -12857,6 +12787,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -15365,8 +15296,8 @@ "version": "5.0.0-beta.0", "license": "ISC", "dependencies": { - "@xrplf/isomorphic": "^1.0.0-beta.0", - "base-x": "^3.0.9" + "@scure/base": "^1.1.3", + "@xrplf/isomorphic": "^1.0.0-beta.0" }, "engines": { "node": ">= 16" @@ -15378,7 +15309,6 @@ "dependencies": { "@xrplf/isomorphic": "^1.0.0-beta.0", "bignumber.js": "^9.0.0", - "buffer": "6.0.3", "ripple-address-codec": "^5.0.0-beta.0" }, "engines": { @@ -18870,19 +18800,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, "base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -19010,15 +18927,6 @@ "node-int64": "^0.4.0" } }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -21619,11 +21527,6 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -25453,8 +25356,8 @@ "ripple-address-codec": { "version": "file:packages/ripple-address-codec", "requires": { - "@xrplf/isomorphic": "^1.0.0-beta.0", - "base-x": "^3.0.9" + "@scure/base": "^1.1.3", + "@xrplf/isomorphic": "^1.0.0-beta.0" } }, "ripple-binary-codec": { @@ -25462,7 +25365,6 @@ "requires": { "@xrplf/isomorphic": "^1.0.0-beta.0", "bignumber.js": "^9.0.0", - "buffer": "6.0.3", "ripple-address-codec": "^5.0.0-beta.0" } }, @@ -25519,7 +25421,8 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true }, "safe-regex-test": { "version": "1.0.0", diff --git a/package.json b/package.json index d4b63d89..2e55b407 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "@typescript-eslint/parser": "^5.28.0", "@xrplf/eslint-config": "^1.9.1", "@xrplf/prettier-config": "^1.9.1", - "buffer": "^6.0.2", "chai": "^4.3.4", "copyfiles": "^2.4.1", "eslint": "^8.18.0", diff --git a/packages/isomorphic/HISTORY.md b/packages/isomorphic/HISTORY.md index 31f2f797..d2a2fe56 100644 --- a/packages/isomorphic/HISTORY.md +++ b/packages/isomorphic/HISTORY.md @@ -1,5 +1,11 @@ # @xrplf/isomorphic Release History +## Unreleased + +## Added +- hexToString +- stringToHex + ## 1.0.0 Beta 1 (2023-10-19) Initial release providing isomorphic and tree-shakable implementations of: diff --git a/packages/isomorphic/README.md b/packages/isomorphic/README.md index 3b305252..0520f66b 100644 --- a/packages/isomorphic/README.md +++ b/packages/isomorphic/README.md @@ -95,6 +95,26 @@ import { hexToBytes } from @xrplf/isomorphic/utils console.log(hexToBytes('DEADBEEF')) // [222, 173, 190, 239] ``` +#### hexToString + +Converts hex to its string equivalent. Useful to read the Domain field and some Memos. + +```typescript +import { hexToString } from @xrplf/isomorphic/utils + +console.log(hexToString('6465616462656566D68D')) // "deadbeef֍" +``` + +#### stringToHex + +Converts a utf-8 to its hex equivalent. Useful for Memos. + +```typescript +import { stringToHex } from @xrplf/isomorphic/utils + +console.log(stringToHex('deadbeef֍')) // "6465616462656566D68D" +``` + ### `@xrplf/isomorphic/ws` ```typescript diff --git a/packages/isomorphic/src/utils/browser.ts b/packages/isomorphic/src/utils/browser.ts index 49a71c1c..89bcf8f8 100644 --- a/packages/isomorphic/src/utils/browser.ts +++ b/packages/isomorphic/src/utils/browser.ts @@ -1,11 +1,16 @@ import { bytesToHex as nobleBytesToHex, - hexToBytes as nobleHexToBytes, randomBytes as nobleRandomBytes, } from '@noble/hashes/utils' -import type { BytesToHexFn, HexToBytesFn, RandomBytesFn } from './types' +import type { + BytesToHexFn, + HexToBytesFn, + HexToStringFn, + RandomBytesFn, + StringToHexFn, +} from './types' -/* eslint-disable-next-line func-style -- Typed to ensure uniformity between node and browser implementations and docs */ +/* eslint-disable func-style -- Typed to ensure uniformity between node and browser implementations and docs */ export const bytesToHex: typeof BytesToHexFn = (bytes) => { const hex = nobleBytesToHex( bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes), @@ -13,5 +18,33 @@ export const bytesToHex: typeof BytesToHexFn = (bytes) => { return hex.toUpperCase() } -export const hexToBytes: typeof HexToBytesFn = nobleHexToBytes +// A clone of hexToBytes from @noble/hashes without the length checks. This allows us to do our own checks. +export const hexToBytes: typeof HexToBytesFn = (hex): Uint8Array => { + const len = hex.length + const array = new Uint8Array(len / 2) + for (let i = 0; i < array.length; i++) { + const j = i * 2 + const hexByte = hex.slice(j, j + 2) + const byte = Number.parseInt(hexByte, 16) + if (Number.isNaN(byte) || byte < 0) { + throw new Error('Invalid byte sequence') + } + array[i] = byte + } + return array +} + +export const hexToString: typeof HexToStringFn = ( + hex: string, + encoding = 'utf8', +): string => { + return new TextDecoder(encoding).decode(hexToBytes(hex)) +} + +export const stringToHex: typeof StringToHexFn = (string: string): string => { + return bytesToHex(new TextEncoder().encode(string)) +} +/* eslint-enable func-style */ + export const randomBytes: typeof RandomBytesFn = nobleRandomBytes +export * from './shared' diff --git a/packages/isomorphic/src/utils/index.ts b/packages/isomorphic/src/utils/index.ts index 3949efda..0a57ee55 100644 --- a/packages/isomorphic/src/utils/index.ts +++ b/packages/isomorphic/src/utils/index.ts @@ -1,5 +1,6 @@ import { randomBytes as cryptoRandomBytes } from 'crypto' import type { BytesToHexFn, HexToBytesFn, RandomBytesFn } from './types' +import { HexToStringFn, StringToHexFn } from './types' const OriginalBuffer = Symbol('OriginalBuffer') @@ -69,4 +70,17 @@ export const hexToBytes: typeof HexToBytesFn = (hex) => { export const randomBytes: typeof RandomBytesFn = (size) => { return toUint8Array(cryptoRandomBytes(size)) } + +export const hexToString: typeof HexToStringFn = ( + hex: string, + encoding = 'utf8', +): string => { + return new TextDecoder(encoding).decode(hexToBytes(hex)) +} + +export const stringToHex: typeof StringToHexFn = (string: string): string => { + return bytesToHex(new TextEncoder().encode(string)) +} /* eslint-enable func-style */ + +export * from './shared' diff --git a/packages/isomorphic/src/utils/shared.ts b/packages/isomorphic/src/utils/shared.ts new file mode 100644 index 00000000..408c07f9 --- /dev/null +++ b/packages/isomorphic/src/utils/shared.ts @@ -0,0 +1,19 @@ +import { concatBytes } from '@noble/hashes/utils' + +export function concat(views: Uint8Array[]): Uint8Array { + return concatBytes(...views) +} + +export function equal(buf1: Uint8Array, buf2: Uint8Array): boolean { + if (buf1.byteLength !== buf2.byteLength) { + return false + } + const dv1 = new Int8Array(buf1) + const dv2 = new Int8Array(buf2) + for (let i = 0; i !== buf1.byteLength; i++) { + if (dv1[i] !== dv2[i]) { + return false + } + } + return true +} diff --git a/packages/isomorphic/src/utils/types.ts b/packages/isomorphic/src/utils/types.ts index ba4854d8..341d1556 100644 --- a/packages/isomorphic/src/utils/types.ts +++ b/packages/isomorphic/src/utils/types.ts @@ -18,3 +18,20 @@ export declare function HexToBytesFn(hex: string): Uint8Array * @param size - number of bytes to generate */ export declare function RandomBytesFn(size: number): Uint8Array + +/** + * Converts hex to its string equivalent. Useful to read the Domain field and some Memos. + * + * @param hex - The hex to convert to a string. + * @param encoding - The encoding to use. Defaults to 'utf8' (UTF-8). 'ascii' is also allowed. + * @returns The converted string. + */ +export declare function HexToStringFn(hex: string, encoding?: string): string + +/** + * Converts a utf-8 to its hex equivalent. Useful for Memos. + * + * @param string - The string to convert to Hex. + * @returns The Hex equivalent of the string. + */ +export declare function StringToHexFn(string: string): string diff --git a/packages/isomorphic/src/ws/browser.ts b/packages/isomorphic/src/ws/browser.ts index 4375e938..7036efb9 100644 --- a/packages/isomorphic/src/ws/browser.ts +++ b/packages/isomorphic/src/ws/browser.ts @@ -9,7 +9,7 @@ declare class WebSocket { public onmessage?: (message: MessageEvent) => void public readyState: number public constructor(url: string) - public close(code?: number, reason?: Buffer): void + public close(code?: number, reason?: Uint8Array): void public send(message: string): void } diff --git a/packages/isomorphic/test/utils.test.ts b/packages/isomorphic/test/utils.test.ts index ce4f4f18..4953a457 100644 --- a/packages/isomorphic/test/utils.test.ts +++ b/packages/isomorphic/test/utils.test.ts @@ -1,4 +1,10 @@ -import { bytesToHex, hexToBytes, randomBytes } from '../utils' +import { + bytesToHex, + hexToBytes, + hexToString, + randomBytes, + stringToHex, +} from '../utils' describe('utils', function () { it('randomBytes', () => { @@ -28,4 +34,16 @@ describe('utils', function () { it('bytesToHex - DEADBEEF (Uint8Array)', () => { expect(bytesToHex(new Uint8Array([222, 173, 190, 239]))).toEqual('DEADBEEF') }) + + it('hexToString - deadbeef+infinity symbol (HEX ASCII)', () => { + expect(hexToString('646561646265656658D', 'ascii')).toEqual('deadbeefX') + }) + + it('hexToString - deadbeef+infinity symbol (HEX)', () => { + expect(hexToString('6465616462656566D68D')).toEqual('deadbeef֍') + }) + + it('stringToHex - deadbeef+infinity symbol (utf8)', () => { + expect(stringToHex('deadbeef֍')).toEqual('6465616462656566D68D') + }) }) diff --git a/packages/ripple-address-codec/HISTORY.md b/packages/ripple-address-codec/HISTORY.md index 3782ff82..c7d1def2 100644 --- a/packages/ripple-address-codec/HISTORY.md +++ b/packages/ripple-address-codec/HISTORY.md @@ -2,6 +2,12 @@ ## Unreleased +### Breaking Changes +* `Buffer` has been replaced with `UInt8Array` for both params and return values. `Buffer` may continue to work with params since they extend `UInt8Arrays`. + +### Changes +* Eliminates 4 runtime dependencies: `base-x`, `base64-js`, `buffer`, and `ieee754`. + ## 5.0.0 Beta 1 ### Breaking Changes diff --git a/packages/ripple-address-codec/README.md b/packages/ripple-address-codec/README.md index 59159740..142dfb10 100644 --- a/packages/ripple-address-codec/README.md +++ b/packages/ripple-address-codec/README.md @@ -67,7 +67,7 @@ Check whether a classic address (starting with `r`...) is valid. Returns `false` for X-addresses (extended addresses). To validate an X-address, use `isValidXAddress`. -### encodeSeed(entropy: Buffer, type: 'ed25519' | 'secp256k1'): string +### encodeSeed(entropy: UInt8Array, type: 'ed25519' | 'secp256k1'): string Encode the given entropy as an XRP Ledger seed (secret). The entropy must be exactly 16 bytes (128 bits). The encoding includes which elliptic curve digital signature algorithm (ECDSA) the seed is intended to be used with. The seed is used to produce the private key. @@ -79,38 +79,38 @@ Return object type: ``` { version: number[], - bytes: Buffer, + bytes: UInt8Array, type: string | null } ``` -### encodeAccountID(bytes: Buffer): string +### encodeAccountID(bytes: UInt8Array): string Encode bytes as a classic address (starting with `r`...). -### decodeAccountID(accountId: string): Buffer +### decodeAccountID(accountId: string): UInt8Array Decode a classic address (starting with `r`...) to its raw bytes. -### encodeNodePublic(bytes: Buffer): string +### encodeNodePublic(bytes: UInt8Array): string Encode bytes to the XRP Ledger "node public key" format (base58). This is useful for rippled validators. -### decodeNodePublic(base58string: string): Buffer +### decodeNodePublic(base58string: string): UInt8Array Decode an XRP Ledger "node public key" (in base58 format) into its raw bytes. -### encodeAccountPublic(bytes: Buffer): string +### encodeAccountPublic(bytes: UInt8Array): string Encode a public key, as for payment channels. -### decodeAccountPublic(base58string: string): Buffer +### decodeAccountPublic(base58string: string): UInt8Array Decode a public key, as for payment channels. -### encodeXAddress(accountId: Buffer, tag: number | false, test: boolean): string +### encodeXAddress(accountId: UInt8Array, tag: number | false, test: boolean): string Encode account ID, tag, and network ID to X-address. @@ -120,7 +120,7 @@ At this time, `tag` must be <= MAX_32_BIT_UNSIGNED_INT (4294967295) as the XRP L If `test` is `true`, this address is intended for use with a test network such as Testnet or Devnet. -### decodeXAddress(xAddress: string): {accountId: Buffer, tag: number | false, test: boolean} +### decodeXAddress(xAddress: string): {accountId: UInt8Array, tag: number | false, test: boolean} Convert an X-address to its classic address, tag, and network ID. diff --git a/packages/ripple-address-codec/package.json b/packages/ripple-address-codec/package.json index 31905d5f..206630b2 100644 --- a/packages/ripple-address-codec/package.json +++ b/packages/ripple-address-codec/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "@xrplf/isomorphic": "^1.0.0-beta.0", - "base-x": "^3.0.9" + "@scure/base": "^1.1.3" }, "keywords": [ "ripple", diff --git a/packages/ripple-address-codec/src/index.ts b/packages/ripple-address-codec/src/index.ts index d415848a..594b8bcb 100644 --- a/packages/ripple-address-codec/src/index.ts +++ b/packages/ripple-address-codec/src/index.ts @@ -1,3 +1,5 @@ +import { concat, equal, hexToBytes } from '@xrplf/isomorphic/utils' + import { codec, encodeSeed, @@ -13,9 +15,9 @@ import { const PREFIX_BYTES = { // 5, 68 - main: Buffer.from([0x05, 0x44]), + main: Uint8Array.from([0x05, 0x44]), // 4, 147 - test: Buffer.from([0x04, 0x93]), + test: Uint8Array.from([0x04, 0x93]), } const MAX_32_BIT_UNSIGNED_INT = 4294967295 @@ -30,7 +32,7 @@ function classicAddressToXAddress( } function encodeXAddress( - accountId: Buffer, + accountId: Uint8Array, tag: number | false, test: boolean, ): string { @@ -46,10 +48,10 @@ function encodeXAddress( const flag = tag === false || tag == null ? 0 : 1 /* eslint-disable no-bitwise --- * need to use bitwise operations here */ - const bytes = Buffer.concat([ + const bytes = concat([ test ? PREFIX_BYTES.test : PREFIX_BYTES.main, accountId, - Buffer.from([ + Uint8Array.from([ // 0x00 if no tag, 0x01 if 32-bit tag flag, // first byte @@ -90,7 +92,7 @@ function xAddressToClassicAddress(xAddress: string): { } function decodeXAddress(xAddress: string): { - accountId: Buffer + accountId: Uint8Array tag: number | false test: boolean } { @@ -98,10 +100,10 @@ function decodeXAddress(xAddress: string): { /* eslint-disable @typescript-eslint/naming-convention -- * TODO 'test' should be something like 'isTest', do this later */ - const test = isBufferForTestAddress(decoded) + const test = isUint8ArrayForTestAddress(decoded) /* eslint-enable @typescript-eslint/naming-convention */ const accountId = decoded.slice(2, 22) - const tag = tagFromBuffer(decoded) + const tag = tagFromUint8Array(decoded) return { accountId, tag, @@ -109,19 +111,19 @@ function decodeXAddress(xAddress: string): { } } -function isBufferForTestAddress(buf: Buffer): boolean { +function isUint8ArrayForTestAddress(buf: Uint8Array): boolean { const decodedPrefix = buf.slice(0, 2) - if (PREFIX_BYTES.main.equals(decodedPrefix)) { + if (equal(PREFIX_BYTES.main, decodedPrefix)) { return false } - if (PREFIX_BYTES.test.equals(decodedPrefix)) { + if (equal(PREFIX_BYTES.test, decodedPrefix)) { return true } throw new Error('Invalid X-address: bad prefix') } -function tagFromBuffer(buf: Buffer): number | false { +function tagFromUint8Array(buf: Uint8Array): number | false { const flag = buf[22] if (flag >= 2) { // No support for 64-bit tags at this time @@ -134,7 +136,7 @@ function tagFromBuffer(buf: Buffer): number | false { if (flag !== 0) { throw new Error('flag must be zero to indicate no tag') } - if (!Buffer.from('0000000000000000', 'hex').equals(buf.slice(23, 23 + 8))) { + if (!equal(hexToBytes('0000000000000000'), buf.slice(23, 23 + 8))) { throw new Error('remaining bytes must be zero') } return false diff --git a/packages/ripple-address-codec/src/utils.ts b/packages/ripple-address-codec/src/utils.ts index fe977f24..a6450863 100644 --- a/packages/ripple-address-codec/src/utils.ts +++ b/packages/ripple-address-codec/src/utils.ts @@ -1,6 +1,4 @@ -// Buffer is technically not needed, as a Buffer IS a Uint8Array. -// However, for communication purposes it's listed here -export type ByteArray = number[] | Uint8Array | Buffer +export type ByteArray = number[] | Uint8Array /** * Check whether two sequences (e.g. Arrays of numbers) are equal. @@ -29,7 +27,7 @@ function isScalar(val: ByteArray | number): val is number { * a single element or a sequence, which has a `length` property and supports * element retrieval via sequence[ix]. * - * > concatArgs(1, [2, 3], Buffer.from([4,5]), new Uint8Array([6, 7])); + * > concatArgs(1, [2, 3], Uint8Array.from([4,5]), new Uint8Array([6, 7])); * [1,2,3,4,5,6,7] * * @param args - Concatenate of these args into a single array. diff --git a/packages/ripple-address-codec/src/xrp-codec.ts b/packages/ripple-address-codec/src/xrp-codec.ts index 8589b313..29637b82 100644 --- a/packages/ripple-address-codec/src/xrp-codec.ts +++ b/packages/ripple-address-codec/src/xrp-codec.ts @@ -2,30 +2,24 @@ * Codec class */ +import { base58xrp, BytesCoder } from '@scure/base' import { sha256 } from '@xrplf/isomorphic/sha256' -import baseCodec = require('base-x') -import type { BaseConverter } from 'base-x' import { arrayEqual, concatArgs, ByteArray } from './utils' class Codec { private readonly _sha256: (bytes: ByteArray) => Uint8Array - private readonly _alphabet: string - private readonly _codec: BaseConverter + private readonly _codec: BytesCoder - public constructor(options: { - sha256: (bytes: ByteArray) => Uint8Array - alphabet: string - }) { + public constructor(options: { sha256: (bytes: ByteArray) => Uint8Array }) { this._sha256 = options.sha256 - this._alphabet = options.alphabet - this._codec = baseCodec(this._alphabet) + this._codec = base58xrp } /** * Encoder. * - * @param bytes - Buffer of data to encode. + * @param bytes - Uint8Array of data to encode. * @param opts - Options object including the version bytes and the expected length of the data to encode. */ public encode( @@ -56,7 +50,7 @@ class Codec { }, ): { version: number[] - bytes: Buffer + bytes: Uint8Array type: 'ed25519' | 'secp256k1' | null } { const versions = opts.versions @@ -97,20 +91,20 @@ class Codec { ) } - public encodeChecked(buffer: ByteArray): string { - const check = this._sha256(this._sha256(buffer)).slice(0, 4) - return this._encodeRaw(Buffer.from(concatArgs(buffer, check))) + public encodeChecked(bytes: ByteArray): string { + const check = this._sha256(this._sha256(bytes)).slice(0, 4) + return this._encodeRaw(Uint8Array.from(concatArgs(bytes, check))) } - public decodeChecked(base58string: string): Buffer { - const buffer = this._decodeRaw(base58string) - if (buffer.length < 5) { + public decodeChecked(base58string: string): Uint8Array { + const intArray = this._decodeRaw(base58string) + if (intArray.byteLength < 5) { throw new Error('invalid_input_size: decoded data must have length >= 5') } - if (!this._verifyCheckSum(buffer)) { + if (!this._verifyCheckSum(intArray)) { throw new Error('checksum_invalid') } - return buffer.slice(0, -4) + return intArray.slice(0, -4) } private _encodeVersioned( @@ -118,21 +112,21 @@ class Codec { versions: number[], expectedLength: number, ): string { - if (expectedLength && bytes.length !== expectedLength) { + if (!checkByteLength(bytes, expectedLength)) { throw new Error( 'unexpected_payload_length: bytes.length does not match expectedLength.' + - ' Ensure that the bytes are a Buffer.', + ' Ensure that the bytes are a Uint8Array.', ) } return this.encodeChecked(concatArgs(versions, bytes)) } private _encodeRaw(bytes: ByteArray): string { - return this._codec.encode(bytes) + return this._codec.encode(Uint8Array.from(bytes)) } /* eslint-enable max-lines-per-function */ - private _decodeRaw(base58string: string): Buffer { + private _decodeRaw(base58string: string): Uint8Array { return this._codec.decode(base58string) } @@ -162,20 +156,19 @@ const ED25519_SEED = [0x01, 0xe1, 0x4b] const codecOptions = { sha256, - alphabet: 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz', } const codecWithXrpAlphabet = new Codec(codecOptions) export const codec = codecWithXrpAlphabet -// entropy is a Buffer of size 16 +// entropy is a Uint8Array of size 16 // type is 'ed25519' or 'secp256k1' export function encodeSeed( entropy: ByteArray, type: 'ed25519' | 'secp256k1', ): string { - if (entropy.length !== 16) { + if (!checkByteLength(entropy, 16)) { throw new Error('entropy must have length 16') } const opts = { @@ -202,7 +195,7 @@ export function decodeSeed( }, ): { version: number[] - bytes: Buffer + bytes: Uint8Array type: 'ed25519' | 'secp256k1' | null } { return codecWithXrpAlphabet.decode(seed, opts) @@ -219,7 +212,7 @@ export function encodeAccountID(bytes: ByteArray): string { export const encodeAddress = encodeAccountID /* eslint-enable import/no-unused-modules */ -export function decodeAccountID(accountId: string): Buffer { +export function decodeAccountID(accountId: string): Uint8Array { const opts = { versions: [ACCOUNT_ID], expectedLength: 20 } return codecWithXrpAlphabet.decode(accountId, opts).bytes } @@ -230,7 +223,7 @@ export function decodeAccountID(accountId: string): Buffer { export const decodeAddress = decodeAccountID /* eslint-enable import/no-unused-modules */ -export function decodeNodePublic(base58string: string): Buffer { +export function decodeNodePublic(base58string: string): Uint8Array { const opts = { versions: [NODE_PUBLIC], expectedLength: 33 } return codecWithXrpAlphabet.decode(base58string, opts).bytes } @@ -245,7 +238,7 @@ export function encodeAccountPublic(bytes: ByteArray): string { return codecWithXrpAlphabet.encode(bytes, opts) } -export function decodeAccountPublic(base58string: string): Buffer { +export function decodeAccountPublic(base58string: string): Uint8Array { const opts = { versions: [ACCOUNT_PUBLIC_KEY], expectedLength: 33 } return codecWithXrpAlphabet.decode(base58string, opts).bytes } @@ -258,3 +251,9 @@ export function isValidClassicAddress(address: string): boolean { } return true } + +function checkByteLength(bytes: ByteArray, expectedLength: number): boolean { + return 'byteLength' in bytes + ? bytes.byteLength === expectedLength + : bytes.length === expectedLength +} diff --git a/packages/ripple-address-codec/test/index.test.ts b/packages/ripple-address-codec/test/index.test.ts index 49f470df..63c3b6ca 100644 --- a/packages/ripple-address-codec/test/index.test.ts +++ b/packages/ripple-address-codec/test/index.test.ts @@ -1,3 +1,5 @@ +import { bytesToHex, hexToBytes } from '@xrplf/isomorphic/utils' + import { classicAddressToXAddress, xAddressToClassicAddress, @@ -199,10 +201,10 @@ const testCases: AddressTestCase[] = [ { const highAndLowAccounts = [ - Buffer.from('00'.repeat(20), 'hex'), - Buffer.from(`${'00'.repeat(19)}01`, 'hex'), - Buffer.from('01'.repeat(20), 'hex'), - Buffer.from('FF'.repeat(20), 'hex'), + hexToBytes('00'.repeat(20)), + hexToBytes(`${'00'.repeat(19)}01`), + hexToBytes('01'.repeat(20)), + hexToBytes('FF'.repeat(20)), ] highAndLowAccounts.forEach((accountId) => { @@ -215,7 +217,7 @@ const testCases: AddressTestCase[] = [ tagTestCases.forEach((testCase) => { const tag = testCase || false const xAddress = encodeXAddress(accountId, tag, isTestAddress) - it(`Encoding ${accountId.toString('hex')}${ + it(`Encoding ${bytesToHex(accountId)}${ tag ? `:${tag}` : '' } to ${xAddress} has expected length`, () => { expect(xAddress.length).toBe(47) @@ -256,8 +258,8 @@ it(`Invalid X-address (64-bit tag) throws`, () => { it(`Invalid Account ID throws`, () => { expect(() => { - encodeXAddress(Buffer.from('00'.repeat(19), 'hex'), false, false) - }).toThrow(new Error('Account ID must be 20 bytes')) + encodeXAddress(hexToBytes('00'.repeat(19)), false, false) + }).toThrowError('Account ID must be 20 bytes') }) it(`isValidXAddress returns false for invalid X-address`, () => { diff --git a/packages/ripple-address-codec/test/utils.test.ts b/packages/ripple-address-codec/test/utils.test.ts index c0ff3f9a..87036d70 100644 --- a/packages/ripple-address-codec/test/utils.test.ts +++ b/packages/ripple-address-codec/test/utils.test.ts @@ -1,44 +1,48 @@ import { arrayEqual, concatArgs } from '../src/utils' -it('two sequences are equal', () => { - expect(arrayEqual([1, 2, 3], [1, 2, 3])).toBe(true) +describe('Function: arrayEqual', () => { + it('two sequences are equal', () => { + expect(arrayEqual([1, 2, 3], [1, 2, 3])).toBe(true) + }) + + it('elements must be in the same order', () => { + expect(arrayEqual([3, 2, 1], [1, 2, 3])).toBe(false) + }) + + it('sequences do not need to be the same type', () => { + expect(arrayEqual(Uint8Array.from([1, 2, 3]), [1, 2, 3])).toBe(true) + expect( + arrayEqual(Uint8Array.from([1, 2, 3]), new Uint8Array([1, 2, 3])), + ).toBe(true) + }) + + it('single element sequences do not need to be the same type', () => { + expect(arrayEqual(Uint8Array.from([1]), [1])).toBe(true) + expect(arrayEqual(Uint8Array.from([1]), new Uint8Array([1]))).toBe(true) + }) + + it('empty sequences do not need to be the same type', () => { + expect(arrayEqual(Uint8Array.from([]), [])).toBe(true) + expect(arrayEqual(Uint8Array.from([]), new Uint8Array([]))).toBe(true) + }) }) -it('elements must be in the same order', () => { - expect(arrayEqual([3, 2, 1], [1, 2, 3])).toBe(false) -}) +describe('Function: concatArgs', () => { + it('plain numbers are concatenated', () => { + expect(concatArgs(10, 20, 30, 40)).toEqual([10, 20, 30, 40]) + }) -it('sequences do not need to be the same type', () => { - expect(arrayEqual(Buffer.from([1, 2, 3]), [1, 2, 3])).toBe(true) - expect(arrayEqual(Buffer.from([1, 2, 3]), new Uint8Array([1, 2, 3]))).toBe( - true, - ) -}) + it('a variety of values are concatenated', () => { + expect( + concatArgs(1, [2, 3], Uint8Array.from([4, 5]), new Uint8Array([6, 7])), + ).toEqual([1, 2, 3, 4, 5, 6, 7]) + }) -it('sequences with a single element', () => { - expect(arrayEqual(Buffer.from([1]), [1])).toBe(true) - expect(arrayEqual(Buffer.from([1]), new Uint8Array([1]))).toBe(true) -}) + it('a single value is returned as an array', () => { + expect(concatArgs(Uint8Array.from([7]))).toEqual([7]) + }) -it('empty sequences', () => { - expect(arrayEqual(Buffer.from([]), [])).toBe(true) - expect(arrayEqual(Buffer.from([]), new Uint8Array([]))).toBe(true) -}) - -it('plain numbers are concatenated', () => { - expect(concatArgs(10, 20, 30, 40)).toEqual([10, 20, 30, 40]) -}) - -it('a variety of values are concatenated', () => { - expect( - concatArgs(1, [2, 3], Buffer.from([4, 5]), new Uint8Array([6, 7])), - ).toEqual([1, 2, 3, 4, 5, 6, 7]) -}) - -it('a single value is returned as an array', () => { - expect(concatArgs(Buffer.from([7]))).toEqual([7]) -}) - -it('no arguments returns an empty array', () => { - expect(concatArgs()).toEqual([]) + it('no arguments returns an empty array', () => { + expect(concatArgs()).toEqual([]) + }) }) diff --git a/packages/ripple-address-codec/test/xrp-codec.test.ts b/packages/ripple-address-codec/test/xrp-codec.test.ts index 6c1ee428..7d35323d 100644 --- a/packages/ripple-address-codec/test/xrp-codec.test.ts +++ b/packages/ripple-address-codec/test/xrp-codec.test.ts @@ -1,3 +1,5 @@ +import { bytesToHex, hexToBytes, stringToHex } from '@xrplf/isomorphic/utils' + import { codec, decodeAccountID, @@ -11,12 +13,8 @@ import { isValidClassicAddress, } from '../src' -function toHex(bytes: Buffer): string { - return Buffer.from(bytes).toString('hex').toUpperCase() -} - -function toBytes(hex: string): Buffer { - return Buffer.from(hex, 'hex') +function stringToBytes(str: string): Uint8Array { + return hexToBytes(stringToHex(str)) } /** @@ -29,18 +27,18 @@ function toBytes(hex: string): Buffer { */ // eslint-disable-next-line max-params -- needs them function makeEncodeDecodeTest( - encoder: (val: Buffer) => string, - decoder: (val: string) => Buffer, + encoder: (val: Uint8Array) => string, + decoder: (val: string) => Uint8Array, base58: string, hex: string, ): void { it(`can translate between ${hex} and ${base58}`, function () { - const actual = encoder(toBytes(hex)) + const actual = encoder(hexToBytes(hex)) expect(actual).toBe(base58) }) it(`can translate between ${base58} and ${hex})`, function () { const buf = decoder(base58) - expect(toHex(buf)).toBe(hex) + expect(bytesToHex(buf)).toBe(hex) }) } @@ -67,11 +65,11 @@ makeEncodeDecodeTest( it('can decode arbitrary seeds', function () { const decoded = decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2') - expect(toHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341') + expect(bytesToHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341') expect(decoded.type).toBe('ed25519') const decoded2 = decodeSeed('sn259rEFXrQrWyx3Q7XneWcwV6dfL') - expect(toHex(decoded2.bytes)).toBe('CF2DE378FBDD7E2EE87D486DFB5A7BFF') + expect(bytesToHex(decoded2.bytes)).toBe('CF2DE378FBDD7E2EE87D486DFB5A7BFF') expect(decoded2.type).toBe('secp256k1') }) @@ -79,7 +77,7 @@ it('can pass a type as second arg to encodeSeed', function () { const edSeed = 'sEdTM1uX8pu2do5XvTnutH6HsouMaM2' const decoded = decodeSeed(edSeed) const type = 'ed25519' - expect(toHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341') + expect(bytesToHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341') expect(decoded.type).toBe(type) expect(encodeSeed(decoded.bytes, type)).toBe(edSeed) }) @@ -105,7 +103,7 @@ it('isValidClassicAddress - empty', function () { describe('encodeSeed', function () { it('encodes a secp256k1 seed', function () { const result = encodeSeed( - Buffer.from('CF2DE378FBDD7E2EE87D486DFB5A7BFF', 'hex'), + hexToBytes('CF2DE378FBDD7E2EE87D486DFB5A7BFF'), 'secp256k1', ) expect(result).toBe('sn259rEFXrQrWyx3Q7XneWcwV6dfL') @@ -113,7 +111,7 @@ describe('encodeSeed', function () { it('encodes low secp256k1 seed', function () { const result = encodeSeed( - Buffer.from('00000000000000000000000000000000', 'hex'), + hexToBytes('00000000000000000000000000000000'), 'secp256k1', ) expect(result).toBe('sp6JS7f14BuwFY8Mw6bTtLKWauoUs') @@ -121,7 +119,7 @@ describe('encodeSeed', function () { it('encodes high secp256k1 seed', function () { const result = encodeSeed( - Buffer.from('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 'hex'), + hexToBytes('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), 'secp256k1', ) expect(result).toBe('saGwBRReqUNKuWNLpUAq8i8NkXEPN') @@ -129,7 +127,7 @@ describe('encodeSeed', function () { it('encodes an ed25519 seed', function () { const result = encodeSeed( - Buffer.from('4C3A1D213FBDFB14C7C28D609469B341', 'hex'), + hexToBytes('4C3A1D213FBDFB14C7C28D609469B341'), 'ed25519', ) expect(result).toBe('sEdTM1uX8pu2do5XvTnutH6HsouMaM2') @@ -137,7 +135,7 @@ describe('encodeSeed', function () { it('encodes low ed25519 seed', function () { const result = encodeSeed( - Buffer.from('00000000000000000000000000000000', 'hex'), + hexToBytes('00000000000000000000000000000000'), 'ed25519', ) expect(result).toBe('sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE') @@ -145,7 +143,7 @@ describe('encodeSeed', function () { it('encodes high ed25519 seed', function () { const result = encodeSeed( - Buffer.from('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 'hex'), + hexToBytes('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), 'ed25519', ) expect(result).toBe('sEdV19BLfeQeKdEXyYA4NhjPJe6XBfG') @@ -153,19 +151,13 @@ describe('encodeSeed', function () { it('attempting to encode a seed with less than 16 bytes of entropy throws', function () { expect(() => { - encodeSeed( - Buffer.from('CF2DE378FBDD7E2EE87D486DFB5A7B', 'hex'), - 'secp256k1', - ) + encodeSeed(hexToBytes('CF2DE378FBDD7E2EE87D486DFB5A7B'), 'secp256k1') }).toThrow(new Error('entropy must have length 16')) }) it('attempting to encode a seed with more than 16 bytes of entropy throws', function () { expect(() => { - encodeSeed( - Buffer.from('CF2DE378FBDD7E2EE87D486DFB5A7BFFFF', 'hex'), - 'secp256k1', - ) + encodeSeed(hexToBytes('CF2DE378FBDD7E2EE87D486DFB5A7BFFFF'), 'secp256k1') }).toThrow(new Error('entropy must have length 16')) }) }) @@ -173,13 +165,13 @@ describe('encodeSeed', function () { describe('decodeSeed', function () { it('can decode an Ed25519 seed', function () { const decoded = decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2') - expect(toHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341') + expect(bytesToHex(decoded.bytes)).toBe('4C3A1D213FBDFB14C7C28D609469B341') expect(decoded.type).toBe('ed25519') }) it('can decode a secp256k1 seed', function () { const decoded = decodeSeed('sn259rEFXrQrWyx3Q7XneWcwV6dfL') - expect(toHex(decoded.bytes)).toBe('CF2DE378FBDD7E2EE87D486DFB5A7BFF') + expect(bytesToHex(decoded.bytes)).toBe('CF2DE378FBDD7E2EE87D486DFB5A7BFF') expect(decoded.type).toBe('secp256k1') }) }) @@ -187,17 +179,17 @@ describe('decodeSeed', function () { describe('encodeAccountID', function () { it('can encode an AccountID', function () { const encoded = encodeAccountID( - Buffer.from('BA8E78626EE42C41B46D46C3048DF3A1C3C87072', 'hex'), + hexToBytes('BA8E78626EE42C41B46D46C3048DF3A1C3C87072'), ) expect(encoded).toBe('rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN') }) it('unexpected length should throw', function () { expect(() => { - encodeAccountID(Buffer.from('ABCDEF', 'hex')) + encodeAccountID(hexToBytes('ABCDEF')) }).toThrow( new Error( - 'unexpected_payload_length: bytes.length does not match expectedLength. Ensure that the bytes are a Buffer.', + 'unexpected_payload_length: bytes.length does not match expectedLength. Ensure that the bytes are a Uint8Array.', ), ) }) @@ -208,7 +200,7 @@ describe('decodeNodePublic', function () { const decoded = decodeNodePublic( 'n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH', ) - expect(toHex(decoded)).toBe( + expect(bytesToHex(decoded)).toBe( '0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828', ) }) @@ -216,7 +208,7 @@ describe('decodeNodePublic', function () { it('encodes 123456789 with version byte of 0', () => { expect( - codec.encode(Buffer.from('123456789'), { + codec.encode(stringToBytes('123456789'), { versions: [0], expectedLength: 9, }), @@ -282,7 +274,7 @@ it('decode data', () => { }), ).toEqual({ version: [0], - bytes: Buffer.from('123456789'), + bytes: stringToBytes('123456789'), type: null, }) }) @@ -295,7 +287,7 @@ it('decode data with expected length', function () { }), ).toEqual({ version: [0], - bytes: Buffer.from('123456789'), + bytes: stringToBytes('123456789'), type: null, }) }) diff --git a/packages/ripple-binary-codec/HISTORY.md b/packages/ripple-binary-codec/HISTORY.md index 723a7907..4835b18a 100644 --- a/packages/ripple-binary-codec/HISTORY.md +++ b/packages/ripple-binary-codec/HISTORY.md @@ -2,6 +2,12 @@ ## Unreleased +### Breaking Changes +* `Buffer` has been replaced with `UInt8Array` for both params and return values. `Buffer` may continue to work with params since they extend `UInt8Arrays`. + +### Changes +* Eliminates 4 runtime dependencies: `base-x`, `base64-js`, `buffer`, and `ieee754`. + ## 2.0.0 Beta 1 (2023-10-19) ### Breaking Changes diff --git a/packages/ripple-binary-codec/package.json b/packages/ripple-binary-codec/package.json index 0fa655b1..e395e824 100644 --- a/packages/ripple-binary-codec/package.json +++ b/packages/ripple-binary-codec/package.json @@ -13,7 +13,6 @@ "dependencies": { "@xrplf/isomorphic": "^1.0.0-beta.0", "bignumber.js": "^9.0.0", - "buffer": "6.0.3", "ripple-address-codec": "^5.0.0-beta.0" }, "scripts": { diff --git a/packages/ripple-binary-codec/src/binary.ts b/packages/ripple-binary-codec/src/binary.ts index 507f1c46..8ca067d9 100644 --- a/packages/ripple-binary-codec/src/binary.ts +++ b/packages/ripple-binary-codec/src/binary.ts @@ -1,5 +1,6 @@ /* eslint-disable func-style */ +import { bytesToHex } from '@xrplf/isomorphic/utils' import { coreTypes } from './types' import { BinaryParser } from './serdes/binary-parser' import { AccountID } from './types/account-id' @@ -17,17 +18,17 @@ import { JsonObject } from './types/serialized-type' /** * Construct a BinaryParser * - * @param bytes hex-string or Buffer to construct BinaryParser from + * @param bytes hex-string or Uint8Array to construct BinaryParser from * @param definitions rippled definitions used to parse the values of transaction types and such. * Can be customized for sidechains and amendments. * @returns BinaryParser */ const makeParser = ( - bytes: string | Buffer, + bytes: string | Uint8Array, definitions?: XrplDefinitionsBase, ): BinaryParser => new BinaryParser( - bytes instanceof Buffer ? bytes.toString('hex') : bytes, + bytes instanceof Uint8Array ? bytesToHex(bytes) : bytes, definitions, ) @@ -64,8 +65,8 @@ const binaryToJSON = ( * @field set signingFieldOnly to true if you want to serialize only signing fields */ interface OptionObject { - prefix?: Buffer - suffix?: Buffer + prefix?: Uint8Array + suffix?: Uint8Array signingFieldsOnly?: boolean definitions?: XrplDefinitionsBase } @@ -75,9 +76,12 @@ interface OptionObject { * * @param object JSON object to serialize * @param opts options for serializing, including optional prefix, suffix, signingFieldOnly, and definitions - * @returns A Buffer containing the serialized object + * @returns A Uint8Array containing the serialized object */ -function serializeObject(object: JsonObject, opts: OptionObject = {}): Buffer { +function serializeObject( + object: JsonObject, + opts: OptionObject = {}, +): Uint8Array { const { prefix, suffix, signingFieldsOnly = false, definitions } = opts const bytesList = new BytesList() @@ -105,13 +109,13 @@ function serializeObject(object: JsonObject, opts: OptionObject = {}): Buffer { * @param transaction Transaction to serialize * @param prefix Prefix bytes to put before the serialized object * @param opts.definitions Custom rippled types to use instead of the default. Used for sidechains and amendments. - * @returns A Buffer with the serialized object + * @returns A Uint8Array with the serialized object */ function signingData( transaction: JsonObject, - prefix: Buffer = HashPrefix.transactionSig, + prefix: Uint8Array = HashPrefix.transactionSig, opts: { definitions?: XrplDefinitionsBase } = {}, -): Buffer { +): Uint8Array { return serializeObject(transaction, { prefix, signingFieldsOnly: true, @@ -134,7 +138,7 @@ interface ClaimObject extends JsonObject { * @param opts.definitions Custom rippled types to use instead of the default. Used for sidechains and amendments. * @returns the serialized object with appropriate prefix */ -function signingClaimData(claim: ClaimObject): Buffer { +function signingClaimData(claim: ClaimObject): Uint8Array { const num = BigInt(String(claim.amount)) const prefix = HashPrefix.paymentChannelClaim const channel = coreTypes.Hash256.from(claim.channel).toBytes() @@ -162,7 +166,7 @@ function multiSigningData( opts: { definitions: XrplDefinitionsBase } = { definitions: DEFAULT_DEFINITIONS, }, -): Buffer { +): Uint8Array { const prefix = HashPrefix.transactionMultiSig const suffix = coreTypes.AccountID.from(signingAccount).toBytes() return serializeObject(transaction, { diff --git a/packages/ripple-binary-codec/src/enums/bytes.ts b/packages/ripple-binary-codec/src/enums/bytes.ts index a2b79e53..33a7e539 100644 --- a/packages/ripple-binary-codec/src/enums/bytes.ts +++ b/packages/ripple-binary-codec/src/enums/bytes.ts @@ -4,14 +4,14 @@ import { BytesList, BinaryParser } from '../binary' * @brief: Bytes, name, and ordinal representing one type, ledger_type, transaction type, or result */ export class Bytes { - readonly bytes: Buffer + readonly bytes: Uint8Array constructor( readonly name: string, readonly ordinal: number, readonly ordinalWidth: number, ) { - this.bytes = Buffer.alloc(ordinalWidth) + this.bytes = new Uint8Array(ordinalWidth) for (let i = 0; i < ordinalWidth; i++) { this.bytes[ordinalWidth - i - 1] = (ordinal >>> (i * 8)) & 0xff } diff --git a/packages/ripple-binary-codec/src/enums/field.ts b/packages/ripple-binary-codec/src/enums/field.ts index 90a26a9b..94b5c6d3 100644 --- a/packages/ripple-binary-codec/src/enums/field.ts +++ b/packages/ripple-binary-codec/src/enums/field.ts @@ -22,14 +22,14 @@ export interface FieldInstance { readonly type: Bytes readonly ordinal: number readonly name: string - readonly header: Buffer + readonly header: Uint8Array readonly associatedType: typeof SerializedType } /* * @brief: Serialize a field based on type_code and Field.nth */ -function fieldHeader(type: number, nth: number): Buffer { +function fieldHeader(type: number, nth: number): Uint8Array { const header: Array = [] if (type < 16) { if (nth < 16) { @@ -42,7 +42,7 @@ function fieldHeader(type: number, nth: number): Buffer { } else { header.push(0, type, nth) } - return Buffer.from(header) + return Uint8Array.from(header) } function buildField( diff --git a/packages/ripple-binary-codec/src/hash-prefixes.ts b/packages/ripple-binary-codec/src/hash-prefixes.ts index 9c5aeec2..98035167 100644 --- a/packages/ripple-binary-codec/src/hash-prefixes.ts +++ b/packages/ripple-binary-codec/src/hash-prefixes.ts @@ -1,19 +1,21 @@ +import { writeUInt32BE } from './utils' + /** - * Write a 32 bit integer to a Buffer + * Write a 32 bit integer to a Uint8Array * - * @param uint32 32 bit integer to write to buffer - * @returns a buffer with the bytes representation of uint32 + * @param uint32 32 bit integer to write to Uint8Array + * @returns a Uint8Array with the bytes representation of uint32 */ -function bytes(uint32: number): Buffer { - const result = Buffer.alloc(4) - result.writeUInt32BE(uint32, 0) +function bytes(uint32: number): Uint8Array { + const result = new Uint8Array(4) + writeUInt32BE(result, uint32, 0) return result } /** * Maps HashPrefix names to their byte representation */ -const HashPrefix: Record = { +const HashPrefix: Record = { transactionID: bytes(0x54584e00), // transaction plus metadata transaction: bytes(0x534e4400), diff --git a/packages/ripple-binary-codec/src/hashes.ts b/packages/ripple-binary-codec/src/hashes.ts index e4186607..ad5e4c3b 100644 --- a/packages/ripple-binary-codec/src/hashes.ts +++ b/packages/ripple-binary-codec/src/hashes.ts @@ -1,7 +1,6 @@ import { HashPrefix } from './hash-prefixes' import { Hash256 } from './types' import { BytesList } from './serdes/binary-serializer' - import { sha512 } from '@xrplf/isomorphic/sha512' /** @@ -17,7 +16,7 @@ class Sha512Half extends BytesList { * @param bytes bytes to write to this.hash * @returns the new Sha512Hash object */ - static put(bytes: Buffer): Sha512Half { + static put(bytes: Uint8Array): Sha512Half { return new Sha512Half().put(bytes) } @@ -27,7 +26,7 @@ class Sha512Half extends BytesList { * @param bytes bytes to write to object * @returns the Sha512 object */ - put(bytes: Buffer): Sha512Half { + put(bytes: Uint8Array): Sha512Half { this.hash.update(bytes) return this } @@ -37,8 +36,8 @@ class Sha512Half extends BytesList { * * @returns half of a SHA512 hash */ - finish256(): Buffer { - return Buffer.from(this.hash.digest().slice(0, 32)) + finish256(): Uint8Array { + return Uint8Array.from(this.hash.digest().slice(0, 32)) } /** @@ -57,7 +56,7 @@ class Sha512Half extends BytesList { * @param args zero or more arguments to hash * @returns the sha512half hash of the arguments. */ -function sha512Half(...args: Buffer[]): Buffer { +function sha512Half(...args: Uint8Array[]): Uint8Array { const hash = new Sha512Half() args.forEach((a) => hash.put(a)) return hash.finish256() @@ -69,7 +68,7 @@ function sha512Half(...args: Buffer[]): Buffer { * @param serialized bytes to hash * @returns a Hash256 object */ -function transactionID(serialized: Buffer): Hash256 { +function transactionID(serialized: Uint8Array): Hash256 { return new Hash256(sha512Half(HashPrefix.transactionID, serialized)) } diff --git a/packages/ripple-binary-codec/src/index.ts b/packages/ripple-binary-codec/src/index.ts index c7d4624e..d0e44b5b 100644 --- a/packages/ripple-binary-codec/src/index.ts +++ b/packages/ripple-binary-codec/src/index.ts @@ -9,6 +9,7 @@ import { } from './enums' import { XrplDefinitions } from './enums/xrpl-definitions' import { coreTypes } from './types' +import { bytesToHex } from '@xrplf/isomorphic/utils' const { signingData, @@ -44,9 +45,7 @@ function encode(json: object, definitions?: XrplDefinitionsBase): string { if (typeof json !== 'object') { throw new Error() } - return serializeObject(json as JsonObject, { definitions }) - .toString('hex') - .toUpperCase() + return bytesToHex(serializeObject(json as JsonObject, { definitions })) } /** @@ -64,11 +63,11 @@ function encodeForSigning( if (typeof json !== 'object') { throw new Error() } - return signingData(json as JsonObject, HashPrefix.transactionSig, { - definitions, - }) - .toString('hex') - .toUpperCase() + return bytesToHex( + signingData(json as JsonObject, HashPrefix.transactionSig, { + definitions, + }), + ) } /** @@ -83,9 +82,7 @@ function encodeForSigningClaim(json: object): string { if (typeof json !== 'object') { throw new Error() } - return signingClaimData(json as ClaimObject) - .toString('hex') - .toUpperCase() + return bytesToHex(signingClaimData(json as ClaimObject)) } /** @@ -108,9 +105,9 @@ function encodeForMultisigning( throw new Error() } const definitionsOpt = definitions ? { definitions } : undefined - return multiSigningData(json as JsonObject, signer, definitionsOpt) - .toString('hex') - .toUpperCase() + return bytesToHex( + multiSigningData(json as JsonObject, signer, definitionsOpt), + ) } /** @@ -123,7 +120,7 @@ function encodeQuality(value: string): string { if (typeof value !== 'string') { throw new Error() } - return quality.encode(value).toString('hex').toUpperCase() + return bytesToHex(quality.encode(value)) } /** diff --git a/packages/ripple-binary-codec/src/quality.ts b/packages/ripple-binary-codec/src/quality.ts index 4d49b1fc..07059832 100644 --- a/packages/ripple-binary-codec/src/quality.ts +++ b/packages/ripple-binary-codec/src/quality.ts @@ -1,6 +1,6 @@ import { coreTypes } from './types' - import BigNumber from 'bignumber.js' +import { bytesToHex, hexToBytes } from '@xrplf/isomorphic/utils' /** * class for encoding and decoding quality @@ -12,7 +12,7 @@ class quality { * @param arg string representation of an amount * @returns Serialized quality */ - static encode(quality: string): Buffer { + static encode(quality: string): Uint8Array { const decimal = BigNumber(quality) const exponent = (decimal?.e || 0) - 15 const qualityString = decimal.times(`1e${-exponent}`).abs().toString() @@ -28,9 +28,9 @@ class quality { * @returns deserialized quality */ static decode(quality: string): BigNumber { - const bytes = Buffer.from(quality, 'hex').slice(-8) + const bytes = hexToBytes(quality).slice(-8) const exponent = bytes[0] - 100 - const mantissa = new BigNumber(`0x${bytes.slice(1).toString('hex')}`) + const mantissa = new BigNumber(`0x${bytesToHex(bytes.slice(1))}`) return mantissa.times(`1e${exponent}`) } } diff --git a/packages/ripple-binary-codec/src/serdes/binary-parser.ts b/packages/ripple-binary-codec/src/serdes/binary-parser.ts index 216adb06..007fe71f 100644 --- a/packages/ripple-binary-codec/src/serdes/binary-parser.ts +++ b/packages/ripple-binary-codec/src/serdes/binary-parser.ts @@ -4,12 +4,13 @@ import { FieldInstance, } from '../enums' import { type SerializedType } from '../types/serialized-type' +import { hexToBytes } from '@xrplf/isomorphic/utils' /** * BinaryParser is used to compute fields and values from a HexString */ class BinaryParser { - private bytes: Buffer + private bytes: Uint8Array definitions: XrplDefinitionsBase /** @@ -23,7 +24,7 @@ class BinaryParser { hexBytes: string, definitions: XrplDefinitionsBase = DEFAULT_DEFINITIONS, ) { - this.bytes = Buffer.from(hexBytes, 'hex') + this.bytes = hexToBytes(hexBytes) this.definitions = definitions } @@ -57,7 +58,7 @@ class BinaryParser { * @param n The number of bytes to read * @return The bytes */ - read(n: number): Buffer { + read(n: number): Uint8Array { if (n > this.bytes.byteLength) { throw new Error() } @@ -106,7 +107,7 @@ class BinaryParser { * * @return The variable length bytes */ - readVariableLength(): Buffer { + readVariableLength(): Uint8Array { return this.read(this.readVariableLengthLength()) } diff --git a/packages/ripple-binary-codec/src/serdes/binary-serializer.ts b/packages/ripple-binary-codec/src/serdes/binary-serializer.ts index 7ca9d7a5..08de8ad0 100644 --- a/packages/ripple-binary-codec/src/serdes/binary-serializer.ts +++ b/packages/ripple-binary-codec/src/serdes/binary-serializer.ts @@ -1,11 +1,12 @@ import { FieldInstance } from '../enums' import { type SerializedType } from '../types/serialized-type' +import { bytesToHex, concat } from '@xrplf/isomorphic/utils' /** - * Bytes list is a collection of buffer objects + * Bytes list is a collection of Uint8Array objects */ class BytesList { - private bytesArray: Array = [] + private bytesArray: Array = [] /** * Get the total number of bytes in the BytesList @@ -13,17 +14,17 @@ class BytesList { * @return the number of bytes */ public getLength(): number { - return Buffer.concat(this.bytesArray).byteLength + return concat(this.bytesArray).byteLength } /** * Put bytes in the BytesList * - * @param bytesArg A Buffer + * @param bytesArg A Uint8Array * @return this BytesList */ - public put(bytesArg: Buffer): BytesList { - const bytes = Buffer.from(bytesArg) // Temporary, to catch instances of Uint8Array being passed in + public put(bytesArg: Uint8Array): BytesList { + const bytes = Uint8Array.from(bytesArg) // Temporary, to catch instances of Uint8Array being passed in this.bytesArray.push(bytes) return this } @@ -37,17 +38,17 @@ class BytesList { list.put(this.toBytes()) } - public toBytes(): Buffer { - return Buffer.concat(this.bytesArray) + public toBytes(): Uint8Array { + return concat(this.bytesArray) } toHex(): string { - return this.toBytes().toString('hex').toUpperCase() + return bytesToHex(this.toBytes()) } } /** - * BinarySerializer is used to write fields and values to buffers + * BinarySerializer is used to write fields and values to Uint8Arrays */ class BinarySerializer { private sink: BytesList = new BytesList() @@ -70,7 +71,7 @@ class BinarySerializer { * * @param bytes the bytes to write */ - put(bytes: Buffer): void { + put(bytes: Uint8Array): void { this.sink.put(bytes) } @@ -98,8 +99,8 @@ class BinarySerializer { * * @param length the length of the bytes */ - private encodeVariableLength(length: number): Buffer { - const lenBytes = Buffer.alloc(3) + private encodeVariableLength(length: number): Uint8Array { + const lenBytes = new Uint8Array(3) if (length <= 192) { lenBytes[0] = length return lenBytes.slice(0, 1) diff --git a/packages/ripple-binary-codec/src/shamap.ts b/packages/ripple-binary-codec/src/shamap.ts index 7909b2f9..ee86535c 100644 --- a/packages/ripple-binary-codec/src/shamap.ts +++ b/packages/ripple-binary-codec/src/shamap.ts @@ -8,7 +8,7 @@ import { BytesList } from './serdes/binary-serializer' * Abstract class describing a SHAMapNode */ abstract class ShaMapNode { - abstract hashPrefix(): Buffer + abstract hashPrefix(): Uint8Array abstract isLeaf(): boolean abstract isInner(): boolean abstract toBytesSink(list: BytesList): void @@ -40,10 +40,10 @@ class ShaMapLeaf extends ShaMapNode { /** * Get the prefix of the this.item * - * @returns The hash prefix, unless this.item is undefined, then it returns an empty Buffer + * @returns The hash prefix, unless this.item is undefined, then it returns an empty Uint8Array */ - hashPrefix(): Buffer { - return this.item === undefined ? Buffer.alloc(0) : this.item.hashPrefix() + hashPrefix(): Uint8Array { + return this.item === undefined ? new Uint8Array(0) : this.item.hashPrefix() } /** @@ -99,7 +99,7 @@ class ShaMapInner extends ShaMapNode { * * @returns hash prefix describing an inner node */ - hashPrefix(): Buffer { + hashPrefix(): Uint8Array { return HashPrefix.innerNode } diff --git a/packages/ripple-binary-codec/src/types/account-id.ts b/packages/ripple-binary-codec/src/types/account-id.ts index 8f798fe7..8bfa2c59 100644 --- a/packages/ripple-binary-codec/src/types/account-id.ts +++ b/packages/ripple-binary-codec/src/types/account-id.ts @@ -5,6 +5,7 @@ import { xAddressToClassicAddress, } from 'ripple-address-codec' import { Hash160 } from './hash-160' +import { hexToBytes } from '@xrplf/isomorphic/utils' const HEX_REGEX = /^[A-F0-9]{40}$/ @@ -12,9 +13,11 @@ const HEX_REGEX = /^[A-F0-9]{40}$/ * Class defining how to encode and decode an AccountID */ class AccountID extends Hash160 { - static readonly defaultAccountID: AccountID = new AccountID(Buffer.alloc(20)) + static readonly defaultAccountID: AccountID = new AccountID( + new Uint8Array(20), + ) - constructor(bytes?: Buffer) { + constructor(bytes?: Uint8Array) { super(bytes ?? AccountID.defaultAccountID.bytes) } @@ -35,7 +38,7 @@ class AccountID extends Hash160 { } return HEX_REGEX.test(value) - ? new AccountID(Buffer.from(value, 'hex')) + ? new AccountID(hexToBytes(value)) : this.fromBase58(value) } @@ -58,7 +61,7 @@ class AccountID extends Hash160 { value = classic.classicAddress } - return new AccountID(Buffer.from(decodeAccountID(value))) + return new AccountID(Uint8Array.from(decodeAccountID(value))) } /** @@ -76,9 +79,7 @@ class AccountID extends Hash160 { * @returns the base58 string defined by this.bytes */ toBase58(): string { - /* eslint-disable @typescript-eslint/no-explicit-any */ - return encodeAccountID(this.bytes as any) - /* eslint-enable @typescript-eslint/no-explicit-any */ + return encodeAccountID(this.bytes) } } diff --git a/packages/ripple-binary-codec/src/types/amount.ts b/packages/ripple-binary-codec/src/types/amount.ts index 37797331..b92ebf4f 100644 --- a/packages/ripple-binary-codec/src/types/amount.ts +++ b/packages/ripple-binary-codec/src/types/amount.ts @@ -3,8 +3,9 @@ import { BinaryParser } from '../serdes/binary-parser' import { AccountID } from './account-id' import { Currency } from './currency' import { JsonObject, SerializedType } from './serialized-type' - import BigNumber from 'bignumber.js' +import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils' +import { readUInt32BE, writeUInt32BE } from '../utils' /** * Constants for validating amounts @@ -52,11 +53,9 @@ function isAmountObject(arg): arg is AmountObject { * Class for serializing/Deserializing Amounts */ class Amount extends SerializedType { - static defaultAmount: Amount = new Amount( - Buffer.from('4000000000000000', 'hex'), - ) + static defaultAmount: Amount = new Amount(hexToBytes('4000000000000000')) - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes ?? Amount.defaultAmount.bytes) } @@ -72,17 +71,17 @@ class Amount extends SerializedType { return value } - let amount = Buffer.alloc(8) + let amount = new Uint8Array(8) if (typeof value === 'string') { Amount.assertXrpIsValid(value) const number = BigInt(value) - const intBuf = [Buffer.alloc(4), Buffer.alloc(4)] - intBuf[0].writeUInt32BE(Number(number >> BigInt(32)), 0) - intBuf[1].writeUInt32BE(Number(number & BigInt(mask)), 0) + const intBuf = [new Uint8Array(4), new Uint8Array(4)] + writeUInt32BE(intBuf[0], Number(number >> BigInt(32)), 0) + writeUInt32BE(intBuf[1], Number(number & BigInt(mask)), 0) - amount = Buffer.concat(intBuf) + amount = concat(intBuf) amount[0] |= 0x40 @@ -102,11 +101,11 @@ class Amount extends SerializedType { .toString() const num = BigInt(integerNumberString) - const intBuf = [Buffer.alloc(4), Buffer.alloc(4)] - intBuf[0].writeUInt32BE(Number(num >> BigInt(32)), 0) - intBuf[1].writeUInt32BE(Number(num & BigInt(mask)), 0) + const intBuf = [new Uint8Array(4), new Uint8Array(4)] + writeUInt32BE(intBuf[0], Number(num >> BigInt(32)), 0) + writeUInt32BE(intBuf[1], Number(num & BigInt(mask)), 0) - amount = Buffer.concat(intBuf) + amount = concat(intBuf) amount[0] |= 0x80 @@ -122,7 +121,7 @@ class Amount extends SerializedType { const currency = Currency.from(value.currency).toBytes() const issuer = AccountID.from(value.issuer).toBytes() - return new Amount(Buffer.concat([amount, currency, issuer])) + return new Amount(concat([amount, currency, issuer])) } throw new Error('Invalid type to construct an Amount') @@ -152,8 +151,8 @@ class Amount extends SerializedType { const sign = isPositive ? '' : '-' bytes[0] &= 0x3f - const msb = BigInt(bytes.slice(0, 4).readUInt32BE(0)) - const lsb = BigInt(bytes.slice(4).readUInt32BE(0)) + const msb = BigInt(readUInt32BE(bytes.slice(0, 4), 0)) + const lsb = BigInt(readUInt32BE(bytes.slice(4), 0)) const num = (msb << BigInt(32)) | lsb return `${sign}${num.toString()}` @@ -172,7 +171,7 @@ class Amount extends SerializedType { mantissa[0] = 0 mantissa[1] &= 0x3f - const value = new BigNumber(`${sign}0x${mantissa.toString('hex')}`).times( + const value = new BigNumber(`${sign}0x${bytesToHex(mantissa)}`).times( `1e${exponent}`, ) Amount.assertIouIsValid(value) diff --git a/packages/ripple-binary-codec/src/types/blob.ts b/packages/ripple-binary-codec/src/types/blob.ts index 2c1fad8b..3d7f26d0 100644 --- a/packages/ripple-binary-codec/src/types/blob.ts +++ b/packages/ripple-binary-codec/src/types/blob.ts @@ -1,11 +1,12 @@ import { SerializedType } from './serialized-type' import { BinaryParser } from '../serdes/binary-parser' +import { hexToBytes } from '@xrplf/isomorphic/utils' /** * Variable length encoded type */ class Blob extends SerializedType { - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes) } @@ -32,7 +33,7 @@ class Blob extends SerializedType { } if (typeof value === 'string') { - return new Blob(Buffer.from(value, 'hex')) + return new Blob(hexToBytes(value)) } throw new Error('Cannot construct Blob from value given') diff --git a/packages/ripple-binary-codec/src/types/currency.ts b/packages/ripple-binary-codec/src/types/currency.ts index 6896953d..d6c5a1c1 100644 --- a/packages/ripple-binary-codec/src/types/currency.ts +++ b/packages/ripple-binary-codec/src/types/currency.ts @@ -1,4 +1,5 @@ import { Hash160 } from './hash-160' +import { bytesToHex, hexToBytes, hexToString } from '@xrplf/isomorphic/utils' const XRP_HEX_REGEX = /^0{40}$/ const ISO_REGEX = /^[A-Z0-9a-z?!@#$%^&*(){}[\]|]{3}$/ @@ -9,8 +10,8 @@ const STANDARD_FORMAT_HEX_REGEX = /^0{24}[\x00-\x7F]{6}0{10}$/ /** * Convert an ISO code to a currency bytes representation */ -function isoToBytes(iso: string): Buffer { - const bytes = Buffer.alloc(20) +function isoToBytes(iso: string): Uint8Array { + const bytes = new Uint8Array(20) if (iso !== 'XRP') { const isoBytes = iso.split('').map((c) => c.charCodeAt(0)) bytes.set(isoBytes, 12) @@ -25,8 +26,8 @@ function isIsoCode(iso: string): boolean { return ISO_REGEX.test(iso) } -function isoCodeFromHex(code: Buffer): string | null { - const iso = code.toString() +function isoCodeFromHex(code: Uint8Array): string | null { + const iso = hexToString(bytesToHex(code)) if (iso === 'XRP') { return null } @@ -51,41 +52,41 @@ function isStringRepresentation(input: string): boolean { } /** - * Tests if a Buffer is a valid representation of a currency + * Tests if a Uint8Array is a valid representation of a currency */ -function isBytesArray(bytes: Buffer): boolean { +function isBytesArray(bytes: Uint8Array): boolean { return bytes.byteLength === 20 } /** * Ensures that a value is a valid representation of a currency */ -function isValidRepresentation(input: Buffer | string): boolean { - return input instanceof Buffer +function isValidRepresentation(input: Uint8Array | string): boolean { + return input instanceof Uint8Array ? isBytesArray(input) : isStringRepresentation(input) } /** - * Generate bytes from a string or buffer representation of a currency + * Generate bytes from a string or UInt8Array representation of a currency */ -function bytesFromRepresentation(input: string): Buffer { +function bytesFromRepresentation(input: string): Uint8Array { if (!isValidRepresentation(input)) { throw new Error(`Unsupported Currency representation: ${input}`) } - return input.length === 3 ? isoToBytes(input) : Buffer.from(input, 'hex') + return input.length === 3 ? isoToBytes(input) : hexToBytes(input) } /** * Class defining how to encode and decode Currencies */ class Currency extends Hash160 { - static readonly XRP = new Currency(Buffer.alloc(20)) + static readonly XRP = new Currency(new Uint8Array(20)) private readonly _iso: string | null - constructor(byteBuf: Buffer) { + constructor(byteBuf: Uint8Array) { super(byteBuf ?? Currency.XRP.bytes) - const hex = this.bytes.toString('hex') + const hex = bytesToHex(this.bytes) if (XRP_HEX_REGEX.test(hex)) { this._iso = 'XRP' @@ -132,7 +133,7 @@ class Currency extends Hash160 { if (iso !== null) { return iso } - return this.bytes.toString('hex').toUpperCase() + return bytesToHex(this.bytes) } } diff --git a/packages/ripple-binary-codec/src/types/hash-128.ts b/packages/ripple-binary-codec/src/types/hash-128.ts index e26b5adb..577f1668 100644 --- a/packages/ripple-binary-codec/src/types/hash-128.ts +++ b/packages/ripple-binary-codec/src/types/hash-128.ts @@ -1,13 +1,14 @@ import { Hash } from './hash' +import { bytesToHex } from '@xrplf/isomorphic/utils' /** * Hash with a width of 128 bits */ class Hash128 extends Hash { static readonly width = 16 - static readonly ZERO_128: Hash128 = new Hash128(Buffer.alloc(Hash128.width)) + static readonly ZERO_128: Hash128 = new Hash128(new Uint8Array(Hash128.width)) - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { if (bytes && bytes.byteLength === 0) { bytes = Hash128.ZERO_128.bytes } @@ -21,7 +22,7 @@ class Hash128 extends Hash { * @returns hex String of this.bytes */ toHex(): string { - const hex = this.toBytes().toString('hex').toUpperCase() + const hex = bytesToHex(this.toBytes()) if (/^0+$/.exec(hex)) { return '' } diff --git a/packages/ripple-binary-codec/src/types/hash-160.ts b/packages/ripple-binary-codec/src/types/hash-160.ts index 9dc97668..3f0c9c33 100644 --- a/packages/ripple-binary-codec/src/types/hash-160.ts +++ b/packages/ripple-binary-codec/src/types/hash-160.ts @@ -5,9 +5,9 @@ import { Hash } from './hash' */ class Hash160 extends Hash { static readonly width = 20 - static readonly ZERO_160: Hash160 = new Hash160(Buffer.alloc(Hash160.width)) + static readonly ZERO_160: Hash160 = new Hash160(new Uint8Array(Hash160.width)) - constructor(bytes?: Buffer) { + constructor(bytes?: Uint8Array) { if (bytes && bytes.byteLength === 0) { bytes = Hash160.ZERO_160.bytes } diff --git a/packages/ripple-binary-codec/src/types/hash-256.ts b/packages/ripple-binary-codec/src/types/hash-256.ts index 4f277956..a8a48ff2 100644 --- a/packages/ripple-binary-codec/src/types/hash-256.ts +++ b/packages/ripple-binary-codec/src/types/hash-256.ts @@ -5,9 +5,9 @@ import { Hash } from './hash' */ class Hash256 extends Hash { static readonly width = 32 - static readonly ZERO_256 = new Hash256(Buffer.alloc(Hash256.width)) + static readonly ZERO_256 = new Hash256(new Uint8Array(Hash256.width)) - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes ?? Hash256.ZERO_256.bytes) } } diff --git a/packages/ripple-binary-codec/src/types/hash.ts b/packages/ripple-binary-codec/src/types/hash.ts index 9cb89666..8643578a 100644 --- a/packages/ripple-binary-codec/src/types/hash.ts +++ b/packages/ripple-binary-codec/src/types/hash.ts @@ -1,5 +1,7 @@ import { Comparable } from './serialized-type' import { BinaryParser } from '../serdes/binary-parser' +import { hexToBytes } from '@xrplf/isomorphic/utils' +import { compare } from '../utils' /** * Base class defining how to encode and decode hashes @@ -7,9 +9,9 @@ import { BinaryParser } from '../serdes/binary-parser' class Hash extends Comparable { static readonly width: number - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes) - if (this.bytes.byteLength !== (this.constructor as typeof Hash).width) { + if (this.bytes.length !== (this.constructor as typeof Hash).width) { throw new Error(`Invalid Hash length ${this.bytes.byteLength}`) } } @@ -25,7 +27,7 @@ class Hash extends Comparable { } if (typeof value === 'string') { - return new this(Buffer.from(value, 'hex')) + return new this(hexToBytes(value)) } throw new Error('Cannot construct Hash from given value') @@ -47,7 +49,8 @@ class Hash extends Comparable { * @param other The Hash to compare this to */ compareTo(other: Hash): number { - return this.bytes.compare( + return compare( + this.bytes, (this.constructor as typeof Hash).from(other).bytes, ) } diff --git a/packages/ripple-binary-codec/src/types/issue.ts b/packages/ripple-binary-codec/src/types/issue.ts index 51cf2be3..a7c22b62 100644 --- a/packages/ripple-binary-codec/src/types/issue.ts +++ b/packages/ripple-binary-codec/src/types/issue.ts @@ -1,3 +1,4 @@ +import { concat } from '@xrplf/isomorphic/utils' import { BinaryParser } from '../serdes/binary-parser' import { AccountID } from './account-id' @@ -27,9 +28,9 @@ function isIssueObject(arg): arg is IssueObject { * Class for serializing/Deserializing Amounts */ class Issue extends SerializedType { - static readonly ZERO_ISSUED_CURRENCY: Issue = new Issue(Buffer.alloc(20)) + static readonly ZERO_ISSUED_CURRENCY: Issue = new Issue(new Uint8Array(20)) - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes ?? Issue.ZERO_ISSUED_CURRENCY.bytes) } @@ -51,7 +52,7 @@ class Issue extends SerializedType { return new Issue(currency) } const issuer = AccountID.from(value.issuer).toBytes() - return new Issue(Buffer.concat([currency, issuer])) + return new Issue(concat([currency, issuer])) } throw new Error('Invalid type to construct an Amount') @@ -69,7 +70,7 @@ class Issue extends SerializedType { return new Issue(currency) } const currencyAndIssuer = [currency, parser.read(20)] - return new Issue(Buffer.concat(currencyAndIssuer)) + return new Issue(concat(currencyAndIssuer)) } /** diff --git a/packages/ripple-binary-codec/src/types/path-set.ts b/packages/ripple-binary-codec/src/types/path-set.ts index 019b8959..4359255d 100644 --- a/packages/ripple-binary-codec/src/types/path-set.ts +++ b/packages/ripple-binary-codec/src/types/path-set.ts @@ -2,6 +2,7 @@ import { AccountID } from './account-id' import { Currency } from './currency' import { BinaryParser } from '../serdes/binary-parser' import { SerializedType, JsonObject } from './serialized-type' +import { bytesToHex, concat } from '@xrplf/isomorphic/utils' /** * Constants for separating Paths in a PathSet @@ -62,7 +63,7 @@ class Hop extends SerializedType { return value } - const bytes: Array = [Buffer.from([0])] + const bytes: Array = [Uint8Array.from([0])] if (value.account) { bytes.push(AccountID.from(value.account).toBytes()) @@ -79,7 +80,7 @@ class Hop extends SerializedType { bytes[0][0] |= TYPE_ISSUER } - return new Hop(Buffer.concat(bytes)) + return new Hop(concat(bytes)) } /** @@ -90,7 +91,7 @@ class Hop extends SerializedType { */ static fromParser(parser: BinaryParser): Hop { const type = parser.readUInt8() - const bytes: Array = [Buffer.from([type])] + const bytes: Array = [Uint8Array.from([type])] if (type & TYPE_ACCOUNT) { bytes.push(parser.read(AccountID.width)) @@ -104,7 +105,7 @@ class Hop extends SerializedType { bytes.push(parser.read(AccountID.width)) } - return new Hop(Buffer.concat(bytes)) + return new Hop(concat(bytes)) } /** @@ -113,7 +114,7 @@ class Hop extends SerializedType { * @returns a HopObject, an JS object with optional account, issuer, and currency */ toJSON(): HopObject { - const hopParser = new BinaryParser(this.bytes.toString('hex')) + const hopParser = new BinaryParser(bytesToHex(this.bytes)) const type = hopParser.readUInt8() let account, currency, issuer @@ -170,12 +171,12 @@ class Path extends SerializedType { return value } - const bytes: Array = [] + const bytes: Array = [] value.forEach((hop: HopObject) => { bytes.push(Hop.from(hop).toBytes()) }) - return new Path(Buffer.concat(bytes)) + return new Path(concat(bytes)) } /** @@ -185,7 +186,7 @@ class Path extends SerializedType { * @returns the Path represented by the bytes read from the BinaryParser */ static fromParser(parser: BinaryParser): Path { - const bytes: Array = [] + const bytes: Array = [] while (!parser.end()) { bytes.push(Hop.fromParser(parser).toBytes()) @@ -196,7 +197,7 @@ class Path extends SerializedType { break } } - return new Path(Buffer.concat(bytes)) + return new Path(concat(bytes)) } /** @@ -232,16 +233,16 @@ class PathSet extends SerializedType { } if (isPathSet(value)) { - const bytes: Array = [] + const bytes: Array = [] value.forEach((path: Array) => { bytes.push(Path.from(path).toBytes()) - bytes.push(Buffer.from([PATH_SEPARATOR_BYTE])) + bytes.push(Uint8Array.from([PATH_SEPARATOR_BYTE])) }) - bytes[bytes.length - 1] = Buffer.from([PATHSET_END_BYTE]) + bytes[bytes.length - 1] = Uint8Array.from([PATHSET_END_BYTE]) - return new PathSet(Buffer.concat(bytes)) + return new PathSet(concat(bytes)) } throw new Error('Cannot construct PathSet from given value') @@ -254,7 +255,7 @@ class PathSet extends SerializedType { * @returns the PathSet read from parser */ static fromParser(parser: BinaryParser): PathSet { - const bytes: Array = [] + const bytes: Array = [] while (!parser.end()) { bytes.push(Path.fromParser(parser).toBytes()) @@ -265,7 +266,7 @@ class PathSet extends SerializedType { } } - return new PathSet(Buffer.concat(bytes)) + return new PathSet(concat(bytes)) } /** diff --git a/packages/ripple-binary-codec/src/types/serialized-type.ts b/packages/ripple-binary-codec/src/types/serialized-type.ts index c411124b..eb27f5d5 100644 --- a/packages/ripple-binary-codec/src/types/serialized-type.ts +++ b/packages/ripple-binary-codec/src/types/serialized-type.ts @@ -1,7 +1,7 @@ import { BytesList } from '../serdes/binary-serializer' import { BinaryParser } from '../serdes/binary-parser' - import { XrplDefinitionsBase } from '../enums' +import { bytesToHex } from '@xrplf/isomorphic/utils' type JSON = string | number | boolean | null | undefined | JSON[] | JsonObject @@ -11,10 +11,10 @@ type JsonObject = { [key: string]: JSON } * The base class for all binary-codec types */ class SerializedType { - protected readonly bytes: Buffer = Buffer.alloc(0) + protected readonly bytes: Uint8Array = new Uint8Array(0) - constructor(bytes?: Buffer) { - this.bytes = bytes ?? Buffer.alloc(0) + constructor(bytes?: Uint8Array) { + this.bytes = bytes ?? new Uint8Array(0) } static fromParser(parser: BinaryParser, hint?: number): SerializedType { @@ -42,15 +42,15 @@ class SerializedType { * @returns hex String of this.bytes */ toHex(): string { - return this.toBytes().toString('hex').toUpperCase() + return bytesToHex(this.toBytes()) } /** * Get the bytes representation of a SerializedType * - * @returns A buffer of the bytes + * @returns A Uint8Array of the bytes */ - toBytes(): Buffer { + toBytes(): Uint8Array { if (this.bytes) { return this.bytes } diff --git a/packages/ripple-binary-codec/src/types/st-array.ts b/packages/ripple-binary-codec/src/types/st-array.ts index c4db8346..1b50f3e2 100644 --- a/packages/ripple-binary-codec/src/types/st-array.ts +++ b/packages/ripple-binary-codec/src/types/st-array.ts @@ -2,11 +2,12 @@ import { DEFAULT_DEFINITIONS, XrplDefinitionsBase } from '../enums' import { SerializedType, JsonObject } from './serialized-type' import { STObject } from './st-object' import { BinaryParser } from '../serdes/binary-parser' +import { concat } from '@xrplf/isomorphic/utils' -const ARRAY_END_MARKER = Buffer.from([0xf1]) +const ARRAY_END_MARKER = Uint8Array.from([0xf1]) const ARRAY_END_MARKER_NAME = 'ArrayEndMarker' -const OBJECT_END_MARKER = Buffer.from([0xe1]) +const OBJECT_END_MARKER = Uint8Array.from([0xe1]) /** * TypeGuard for Array @@ -28,7 +29,7 @@ class STArray extends SerializedType { * @returns An STArray Object */ static fromParser(parser: BinaryParser): STArray { - const bytes: Array = [] + const bytes: Array = [] while (!parser.end()) { const field = parser.readField() @@ -44,7 +45,7 @@ class STArray extends SerializedType { } bytes.push(ARRAY_END_MARKER) - return new STArray(Buffer.concat(bytes)) + return new STArray(concat(bytes)) } /** @@ -63,13 +64,13 @@ class STArray extends SerializedType { } if (isObjects(value)) { - const bytes: Array = [] + const bytes: Array = [] value.forEach((obj) => { bytes.push(STObject.from(obj, undefined, definitions).toBytes()) }) bytes.push(ARRAY_END_MARKER) - return new STArray(Buffer.concat(bytes)) + return new STArray(concat(bytes)) } throw new Error('Cannot construct STArray from value given') diff --git a/packages/ripple-binary-codec/src/types/st-object.ts b/packages/ripple-binary-codec/src/types/st-object.ts index 0e0a37b9..75fec3a8 100644 --- a/packages/ripple-binary-codec/src/types/st-object.ts +++ b/packages/ripple-binary-codec/src/types/st-object.ts @@ -11,7 +11,7 @@ import { BinarySerializer, BytesList } from '../serdes/binary-serializer' import { STArray } from './st-array' -const OBJECT_END_MARKER_BYTE = Buffer.from([0xe1]) +const OBJECT_END_MARKER_BYTE = Uint8Array.from([0xe1]) const OBJECT_END_MARKER = 'ObjectEndMarker' const ST_OBJECT = 'STObject' const DESTINATION = 'Destination' diff --git a/packages/ripple-binary-codec/src/types/uint-16.ts b/packages/ripple-binary-codec/src/types/uint-16.ts index 5e989b2f..5d680384 100644 --- a/packages/ripple-binary-codec/src/types/uint-16.ts +++ b/packages/ripple-binary-codec/src/types/uint-16.ts @@ -1,14 +1,17 @@ import { UInt } from './uint' import { BinaryParser } from '../serdes/binary-parser' +import { readUInt16BE, writeUInt16BE } from '../utils' /** * Derived UInt class for serializing/deserializing 16 bit UInt */ class UInt16 extends UInt { protected static readonly width: number = 16 / 8 // 2 - static readonly defaultUInt16: UInt16 = new UInt16(Buffer.alloc(UInt16.width)) + static readonly defaultUInt16: UInt16 = new UInt16( + new Uint8Array(UInt16.width), + ) - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes ?? UInt16.defaultUInt16.bytes) } @@ -27,8 +30,10 @@ class UInt16 extends UInt { } if (typeof val === 'number') { - const buf = Buffer.alloc(UInt16.width) - buf.writeUInt16BE(val, 0) + UInt16.checkUintRange(val, 0, 0xffff) + + const buf = new Uint8Array(UInt16.width) + writeUInt16BE(buf, val, 0) return new UInt16(buf) } @@ -41,7 +46,7 @@ class UInt16 extends UInt { * @returns the number represented by this.bytes */ valueOf(): number { - return this.bytes.readUInt16BE(0) + return parseInt(readUInt16BE(this.bytes, 0)) } } diff --git a/packages/ripple-binary-codec/src/types/uint-32.ts b/packages/ripple-binary-codec/src/types/uint-32.ts index 82285fbc..abb5d8a9 100644 --- a/packages/ripple-binary-codec/src/types/uint-32.ts +++ b/packages/ripple-binary-codec/src/types/uint-32.ts @@ -1,14 +1,17 @@ import { UInt } from './uint' import { BinaryParser } from '../serdes/binary-parser' +import { readUInt32BE, writeUInt32BE } from '../utils' /** * Derived UInt class for serializing/deserializing 32 bit UInt */ class UInt32 extends UInt { protected static readonly width: number = 32 / 8 // 4 - static readonly defaultUInt32: UInt32 = new UInt32(Buffer.alloc(UInt32.width)) + static readonly defaultUInt32: UInt32 = new UInt32( + new Uint8Array(UInt32.width), + ) - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes ?? UInt32.defaultUInt32.bytes) } @@ -26,16 +29,17 @@ class UInt32 extends UInt { return val } - const buf = Buffer.alloc(UInt32.width) + const buf = new Uint8Array(UInt32.width) if (typeof val === 'string') { const num = Number.parseInt(val) - buf.writeUInt32BE(num, 0) + writeUInt32BE(buf, num, 0) return new UInt32(buf) } if (typeof val === 'number') { - buf.writeUInt32BE(val, 0) + UInt32.checkUintRange(val, 0, 0xffffffff) + writeUInt32BE(buf, val, 0) return new UInt32(buf) } @@ -48,7 +52,7 @@ class UInt32 extends UInt { * @returns the number represented by this.bytes */ valueOf(): number { - return this.bytes.readUInt32BE(0) + return parseInt(readUInt32BE(this.bytes, 0), 10) } } diff --git a/packages/ripple-binary-codec/src/types/uint-64.ts b/packages/ripple-binary-codec/src/types/uint-64.ts index 6ee0ba76..584b468d 100644 --- a/packages/ripple-binary-codec/src/types/uint-64.ts +++ b/packages/ripple-binary-codec/src/types/uint-64.ts @@ -1,5 +1,7 @@ import { UInt } from './uint' import { BinaryParser } from '../serdes/binary-parser' +import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils' +import { readUInt32BE, writeUInt32BE } from '../utils' const HEX_REGEX = /^[a-fA-F0-9]{1,16}$/ const mask = BigInt(0x00000000ffffffff) @@ -9,9 +11,11 @@ const mask = BigInt(0x00000000ffffffff) */ class UInt64 extends UInt { protected static readonly width: number = 64 / 8 // 8 - static readonly defaultUInt64: UInt64 = new UInt64(Buffer.alloc(UInt64.width)) + static readonly defaultUInt64: UInt64 = new UInt64( + new Uint8Array(UInt64.width), + ) - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes ?? UInt64.defaultUInt64.bytes) } @@ -30,7 +34,7 @@ class UInt64 extends UInt { return val } - let buf = Buffer.alloc(UInt64.width) + let buf = new Uint8Array(UInt64.width) if (typeof val === 'number') { if (val < 0) { @@ -39,11 +43,11 @@ class UInt64 extends UInt { const number = BigInt(val) - const intBuf = [Buffer.alloc(4), Buffer.alloc(4)] - intBuf[0].writeUInt32BE(Number(number >> BigInt(32)), 0) - intBuf[1].writeUInt32BE(Number(number & BigInt(mask)), 0) + const intBuf = [new Uint8Array(4), new Uint8Array(4)] + writeUInt32BE(intBuf[0], Number(number >> BigInt(32)), 0) + writeUInt32BE(intBuf[1], Number(number & BigInt(mask)), 0) - return new UInt64(Buffer.concat(intBuf)) + return new UInt64(concat(intBuf)) } if (typeof val === 'string') { @@ -52,16 +56,16 @@ class UInt64 extends UInt { } const strBuf = val.padStart(16, '0') - buf = Buffer.from(strBuf, 'hex') + buf = hexToBytes(strBuf) return new UInt64(buf) } if (typeof val === 'bigint') { - const intBuf = [Buffer.alloc(4), Buffer.alloc(4)] - intBuf[0].writeUInt32BE(Number(val >> BigInt(32)), 0) - intBuf[1].writeUInt32BE(Number(val & BigInt(mask)), 0) + const intBuf = [new Uint8Array(4), new Uint8Array(4)] + writeUInt32BE(intBuf[0], Number(Number(val >> BigInt(32))), 0) + writeUInt32BE(intBuf[1], Number(val & BigInt(mask)), 0) - return new UInt64(Buffer.concat(intBuf)) + return new UInt64(concat(intBuf)) } throw new Error('Cannot construct UInt64 from given value') @@ -73,7 +77,7 @@ class UInt64 extends UInt { * @returns a hex-string */ toJSON(): string { - return this.bytes.toString('hex').toUpperCase() + return bytesToHex(this.bytes) } /** @@ -82,8 +86,8 @@ class UInt64 extends UInt { * @returns the number represented buy this.bytes */ valueOf(): bigint { - const msb = BigInt(this.bytes.slice(0, 4).readUInt32BE(0)) - const lsb = BigInt(this.bytes.slice(4).readUInt32BE(0)) + const msb = BigInt(readUInt32BE(this.bytes.slice(0, 4), 0)) + const lsb = BigInt(readUInt32BE(this.bytes.slice(4), 0)) return (msb << BigInt(32)) | lsb } @@ -92,7 +96,7 @@ class UInt64 extends UInt { * * @returns 8 bytes representing the UInt64 */ - toBytes(): Buffer { + toBytes(): Uint8Array { return this.bytes } } diff --git a/packages/ripple-binary-codec/src/types/uint-8.ts b/packages/ripple-binary-codec/src/types/uint-8.ts index e116089d..50c4cff7 100644 --- a/packages/ripple-binary-codec/src/types/uint-8.ts +++ b/packages/ripple-binary-codec/src/types/uint-8.ts @@ -1,14 +1,16 @@ import { UInt } from './uint' import { BinaryParser } from '../serdes/binary-parser' +import { bytesToHex } from '@xrplf/isomorphic/utils' +import { writeUInt8 } from '../utils' /** * Derived UInt class for serializing/deserializing 8 bit UInt */ class UInt8 extends UInt { protected static readonly width: number = 8 / 8 // 1 - static readonly defaultUInt8: UInt8 = new UInt8(Buffer.alloc(UInt8.width)) + static readonly defaultUInt8: UInt8 = new UInt8(new Uint8Array(UInt8.width)) - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes ?? UInt8.defaultUInt8.bytes) } @@ -27,8 +29,10 @@ class UInt8 extends UInt { } if (typeof val === 'number') { - const buf = Buffer.alloc(UInt8.width) - buf.writeUInt8(val, 0) + UInt8.checkUintRange(val, 0, 0xff) + + const buf = new Uint8Array(UInt8.width) + writeUInt8(buf, val, 0) return new UInt8(buf) } @@ -41,7 +45,7 @@ class UInt8 extends UInt { * @returns the number represented by this.bytes */ valueOf(): number { - return this.bytes.readUInt8(0) + return parseInt(bytesToHex(this.bytes), 16) } } diff --git a/packages/ripple-binary-codec/src/types/uint.ts b/packages/ripple-binary-codec/src/types/uint.ts index dbcd7948..62c9f7fa 100644 --- a/packages/ripple-binary-codec/src/types/uint.ts +++ b/packages/ripple-binary-codec/src/types/uint.ts @@ -17,7 +17,7 @@ function compare(n1: number | bigint, n2: number | bigint): number { abstract class UInt extends Comparable { protected static width: number - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes) } @@ -47,6 +47,14 @@ abstract class UInt extends Comparable { * @returns the value */ abstract valueOf(): number | bigint + + static checkUintRange(val: number, min: number, max: number): void { + if (val < min || val > max) { + throw new Error( + `Invalid ${this.constructor.name}: ${val} must be >= ${min} and <= ${max}`, + ) + } + } } export { UInt } diff --git a/packages/ripple-binary-codec/src/types/vector-256.ts b/packages/ripple-binary-codec/src/types/vector-256.ts index c5a4e31f..0f7bc2cc 100644 --- a/packages/ripple-binary-codec/src/types/vector-256.ts +++ b/packages/ripple-binary-codec/src/types/vector-256.ts @@ -2,6 +2,7 @@ import { SerializedType } from './serialized-type' import { BinaryParser } from '../serdes/binary-parser' import { Hash256 } from './hash-256' import { BytesList } from '../serdes/binary-serializer' +import { bytesToHex } from '@xrplf/isomorphic/utils' /** * TypeGuard for Array @@ -14,7 +15,7 @@ function isStrings(arg): arg is Array { * Class for serializing and deserializing vectors of Hash256 */ class Vector256 extends SerializedType { - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes) } @@ -69,12 +70,7 @@ class Vector256 extends SerializedType { const result: Array = [] for (let i = 0; i < this.bytes.byteLength; i += 32) { - result.push( - this.bytes - .slice(i, i + 32) - .toString('hex') - .toUpperCase(), - ) + result.push(bytesToHex(this.bytes.slice(i, i + 32))) } return result } diff --git a/packages/ripple-binary-codec/src/types/xchain-bridge.ts b/packages/ripple-binary-codec/src/types/xchain-bridge.ts index aae1be22..6bda43ae 100644 --- a/packages/ripple-binary-codec/src/types/xchain-bridge.ts +++ b/packages/ripple-binary-codec/src/types/xchain-bridge.ts @@ -2,8 +2,8 @@ import { BinaryParser } from '../serdes/binary-parser' import { AccountID } from './account-id' import { JsonObject, SerializedType } from './serialized-type' - import { Issue, IssueObject } from './issue' +import { concat } from '@xrplf/isomorphic/utils' /** * Interface for JSON objects that represent cross-chain bridges @@ -34,11 +34,11 @@ function isXChainBridgeObject(arg): arg is XChainBridgeObject { */ class XChainBridge extends SerializedType { static readonly ZERO_XCHAIN_BRIDGE: XChainBridge = new XChainBridge( - Buffer.concat([ - Buffer.from([0x14]), - Buffer.alloc(40), - Buffer.from([0x14]), - Buffer.alloc(40), + concat([ + Uint8Array.from([0x14]), + new Uint8Array(40), + Uint8Array.from([0x14]), + new Uint8Array(40), ]), ) @@ -50,7 +50,7 @@ class XChainBridge extends SerializedType { { name: 'IssuingChainIssue', type: Issue }, ] - constructor(bytes: Buffer) { + constructor(bytes: Uint8Array) { super(bytes ?? XChainBridge.ZERO_XCHAIN_BRIDGE.bytes) } @@ -71,16 +71,16 @@ class XChainBridge extends SerializedType { throw new Error('Invalid type to construct an XChainBridge') } - const bytes: Array = [] + const bytes: Array = [] this.TYPE_ORDER.forEach((item) => { const { name, type } = item if (type === AccountID) { - bytes.push(Buffer.from([0x14])) + bytes.push(Uint8Array.from([0x14])) } const object = type.from(value[name]) bytes.push(object.toBytes()) }) - return new XChainBridge(Buffer.concat(bytes)) + return new XChainBridge(concat(bytes)) } /** @@ -90,19 +90,19 @@ class XChainBridge extends SerializedType { * @returns An XChainBridge object */ static fromParser(parser: BinaryParser): XChainBridge { - const bytes: Array = [] + const bytes: Array = [] this.TYPE_ORDER.forEach((item) => { const { type } = item if (type === AccountID) { parser.skip(1) - bytes.push(Buffer.from([0x14])) + bytes.push(Uint8Array.from([0x14])) } const object = type.fromParser(parser) bytes.push(object.toBytes()) }) - return new XChainBridge(Buffer.concat(bytes)) + return new XChainBridge(concat(bytes)) } /** diff --git a/packages/ripple-binary-codec/src/utils.ts b/packages/ripple-binary-codec/src/utils.ts new file mode 100644 index 00000000..27b2ce1b --- /dev/null +++ b/packages/ripple-binary-codec/src/utils.ts @@ -0,0 +1,152 @@ +// Even though this comes from NodeJS it is valid in the browser +import TypedArray = NodeJS.TypedArray + +/** + * Writes value to array at the specified offset. The value must be a valid unsigned 8-bit integer. + * @param array Uint8Array to be written to + * @param value Number to be written to array. + * @param offset plus the number of bytes written. + */ +export function writeUInt8( + array: Uint8Array, + value: number, + offset: number, +): void { + value = Number(value) + array[offset] = value +} + +/** + * Writes value to array at the specified offset as big-endian. The value must be a valid unsigned 16-bit integer. + * @param array Uint8Array to be written to + * @param value Number to be written to array. + * @param offset plus the number of bytes written. + */ +export function writeUInt16BE( + array: Uint8Array, + value: number, + offset: number, +): void { + value = Number(value) + + array[offset] = value >>> 8 + array[offset + 1] = value +} + +/** + * Writes value to array at the specified offset as big-endian. The value must be a valid unsigned 32-bit integer. + * @param array Uint8Array to be written to + * @param value Number to be written to array. + * @param offset plus the number of bytes written. + */ +export function writeUInt32BE( + array: Uint8Array, + value: number, + offset: number, +): void { + array[offset] = (value >>> 24) & 0xff + array[offset + 1] = (value >>> 16) & 0xff + array[offset + 2] = (value >>> 8) & 0xff + array[offset + 3] = value & 0xff +} + +/** + * Reads an unsigned, big-endian 16-bit integer from the array at the specified offset. + * @param array Uint8Array to read + * @param offset Number of bytes to skip before starting to read. Must satisfy 0 <= offset <= buf.length - 2 + */ +export function readUInt16BE(array: Uint8Array, offset: number): string { + return new DataView(array.buffer).getUint16(offset, false).toString(10) +} + +/** + * Reads an unsigned, big-endian 16-bit integer from the array at the specified offset. + * @param array Uint8Array to read + * @param offset Number of bytes to skip before starting to read. Must satisfy 0 <= offset <= buf.length - 4 + */ +export function readUInt32BE(array: Uint8Array, offset: number): string { + return new DataView(array.buffer).getUint32(offset, false).toString(10) +} + +/** + * Compares two Uint8Array or ArrayBuffers + * @param a first array to compare + * @param b second array to compare + */ +export function equal( + a: Uint8Array | ArrayBuffer, + b: Uint8Array | ArrayBuffer, +): boolean { + const aUInt = a instanceof ArrayBuffer ? new Uint8Array(a, 0) : a + const bUInt = b instanceof ArrayBuffer ? new Uint8Array(b, 0) : b + if (aUInt.byteLength != bUInt.byteLength) return false + if (aligned32(aUInt) && aligned32(bUInt)) return compare32(aUInt, bUInt) === 0 + if (aligned16(aUInt) && aligned16(bUInt)) return compare16(aUInt, bUInt) === 0 + return compare8(aUInt, bUInt) === 0 +} + +/** + * Compares two 8 bit aligned arrays + * @param a first array to compare + * @param b second array to compare + */ +function compare8(a, b) { + const ua = new Uint8Array(a.buffer, a.byteOffset, a.byteLength) + const ub = new Uint8Array(b.buffer, b.byteOffset, b.byteLength) + return compare(ua, ub) +} + +/** + * Compares two 16 bit aligned arrays + * @param a first array to compare + * @param b second array to compare + */ +function compare16(a: Uint8Array, b: Uint8Array) { + const ua = new Uint16Array(a.buffer, a.byteOffset, a.byteLength / 2) + const ub = new Uint16Array(b.buffer, b.byteOffset, b.byteLength / 2) + return compare(ua, ub) +} + +/** + * Compares two 32 bit aligned arrays + * @param a first array to compare + * @param b second array to compare + */ +function compare32(a: Uint8Array, b: Uint8Array) { + const ua = new Uint32Array(a.buffer, a.byteOffset, a.byteLength / 4) + const ub = new Uint32Array(b.buffer, b.byteOffset, b.byteLength / 4) + return compare(ua, ub) +} + +/** + * Compare two TypedArrays + * @param a first array to compare + * @param b second array to compare + */ +export function compare(a: TypedArray, b: TypedArray): 1 | -1 | 0 { + if (a.byteLength !== b.byteLength) { + throw new Error('Cannot compare arrays of different length') + } + + for (let i = 0; i < a.length - 1; i += 1) { + if (a[i] > b[i]) return 1 + if (a[i] < b[i]) return -1 + } + return 0 +} + +/** + * Determine if TypedArray is 16 bit aligned + * @param array The array to check + */ +function aligned16(array: TypedArray) { + return array.byteOffset % 2 === 0 && array.byteLength % 2 === 0 +} + +/** + * Determine if TypedArray is 32 bit aligned + * @param array The array to check + */ +function aligned32(array: TypedArray) { + return array.byteOffset % 4 === 0 && array.byteLength % 4 === 0 +} diff --git a/packages/ripple-binary-codec/test/binary-parser.test.ts b/packages/ripple-binary-codec/test/binary-parser.test.ts index 77162c0c..eef94c4f 100644 --- a/packages/ripple-binary-codec/test/binary-parser.test.ts +++ b/packages/ripple-binary-codec/test/binary-parser.test.ts @@ -1,14 +1,15 @@ +import { hexOnly } from './utils' import { coreTypes, Amount, Hash160 } from '../src/types' import BigNumber from 'bignumber.js' import { encodeAccountID } from 'ripple-address-codec' import { Field, TransactionType } from '../src/enums' import { makeParser, readJSON } from '../src/binary' -import { parseHexOnly, hexOnly } from './utils' import { BytesList } from '../src/serdes/binary-serializer' import fixtures from './fixtures/data-driven-tests.json' -const __ = hexOnly +const { bytesToHex } = require('@xrplf/isomorphic/utils') + function toJSON(v) { return v.toJSON ? v.toJSON() : v } @@ -28,16 +29,15 @@ function assertEqualAmountJSON(actual, expected) { } function basicApiTests() { - const bytes = parseHexOnly('00,01020304,0506') it('can read slices of bytes', () => { - const parser = makeParser(bytes) + const parser = makeParser('00010203040506') // @ts-expect-error -- checking private variable type - expect(parser.bytes instanceof Buffer).toBe(true) + expect(parser.bytes instanceof Uint8Array).toBe(true) const read1 = parser.read(1) - expect(read1 instanceof Buffer).toBe(true) - expect(read1).toEqual(Buffer.from([0])) - expect(parser.read(4)).toEqual(Buffer.from([1, 2, 3, 4])) - expect(parser.read(2)).toEqual(Buffer.from([5, 6])) + expect(read1 instanceof Uint8Array).toBe(true) + expect(read1).toEqual(Uint8Array.from([0])) + expect(parser.read(4)).toEqual(Uint8Array.from([1, 2, 3, 4])) + expect(parser.read(2)).toEqual(Uint8Array.from([5, 6])) expect(() => parser.read(1)).toThrow() }) it('can read a Uint32 at full', () => { @@ -62,12 +62,12 @@ function transactionParsingTests() { }, TakerPays: '98957503520', TransactionType: 'OfferCreate', - TxnSignature: __(` + TxnSignature: hexOnly(` 304502202ABE08D5E78D1E74A4C18F2714F64E87B8BD57444AF A5733109EB3C077077520022100DB335EE97386E4C0591CAC02 4D50E9230D8F171EEB901B5E5E4BD6D1E0AEF98C`), }, - binary: __(` + binary: hexOnly(` 120007220000000024000195F964400000170A53AC2065D5460561E C9DE000000000000000000000000000494C53000000000092D70596 8936C419CE614BF264B5EEB1CEA47FF468400000000000000A73210 @@ -102,11 +102,9 @@ function transactionParsingTests() { expect(parser.read(8)).not.toEqual([]) expect(parser.readField()).toEqual(Field['SigningPubKey']) expect(parser.readVariableLengthLength()).toBe(33) - expect(parser.read(33).toString('hex').toUpperCase()).toEqual( - tx_json.SigningPubKey, - ) + expect(bytesToHex(parser.read(33))).toEqual(tx_json.SigningPubKey) expect(parser.readField()).toEqual(Field['TxnSignature']) - expect(parser.readVariableLength().toString('hex').toUpperCase()).toEqual( + expect(bytesToHex(parser.readVariableLength())).toEqual( tx_json.TxnSignature, ) expect(parser.readField()).toEqual(Field['Account']) @@ -303,7 +301,7 @@ function nestedObjectTests() { } function pathSetBinaryTests() { - const bytes = __( + const bytes = hexOnly( `1200002200000000240000002E2E00004BF161D4C71AFD498D00000000000000 0000000000000055534400000000000A20B3C85F482532A9578DBB3950B85CA0 6594D168400000000000000A69D446F8038585E9400000000000000000000000 diff --git a/packages/ripple-binary-codec/test/binary-serializer.test.ts b/packages/ripple-binary-codec/test/binary-serializer.test.ts index 11fc771a..5d690d52 100644 --- a/packages/ripple-binary-codec/test/binary-serializer.test.ts +++ b/packages/ripple-binary-codec/test/binary-serializer.test.ts @@ -5,6 +5,7 @@ const { encode, decode } = require('../src') const { makeParser, BytesList, BinarySerializer } = binary const { coreTypes } = require('../src/types') const { UInt8, UInt16, UInt32, UInt64, STObject } = coreTypes + const deliverMinTx = require('./fixtures/delivermin-tx.json') const deliverMinTxBinary = require('./fixtures/delivermin-tx-binary.json') const SignerListSet = { @@ -105,12 +106,12 @@ const NegativeUNL = require('./fixtures/negative-unl.json') function bytesListTest() { const list = new BytesList() - .put(Buffer.from([0])) - .put(Buffer.from([2, 3])) - .put(Buffer.from([4, 5])) - it('is an Array', function () { + .put(Uint8Array.from([0])) + .put(Uint8Array.from([2, 3])) + .put(Uint8Array.from([4, 5])) + it('is an Array', function () { expect(Array.isArray(list.bytesArray)).toBe(true) - expect(list.bytesArray[0] instanceof Buffer).toBe(true) + expect(list.bytesArray[0] instanceof Uint8Array).toBe(true) }) it('keeps track of the length itself', function () { expect(list.getLength()).toBe(5) @@ -118,7 +119,7 @@ function bytesListTest() { it('can join all arrays into one via toBytes', function () { const joined = list.toBytes() expect(joined.length).toEqual(5) - expect(joined).toEqual(Buffer.from([0, 2, 3, 4, 5])) + expect(joined).toEqual(Uint8Array.from([0, 2, 3, 4, 5])) }) } @@ -149,10 +150,18 @@ function check(type, n, expected) { return } serializer.writeType(type, n) - expect(bl.toBytes()).toEqual(Buffer.from(expected)) + expect(bl.toBytes()).toEqual(Uint8Array.from(expected)) }) } +it(`Uint16 serializes 5 as 0,5`, function () { + const bl = new BytesList() + const serializer = new BinarySerializer(bl) + const expected = [0, 5] + serializer.writeType(UInt16, 5) + expect(bl.toBytes()).toEqual(Uint8Array.from(expected)) +}) + check(UInt8, 5, [5]) check(UInt16, 5, [0, 5]) check(UInt32, 5, [0, 0, 0, 5]) diff --git a/packages/ripple-binary-codec/test/hash.test.ts b/packages/ripple-binary-codec/test/hash.test.ts index 6fa93993..246ac4c7 100644 --- a/packages/ripple-binary-codec/test/hash.test.ts +++ b/packages/ripple-binary-codec/test/hash.test.ts @@ -1,5 +1,4 @@ -const { coreTypes } = require('../src/types') -const { Hash128, Hash160, Hash256, AccountID, Currency } = coreTypes +import { Hash128, Hash160, Hash256, AccountID, Currency } from '../src/types' describe('Hash128', function () { it('has a static width member', function () { @@ -110,8 +109,8 @@ describe('Currency', function () { ).toBe(null) }) - it('can be constructed from a Buffer', function () { - const xrp = new Currency(Buffer.alloc(20)) + it('can be constructed from a Uint8Array', function () { + const xrp = new Currency(new Uint8Array(20)) expect(xrp.iso()).toBe('XRP') }) it('Can handle non-standard currency codes', () => { @@ -125,7 +124,9 @@ describe('Currency', function () { }) it('throws on invalid reprs', function () { - expect(() => Currency.from(Buffer.alloc(19))).toThrow() + // @ts-expect-error -- invalid type check + expect(() => Currency.from(new Uint8Array(19))).toThrow() + // @ts-expect-error -- invalid type check expect(() => Currency.from(1)).toThrow() expect(() => Currency.from('00000000000000000000000000000000000000m'), diff --git a/packages/ripple-binary-codec/test/quality.test.ts b/packages/ripple-binary-codec/test/quality.test.ts index 8c9d0dfa..41a53fd5 100644 --- a/packages/ripple-binary-codec/test/quality.test.ts +++ b/packages/ripple-binary-codec/test/quality.test.ts @@ -1,4 +1,5 @@ const { quality } = require('../src/coretypes') +const { bytesToHex } = require('@xrplf/isomorphic/utils') describe('Quality encode/decode', function () { const bookDirectory = @@ -10,6 +11,6 @@ describe('Quality encode/decode', function () { }) it('can encode', function () { const bytes = quality.encode(expectedQuality) - expect(bytes.toString('hex').toUpperCase()).toBe(bookDirectory.slice(-16)) + expect(bytesToHex(bytes)).toBe(bookDirectory.slice(-16)) }) }) diff --git a/packages/ripple-binary-codec/test/shamap.test.ts b/packages/ripple-binary-codec/test/shamap.test.ts index 8e308469..e9674c1c 100644 --- a/packages/ripple-binary-codec/test/shamap.test.ts +++ b/packages/ripple-binary-codec/test/shamap.test.ts @@ -16,7 +16,7 @@ function makeItem( indexArg: string, ): [ Hash256, - { toBytesSink: (sink: BytesList) => void; hashPrefix: () => Buffer }, + { toBytesSink: (sink: BytesList) => void; hashPrefix: () => Uint8Array }, ] { let str = indexArg while (str.length < 64) { @@ -28,7 +28,7 @@ function makeItem( index.toBytesSink(sink) }, hashPrefix() { - return Buffer.from([1, 3, 3, 7]) + return Uint8Array.from([1, 3, 3, 7]) }, } return [index, item] diff --git a/packages/ripple-binary-codec/test/utils.ts b/packages/ripple-binary-codec/test/utils.ts index f5da5d23..625d77d7 100644 --- a/packages/ripple-binary-codec/test/utils.ts +++ b/packages/ripple-binary-codec/test/utils.ts @@ -1,7 +1,9 @@ +import { hexToBytes } from '@xrplf/isomorphic/utils' + export function hexOnly(hex: string): string { return hex.replace(/[^a-fA-F0-9]/g, '') } -export function parseHexOnly(hex: string): Buffer { - return Buffer.from(hexOnly(hex), 'hex') +export function parseHexOnly(hex: string): Uint8Array { + return hexToBytes(hexOnly(hex)) } diff --git a/packages/ripple-keypairs/HISTORY.md b/packages/ripple-keypairs/HISTORY.md index 070dc18e..a8352294 100644 --- a/packages/ripple-keypairs/HISTORY.md +++ b/packages/ripple-keypairs/HISTORY.md @@ -2,6 +2,12 @@ ## Unreleased +### Breaking Changes +* `Buffer` has been replaced with `UInt8Array` for both params and return values. `Buffer` may continue to work with params since they extend `UInt8Arrays`. + +### Changes +* Eliminates 4 runtime dependencies: `base-x`, `base64-js`, `buffer`, and `ieee754`. + ## 2.0.0 Beta 1 (2023-10-19) ### Breaking Changes diff --git a/packages/ripple-keypairs/test/api.test.ts b/packages/ripple-keypairs/test/api.test.ts index 274e9ada..91c19d61 100644 --- a/packages/ripple-keypairs/test/api.test.ts +++ b/packages/ripple-keypairs/test/api.test.ts @@ -8,6 +8,7 @@ import { sign, verify, } from '../src' +import { stringToHex } from '@xrplf/isomorphic/utils' const entropy = new Uint8Array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, @@ -77,7 +78,7 @@ describe('api', () => { it('sign - secp256k1', () => { const privateKey = fixtures.secp256k1.keypair.privateKey const message = fixtures.secp256k1.message - const messageHex = Buffer.from(message, 'utf8').toString('hex') + const messageHex = stringToHex(message) const signature = sign(messageHex, privateKey) expect(signature).toEqual(fixtures.secp256k1.signature) }) @@ -86,14 +87,14 @@ describe('api', () => { const signature = fixtures.secp256k1.signature const publicKey = fixtures.secp256k1.keypair.publicKey const message = fixtures.secp256k1.message - const messageHex = Buffer.from(message, 'utf8').toString('hex') + const messageHex = stringToHex(message) expect(verify(messageHex, signature, publicKey)).toBeTruthy() }) it('sign - ed25519', () => { const privateKey = fixtures.ed25519.keypair.privateKey const message = fixtures.ed25519.message - const messageHex = Buffer.from(message, 'utf8').toString('hex') + const messageHex = stringToHex(message) const signature = sign(messageHex, privateKey) expect(signature).toEqual(fixtures.ed25519.signature) }) @@ -102,7 +103,7 @@ describe('api', () => { const signature = fixtures.ed25519.signature const publicKey = fixtures.ed25519.keypair.publicKey const message = fixtures.ed25519.message - const messageHex = Buffer.from(message, 'utf8').toString('hex') + const messageHex = stringToHex(message) expect(verify(messageHex, signature, publicKey)).toBeTruthy() }) diff --git a/packages/secret-numbers/HISTORY.md b/packages/secret-numbers/HISTORY.md index 2b45df7b..c318edff 100644 --- a/packages/secret-numbers/HISTORY.md +++ b/packages/secret-numbers/HISTORY.md @@ -6,6 +6,10 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ### BREAKING CHANGES: - Moved all methods that were on `Utils` are now individually exported. +* `Buffer` has been replaced with `UInt8Array` for both params and return values. `Buffer` may continue to work with params since they extend `UInt8Arrays`. + +### Changes +* Eliminates 4 runtime dependencies: `base-x`, `base64-js`, `buffer`, and `ieee754`. ## 1.0.0 Beta 1 (2023-10-19) diff --git a/packages/secret-numbers/src/schema/Account.ts b/packages/secret-numbers/src/schema/Account.ts index a3784371..03678d07 100644 --- a/packages/secret-numbers/src/schema/Account.ts +++ b/packages/secret-numbers/src/schema/Account.ts @@ -33,12 +33,12 @@ export class Account { }, } - constructor(secretNumbers?: string[] | string | Buffer) { + constructor(secretNumbers?: string[] | string | Uint8Array) { if (typeof secretNumbers === 'string') { this._secret = parseSecretString(secretNumbers) } else if (Array.isArray(secretNumbers)) { this._secret = secretNumbers - } else if (Buffer.isBuffer(secretNumbers)) { + } else if (secretNumbers instanceof Uint8Array) { this._secret = entropyToSecret(secretNumbers) } else { this._secret = randomSecret() diff --git a/packages/secret-numbers/src/utils/index.ts b/packages/secret-numbers/src/utils/index.ts index b75c4e4e..47b9bd9a 100644 --- a/packages/secret-numbers/src/utils/index.ts +++ b/packages/secret-numbers/src/utils/index.ts @@ -1,7 +1,12 @@ -import { randomBytes } from '@xrplf/isomorphic/utils' +import { + bytesToHex, + concat, + hexToBytes, + randomBytes, +} from '@xrplf/isomorphic/utils' -function randomEntropy(): Buffer { - return Buffer.from(randomBytes(16)) +function randomEntropy(): Uint8Array { + return randomBytes(16) } function calculateChecksum(position: number, value: number): number { @@ -32,11 +37,11 @@ function checkChecksum( return (normalizedValue * (position * 2 + 1)) % 9 === normalizedChecksum } -function entropyToSecret(entropy: Buffer): string[] { +function entropyToSecret(entropy: Uint8Array): string[] { const len = new Array(Math.ceil(entropy.length / 2)) const chunks = Array.from(len, (_a, chunk) => { const buffChunk = entropy.slice(chunk * 2, (chunk + 1) * 2) - const no = parseInt(buffChunk.toString('hex'), 16) + const no = parseInt(bytesToHex(buffChunk), 16) const fill = '0'.repeat(5 - String(no).length) return fill + String(no) + String(calculateChecksum(chunk, no)) }) @@ -50,8 +55,8 @@ function randomSecret(): string[] { return entropyToSecret(randomEntropy()) } -function secretToEntropy(secret: string[]): Buffer { - return Buffer.concat( +function secretToEntropy(secret: string[]): Uint8Array { + return concat( secret.map((chunk, i) => { const no = Number(chunk.slice(0, 5)) const checksum = Number(chunk.slice(5)) @@ -62,7 +67,7 @@ function secretToEntropy(secret: string[]): Buffer { throw new Error('Invalid secret part: checksum invalid') } const hex = `0000${no.toString(16)}`.slice(-4) - return Buffer.from(hex, 'hex') + return hexToBytes(hex) }), ) } diff --git a/packages/secret-numbers/test/api.test.ts b/packages/secret-numbers/test/api.test.ts index 09d931ff..b9c4c7cb 100644 --- a/packages/secret-numbers/test/api.test.ts +++ b/packages/secret-numbers/test/api.test.ts @@ -1,3 +1,4 @@ +import { hexToBytes } from '@xrplf/isomorphic/utils' import { deriveAddress, deriveKeypair, generateSeed } from 'ripple-keypairs' import { Account, secretToEntropy } from '../src' @@ -17,7 +18,7 @@ describe('API: XRPL Secret Numbers', () => { }) describe('Account based on entropy', () => { - const entropy = Buffer.from('0123456789ABCDEF0123456789ABCDEF', 'hex') + const entropy = hexToBytes('0123456789ABCDEF0123456789ABCDEF') const account = new Account(entropy) it('familySeed as expected', () => { diff --git a/packages/secret-numbers/test/utils.test.ts b/packages/secret-numbers/test/utils.test.ts index 234240e5..4dc725bf 100644 --- a/packages/secret-numbers/test/utils.test.ts +++ b/packages/secret-numbers/test/utils.test.ts @@ -1,3 +1,5 @@ +import { bytesToHex, hexToBytes } from '@xrplf/isomorphic/utils' + import { calculateChecksum, checkChecksum, @@ -12,10 +14,10 @@ describe('Utils', () => { it('randomEntropy: valid output', () => { const data = randomEntropy() expect(typeof data).toEqual('object') - expect(data instanceof Buffer).toBeTruthy() + expect(data instanceof Uint8Array).toBeTruthy() expect(data.length).toEqual(16) - expect(data.toString('hex').length).toEqual(32) - expect(data.toString('hex')).toMatch(/^[a-f0-9]+$/u) + expect(bytesToHex(data).length).toEqual(32) + expect(bytesToHex(data)).toMatch(/^[A-F0-9]+$/u) }) it('calculateChecksum: 1st position', () => { @@ -53,7 +55,7 @@ describe('Utils', () => { }) it('entropyToSecret', () => { - const entropy = Buffer.from('76ebb2d06879b45b7568fb9c1ded097c', 'hex') + const entropy = hexToBytes('76ebb2d06879b45b7568fb9c1ded097c') const secret = [ '304435', '457766', @@ -78,7 +80,7 @@ describe('Utils', () => { '076618', '024286', ] - const entropy = Buffer.from('76ebb2d06879b45b7568fb9c1ded097c', 'hex') + const entropy = hexToBytes('76ebb2d06879b45b7568fb9c1ded097c') expect(secretToEntropy(secret)).toEqual(entropy) }) diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index e71c2edb..70fcad69 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -13,6 +13,15 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr * `getSignedTx` * `isAccountDelete` * `dropsToXRP` and `Client.getXrpBalance` now return a `number` instead of a `string` +* `Buffer` has been replaced with `UInt8Array` for both params and return values. `Buffer` may continue to work with params since they extend `UInt8Arrays`. + +### Bundling Changes +* `Buffer` and `process` polyfills are no longer required. + +### Changes +* Deprecated: + * `convertHexToString` in favor of `@xrplf/isomorphic/utils`'s `hexToString` + * `convertStringToHex` in favor of `@xrplf/isomorphic/utils`'s `stringToHex` ## 3.0.0 Beta 1 (2023-10-19) diff --git a/packages/xrpl/src/Wallet/rfc1751.ts b/packages/xrpl/src/Wallet/rfc1751.ts index 1080d036..b825a87e 100644 --- a/packages/xrpl/src/Wallet/rfc1751.ts +++ b/packages/xrpl/src/Wallet/rfc1751.ts @@ -10,6 +10,8 @@ *is part of the public domain. */ +import { hexToBytes, concat } from '@xrplf/isomorphic/utils' + import rfc1751Words from './rfc1751Words.json' const rfc1751WordList: string[] = rfc1751Words @@ -59,7 +61,7 @@ function extract(key: string, start: number, length: number): number { */ function keyToRFC1751Mnemonic(hex_key: string): string { // Remove whitespace and interpret hex - const buf = Buffer.from(hex_key.replace(/\s+/gu, ''), 'hex') + const buf = hexToBytes(hex_key.replace(/\s+/gu, '')) // Swap byte order and use rfc1751 let key: number[] = bufferToArray(swap128(buf)) @@ -97,7 +99,7 @@ function keyToRFC1751Mnemonic(hex_key: string): string { * @throws Error if the parity after decoding does not match. * @returns A Buffer containing an encoded secret. */ -function rfc1751MnemonicToKey(english: string): Buffer { +function rfc1751MnemonicToKey(english: string): Uint8Array { const words = english.split(' ') let key: number[] = [] @@ -123,7 +125,7 @@ function rfc1751MnemonicToKey(english: string): Buffer { } // This is a step specific to the XRPL's implementation - const bufferKey = swap128(Buffer.from(key)) + const bufferKey = swap128(Uint8Array.from(key)) return bufferKey } @@ -165,26 +167,53 @@ function getSubKey( return { subKey, word } } -function bufferToArray(buf: Buffer): number[] { +function bufferToArray(buf: Uint8Array): number[] { /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We know the end type */ return Array.prototype.slice.call(buf) as number[] } -/** - * Swap the byte order of a 128-bit buffer. - * - * @param buf - A 128-bit (16 byte) buffer - * @returns A buffer containing the same data with reversed endianness - */ -function swap128(buf: Buffer): Buffer { - // Interprets buffer as an array of (two, in this case) 64-bit numbers and swaps byte order in-place. - const reversedBytes = buf.swap64() +function swap(arr: Uint8Array, n: number, m: number): void { + const i = arr[n] + // eslint-disable-next-line no-param-reassign -- we have to swap + arr[n] = arr[m] + // eslint-disable-next-line no-param-reassign -- see above + arr[m] = i +} - // Swap the two 64-bit numbers since our buffer is 128 bits. - return Buffer.concat( - [reversedBytes.slice(8, 16), reversedBytes.slice(0, 8)], - 16, - ) +/** + * Interprets arr as an array of 64-bit numbers and swaps byte order in 64 bit chunks. + * Example of two 64 bit numbers 0000000100000002 => 1000000020000000 + * + * @param arr A Uint8Array representation of one or more 64 bit numbers + * @returns Uint8Array An array containing the bytes of 64 bit numbers each with reversed endianness + */ +function swap64(arr: Uint8Array): Uint8Array { + const len = arr.length + + for (let i = 0; i < len; i += 8) { + swap(arr, i, i + 7) + swap(arr, i + 1, i + 6) + swap(arr, i + 2, i + 5) + swap(arr, i + 3, i + 4) + } + + return arr +} + +/** + * Swap the byte order of a 128-bit array. + * Ex. 0000000100000002 => 2000000010000000 + * + * @param arr - A 128-bit (16 byte) array + * @returns An array containing the same data with reversed endianness + */ +function swap128(arr: Uint8Array): Uint8Array { + // Interprets arr as an array of (two, in this case) 64-bit numbers and swaps byte order in 64 bit chunks. + // Ex. 0000000100000002 => 1000000020000000 + const reversedBytes = swap64(arr) + // Further swap the two 64-bit numbers since our buffer is 128 bits. + // Ex. 1000000020000000 => 2000000010000000 + return concat([reversedBytes.slice(8, 16), reversedBytes.slice(0, 8)]) } export { rfc1751MnemonicToKey, keyToRFC1751Mnemonic } diff --git a/packages/xrpl/src/Wallet/signer.ts b/packages/xrpl/src/Wallet/signer.ts index 8785dabf..126e629c 100644 --- a/packages/xrpl/src/Wallet/signer.ts +++ b/packages/xrpl/src/Wallet/signer.ts @@ -1,3 +1,4 @@ +import { bytesToHex } from '@xrplf/isomorphic/utils' import { BigNumber } from 'bignumber.js' import { decodeAccountID } from 'ripple-address-codec' import { decode, encode, encodeForSigning } from 'ripple-binary-codec' @@ -144,7 +145,7 @@ function compareSigners(left: Signer, right: Signer): number { const NUM_BITS_IN_HEX = 16 function addressToBigNumber(address: string): BigNumber { - const hex = Buffer.from(decodeAccountID(address)).toString('hex') + const hex = bytesToHex(decodeAccountID(address)) return new BigNumber(hex, NUM_BITS_IN_HEX) } diff --git a/packages/xrpl/src/client/connection.ts b/packages/xrpl/src/client/connection.ts index 84bea8dd..a8796e92 100644 --- a/packages/xrpl/src/client/connection.ts +++ b/packages/xrpl/src/client/connection.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines -- Connection is a large file w/ lots of imports/exports */ import type { Agent } from 'http' +import { bytesToHex, hexToString } from '@xrplf/isomorphic/utils' import WebSocket, { ClientOptions } from '@xrplf/isomorphic/ws' import { EventEmitter } from 'eventemitter3' @@ -68,10 +69,9 @@ function createWebSocket( options.headers = config.headers } if (config.authorization != null) { - const base64 = Buffer.from(config.authorization).toString('base64') options.headers = { ...options.headers, - Authorization: `Basic ${base64}`, + Authorization: `Basic ${btoa(config.authorization)}`, } } const websocketOptions = { ...options } @@ -382,7 +382,7 @@ export class Connection extends EventEmitter { this.emit('error', 'websocket', error.message, error), ) // Handle a closed connection: reconnect if it was unexpected - this.ws.once('close', (code?: number, reason?: Buffer) => { + this.ws.once('close', (code?: number, reason?: Uint8Array) => { if (this.ws == null) { throw new XrplError('onceClose: ws is null') } @@ -390,7 +390,9 @@ export class Connection extends EventEmitter { this.clearHeartbeatInterval() this.requestManager.rejectAll( new DisconnectedError( - `websocket was closed, ${new TextDecoder('utf-8').decode(reason)}`, + `websocket was closed, ${ + reason ? hexToString(bytesToHex(reason)) : '' + }`, ), ) this.ws.removeAllListeners() diff --git a/packages/xrpl/src/utils/hashes/hashLedger.ts b/packages/xrpl/src/utils/hashes/hashLedger.ts index c63af206..1cf99972 100644 --- a/packages/xrpl/src/utils/hashes/hashLedger.ts +++ b/packages/xrpl/src/utils/hashes/hashLedger.ts @@ -3,6 +3,7 @@ /* eslint-disable no-bitwise -- this file mimics behavior in rippled. It uses bitwise operators for and-ing numbers with a mask and bit shifting. */ +import { bytesToHex } from '@xrplf/isomorphic/utils' import BigNumber from 'bignumber.js' import { decode, encode } from 'ripple-binary-codec' @@ -29,10 +30,6 @@ function intToHex(integer: number, byteLength: number): string { return foo } -function bytesToHex(bytes: number[]): string { - return Buffer.from(bytes).toString('hex') -} - function bigintToHex( integerString: string | number | BigNumber, byteLength: number, diff --git a/packages/xrpl/src/utils/hashes/index.ts b/packages/xrpl/src/utils/hashes/index.ts index 49f16faf..9d32f6bf 100644 --- a/packages/xrpl/src/utils/hashes/index.ts +++ b/packages/xrpl/src/utils/hashes/index.ts @@ -3,6 +3,7 @@ /* eslint-disable no-bitwise -- this file mimics behavior in rippled. It uses bitwise operators for and-ing numbers with a mask and bit shifting. */ +import { bytesToHex } from '@xrplf/isomorphic/utils' import BigNumber from 'bignumber.js' import { decodeAccountID } from 'ripple-address-codec' @@ -20,7 +21,7 @@ const HEX = 16 const BYTE_LENGTH = 4 function addressToHex(address: string): string { - return Buffer.from(decodeAccountID(address)).toString('hex') + return bytesToHex(decodeAccountID(address)) } function ledgerSpaceHex(name: keyof typeof ledgerSpaces): string { @@ -37,7 +38,7 @@ function currencyToHex(currency: string): string { bytes[12] = currency.charCodeAt(0) & MASK bytes[13] = currency.charCodeAt(1) & MASK bytes[14] = currency.charCodeAt(2) & MASK - return Buffer.from(bytes).toString('hex') + return bytesToHex(Uint8Array.from(bytes)) } /** diff --git a/packages/xrpl/src/utils/parseNFTokenID.ts b/packages/xrpl/src/utils/parseNFTokenID.ts index 0419b4f1..0315d4bd 100644 --- a/packages/xrpl/src/utils/parseNFTokenID.ts +++ b/packages/xrpl/src/utils/parseNFTokenID.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-magic-numbers -- Doing hex string parsing. */ +import { hexToBytes } from '@xrplf/isomorphic/utils' import BigNumber from 'bignumber.js' import { encodeAccountID } from 'ripple-address-codec' @@ -85,7 +86,7 @@ export default function parseNFTokenID(nftokenID: string): { NFTokenID: nftokenID, Flags: new BigNumber(nftokenID.substring(0, 4), 16).toNumber(), TransferFee: new BigNumber(nftokenID.substring(4, 8), 16).toNumber(), - Issuer: encodeAccountID(Buffer.from(nftokenID.substring(8, 48), 'hex')), + Issuer: encodeAccountID(hexToBytes(nftokenID.substring(8, 48))), Taxon: unscrambleTaxon(scrambledTaxon, sequence), Sequence: sequence, } diff --git a/packages/xrpl/src/utils/stringConversion.ts b/packages/xrpl/src/utils/stringConversion.ts index fd56dd47..c2ab9261 100644 --- a/packages/xrpl/src/utils/stringConversion.ts +++ b/packages/xrpl/src/utils/stringConversion.ts @@ -1,12 +1,17 @@ +import { stringToHex, hexToString } from '@xrplf/isomorphic/utils' + /** * Converts a string to its hex equivalent. Useful for Memos. * * @param string - The string to convert to Hex. * @returns The Hex equivalent of the string. + * + * @deprecated use `@xrplf/isomorphic/utils`'s `stringToHex` + * * @category Utilities */ function convertStringToHex(string: string): string { - return Buffer.from(string, 'utf8').toString('hex').toUpperCase() + return stringToHex(string) } /** @@ -15,13 +20,13 @@ function convertStringToHex(string: string): string { * @param hex - The hex to convert to a string. * @param encoding - The encoding to use. Defaults to 'utf8' (UTF-8). 'ascii' is also allowed. * @returns The converted string. + * + * @deprecated use `@xrplf/isomorphic/utils`'s `hexToString` + * * @category Utilities */ -function convertHexToString( - hex: string, - encoding: BufferEncoding = 'utf8', -): string { - return Buffer.from(hex, 'hex').toString(encoding) +function convertHexToString(hex: string, encoding = 'utf8'): string { + return hexToString(hex, encoding) } export { convertHexToString, convertStringToHex } diff --git a/packages/xrpl/test/connection.test.ts b/packages/xrpl/test/connection.test.ts index 8f2d5805..ef300f60 100644 --- a/packages/xrpl/test/connection.test.ts +++ b/packages/xrpl/test/connection.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-len -- Some large lines necessary */ /* eslint-disable max-statements -- test has a lot of statements */ import net from 'net' @@ -25,22 +24,6 @@ import { } from './setupClient' import { assertRejects, ignoreWebSocketDisconnect } from './testUtils' -type GlobalThis = typeof globalThis -type Global = GlobalThis & { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Necessary for Jest in browser - TextEncoder: any - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Necessary for Jest in browser - TextDecoder: any -} -declare const global: Global - -if (typeof TextDecoder === 'undefined') { - // eslint-disable-next-line node/global-require, @typescript-eslint/no-require-imports, node/prefer-global/text-encoder, global-require, @typescript-eslint/no-var-requires -- Needed for Jest - global.TextEncoder = require('util').TextEncoder - // eslint-disable-next-line node/global-require, @typescript-eslint/no-require-imports, node/prefer-global/text-decoder, global-require, @typescript-eslint/no-var-requires -- Needed for Jest - global.TextDecoder = require('util').TextDecoder -} - // how long before each test case times out const TIMEOUT = 20000 diff --git a/weback.test.config.js b/weback.test.config.js index 3bd9df5a..d5c3c38f 100644 --- a/weback.test.config.js +++ b/weback.test.config.js @@ -22,6 +22,7 @@ function webpackForTest(testFileName, basePath) { new webpack.DefinePlugin({ "process.stdout": {}, }), + new webpack.ProvidePlugin({ process: "process/browser" }), ], module: { rules: [ diff --git a/webpack.config.js b/webpack.config.js index 0f5bacc1..63bb0dba 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,10 +13,6 @@ function getDefaultConfiguration() { }, stats: "errors-only", devtool: "source-map", - plugins: [ - new webpack.ProvidePlugin({ process: "process/browser" }), - new webpack.ProvidePlugin({ Buffer: ["buffer", "Buffer"] }), - ], module: { rules: [ { @@ -31,9 +27,6 @@ function getDefaultConfiguration() { // ripple-address-codec, ripple-binary-codec, ripple-keypairs, which are // symlinked together via lerna symlinks: false, - fallback: { - buffer: require.resolve("buffer"), - }, }, }; } @@ -64,7 +57,7 @@ module.exports = { new BundleAnalyzerPlugin({ analyzerPort: `auto`, analyzerMode: "static", - }) + }), ); } return localConfig;