From 0bc74f088cade65f20e115cc13261535f848f2e6 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:04:34 -0300 Subject: [PATCH 1/8] avoid recreating base10 coder --- biome.json | 1 - package.json | 1 - pnpm-lock.yaml | 7 +++---- src/serialization/deserialize.ts | 6 ++---- src/serialization/serialize.ts | 6 ++---- src/serialization/utils.ts | 3 +++ tsconfig.json | 4 +--- 7 files changed, 11 insertions(+), 17 deletions(-) diff --git a/biome.json b/biome.json index 7aec70e..96b3dd8 100644 --- a/biome.json +++ b/biome.json @@ -5,7 +5,6 @@ "**/coverage/*", "**/dist/*", "**/node_modules/*", - "**/_test-vectors/mockedGraphQLBoxResponses.json", "**/package.json" ] }, diff --git a/package.json b/package.json index 806a573..23eec4e 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "@biomejs/biome": "^1.8.3", "@ledgerhq/hw-transport": "^6.31.0", "@ledgerhq/hw-transport-mocker": "^6.29.0", - "@types/node": "^20.14.9", "@vitest/coverage-v8": "^1.6.0", "open-cli": "^8.0.0", "tsup": "^8.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e579a8f..cb791ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,9 +33,6 @@ importers: '@ledgerhq/hw-transport-mocker': specifier: ^6.29.0 version: 6.29.0 - '@types/node': - specifier: ^20.14.9 - version: 20.14.9 '@vitest/coverage-v8': specifier: ^1.6.0 version: 1.6.0(vitest@1.6.0(@types/node@20.14.9)) @@ -1601,6 +1598,7 @@ snapshots: '@types/node@20.14.9': dependencies: undici-types: 5.26.5 + optional: true '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.9))': dependencies: @@ -2383,7 +2381,8 @@ snapshots: ufo@1.5.3: {} - undici-types@5.26.5: {} + undici-types@5.26.5: + optional: true unique-string@3.0.0: dependencies: diff --git a/src/serialization/deserialize.ts b/src/serialization/deserialize.ts index ce91a9d..2aa5c38 100644 --- a/src/serialization/deserialize.ts +++ b/src/serialization/deserialize.ts @@ -1,7 +1,5 @@ -import basex from "base-x"; import { assert } from "@fleet-sdk/common"; - -const bs10 = basex("0123456789"); +import { base10 } from "./utils"; export const deserialize = { hex(buffer: Buffer): string { @@ -19,7 +17,7 @@ export const deserialize = { uint64(buffer: Buffer): string { assert(buffer.length === 8, "invalid uint64 buffer"); - return trimLeadingZeros(bs10.encode(buffer)); + return trimLeadingZeros(base10.encode(buffer)); } }; diff --git a/src/serialization/serialize.ts b/src/serialization/serialize.ts index 0885832..2d2d4ce 100644 --- a/src/serialization/serialize.ts +++ b/src/serialization/serialize.ts @@ -1,9 +1,7 @@ import { isHex, assert } from "@fleet-sdk/common"; import { isUint16, isUint32, isUint64String, isUint8, isErgoPath } from "../assertions"; -import basex from "base-x"; import bip32Path from "bip32-path"; - -const bs10 = basex("0123456789"); +import { base10 } from "./utils"; export const serialize = { path(path: number[] | string): Buffer { @@ -46,7 +44,7 @@ export const serialize = { uint64(value: string): Buffer { assert(isUint64String(value), "invalid uint64 string"); - const data = bs10.decode(value); + const data = base10.decode(value); const padding = Buffer.alloc(8 - data.length); return Buffer.concat([padding, Buffer.from(data)]); diff --git a/src/serialization/utils.ts b/src/serialization/utils.ts index d475118..ec72f77 100644 --- a/src/serialization/utils.ts +++ b/src/serialization/utils.ts @@ -1,4 +1,7 @@ import { assert } from "@fleet-sdk/common"; +import base from "base-x"; + +export const base10 = base("0123456789"); const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0); diff --git a/tsconfig.json b/tsconfig.json index 0a9414a..0bed6ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,15 +3,13 @@ "target": "ESNext", "moduleResolution": "Node", "module": "ESNext", - "lib": ["ESNext", "DOM"], "strict": true, "sourceMap": true, "declaration": true, "esModuleInterop": true, "importHelpers": true, "allowSyntheticDefaultImports": true, - "typeRoots": ["node_modules/@types"], "baseUrl": "." }, - "include": ["src", "bip32-path.d.ts"] + "include": ["src"] } From e906bf7caa2ed6710c4c53ec069a9db290c0075b Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:54:15 -0300 Subject: [PATCH 2/8] use buffer from npm --- biome.json | 3 ++- package.json | 3 ++- pnpm-lock.yaml | 16 ++++++++++++++ src/device.ts | 3 ++- src/interactions/attestInput.ts | 25 +++++++++++---------- src/interactions/deriveAddress.ts | 1 + src/interactions/getAppName.ts | 1 + src/interactions/getExtendedPublicKey.ts | 7 +++--- src/interactions/getVersion.ts | 1 + src/interactions/signTx.ts | 28 ++++++++++++++++-------- src/serialization/deserialize.ts | 1 + src/serialization/serialize.ts | 1 + src/serialization/utils.ts | 2 +- src/types/internal.ts | 1 + src/types/public.ts | 12 +++++----- tsconfig.json | 1 - 16 files changed, 71 insertions(+), 35 deletions(-) diff --git a/biome.json b/biome.json index 96b3dd8..8753be1 100644 --- a/biome.json +++ b/biome.json @@ -25,7 +25,8 @@ "rules": { "recommended": true, "style": { - "noParameterAssign": { "level": "off" } + "noParameterAssign": { "level": "off" }, + "useNodejsImportProtocol": { "level": "off" } }, "suspicious": { "noConsoleLog": { "level": "error" }, diff --git a/package.json b/package.json index 23eec4e..57cd9c3 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "@fleet-sdk/core": "0.5.0", "@fleet-sdk/crypto": "^0.5.0", "base-x": "^5.0.0", - "bip32-path": "^0.4.2" + "bip32-path": "^0.4.2", + "buffer": "^6.0.3" }, "devDependencies": { "@biomejs/biome": "^1.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb791ff..f6229df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: bip32-path: specifier: ^0.4.2 version: 0.4.2 + buffer: + specifier: ^6.0.3 + version: 6.0.3 devDependencies: '@biomejs/biome': specifier: ^1.8.3 @@ -511,6 +514,9 @@ packages: base-x@5.0.0: resolution: {integrity: sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -528,6 +534,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -1681,6 +1690,8 @@ snapshots: base-x@5.0.0: {} + base64-js@1.5.1: {} + binary-extensions@2.3.0: {} bip32-path@0.4.2: {} @@ -1698,6 +1709,11 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-name@4.1.0: dependencies: run-applescript: 7.0.0 diff --git a/src/device.ts b/src/device.ts index 720f807..133085b 100644 --- a/src/device.ts +++ b/src/device.ts @@ -1,6 +1,7 @@ import type Transport from "@ledgerhq/hw-transport"; import type { DeviceResponse } from "./types/internal"; import { serialize } from "./serialization/serialize"; +import { Buffer } from "buffer"; export const enum COMMAND { GET_APP_VERSION = 0x01, @@ -36,7 +37,7 @@ export class Device { ): Promise { const responses: DeviceResponse[] = []; for (let i = 0; i < Math.ceil(data.length / MAX_DATA_LENGTH); i++) { - const chunk = data.subarray( + const chunk = data.slice( i * MAX_DATA_LENGTH, Math.min((i + 1) * MAX_DATA_LENGTH, data.length) ); diff --git a/src/interactions/attestInput.ts b/src/interactions/attestInput.ts index 7ea6902..5a46b0c 100644 --- a/src/interactions/attestInput.ts +++ b/src/interactions/attestInput.ts @@ -4,6 +4,7 @@ import type { DeviceResponse } from "../types/internal"; import { serialize } from "../serialization/serialize"; import { deserialize } from "../serialization/deserialize"; import { AttestedBox } from "../types/attestedBox"; +import { Buffer } from "buffer"; const enum P1 { BOX_START = 0x01, @@ -62,14 +63,14 @@ async function sendHeader( async function sendErgoTree( device: Device, - data: Buffer, + data: Uint8Array, sessionId: number ): Promise { const results = await device.sendData( COMMAND.ATTEST_INPUT, P1.ADD_ERGO_TREE_CHUNK, sessionId, - data + Buffer.from(data) ); return results.pop()?.data[0] || 0; @@ -96,14 +97,14 @@ async function sendTokens( async function sendRegisters( device: Device, - data: Buffer, + data: Uint8Array, sessionId: number ): Promise { const results = await device.sendData( COMMAND.ATTEST_INPUT, P1.ADD_REGISTERS_CHUNK, sessionId, - data + Buffer.from(data) ); /* v8 ignore next */ @@ -132,21 +133,21 @@ async function getAttestedFrames( export function decodeAttestedFrameResponse(bytes: Buffer): AttestedBoxFrame { let offset = 0; - const boxId = deserialize.hex(bytes.subarray(offset, (offset += 32))); - const count = deserialize.uint8(bytes.subarray(offset, (offset += 1))); - const index = deserialize.uint8(bytes.subarray(offset, (offset += 1))); - const amount = deserialize.uint64(bytes.subarray(offset, (offset += 8))); - const tokenCount = deserialize.uint8(bytes.subarray(offset, (offset += 1))); + const boxId = deserialize.hex(bytes.slice(offset, (offset += 32))); + const count = deserialize.uint8(bytes.slice(offset, (offset += 1))); + const index = deserialize.uint8(bytes.slice(offset, (offset += 1))); + const amount = deserialize.uint64(bytes.slice(offset, (offset += 8))); + const tokenCount = deserialize.uint8(bytes.slice(offset, (offset += 1))); const tokens: Token[] = []; for (let i = 0; i < tokenCount; i++) { tokens.push({ - id: deserialize.hex(bytes.subarray(offset, (offset += 32))), - amount: deserialize.uint64(bytes.subarray(offset, (offset += 8))) + id: deserialize.hex(bytes.slice(offset, (offset += 32))), + amount: deserialize.uint64(bytes.slice(offset, (offset += 8))) }); } - const attestation = deserialize.hex(bytes.subarray(offset, (offset += 16))); + const attestation = deserialize.hex(bytes.slice(offset, (offset += 16))); return { boxId, diff --git a/src/interactions/deriveAddress.ts b/src/interactions/deriveAddress.ts index f43e738..c4d2194 100644 --- a/src/interactions/deriveAddress.ts +++ b/src/interactions/deriveAddress.ts @@ -4,6 +4,7 @@ import type { DeviceResponse } from "../types/internal"; import { pathToArray, serialize } from "../serialization/serialize"; import { deserialize } from "../serialization/deserialize"; import type { Network } from "@fleet-sdk/common"; +import { Buffer } from "buffer"; const enum ReturnType { Return = 0x01, diff --git a/src/interactions/getAppName.ts b/src/interactions/getAppName.ts index a8ba27e..89382c5 100644 --- a/src/interactions/getAppName.ts +++ b/src/interactions/getAppName.ts @@ -1,6 +1,7 @@ import { COMMAND, type Device } from "../device"; import type { AppName } from "../types/public"; import { deserialize } from "../serialization/deserialize"; +import { Buffer } from "buffer"; const enum P1 { UNUSED = 0x00 diff --git a/src/interactions/getExtendedPublicKey.ts b/src/interactions/getExtendedPublicKey.ts index 36f82fe..360b1b3 100644 --- a/src/interactions/getExtendedPublicKey.ts +++ b/src/interactions/getExtendedPublicKey.ts @@ -2,7 +2,8 @@ import { COMMAND, type Device } from "../device"; import type { ExtendedPublicKey } from "../types/public"; import { chunkBy } from "../serialization/utils"; import { serialize } from "../serialization/serialize"; -import { deserialize } from "../serialization/deserialize"; +import { Buffer } from "buffer"; +import { hex } from "@fleet-sdk/crypto"; const enum P1 { WITHOUT_TOKEN = 0x01, @@ -28,7 +29,7 @@ export async function getExtendedPublicKey( const [publicKey, chainCode] = chunkBy(response.data, [33, 32]); return { - publicKey: deserialize.hex(publicKey), - chainCode: deserialize.hex(chainCode) + publicKey: hex.encode(publicKey), + chainCode: hex.encode(chainCode) }; } diff --git a/src/interactions/getVersion.ts b/src/interactions/getVersion.ts index d3bebc1..74cd7ec 100644 --- a/src/interactions/getVersion.ts +++ b/src/interactions/getVersion.ts @@ -1,5 +1,6 @@ import type { Version } from "../types/public"; import { COMMAND, type Device } from "../device"; +import { Buffer } from "buffer"; const IS_DEBUG_FLAG = 0x01; diff --git a/src/interactions/signTx.ts b/src/interactions/signTx.ts index eed172c..fd28a5b 100644 --- a/src/interactions/signTx.ts +++ b/src/interactions/signTx.ts @@ -1,10 +1,11 @@ -import { deserialize } from "../serialization/deserialize"; import { serialize } from "../serialization/serialize"; import type { ChangeMap, BoxCandidate, Token } from "../types/public"; import { COMMAND, type Device } from "../device"; import { ErgoAddress, type Network } from "@fleet-sdk/core"; import type { AttestedTransaction } from "../types/internal"; import type { AttestedBox } from "../types/attestedBox"; +import { hex } from "@fleet-sdk/crypto"; +import { Buffer } from "buffer"; const MINER_FEE_TREE = "1005040004000e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a701730073011001020402d19683030193a38cc7b2a57300000193c2b2a57301007473027303830108cdeeac93b1a57304"; @@ -109,7 +110,12 @@ async function sendDistinctTokensIds( async function sendInputs(device: Device, sessionId: number, inputs: AttestedBox[]) { for (const input of inputs) { for (const frame of input.frames) { - await device.send(COMMAND.SIGN_TX, P1.ADD_INPUT_BOX_FRAME, sessionId, frame.bytes); + await device.send( + COMMAND.SIGN_TX, + P1.ADD_INPUT_BOX_FRAME, + sessionId, + Buffer.from(frame.bytes) + ); } if (input.extension !== undefined && input.extension.length > 0) { @@ -121,13 +127,13 @@ async function sendInputs(device: Device, sessionId: number, inputs: AttestedBox async function sendBoxContextExtension( device: Device, sessionId: number, - extension: Buffer + extension: Uint8Array ) { await device.sendData( COMMAND.SIGN_TX, P1.ADD_INPUT_BOX_CONTEXT_EXTENSION_CHUNK, sessionId, - extension + Buffer.from(extension) ); } @@ -163,7 +169,7 @@ async function sendOutputs( ]) ); - const tree = deserialize.hex(box.ergoTree); + const tree = hex.encode(box.ergoTree); if (tree === MINER_FEE_TREE) { await addOutputBoxMinersFeeTree(device, sessionId); } else if (ErgoAddress.fromErgoTree(tree).toString() === changeMap.address) { @@ -182,12 +188,16 @@ async function sendOutputs( } } -async function addOutputBoxErgoTree(device: Device, sessionId: number, ergoTree: Buffer) { +async function addOutputBoxErgoTree( + device: Device, + sessionId: number, + ergoTree: Uint8Array +) { await device.sendData( COMMAND.SIGN_TX, P1.ADD_OUTPUT_BOX_ERGO_TREE_CHUNK, sessionId, - ergoTree + Buffer.from(ergoTree) ); } @@ -231,13 +241,13 @@ async function addOutputBoxTokens( async function addOutputBoxRegisters( device: Device, sessionId: number, - registers: Buffer + registers: Uint8Array ) { await device.sendData( COMMAND.SIGN_TX, P1.ADD_OUTPUT_BOX_REGISTERS_CHUNK, sessionId, - registers + Buffer.from(registers) ); } diff --git a/src/serialization/deserialize.ts b/src/serialization/deserialize.ts index 2aa5c38..3c4de9b 100644 --- a/src/serialization/deserialize.ts +++ b/src/serialization/deserialize.ts @@ -1,5 +1,6 @@ import { assert } from "@fleet-sdk/common"; import { base10 } from "./utils"; +import type { Buffer } from "buffer"; export const deserialize = { hex(buffer: Buffer): string { diff --git a/src/serialization/serialize.ts b/src/serialization/serialize.ts index 2d2d4ce..bd1035f 100644 --- a/src/serialization/serialize.ts +++ b/src/serialization/serialize.ts @@ -2,6 +2,7 @@ import { isHex, assert } from "@fleet-sdk/common"; import { isUint16, isUint32, isUint64String, isUint8, isErgoPath } from "../assertions"; import bip32Path from "bip32-path"; import { base10 } from "./utils"; +import { Buffer } from "buffer"; export const serialize = { path(path: number[] | string): Buffer { diff --git a/src/serialization/utils.ts b/src/serialization/utils.ts index ec72f77..2066cfe 100644 --- a/src/serialization/utils.ts +++ b/src/serialization/utils.ts @@ -5,7 +5,7 @@ export const base10 = base("0123456789"); const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0); -export function chunkBy(data: Buffer, chunkLengths: number[]) { +export function chunkBy(data: Uint8Array, chunkLengths: number[]) { assert(data.length >= sum(chunkLengths), "data is too small"); let offset = 0; diff --git a/src/types/internal.ts b/src/types/internal.ts index d1a1a1a..d0ad2ce 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -1,6 +1,7 @@ import type { RETURN_CODE } from "../device"; import type { BoxCandidate, ChangeMap } from "./public"; import type { AttestedBox } from "./attestedBox"; +import type { Buffer } from "buffer"; export type DeviceResponse = { data: Buffer; diff --git a/src/types/public.ts b/src/types/public.ts index 6508fb2..10b6d4d 100644 --- a/src/types/public.ts +++ b/src/types/public.ts @@ -46,20 +46,20 @@ export type UnsignedBox = { txId: string; index: number; value: string; - ergoTree: Buffer; + ergoTree: Uint8Array; creationHeight: number; tokens: Token[]; - additionalRegisters: Buffer; - extension: Buffer; + additionalRegisters: Uint8Array; + extension: Uint8Array; signPath: string; }; export type BoxCandidate = { value: string; - ergoTree: Buffer; + ergoTree: Uint8Array; creationHeight: number; tokens: Token[]; - registers: Buffer; + registers: Uint8Array; }; export type AttestedBoxFrame = { @@ -70,7 +70,7 @@ export type AttestedBoxFrame = { tokens: Token[]; attestation: string; extensionLength?: number; - bytes: Buffer; + bytes: Uint8Array; }; export type UnsignedTransaction = { diff --git a/tsconfig.json b/tsconfig.json index 0bed6ef..30c01c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "declaration": true, "esModuleInterop": true, "importHelpers": true, - "allowSyntheticDefaultImports": true, "baseUrl": "." }, "include": ["src"] From db7a3b780e8ed99083651c7d9d1128831efa2b44 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:06:25 -0300 Subject: [PATCH 3/8] introduce `ByteWriter`class --- src/assertions.spec.ts | 20 ---- src/assertions.ts | 34 ------- src/device.ts | 33 +++--- src/erg.spec.ts | 14 +-- src/interactions/attestInput.ts | 70 +++++++------ src/interactions/deriveAddress.ts | 18 ++-- src/interactions/getAppName.ts | 7 +- src/interactions/getExtendedPublicKey.ts | 21 ++-- src/interactions/getVersion.ts | 4 +- src/interactions/signTx.ts | 123 ++++++++++++----------- src/serialization/byteWriter.ts | 73 ++++++++++++++ src/serialization/deserialize.ts | 27 ----- src/serialization/serialize.spec.ts | 29 ------ src/serialization/serialize.ts | 89 ---------------- src/serialization/utils.spec.ts | 16 ++- src/serialization/utils.ts | 25 ++--- src/types/attestedBox.ts | 17 ++-- 17 files changed, 257 insertions(+), 363 deletions(-) delete mode 100644 src/assertions.spec.ts delete mode 100644 src/assertions.ts create mode 100644 src/serialization/byteWriter.ts delete mode 100644 src/serialization/deserialize.ts delete mode 100644 src/serialization/serialize.spec.ts delete mode 100644 src/serialization/serialize.ts diff --git a/src/assertions.spec.ts b/src/assertions.spec.ts deleted file mode 100644 index d2e43f1..0000000 --- a/src/assertions.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { isErgoPath, isUint64String } from "./assertions"; -import { pathToArray } from "./serialization/serialize"; - -describe("assertions", () => { - it("Ergo path", () => { - expect(isErgoPath(pathToArray("m/44'/429'"))).to.be.true; - expect(isErgoPath(pathToArray("m/44'/2'"))).to.be.false; - expect(isErgoPath(pathToArray("m/44'"))).to.be.false; - }); - - it("UInt64", () => { - expect(isUint64String("0")).to.be.true; - expect(isUint64String("18446744073709551615")).to.be.true; - - expect(isUint64String("18446744073709551616")).to.be.false; - expect(isUint64String("1.2")).to.be.false; - expect(isUint64String("11a")).to.be.false; - }); -}); diff --git a/src/assertions.ts b/src/assertions.ts deleted file mode 100644 index 3d75cbe..0000000 --- a/src/assertions.ts +++ /dev/null @@ -1,34 +0,0 @@ -import bip32Path from "bip32-path"; - -const MIN_UINT_64 = 0n; -const MAX_UINT_64 = 18446744073709551615n; -const MIN_UINT_VALUE = 0; -const MAX_UINT32_VALUE = 4294967295; -const MAX_UINT16_VALUE = 65535; -const MAX_UNIT8_VALUE = 255; - -const [ERGO_PURPOSE, ERGO_COIN_TYPE] = bip32Path.fromString("m/44'/429'").toPathArray(); - -export function isErgoPath(path: number[]): boolean { - if (path.length < 2) return false; - const [pathPurpose, pathCoinType] = path; - return pathPurpose === ERGO_PURPOSE && pathCoinType === ERGO_COIN_TYPE; -} - -export function isUint32(data: number): boolean { - return Number.isInteger(data) && data >= MIN_UINT_VALUE && data <= MAX_UINT32_VALUE; -} - -export function isUint16(data: number): boolean { - return Number.isInteger(data) && data >= MIN_UINT_VALUE && data <= MAX_UINT16_VALUE; -} - -export function isUint8(data: number): boolean { - return Number.isInteger(data) && data >= MIN_UINT_VALUE && data <= MAX_UNIT8_VALUE; -} - -export function isUint64String(value: string): boolean { - if (!/^[0-9]*$/.test(value)) return false; - const parsed = BigInt(value); - return parsed >= MIN_UINT_64 && parsed <= MAX_UINT_64; -} diff --git a/src/device.ts b/src/device.ts index 133085b..6540312 100644 --- a/src/device.ts +++ b/src/device.ts @@ -1,7 +1,7 @@ import type Transport from "@ledgerhq/hw-transport"; import type { DeviceResponse } from "./types/internal"; -import { serialize } from "./serialization/serialize"; -import { Buffer } from "buffer"; +import type { Buffer } from "buffer"; +import { ByteWriter } from "./serialization/byteWriter"; export const enum COMMAND { GET_APP_VERSION = 0x01, @@ -13,8 +13,9 @@ export const enum COMMAND { SIGN_TX = 0x21 } -const MAX_DATA_LENGTH = 255; +export const MAX_DATA_LENGTH = 255; const MIN_RESPONSE_LENGTH = 2; +const MIN_APDU_LENGTH = 5; export class Device { #transport: Transport; @@ -33,7 +34,7 @@ export class Device { ins: COMMAND, p1: number, p2: number, - data: Buffer + data: Uint8Array ): Promise { const responses: DeviceResponse[] = []; for (let i = 0; i < Math.ceil(data.length / MAX_DATA_LENGTH); i++) { @@ -52,14 +53,14 @@ export class Device { ins: COMMAND, p1: number, p2: number, - data: Buffer + data: Uint8Array ): Promise { if (data.length > MAX_DATA_LENGTH) { throw new DeviceError(RETURN_CODE.TOO_MUCH_DATA); } - const adpu = mountApdu(this.#cla, ins, p1, p2, data); - const response = await this.transport.exchange(adpu); + const apdu = mountApdu(this.#cla, ins, p1, p2, data); + const response = await this.transport.exchange(apdu); if (response.length < MIN_RESPONSE_LENGTH) { throw new DeviceError(RETURN_CODE.WRONG_RESPONSE_LENGTH); @@ -77,16 +78,16 @@ function mountApdu( ins: COMMAND, p1: number, p2: number, - data: Buffer + data: Uint8Array ): Buffer { - return Buffer.concat([ - serialize.uint8(cla), - serialize.uint8(ins), - serialize.uint8(p1), - serialize.uint8(p2), - serialize.uint8(data.length), - data - ]); + return new ByteWriter(MIN_APDU_LENGTH + data.length) + .write(cla) + .write(ins) + .write(p1) + .write(p2) + .write(data.length) + .writeBytes(data) + .toBuffer(); } export class DeviceError extends Error { diff --git a/src/erg.spec.ts b/src/erg.spec.ts index 6a0cf1e..10e7e44 100644 --- a/src/erg.spec.ts +++ b/src/erg.spec.ts @@ -220,8 +220,8 @@ describe("public key management without auth token", () => { describe("transaction signing", () => { test.each(txTestVectors)( "should sign $name", - async ({ adpuQueue, authToken, proofs, tx }) => { - const transport = await openTransportReplayer(RecordStore.fromString(adpuQueue)); + async ({ apduQueue, authToken, proofs, tx }) => { + const transport = await openTransportReplayer(RecordStore.fromString(apduQueue)); const app = new ErgoLedgerApp(transport, authToken).useAuthToken(!!authToken); @@ -231,8 +231,8 @@ describe("transaction signing", () => { ); it("Should throw with empty inputs", async () => { - const { adpuQueue, tx } = txTestVectors[0]; - const transport = await openTransportReplayer(RecordStore.fromString(adpuQueue)); + const { apduQueue, tx } = txTestVectors[0]; + const transport = await openTransportReplayer(RecordStore.fromString(apduQueue)); const app = new ErgoLedgerApp(transport); await expect(() => app.signTx({ ...tx, inputs: [] })).rejects.toThrow( @@ -301,7 +301,7 @@ const txTestVectors = [ { name: "simple erg-only p2p transaction", authToken: 0, - adpuQueue: ` + apduQueue: ` => e0200101378e73d69748e76867f9e71351481137bc6d5e671979d050f61eabfdccee28a513000100000000067f2af0000000240013d0a10000000001 <= 469000 @@ -428,7 +428,7 @@ const txTestVectors = [ { name: "simple p2p transaction with tokens", authToken: Number("0x68771637"), - adpuQueue: ` + apduQueue: ` => e02001023b27c4d6a5a5e883282a5a1246975a1b42df78aa270638c8d843d63c14fae7a31f000b0000000000009ee8000000240013d08c010000000168771637 <= aa9000 @@ -615,7 +615,7 @@ const txTestVectors = [ { name: "transaction with input extension and data inputs", authToken: 0, - adpuQueue: ` + apduQueue: ` => e0200101375f083bdf25ef9b915205e569c3c0623c5b7ae743a6d26112704cfd8ae1261555000100000001c235c8c0000000c200128d98010000004a <= f09000 diff --git a/src/interactions/attestInput.ts b/src/interactions/attestInput.ts index 5a46b0c..c377ca6 100644 --- a/src/interactions/attestInput.ts +++ b/src/interactions/attestInput.ts @@ -1,10 +1,11 @@ -import { COMMAND, type Device } from "../device"; +import { COMMAND, MAX_DATA_LENGTH, type Device } from "../device"; import type { AttestedBoxFrame, UnsignedBox, Token } from "../types/public"; import type { DeviceResponse } from "../types/internal"; -import { serialize } from "../serialization/serialize"; -import { deserialize } from "../serialization/deserialize"; import { AttestedBox } from "../types/attestedBox"; import { Buffer } from "buffer"; +import { ByteWriter } from "../serialization/byteWriter"; +import { chunk } from "@fleet-sdk/common"; +import { hex } from "@fleet-sdk/crypto"; const enum P1 { BOX_START = 0x01, @@ -19,6 +20,9 @@ const enum P2 { WITH_TOKEN = 0x02 } +const MAX_HEADER_SIZE = 59; // https://github.com/tesseract-one/ledger-app-ergo/blob/main/doc/INS-20-ATTEST-BOX.md#data +const TOKEN_ENTRY_SIZE = 40; // https://github.com/tesseract-one/ledger-app-ergo/blob/main/doc/INS-20-ATTEST-BOX.md#data-2 + export async function attestInput( device: Device, box: UnsignedBox, @@ -41,16 +45,16 @@ async function sendHeader( box: UnsignedBox, authToken?: number ): Promise { - const header = Buffer.concat([ - serialize.hex(box.txId), - serialize.uint16(box.index), - serialize.uint64(box.value), - serialize.uint32(box.ergoTree.length), - serialize.uint32(box.creationHeight), - serialize.uint8(box.tokens.length), - serialize.uint32(box.additionalRegisters.length), - authToken ? serialize.uint32(authToken) : Buffer.alloc(0) - ]); + const header = new ByteWriter(MAX_HEADER_SIZE) + .writeHex(box.txId) + .writeUInt16(box.index) + .writeUInt64(box.value) + .writeUInt32(box.ergoTree.length) + .writeUInt32(box.creationHeight) + .writeUInt8(box.tokens.length) + .writeUInt32(box.additionalRegisters.length) + .writeAuthToken(authToken) + .toBytes(); const response = await device.send( COMMAND.ATTEST_INPUT, @@ -58,6 +62,7 @@ async function sendHeader( authToken ? P2.WITH_TOKEN : P2.WITHOUT_TOKEN, header ); + return response.data[0]; } @@ -70,7 +75,7 @@ async function sendErgoTree( COMMAND.ATTEST_INPUT, P1.ADD_ERGO_TREE_CHUNK, sessionId, - Buffer.from(data) + data ); return results.pop()?.data[0] || 0; @@ -81,14 +86,18 @@ async function sendTokens( tokens: Token[], sessionId: number ): Promise { - const MAX_PACKET_SIZE = 6; - const packets = serialize.arrayAsMappedChunks(tokens, MAX_PACKET_SIZE, (t) => - Buffer.concat([serialize.hex(t.id), serialize.uint64(t.amount)]) - ); - + const chunks = chunk(tokens, Math.floor(MAX_DATA_LENGTH / TOKEN_ENTRY_SIZE)); const results: DeviceResponse[] = []; - for (const p of packets) { - results.push(await device.send(COMMAND.ATTEST_INPUT, P1.ADD_TOKENS, sessionId, p)); + + for (const chunk of chunks) { + if (chunk.length === 0) continue; + + const data = new ByteWriter(chunk.length * TOKEN_ENTRY_SIZE); + for (const token of chunk) data.writeHex(token.id).writeUInt64(token.amount); + + results.push( + await device.send(COMMAND.ATTEST_INPUT, P1.ADD_TOKENS, sessionId, data.toBytes()) + ); } /* v8 ignore next */ @@ -133,21 +142,22 @@ async function getAttestedFrames( export function decodeAttestedFrameResponse(bytes: Buffer): AttestedBoxFrame { let offset = 0; - const boxId = deserialize.hex(bytes.slice(offset, (offset += 32))); - const count = deserialize.uint8(bytes.slice(offset, (offset += 1))); - const index = deserialize.uint8(bytes.slice(offset, (offset += 1))); - const amount = deserialize.uint64(bytes.slice(offset, (offset += 8))); - const tokenCount = deserialize.uint8(bytes.slice(offset, (offset += 1))); - + const boxId = hex.encode(bytes.subarray(0, (offset += 32))); + const count = bytes.readUint8(offset++); + const index = bytes.readUint8(offset++); + const amount = bytes.readBigUint64BE(offset).toString(); + offset += 8; + const tokenCount = bytes.readUint8(offset++); const tokens: Token[] = []; for (let i = 0; i < tokenCount; i++) { tokens.push({ - id: deserialize.hex(bytes.slice(offset, (offset += 32))), - amount: deserialize.uint64(bytes.slice(offset, (offset += 8))) + id: hex.encode(bytes.subarray(offset, (offset += 32))), + amount: bytes.readBigUint64BE(offset).toString() }); + offset += 8; } - const attestation = deserialize.hex(bytes.slice(offset, (offset += 16))); + const attestation = hex.encode(bytes.subarray(offset, offset + 16)); return { boxId, diff --git a/src/interactions/deriveAddress.ts b/src/interactions/deriveAddress.ts index c4d2194..dd535a5 100644 --- a/src/interactions/deriveAddress.ts +++ b/src/interactions/deriveAddress.ts @@ -1,10 +1,10 @@ import { COMMAND, RETURN_CODE, type Device } from "../device"; import type { DerivedAddress } from "../types/public"; import type { DeviceResponse } from "../types/internal"; -import { pathToArray, serialize } from "../serialization/serialize"; -import { deserialize } from "../serialization/deserialize"; import type { Network } from "@fleet-sdk/common"; -import { Buffer } from "buffer"; +import { ByteWriter } from "../serialization/byteWriter"; +import { hex } from "@fleet-sdk/crypto"; +import { pathToArray } from "../serialization/utils"; const enum ReturnType { Return = 0x01, @@ -24,6 +24,8 @@ const enum P2 { const CHANGE_PATH_INDEX = 3; const ALLOWED_CHANGE_PATHS = [0, 1]; +const MAX_APDU_SIZE = 46; // https://github.com/tesseract-one/ledger-app-ergo/blob/main/doc/INS-11-DERIVE-ADDR.md#data + function sendDeriveAddress( device: Device, network: Network, @@ -41,13 +43,17 @@ function sendDeriveAddress( throw new Error(`Invalid change path: ${change}`); } - const data = Buffer.concat([Buffer.alloc(1, network), serialize.path(pathArray)]); + const data = new ByteWriter(MAX_APDU_SIZE) + .write(network) + .writePath(path) + .writeAuthToken(authToken) + .toBytes(); return device.send( COMMAND.DERIVE_ADDRESS, returnType === ReturnType.Return ? P1.RETURN : P1.DISPLAY, authToken ? P2.WITH_TOKEN : P2.WITHOUT_TOKEN, - authToken ? Buffer.concat([data, serialize.uint32(authToken)]) : data + data ); } @@ -64,7 +70,7 @@ export async function deriveAddress( ReturnType.Return, authToken ); - return { addressHex: deserialize.hex(response.data) }; + return { addressHex: hex.encode(response.data) }; } export async function showAddress( diff --git a/src/interactions/getAppName.ts b/src/interactions/getAppName.ts index 89382c5..130b15e 100644 --- a/src/interactions/getAppName.ts +++ b/src/interactions/getAppName.ts @@ -1,7 +1,6 @@ import { COMMAND, type Device } from "../device"; import type { AppName } from "../types/public"; -import { deserialize } from "../serialization/deserialize"; -import { Buffer } from "buffer"; +import { EMPTY_BYTES } from "../serialization/utils"; const enum P1 { UNUSED = 0x00 @@ -16,7 +15,7 @@ export async function getAppName(device: Device): Promise { COMMAND.GET_APP_NAME, P1.UNUSED, P2.UNUSED, - Buffer.from([]) + EMPTY_BYTES ); - return { name: deserialize.ascii(response.data) }; + return { name: String.fromCharCode(...response.data) }; } diff --git a/src/interactions/getExtendedPublicKey.ts b/src/interactions/getExtendedPublicKey.ts index 360b1b3..573016f 100644 --- a/src/interactions/getExtendedPublicKey.ts +++ b/src/interactions/getExtendedPublicKey.ts @@ -1,9 +1,7 @@ import { COMMAND, type Device } from "../device"; import type { ExtendedPublicKey } from "../types/public"; -import { chunkBy } from "../serialization/utils"; -import { serialize } from "../serialization/serialize"; -import { Buffer } from "buffer"; import { hex } from "@fleet-sdk/crypto"; +import { ByteWriter } from "../serialization/byteWriter"; const enum P1 { WITHOUT_TOKEN = 0x01, @@ -14,22 +12,29 @@ const enum P2 { UNUSED = 0x00 } +const MAX_APDU_SIZE = 45; // https://github.com/tesseract-one/ledger-app-ergo/blob/main/doc/INS-10-EXT-PUB-KEY.md#data + export async function getExtendedPublicKey( device: Device, path: string, authToken?: number ): Promise { - const data = serialize.path(path); + const data = new ByteWriter(MAX_APDU_SIZE) + .writePath(path) + .writeAuthToken(authToken) + .toBytes(); + const response = await device.send( COMMAND.GET_EXTENDED_PUB_KEY, authToken ? P1.WITH_TOKEN : P1.WITHOUT_TOKEN, P2.UNUSED, - authToken ? Buffer.concat([data, serialize.uint32(authToken)]) : data + data ); - const [publicKey, chainCode] = chunkBy(response.data, [33, 32]); + const publicKey = hex.encode(response.data.subarray(0, 33)); + const chainCode = hex.encode(response.data.subarray(33, 65)); return { - publicKey: hex.encode(publicKey), - chainCode: hex.encode(chainCode) + publicKey, + chainCode }; } diff --git a/src/interactions/getVersion.ts b/src/interactions/getVersion.ts index 74cd7ec..f9e9c44 100644 --- a/src/interactions/getVersion.ts +++ b/src/interactions/getVersion.ts @@ -1,6 +1,6 @@ import type { Version } from "../types/public"; import { COMMAND, type Device } from "../device"; -import { Buffer } from "buffer"; +import { EMPTY_BYTES } from "../serialization/utils"; const IS_DEBUG_FLAG = 0x01; @@ -17,7 +17,7 @@ export async function getAppVersion(device: Device): Promise { COMMAND.GET_APP_VERSION, P1.UNUSED, P2.UNUSED, - Buffer.from([]) + EMPTY_BYTES ); return { diff --git a/src/interactions/signTx.ts b/src/interactions/signTx.ts index fd28a5b..757e6a9 100644 --- a/src/interactions/signTx.ts +++ b/src/interactions/signTx.ts @@ -1,11 +1,13 @@ -import { serialize } from "../serialization/serialize"; import type { ChangeMap, BoxCandidate, Token } from "../types/public"; -import { COMMAND, type Device } from "../device"; +import { COMMAND, MAX_DATA_LENGTH, type Device } from "../device"; import { ErgoAddress, type Network } from "@fleet-sdk/core"; import type { AttestedTransaction } from "../types/internal"; import type { AttestedBox } from "../types/attestedBox"; import { hex } from "@fleet-sdk/crypto"; import { Buffer } from "buffer"; +import { ByteWriter } from "../serialization/byteWriter"; +import { chunk } from "@fleet-sdk/common"; +import { EMPTY_BYTES } from "../serialization/utils"; const MINER_FEE_TREE = "1005040004000e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a701730073011001020402d19683030193a38cc7b2a57300000193c2b2a57301007473027303830108cdeeac93b1a57304"; @@ -31,6 +33,13 @@ const enum P2 { WITH_TOKEN = 0x02 } +const HASH_SIZE = 32; +const HEADER_SIZE = 46; // https://github.com/tesseract-one/ledger-app-ergo/blob/main/doc/INS-21-SIGN-TRANSACTION.md#data +const START_TX_SIZE = 7; // https://github.com/tesseract-one/ledger-app-ergo/blob/main/doc/INS-21-SIGN-TRANSACTION.md#0x10---start-transaction-data +const ADD_OUTPUT_HEADER_SIZE = 21; // https://github.com/tesseract-one/ledger-app-ergo/blob/main/doc/INS-21-SIGN-TRANSACTION.md#data-5 +const ADD_OUTPUT_CHANGE_PATH_SIZE = 51; // https://github.com/tesseract-one/ledger-app-ergo/blob/main/doc/INS-21-SIGN-TRANSACTION.md#data-6 +const ADD_OUTPUT_TOKENS_SIZE = MAX_DATA_LENGTH; // unclear from docs so setting to max packet size + export async function signTx( device: Device, tx: AttestedTransaction, @@ -46,7 +55,7 @@ export async function signTx( await sendOutputs(device, sessionId, tx.outputs, tx.changeMap, tx.distinctTokenIds); const proof = await sendConfirmAndSign(device, sessionId); - return new Uint8Array(proof); + return proof; } async function sendHeader( @@ -59,11 +68,11 @@ async function sendHeader( COMMAND.SIGN_TX, P1.START_SIGNING, authToken ? P2.WITH_TOKEN : P2.WITHOUT_TOKEN, - Buffer.concat([ - serialize.uint8(network), - serialize.path(path), - authToken ? serialize.uint32(authToken) : Buffer.alloc(0) - ]) + new ByteWriter(HEADER_SIZE) + .writeUInt8(network) + .writePath(path) + .writeAuthToken(authToken) + .toBytes() ); return response.data[0]; @@ -79,12 +88,12 @@ async function sendStartTx( COMMAND.SIGN_TX, P1.START_TRANSACTION, sessionId, - Buffer.concat([ - serialize.uint16(tx.inputs.length), - serialize.uint16(tx.dataInputs.length), - serialize.uint8(uniqueTokenIdsCount), - serialize.uint16(tx.outputs.length) - ]) + new ByteWriter(START_TX_SIZE) + .writeUInt16(tx.inputs.length) + .writeUInt16(tx.dataInputs.length) + .writeUInt8(uniqueTokenIdsCount) + .writeUInt16(tx.outputs.length) + .toBytes() ); return response.data[0]; @@ -93,29 +102,23 @@ async function sendStartTx( async function sendDistinctTokensIds( device: Device, sessionId: number, - ids: Uint8Array[] + tokenIds: Uint8Array[] ) { - if (ids.length === 0) return; - - const MAX_PACKET_SIZE = 7; - const packets = serialize.arrayAsMappedChunks(ids, MAX_PACKET_SIZE, (id) => - Buffer.from(id) - ); + if (tokenIds.length === 0) return; + const chunks = chunk(tokenIds, Math.floor(MAX_DATA_LENGTH / HASH_SIZE)); + for (const chunk of chunks) { + if (chunk.length === 0) continue; + const data = new ByteWriter(chunk.length * HASH_SIZE); + for (const id of chunk) data.writeBytes(id); - for (const p of packets) { - await device.send(COMMAND.SIGN_TX, P1.ADD_TOKEN_IDS, sessionId, p); + await device.send(COMMAND.SIGN_TX, P1.ADD_TOKEN_IDS, sessionId, data.toBytes()); } } async function sendInputs(device: Device, sessionId: number, inputs: AttestedBox[]) { for (const input of inputs) { for (const frame of input.frames) { - await device.send( - COMMAND.SIGN_TX, - P1.ADD_INPUT_BOX_FRAME, - sessionId, - Buffer.from(frame.bytes) - ); + await device.send(COMMAND.SIGN_TX, P1.ADD_INPUT_BOX_FRAME, sessionId, frame.bytes); } if (input.extension !== undefined && input.extension.length > 0) { @@ -133,16 +136,18 @@ async function sendBoxContextExtension( COMMAND.SIGN_TX, P1.ADD_INPUT_BOX_CONTEXT_EXTENSION_CHUNK, sessionId, - Buffer.from(extension) + extension ); } async function sendDataInputs(device: Device, sessionId: number, boxIds: string[]) { - const MAX_PACKET_SIZE = 7; - const packets = serialize.arrayAsMappedChunks(boxIds, MAX_PACKET_SIZE, serialize.hex); + const chunks = chunk(boxIds, Math.floor(MAX_DATA_LENGTH / HASH_SIZE)); + for (const chunk of chunks) { + if (chunk.length === 0) continue; + const data = new ByteWriter(chunk.length * HASH_SIZE); + for (const id of chunk) data.writeHex(id); - for (const p of packets) { - await device.send(COMMAND.SIGN_TX, P1.ADD_DATA_INPUTS, sessionId, p); + await device.send(COMMAND.SIGN_TX, P1.ADD_DATA_INPUTS, sessionId, data.toBytes()); } } @@ -160,20 +165,20 @@ async function sendOutputs( COMMAND.SIGN_TX, P1.ADD_OUTPUT_BOX_START, sessionId, - Buffer.concat([ - serialize.uint64(box.value), - serialize.uint32(box.ergoTree.length), - serialize.uint32(box.creationHeight), - serialize.uint8(box.tokens.length), - serialize.uint32(box.registers.length) - ]) + new ByteWriter(ADD_OUTPUT_HEADER_SIZE) + .writeUInt64(box.value) + .writeUInt32(box.ergoTree.length) + .writeUInt32(box.creationHeight) + .writeUInt8(box.tokens.length) + .writeUInt32(box.registers.length) + .toBytes() ); const tree = hex.encode(box.ergoTree); if (tree === MINER_FEE_TREE) { await addOutputBoxMinersFeeTree(device, sessionId); } else if (ErgoAddress.fromErgoTree(tree).toString() === changeMap.address) { - await addOutputBoxChangeTree(device, sessionId, changeMap.path); + await addOutputBoxChangePath(device, sessionId, changeMap.path); } else { await addOutputBoxErgoTree(device, sessionId, box.ergoTree); } @@ -197,7 +202,7 @@ async function addOutputBoxErgoTree( COMMAND.SIGN_TX, P1.ADD_OUTPUT_BOX_ERGO_TREE_CHUNK, sessionId, - Buffer.from(ergoTree) + ergoTree ); } @@ -206,16 +211,16 @@ async function addOutputBoxMinersFeeTree(device: Device, sessionId: number) { COMMAND.SIGN_TX, P1.ADD_OUTPUT_BOX_MINERS_FEE_TREE, sessionId, - Buffer.from([]) + EMPTY_BYTES ); } -async function addOutputBoxChangeTree(device: Device, sessionId: number, path: string) { +async function addOutputBoxChangePath(device: Device, sessionId: number, path: string) { await device.send( COMMAND.SIGN_TX, P1.ADD_OUTPUT_BOX_CHANGE_TREE, sessionId, - serialize.path(path) + new ByteWriter(ADD_OUTPUT_CHANGE_PATH_SIZE).writePath(path).toBytes() ); } @@ -225,17 +230,12 @@ async function addOutputBoxTokens( tokens: Token[], distinctTokenIds: string[] ) { - await device.send( - COMMAND.SIGN_TX, - P1.ADD_OUTPUT_BOX_TOKENS, - sessionId, - serialize.array(tokens, (t) => - Buffer.concat([ - serialize.uint32(distinctTokenIds.indexOf(t.id)), - serialize.uint64(t.amount) - ]) - ) - ); + const data = new ByteWriter(ADD_OUTPUT_TOKENS_SIZE); + for (const token of tokens) { + data.writeUInt32(distinctTokenIds.indexOf(token.id)).writeUInt64(token.amount); + } + + await device.send(COMMAND.SIGN_TX, P1.ADD_OUTPUT_BOX_TOKENS, sessionId, data.toBytes()); } async function addOutputBoxRegisters( @@ -247,16 +247,19 @@ async function addOutputBoxRegisters( COMMAND.SIGN_TX, P1.ADD_OUTPUT_BOX_REGISTERS_CHUNK, sessionId, - Buffer.from(registers) + registers ); } -async function sendConfirmAndSign(device: Device, sessionId: number): Promise { +async function sendConfirmAndSign( + device: Device, + sessionId: number +): Promise { const response = await device.send( COMMAND.SIGN_TX, P1.CONFIRM_AND_SIGN, sessionId, - Buffer.from([]) + EMPTY_BYTES ); return response.data; diff --git a/src/serialization/byteWriter.ts b/src/serialization/byteWriter.ts new file mode 100644 index 0000000..495bc2f --- /dev/null +++ b/src/serialization/byteWriter.ts @@ -0,0 +1,73 @@ +import bip32Path from "bip32-path"; +import { assert, ensureBigInt } from "@fleet-sdk/common"; +import { type Coder, hex } from "@fleet-sdk/crypto"; +import { Buffer } from "buffer"; +import { isErgoPath, pathToArray } from "./utils"; + +export class ByteWriter { + readonly #buffer: Buffer; + #offset: number; + + constructor(length: number) { + this.#buffer = Buffer.alloc(length); + this.#offset = 0; + } + + write(byte: number): ByteWriter { + this.#buffer[this.#offset++] = byte; + return this; + } + + writeUInt32(value: number): ByteWriter { + this.#offset = this.#buffer.writeUInt32BE(value, this.#offset); + return this; + } + + writeUInt8(value: number): ByteWriter { + this.#offset = this.#buffer.writeUInt8(value, this.#offset); + return this; + } + + writeUInt16(value: number): ByteWriter { + this.#offset = this.#buffer.writeUInt16BE(value, this.#offset); + return this; + } + + writeUInt64(value: string | bigint): ByteWriter { + const data = ensureBigInt(value); + this.#offset = this.#buffer.writeBigUInt64BE(data, this.#offset); + return this; + } + + writeBytes(bytes: ArrayLike): ByteWriter { + this.#buffer.set(bytes, this.#offset); + this.#offset += bytes.length; + return this; + } + + writeHex(bytesHex: string): ByteWriter { + return this.writeBytes(hex.decode(bytesHex)); + } + + writePath(path: string): ByteWriter { + const pathArray = pathToArray(path); + assert(isErgoPath(pathArray), "Invalid Ergo path"); + + this.write(pathArray.length); + for (const index of pathArray) this.writeUInt32(index); + return this; + } + + writeAuthToken(authToken?: number): ByteWriter { + if (!authToken) return this; + return this.writeUInt32(authToken); + } + + toBytes(): Uint8Array { + return this.#buffer.subarray(0, this.#offset); + } + + toBuffer(): Buffer { + return this.#buffer.slice(0, this.#offset); + } +} diff --git a/src/serialization/deserialize.ts b/src/serialization/deserialize.ts deleted file mode 100644 index 3c4de9b..0000000 --- a/src/serialization/deserialize.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { assert } from "@fleet-sdk/common"; -import { base10 } from "./utils"; -import type { Buffer } from "buffer"; - -export const deserialize = { - hex(buffer: Buffer): string { - return buffer.toString("hex"); - }, - - ascii(buffer: Buffer): string { - return buffer.toString("ascii"); - }, - - uint8(data: Buffer): number { - assert(data.length === 1, "invalid uint8 buffer"); - return data.readUIntBE(0, 1); - }, - - uint64(buffer: Buffer): string { - assert(buffer.length === 8, "invalid uint64 buffer"); - return trimLeadingZeros(base10.encode(buffer)); - } -}; - -function trimLeadingZeros(text: string): string { - return text.replace(/^0+/, ""); -} diff --git a/src/serialization/serialize.spec.ts b/src/serialization/serialize.spec.ts deleted file mode 100644 index b36a32f..0000000 --- a/src/serialization/serialize.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { serialize } from "./serialize"; - -describe("serializations", () => { - describe("serialize class", () => { - it("should serialize and split", () => { - const MAX_CHUNK_LENGTH = 3; - const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - const chunks = serialize.arrayAsMappedChunks( - arr, - MAX_CHUNK_LENGTH, - serialize.uint8 - ); - - expect(chunks).toHaveLength(4); - expect(chunks[0]).toHaveLength(MAX_CHUNK_LENGTH); - expect(chunks[1]).toHaveLength(MAX_CHUNK_LENGTH); - expect(chunks[2]).toHaveLength(MAX_CHUNK_LENGTH); - expect(chunks[3]).toHaveLength(1); - - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i]; - for (let j = 0; j < chunk.length; j++) { - expect(chunk[j]).toBe(arr[j + i * 3]); - } - } - }); - }); -}); diff --git a/src/serialization/serialize.ts b/src/serialization/serialize.ts deleted file mode 100644 index bd1035f..0000000 --- a/src/serialization/serialize.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { isHex, assert } from "@fleet-sdk/common"; -import { isUint16, isUint32, isUint64String, isUint8, isErgoPath } from "../assertions"; -import bip32Path from "bip32-path"; -import { base10 } from "./utils"; -import { Buffer } from "buffer"; - -export const serialize = { - path(path: number[] | string): Buffer { - const pathArray = typeof path === "string" ? pathToArray(path) : path; - assert(isErgoPath(pathArray), "Invalid Ergo path"); - - const buffer = Buffer.alloc(1 + pathArray.length * 4); - buffer[0] = pathArray.length; - - for (let i = 0; i < pathArray.length; i++) { - buffer.writeUInt32BE(pathArray[i], 1 + 4 * i); - } - - return buffer; - }, - - uint8(value: number): Buffer { - assert(isUint8(value), "invalid uint8 value"); - - const data = Buffer.alloc(1); - data.writeUInt8(value, 0); - return data; - }, - - uint16(value: number): Buffer { - assert(isUint16(value), "invalid uint16 value"); - - const data = Buffer.alloc(2); - data.writeUInt16BE(value, 0); - return data; - }, - - uint32(value: number): Buffer { - assert(isUint32(value), "invalid uint32 value"); - - const buffer = Buffer.alloc(4); - buffer.writeUInt32BE(value, 0); - return buffer; - }, - - uint64(value: string): Buffer { - assert(isUint64String(value), "invalid uint64 string"); - const data = base10.decode(value); - - const padding = Buffer.alloc(8 - data.length); - return Buffer.concat([padding, Buffer.from(data)]); - }, - - hex(data: string): Buffer { - assert(isHex(data), "invalid hex string"); - return Buffer.from(data, "hex"); - }, - - array(data: T[], serializeCallback: (value: T) => Buffer): Buffer { - const chucks: Buffer[] = []; - for (let i = 0; i < data.length; i++) { - chucks.push(serializeCallback(data[i])); - } - - return Buffer.concat(chucks); - }, - - arrayAsMappedChunks( - data: T[], - maxSize: number, - encode: (value: T) => Buffer - ): Buffer[] { - const packets = []; - for (let i = 0; i < Math.ceil(data.length / maxSize); i++) { - const chunks = []; - for (let j = i * maxSize; j < Math.min((i + 1) * maxSize, data.length); j++) { - chunks.push(encode(data[j])); - } - - packets.push(Buffer.concat(chunks)); - } - - return packets; - } -}; - -export function pathToArray(path: string): number[] { - return bip32Path.fromString(path).toPathArray(); -} diff --git a/src/serialization/utils.spec.ts b/src/serialization/utils.spec.ts index 212acb1..dec3422 100644 --- a/src/serialization/utils.spec.ts +++ b/src/serialization/utils.spec.ts @@ -1,12 +1,10 @@ -import { describe, expect, it } from "vitest"; -import { chunkBy } from "./utils"; +import { describe, it, expect } from "vitest"; +import { isErgoPath, pathToArray } from "./utils"; -describe("Utils test", () => { - it("should chunk buffers", () => { - const buffer = Buffer.alloc(11); - const [first, last] = chunkBy(buffer, [5, 6]); - - expect(first.length).toEqual(5); - expect(last.length).toEqual(6); +describe("assertions", () => { + it("Ergo path", () => { + expect(isErgoPath(pathToArray("m/44'/429'"))).to.be.true; + expect(isErgoPath(pathToArray("m/44'/2'"))).to.be.false; + expect(isErgoPath(pathToArray("m/44'"))).to.be.false; }); }); diff --git a/src/serialization/utils.ts b/src/serialization/utils.ts index 2066cfe..d2f2ba3 100644 --- a/src/serialization/utils.ts +++ b/src/serialization/utils.ts @@ -1,22 +1,17 @@ -import { assert } from "@fleet-sdk/common"; +import bip32Path from "bip32-path"; import base from "base-x"; export const base10 = base("0123456789"); -const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0); +export const EMPTY_BYTES = Uint8Array.from([]); +const [ERGO_PURPOSE, ERGO_COIN_TYPE] = bip32Path.fromString("m/44'/429'").toPathArray(); -export function chunkBy(data: Uint8Array, chunkLengths: number[]) { - assert(data.length >= sum(chunkLengths), "data is too small"); - - let offset = 0; - const result = []; - const restLength = data.length - sum(chunkLengths); - - for (const length of [...chunkLengths, restLength]) { - assert(length >= 0, `bad chunk length: ${length}`); - result.push(data.subarray(offset, offset + length)); - offset += length; - } +export function pathToArray(path: string): number[] { + return bip32Path.fromString(path).toPathArray(); +} - return result; +export function isErgoPath(path: number[]): boolean { + if (path.length < 2) return false; + const [pathPurpose, pathCoinType] = path; + return pathPurpose === ERGO_PURPOSE && pathCoinType === ERGO_COIN_TYPE; } diff --git a/src/types/attestedBox.ts b/src/types/attestedBox.ts index d633473..43e47b8 100644 --- a/src/types/attestedBox.ts +++ b/src/types/attestedBox.ts @@ -1,10 +1,11 @@ import { assert } from "@fleet-sdk/common"; import type { UnsignedBox, AttestedBoxFrame } from "./public"; +import { ByteWriter } from "../serialization/byteWriter"; export class AttestedBox { #box: UnsignedBox; #frames: AttestedBoxFrame[]; - #extension?: Buffer; + #extension?: Uint8Array; constructor(box: UnsignedBox, frames: AttestedBoxFrame[]) { this.#box = box; @@ -19,24 +20,26 @@ export class AttestedBox { return this.#frames; } - public get extension(): Buffer | undefined { + public get extension(): Uint8Array | undefined { return this.#extension; } - public setExtension(extension: Buffer): AttestedBox { + public setExtension(extension: Uint8Array): AttestedBox { assert(!this.#extension, "The extension is already inserted"); - const lengthBuffer = Buffer.alloc(4); const firstFrame = this.#frames[0]; + const length = firstFrame.bytes.length + 4; + const newFrame = new ByteWriter(length).writeBytes(firstFrame.bytes); + if (extension.length === 1 && extension[0] === 0) { - lengthBuffer.writeUInt32BE(0, 0); + newFrame.writeUInt32(0); } else { this.#extension = extension; firstFrame.extensionLength = extension.length; - lengthBuffer.writeUInt32BE(extension.length, 0); + newFrame.writeUInt32(extension.length); } - firstFrame.bytes = Buffer.concat([firstFrame.bytes, lengthBuffer]); + firstFrame.bytes = newFrame.toBytes(); return this; } From 7e7cc548fae90edd22e36ea2b3fdd3152e1cd938 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:08:46 -0300 Subject: [PATCH 4/8] uninstall base-x --- package.json | 1 - pnpm-lock.yaml | 8 -------- src/serialization/utils.ts | 3 --- 3 files changed, 12 deletions(-) diff --git a/package.json b/package.json index 57cd9c3..764ee3b 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "@fleet-sdk/common": "^0.4.1", "@fleet-sdk/core": "0.5.0", "@fleet-sdk/crypto": "^0.5.0", - "base-x": "^5.0.0", "bip32-path": "^0.4.2", "buffer": "^6.0.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6229df..2fb5aa1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@fleet-sdk/crypto': specifier: ^0.5.0 version: 0.5.0 - base-x: - specifier: ^5.0.0 - version: 5.0.0 bip32-path: specifier: ^0.4.2 version: 0.4.2 @@ -511,9 +508,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base-x@5.0.0: - resolution: {integrity: sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1688,8 +1682,6 @@ snapshots: balanced-match@1.0.2: {} - base-x@5.0.0: {} - base64-js@1.5.1: {} binary-extensions@2.3.0: {} diff --git a/src/serialization/utils.ts b/src/serialization/utils.ts index d2f2ba3..7b003fc 100644 --- a/src/serialization/utils.ts +++ b/src/serialization/utils.ts @@ -1,7 +1,4 @@ import bip32Path from "bip32-path"; -import base from "base-x"; - -export const base10 = base("0123456789"); export const EMPTY_BYTES = Uint8Array.from([]); const [ERGO_PURPOSE, ERGO_COIN_TYPE] = bip32Path.fromString("m/44'/429'").toPathArray(); From 2175d17bee90275858a32c022a404a66e245392b Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:28:34 -0300 Subject: [PATCH 5/8] remove buffer and bring @types/node --- biome.json | 7 +------ package.json | 4 ++-- pnpm-lock.yaml | 23 ++++------------------- src/interactions/attestInput.ts | 1 - src/interactions/signTx.ts | 1 - src/serialization/byteWriter.ts | 4 +--- 6 files changed, 8 insertions(+), 32 deletions(-) diff --git a/biome.json b/biome.json index 8753be1..97c0e75 100644 --- a/biome.json +++ b/biome.json @@ -1,12 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/1.8.2/schema.json", "files": { - "ignore": [ - "**/coverage/*", - "**/dist/*", - "**/node_modules/*", - "**/package.json" - ] + "ignore": ["**/coverage/*", "**/dist/*", "**/node_modules/*", "**/package.json"] }, "organizeImports": { "enabled": true diff --git a/package.json b/package.json index 764ee3b..c28ab1e 100644 --- a/package.json +++ b/package.json @@ -60,13 +60,13 @@ "@fleet-sdk/common": "^0.4.1", "@fleet-sdk/core": "0.5.0", "@fleet-sdk/crypto": "^0.5.0", - "bip32-path": "^0.4.2", - "buffer": "^6.0.3" + "bip32-path": "^0.4.2" }, "devDependencies": { "@biomejs/biome": "^1.8.3", "@ledgerhq/hw-transport": "^6.31.0", "@ledgerhq/hw-transport-mocker": "^6.29.0", + "@types/node": "^20.14.9", "@vitest/coverage-v8": "^1.6.0", "open-cli": "^8.0.0", "tsup": "^8.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fb5aa1..535eb5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: bip32-path: specifier: ^0.4.2 version: 0.4.2 - buffer: - specifier: ^6.0.3 - version: 6.0.3 devDependencies: '@biomejs/biome': specifier: ^1.8.3 @@ -33,6 +30,9 @@ importers: '@ledgerhq/hw-transport-mocker': specifier: ^6.29.0 version: 6.29.0 + '@types/node': + specifier: ^20.14.9 + version: 20.14.9 '@vitest/coverage-v8': specifier: ^1.6.0 version: 1.6.0(vitest@1.6.0(@types/node@20.14.9)) @@ -508,9 +508,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -528,9 +525,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -1601,7 +1595,6 @@ snapshots: '@types/node@20.14.9': dependencies: undici-types: 5.26.5 - optional: true '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.9))': dependencies: @@ -1682,8 +1675,6 @@ snapshots: balanced-match@1.0.2: {} - base64-js@1.5.1: {} - binary-extensions@2.3.0: {} bip32-path@0.4.2: {} @@ -1701,11 +1692,6 @@ snapshots: dependencies: fill-range: 7.1.1 - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - bundle-name@4.1.0: dependencies: run-applescript: 7.0.0 @@ -2389,8 +2375,7 @@ snapshots: ufo@1.5.3: {} - undici-types@5.26.5: - optional: true + undici-types@5.26.5: {} unique-string@3.0.0: dependencies: diff --git a/src/interactions/attestInput.ts b/src/interactions/attestInput.ts index c377ca6..438998d 100644 --- a/src/interactions/attestInput.ts +++ b/src/interactions/attestInput.ts @@ -2,7 +2,6 @@ import { COMMAND, MAX_DATA_LENGTH, type Device } from "../device"; import type { AttestedBoxFrame, UnsignedBox, Token } from "../types/public"; import type { DeviceResponse } from "../types/internal"; import { AttestedBox } from "../types/attestedBox"; -import { Buffer } from "buffer"; import { ByteWriter } from "../serialization/byteWriter"; import { chunk } from "@fleet-sdk/common"; import { hex } from "@fleet-sdk/crypto"; diff --git a/src/interactions/signTx.ts b/src/interactions/signTx.ts index 757e6a9..cc01816 100644 --- a/src/interactions/signTx.ts +++ b/src/interactions/signTx.ts @@ -4,7 +4,6 @@ import { ErgoAddress, type Network } from "@fleet-sdk/core"; import type { AttestedTransaction } from "../types/internal"; import type { AttestedBox } from "../types/attestedBox"; import { hex } from "@fleet-sdk/crypto"; -import { Buffer } from "buffer"; import { ByteWriter } from "../serialization/byteWriter"; import { chunk } from "@fleet-sdk/common"; import { EMPTY_BYTES } from "../serialization/utils"; diff --git a/src/serialization/byteWriter.ts b/src/serialization/byteWriter.ts index 495bc2f..506e7d8 100644 --- a/src/serialization/byteWriter.ts +++ b/src/serialization/byteWriter.ts @@ -1,7 +1,5 @@ -import bip32Path from "bip32-path"; import { assert, ensureBigInt } from "@fleet-sdk/common"; -import { type Coder, hex } from "@fleet-sdk/crypto"; -import { Buffer } from "buffer"; +import { hex } from "@fleet-sdk/crypto"; import { isErgoPath, pathToArray } from "./utils"; export class ByteWriter { From 0ac7571692419ee8febfc0dfb0b979c8f7d4f422 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Tue, 2 Jul 2024 21:49:05 -0300 Subject: [PATCH 6/8] sort imports --- src/device.spec.ts | 2 +- src/device.ts | 4 ++-- src/erg.spec.ts | 4 ++-- src/erg.ts | 28 ++++++++++++------------ src/interactions/attestInput.ts | 10 ++++----- src/interactions/deriveAddress.ts | 8 +++---- src/interactions/getAppName.ts | 2 +- src/interactions/getExtendedPublicKey.ts | 4 ++-- src/interactions/getVersion.ts | 2 +- src/interactions/index.ts | 2 +- src/interactions/signTx.ts | 10 ++++----- src/serialization/utils.spec.ts | 2 +- src/types/attestedBox.ts | 2 +- src/types/internal.ts | 4 ++-- 14 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/device.spec.ts b/src/device.spec.ts index da3c130..0d723ee 100644 --- a/src/device.spec.ts +++ b/src/device.spec.ts @@ -1,6 +1,6 @@ +import { RecordStore, openTransportReplayer } from "@ledgerhq/hw-transport-mocker"; import { describe, expect, it } from "vitest"; import { Device, DeviceError, RETURN_CODE } from "./device"; -import { RecordStore, openTransportReplayer } from "@ledgerhq/hw-transport-mocker"; import { CLA } from "./erg"; describe("DeviceError construction", () => { diff --git a/src/device.ts b/src/device.ts index 6540312..4b13a4b 100644 --- a/src/device.ts +++ b/src/device.ts @@ -1,7 +1,7 @@ -import type Transport from "@ledgerhq/hw-transport"; -import type { DeviceResponse } from "./types/internal"; import type { Buffer } from "buffer"; +import type Transport from "@ledgerhq/hw-transport"; import { ByteWriter } from "./serialization/byteWriter"; +import type { DeviceResponse } from "./types/internal"; export const enum COMMAND { GET_APP_VERSION = 0x01, diff --git a/src/erg.spec.ts b/src/erg.spec.ts index 10e7e44..35a9fdb 100644 --- a/src/erg.spec.ts +++ b/src/erg.spec.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, test, vi } from "vitest"; +import { RecordStore, openTransportReplayer } from "@ledgerhq/hw-transport-mocker"; +import { describe, expect, it, test, vi } from "vitest"; import { ErgoLedgerApp } from "./erg"; -import { openTransportReplayer, RecordStore } from "@ledgerhq/hw-transport-mocker"; describe("construction", () => { it("should construct app with transport", async () => { diff --git a/src/erg.ts b/src/erg.ts index 1607a43..7ea75f2 100644 --- a/src/erg.ts +++ b/src/erg.ts @@ -1,28 +1,28 @@ +import { Network, uniq } from "@fleet-sdk/common"; import type Transport from "@ledgerhq/hw-transport"; import { Device, DeviceError, RETURN_CODE } from "./device"; -import type { - AppName, - UnsignedBox, - DerivedAddress, - ExtendedPublicKey, - Version, - UnsignedTransaction -} from "./types/public"; -import type { AttestedBox } from "./types/attestedBox"; import { + attestInput, + deriveAddress, getAppName, - getExtendedPublicKey, getAppVersion, - deriveAddress, + getExtendedPublicKey, showAddress, - attestInput, signTx } from "./interactions"; +import type { AttestedBox } from "./types/attestedBox"; import type { AttestedTransaction, SignTransactionResponse } from "./types/internal"; -import { uniq, Network } from "@fleet-sdk/common"; +import type { + AppName, + DerivedAddress, + ExtendedPublicKey, + UnsignedBox, + UnsignedTransaction, + Version +} from "./types/public"; -export { DeviceError, RETURN_CODE, Network }; export * from "./types/public"; +export { DeviceError, Network, RETURN_CODE }; export const CLA = 0xe0; /** diff --git a/src/interactions/attestInput.ts b/src/interactions/attestInput.ts index 438998d..1be98a2 100644 --- a/src/interactions/attestInput.ts +++ b/src/interactions/attestInput.ts @@ -1,10 +1,10 @@ -import { COMMAND, MAX_DATA_LENGTH, type Device } from "../device"; -import type { AttestedBoxFrame, UnsignedBox, Token } from "../types/public"; -import type { DeviceResponse } from "../types/internal"; -import { AttestedBox } from "../types/attestedBox"; -import { ByteWriter } from "../serialization/byteWriter"; import { chunk } from "@fleet-sdk/common"; import { hex } from "@fleet-sdk/crypto"; +import { COMMAND, type Device, MAX_DATA_LENGTH } from "../device"; +import { ByteWriter } from "../serialization/byteWriter"; +import { AttestedBox } from "../types/attestedBox"; +import type { DeviceResponse } from "../types/internal"; +import type { AttestedBoxFrame, Token, UnsignedBox } from "../types/public"; const enum P1 { BOX_START = 0x01, diff --git a/src/interactions/deriveAddress.ts b/src/interactions/deriveAddress.ts index dd535a5..ee86b48 100644 --- a/src/interactions/deriveAddress.ts +++ b/src/interactions/deriveAddress.ts @@ -1,10 +1,10 @@ -import { COMMAND, RETURN_CODE, type Device } from "../device"; -import type { DerivedAddress } from "../types/public"; -import type { DeviceResponse } from "../types/internal"; import type { Network } from "@fleet-sdk/common"; -import { ByteWriter } from "../serialization/byteWriter"; import { hex } from "@fleet-sdk/crypto"; +import { COMMAND, type Device, RETURN_CODE } from "../device"; +import { ByteWriter } from "../serialization/byteWriter"; import { pathToArray } from "../serialization/utils"; +import type { DeviceResponse } from "../types/internal"; +import type { DerivedAddress } from "../types/public"; const enum ReturnType { Return = 0x01, diff --git a/src/interactions/getAppName.ts b/src/interactions/getAppName.ts index 130b15e..b06e6f1 100644 --- a/src/interactions/getAppName.ts +++ b/src/interactions/getAppName.ts @@ -1,6 +1,6 @@ import { COMMAND, type Device } from "../device"; -import type { AppName } from "../types/public"; import { EMPTY_BYTES } from "../serialization/utils"; +import type { AppName } from "../types/public"; const enum P1 { UNUSED = 0x00 diff --git a/src/interactions/getExtendedPublicKey.ts b/src/interactions/getExtendedPublicKey.ts index 573016f..254db33 100644 --- a/src/interactions/getExtendedPublicKey.ts +++ b/src/interactions/getExtendedPublicKey.ts @@ -1,7 +1,7 @@ -import { COMMAND, type Device } from "../device"; -import type { ExtendedPublicKey } from "../types/public"; import { hex } from "@fleet-sdk/crypto"; +import { COMMAND, type Device } from "../device"; import { ByteWriter } from "../serialization/byteWriter"; +import type { ExtendedPublicKey } from "../types/public"; const enum P1 { WITHOUT_TOKEN = 0x01, diff --git a/src/interactions/getVersion.ts b/src/interactions/getVersion.ts index f9e9c44..d036ab4 100644 --- a/src/interactions/getVersion.ts +++ b/src/interactions/getVersion.ts @@ -1,6 +1,6 @@ -import type { Version } from "../types/public"; import { COMMAND, type Device } from "../device"; import { EMPTY_BYTES } from "../serialization/utils"; +import type { Version } from "../types/public"; const IS_DEBUG_FLAG = 0x01; diff --git a/src/interactions/index.ts b/src/interactions/index.ts index 5a99b60..34f40f8 100644 --- a/src/interactions/index.ts +++ b/src/interactions/index.ts @@ -1,6 +1,6 @@ +export { attestInput } from "./attestInput"; export { deriveAddress, showAddress } from "./deriveAddress"; export { getAppName } from "./getAppName"; export { getExtendedPublicKey } from "./getExtendedPublicKey"; export { getAppVersion } from "./getVersion"; -export { attestInput } from "./attestInput"; export { signTx } from "./signTx"; diff --git a/src/interactions/signTx.ts b/src/interactions/signTx.ts index cc01816..4f972d8 100644 --- a/src/interactions/signTx.ts +++ b/src/interactions/signTx.ts @@ -1,12 +1,12 @@ -import type { ChangeMap, BoxCandidate, Token } from "../types/public"; -import { COMMAND, MAX_DATA_LENGTH, type Device } from "../device"; +import { chunk } from "@fleet-sdk/common"; import { ErgoAddress, type Network } from "@fleet-sdk/core"; -import type { AttestedTransaction } from "../types/internal"; -import type { AttestedBox } from "../types/attestedBox"; import { hex } from "@fleet-sdk/crypto"; +import { COMMAND, type Device, MAX_DATA_LENGTH } from "../device"; import { ByteWriter } from "../serialization/byteWriter"; -import { chunk } from "@fleet-sdk/common"; import { EMPTY_BYTES } from "../serialization/utils"; +import type { AttestedBox } from "../types/attestedBox"; +import type { AttestedTransaction } from "../types/internal"; +import type { BoxCandidate, ChangeMap, Token } from "../types/public"; const MINER_FEE_TREE = "1005040004000e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a701730073011001020402d19683030193a38cc7b2a57300000193c2b2a57301007473027303830108cdeeac93b1a57304"; diff --git a/src/serialization/utils.spec.ts b/src/serialization/utils.spec.ts index dec3422..b31ba47 100644 --- a/src/serialization/utils.spec.ts +++ b/src/serialization/utils.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { isErgoPath, pathToArray } from "./utils"; describe("assertions", () => { diff --git a/src/types/attestedBox.ts b/src/types/attestedBox.ts index 43e47b8..61aacff 100644 --- a/src/types/attestedBox.ts +++ b/src/types/attestedBox.ts @@ -1,6 +1,6 @@ import { assert } from "@fleet-sdk/common"; -import type { UnsignedBox, AttestedBoxFrame } from "./public"; import { ByteWriter } from "../serialization/byteWriter"; +import type { AttestedBoxFrame, UnsignedBox } from "./public"; export class AttestedBox { #box: UnsignedBox; diff --git a/src/types/internal.ts b/src/types/internal.ts index d0ad2ce..5fd9260 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -1,7 +1,7 @@ +import type { Buffer } from "buffer"; import type { RETURN_CODE } from "../device"; -import type { BoxCandidate, ChangeMap } from "./public"; import type { AttestedBox } from "./attestedBox"; -import type { Buffer } from "buffer"; +import type { BoxCandidate, ChangeMap } from "./public"; export type DeviceResponse = { data: Buffer; From df582739732514c6fe42711f3a5551939b58cbd7 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Wed, 3 Jul 2024 08:15:56 -0300 Subject: [PATCH 7/8] clean up --- src/device.ts | 2 +- src/interactions/attestInput.ts | 4 ++-- src/interactions/deriveAddress.ts | 14 +++++++------- src/interactions/getExtendedPublicKey.ts | 13 +++---------- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/device.ts b/src/device.ts index 4b13a4b..72bcd8c 100644 --- a/src/device.ts +++ b/src/device.ts @@ -38,7 +38,7 @@ export class Device { ): Promise { const responses: DeviceResponse[] = []; for (let i = 0; i < Math.ceil(data.length / MAX_DATA_LENGTH); i++) { - const chunk = data.slice( + const chunk = data.subarray( i * MAX_DATA_LENGTH, Math.min((i + 1) * MAX_DATA_LENGTH, data.length) ); diff --git a/src/interactions/attestInput.ts b/src/interactions/attestInput.ts index 1be98a2..6992b8e 100644 --- a/src/interactions/attestInput.ts +++ b/src/interactions/attestInput.ts @@ -112,7 +112,7 @@ async function sendRegisters( COMMAND.ATTEST_INPUT, P1.ADD_REGISTERS_CHUNK, sessionId, - Buffer.from(data) + data ); /* v8 ignore next */ @@ -130,7 +130,7 @@ async function getAttestedFrames( COMMAND.ATTEST_INPUT, P1.GET_ATTESTED_BOX_FRAME, sessionId, - Buffer.from([i]) + Uint8Array.from([i]) ); responses.push(decodeAttestedFrameResponse(response.data)); diff --git a/src/interactions/deriveAddress.ts b/src/interactions/deriveAddress.ts index ee86b48..bf556a4 100644 --- a/src/interactions/deriveAddress.ts +++ b/src/interactions/deriveAddress.ts @@ -43,17 +43,15 @@ function sendDeriveAddress( throw new Error(`Invalid change path: ${change}`); } - const data = new ByteWriter(MAX_APDU_SIZE) - .write(network) - .writePath(path) - .writeAuthToken(authToken) - .toBytes(); - return device.send( COMMAND.DERIVE_ADDRESS, returnType === ReturnType.Return ? P1.RETURN : P1.DISPLAY, authToken ? P2.WITH_TOKEN : P2.WITHOUT_TOKEN, - data + new ByteWriter(MAX_APDU_SIZE) + .write(network) + .writePath(path) + .writeAuthToken(authToken) + .toBytes() ); } @@ -70,6 +68,7 @@ export async function deriveAddress( ReturnType.Return, authToken ); + return { addressHex: hex.encode(response.data) }; } @@ -86,5 +85,6 @@ export async function showAddress( ReturnType.Display, authToken ); + return response.returnCode === RETURN_CODE.OK; } diff --git a/src/interactions/getExtendedPublicKey.ts b/src/interactions/getExtendedPublicKey.ts index 254db33..cfee8ac 100644 --- a/src/interactions/getExtendedPublicKey.ts +++ b/src/interactions/getExtendedPublicKey.ts @@ -19,22 +19,15 @@ export async function getExtendedPublicKey( path: string, authToken?: number ): Promise { - const data = new ByteWriter(MAX_APDU_SIZE) - .writePath(path) - .writeAuthToken(authToken) - .toBytes(); - const response = await device.send( COMMAND.GET_EXTENDED_PUB_KEY, authToken ? P1.WITH_TOKEN : P1.WITHOUT_TOKEN, P2.UNUSED, - data + new ByteWriter(MAX_APDU_SIZE).writePath(path).writeAuthToken(authToken).toBytes() ); - const publicKey = hex.encode(response.data.subarray(0, 33)); - const chainCode = hex.encode(response.data.subarray(33, 65)); return { - publicKey, - chainCode + publicKey: hex.encode(response.data.subarray(0, 33)), + chainCode: hex.encode(response.data.subarray(33, 65)) }; } From 7cec338fa3c125e2a726fb866bc074e2419d464e Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Wed, 3 Jul 2024 08:21:08 -0300 Subject: [PATCH 8/8] refactor front end api --- src/erg.ts | 101 +++++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 54 deletions(-) diff --git a/src/erg.ts b/src/erg.ts index 7ea75f2..80f849c 100644 --- a/src/erg.ts +++ b/src/erg.ts @@ -29,17 +29,17 @@ export const CLA = 0xe0; * Ergo's Ledger hardware wallet API */ export class ErgoLedgerApp { - private _device: Device; - private _authToken: number; - private _useAuthToken: boolean; - private _logging: boolean; + #device: Device; + #authToken: number; + #useAuthToken: boolean; + #logging: boolean; - public get authToken(): number | undefined { - return this._useAuthToken ? this._authToken : undefined; + get authToken(): number | undefined { + return this.#useAuthToken ? this.#authToken : undefined; } - public get transport(): Transport { - return this._device.transport; + get transport(): Transport { + return this.#device.transport; } constructor(transport: Transport); @@ -60,10 +60,10 @@ export class ErgoLedgerApp { scrambleKey ); - this._device = new Device(transport, CLA); - this._authToken = !authToken ? this.newAuthToken() : authToken; - this._useAuthToken = true; - this._logging = false; + this.#device = new Device(transport, CLA); + this.#authToken = !authToken ? this.#newAuthToken() : authToken; + this.#useAuthToken = true; + this.#logging = false; } /** @@ -71,8 +71,8 @@ export class ErgoLedgerApp { * @param use * @returns */ - public useAuthToken(use = true): ErgoLedgerApp { - this._useAuthToken = use; + useAuthToken(use = true): ErgoLedgerApp { + this.#useAuthToken = use; return this; } @@ -81,16 +81,16 @@ export class ErgoLedgerApp { * @param enable * @returns */ - public enableDebugMode(enable = true): ErgoLedgerApp { - this._logging = enable; + enableDebugMode(enable = true): ErgoLedgerApp { + this.#logging = enable; return this; } - private newAuthToken(): number { + #newAuthToken(): number { let newToken = 0; do { newToken = Math.floor(Math.random() * 0xffffffff) + 1; - } while (newToken === this._authToken); + } while (newToken === this.#authToken); return newToken; } @@ -99,18 +99,18 @@ export class ErgoLedgerApp { * Get application version. * @returns a Promise with the Ledger Application version. */ - public async getAppVersion(): Promise { - this._debug("getAppVersion"); - return getAppVersion(this._device); + async getAppVersion(): Promise { + this.#debug("getAppVersion"); + return getAppVersion(this.#device); } /** * Get application name. * @returns a Promise with the Ledger Application name. */ - public async getAppName(): Promise { - this._debug("getAppName"); - return getAppName(this._device); + async getAppName(): Promise { + this.#debug("getAppName"); + return getAppName(this.#device); } /** @@ -118,9 +118,9 @@ export class ErgoLedgerApp { * @param path BIP32 path. * @returns a Promise with the **chain code** and the **public key** for provided BIP32 path. */ - public async getExtendedPublicKey(path: string): Promise { - this._debug("getExtendedPublicKey", path); - return getExtendedPublicKey(this._device, path, this.authToken); + async getExtendedPublicKey(path: string): Promise { + this.#debug("getExtendedPublicKey", path); + return getExtendedPublicKey(this.#device, path, this.authToken); } /** @@ -128,12 +128,9 @@ export class ErgoLedgerApp { * @param path Bip44 path. * @returns a Promise with the derived address in hex format. */ - public async deriveAddress( - path: string, - network = Network.Mainnet - ): Promise { - this._debug("deriveAddress", path); - return deriveAddress(this._device, network, path, this.authToken); + async deriveAddress(path: string, network = Network.Mainnet): Promise { + this.#debug("deriveAddress", path); + return deriveAddress(this.#device, network, path, this.authToken); } /** @@ -141,31 +138,31 @@ export class ErgoLedgerApp { * @param path Bip44 path. * @returns a Promise with true if the user accepts or throws an exception if it get rejected. */ - public async showAddress(path: string, network = Network.Mainnet): Promise { - this._debug("showAddress", path); - return showAddress(this._device, network, path, this.authToken); + async showAddress(path: string, network = Network.Mainnet): Promise { + this.#debug("showAddress", path); + return showAddress(this.#device, network, path, this.authToken); } - public async attestInput(box: UnsignedBox): Promise { - this._debug("attestInput", box); - return this._attestInput(box); + async attestInput(box: UnsignedBox): Promise { + this.#debug("attestInput", box); + return this.#attestInput(box); } - private async _attestInput(box: UnsignedBox): Promise { - return attestInput(this._device, box, this.authToken); + async #attestInput(box: UnsignedBox): Promise { + return attestInput(this.#device, box, this.authToken); } - public async signTx( + async signTx( tx: UnsignedTransaction, network = Network.Mainnet ): Promise { - this._debug("signTx", { tx, network }); + this.#debug("signTx", { tx, network }); if (!tx.inputs || tx.inputs.length === 0) { throw new DeviceError(RETURN_CODE.BAD_INPUT_COUNT); } - const attestedInputs = await this._attestInputs(tx.inputs); + const attestedInputs = await this.#attestInputs(tx.inputs); const signPaths = uniq(tx.inputs.map((i) => i.signPath)); const attestedTx: AttestedTransaction = { inputs: attestedInputs, @@ -178,7 +175,7 @@ export class ErgoLedgerApp { const signatures: SignTransactionResponse = {}; for (const path of signPaths) { signatures[path] = await signTx( - this._device, + this.#device, attestedTx, path, network, @@ -187,17 +184,15 @@ export class ErgoLedgerApp { } const signBytes: Uint8Array[] = []; - for (const input of tx.inputs) { - signBytes.push(signatures[input.signPath]); - } + for (const input of tx.inputs) signBytes.push(signatures[input.signPath]); return signBytes; } - private async _attestInputs(inputs: UnsignedBox[]): Promise { + async #attestInputs(inputs: UnsignedBox[]): Promise { const attestedBoxes: AttestedBox[] = []; for (const box of inputs) { - const attestedBox = await this._attestInput(box); + const attestedBox = await this.#attestInput(box); attestedBox.setExtension(box.extension); attestedBoxes.push(attestedBox); } @@ -205,10 +200,8 @@ export class ErgoLedgerApp { return attestedBoxes; } - private _debug(caller: string, message: unknown = "") { - if (!this._logging) { - return; - } + #debug(caller: string, message: unknown = "") { + if (!this.#logging) return; console.debug( `[ledger-ergo-js][${caller}]${message ? ": " : ""}${message ? JSON.stringify(message) : ""}`