From e48c9d3962629cd616aba1c0aaa307ef4df10fb4 Mon Sep 17 00:00:00 2001 From: Bart Huijgen Date: Thu, 8 Aug 2024 10:11:58 +0200 Subject: [PATCH] feat: initial commit for kadena ledger lib --- packages/libs/ledger/LICENSE | 29 +++ packages/libs/ledger/README.md | 18 ++ packages/libs/ledger/build.config.ts | 10 + packages/libs/ledger/example/.gitignore | 24 +++ packages/libs/ledger/example/index.html | 14 ++ packages/libs/ledger/example/package.json | 24 +++ packages/libs/ledger/example/src/app.tsx | 88 +++++++++ .../ledger/example/src/legacy-transport.ts | 57 ++++++ packages/libs/ledger/example/src/main.tsx | 9 + .../libs/ledger/example/src/vite-env.d.ts | 1 + packages/libs/ledger/example/tsconfig.json | 25 +++ packages/libs/ledger/example/vite.config.ts | 7 + packages/libs/ledger/package.json | 35 ++++ packages/libs/ledger/src/constants.ts | 2 + packages/libs/ledger/src/index.ts | 33 ++++ packages/libs/ledger/src/ledger.ts | 165 +++++++++++++++++ packages/libs/ledger/src/transaction.ts | 172 ++++++++++++++++++ packages/libs/ledger/src/utils.ts | 34 ++++ packages/libs/ledger/tsconfig.json | 21 +++ 19 files changed, 768 insertions(+) create mode 100644 packages/libs/ledger/LICENSE create mode 100644 packages/libs/ledger/README.md create mode 100644 packages/libs/ledger/build.config.ts create mode 100644 packages/libs/ledger/example/.gitignore create mode 100644 packages/libs/ledger/example/index.html create mode 100644 packages/libs/ledger/example/package.json create mode 100644 packages/libs/ledger/example/src/app.tsx create mode 100644 packages/libs/ledger/example/src/legacy-transport.ts create mode 100644 packages/libs/ledger/example/src/main.tsx create mode 100644 packages/libs/ledger/example/src/vite-env.d.ts create mode 100644 packages/libs/ledger/example/tsconfig.json create mode 100644 packages/libs/ledger/example/vite.config.ts create mode 100644 packages/libs/ledger/package.json create mode 100644 packages/libs/ledger/src/constants.ts create mode 100644 packages/libs/ledger/src/index.ts create mode 100644 packages/libs/ledger/src/ledger.ts create mode 100644 packages/libs/ledger/src/transaction.ts create mode 100644 packages/libs/ledger/src/utils.ts create mode 100644 packages/libs/ledger/tsconfig.json diff --git a/packages/libs/ledger/LICENSE b/packages/libs/ledger/LICENSE new file mode 100644 index 0000000000..d8d12fccad --- /dev/null +++ b/packages/libs/ledger/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018 - 2024 Kadena LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/libs/ledger/README.md b/packages/libs/ledger/README.md new file mode 100644 index 0000000000..0fdccb5c43 --- /dev/null +++ b/packages/libs/ledger/README.md @@ -0,0 +1,18 @@ + + +# @kadena/ledger + +Helper methods to interact with the Kadena Ledger app + + + + kadena.js logo + + + + +## Kadena ledger + +> Helper methods to interact with the Kadena Ledger app + +Build with minimal dependencies for a small bundle size. diff --git a/packages/libs/ledger/build.config.ts b/packages/libs/ledger/build.config.ts new file mode 100644 index 0000000000..a26ce68d70 --- /dev/null +++ b/packages/libs/ledger/build.config.ts @@ -0,0 +1,10 @@ +import { defineBuildConfig } from "unbuild"; + +export default defineBuildConfig({ + rollup: { + esbuild: { + minify: true, + }, + }, + declaration: true, +}); diff --git a/packages/libs/ledger/example/.gitignore b/packages/libs/ledger/example/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/packages/libs/ledger/example/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/libs/ledger/example/index.html b/packages/libs/ledger/example/index.html new file mode 100644 index 0000000000..e6dd007c0a --- /dev/null +++ b/packages/libs/ledger/example/index.html @@ -0,0 +1,14 @@ + + + + + + + Kadena Ledger Example + + + +
+ + + diff --git a/packages/libs/ledger/example/package.json b/packages/libs/ledger/example/package.json new file mode 100644 index 0000000000..017a7555fb --- /dev/null +++ b/packages/libs/ledger/example/package.json @@ -0,0 +1,24 @@ +{ + "name": "example", + "version": "1.0.0", + "description": "", + "keywords": [], + "license": "ISC", + "author": "", + "main": "index.js", + "scripts": { + "dev": "vite dev" + }, + "devDependencies": { + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "@vitejs/plugin-react": "^4.3.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vite": "^5.3.3" + }, + "dependencies": { + "@ledgerhq/hw-transport-webhid": "^6.28.3", + "buffer": "^6.0.3" + } +} diff --git a/packages/libs/ledger/example/src/app.tsx b/packages/libs/ledger/example/src/app.tsx new file mode 100644 index 0000000000..160edc874b --- /dev/null +++ b/packages/libs/ledger/example/src/app.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { + getPublicKey, + getVersion, + openApp, + signTransaction, +} from '../../src/index'; + +function App() { + const [openAppResult, setOpenAppResult] = useState(''); + const [getPublicKeyResult, setGetPublicKeyResult] = useState(''); + const [getVersionResult, setGetVersionResult] = useState(''); + const [signTransactionResult, setSignTransactionResult] = useState(''); + + return ( +
+

Kadena ledger example

+
+
+ +
{openAppResult}
+
+ +
+ +
+ {JSON.stringify(getPublicKeyResult)} +
+
+ +
+ +
+ {JSON.stringify(getVersionResult)} +
+
+ +
+ +
+ {JSON.stringify(signTransactionResult)} +
+
+
+
+ ); +} + +export default App; diff --git a/packages/libs/ledger/example/src/legacy-transport.ts b/packages/libs/ledger/example/src/legacy-transport.ts new file mode 100644 index 0000000000..0017b7e8b6 --- /dev/null +++ b/packages/libs/ledger/example/src/legacy-transport.ts @@ -0,0 +1,57 @@ +import { Buffer } from 'buffer'; +globalThis.Buffer = Buffer; + +import type TransportWebHID from '@ledgerhq/hw-transport-webhid'; + +const LEDGER_VENDOR_ID = 0x2c97; +const KADENA_PATH = "m/44'/626'/{index}'/0/0"; + +const connect = async () => { + let devices = await navigator.hid.getDevices(); + let ledger = devices.find((d) => d.vendorId === LEDGER_VENDOR_ID); + + if (!ledger) { + await navigator.hid.requestDevice({ + filters: [{ vendorId: LEDGER_VENDOR_ID }], + }); + + devices = await navigator.hid.getDevices(); + ledger = devices.find((d) => d.vendorId === LEDGER_VENDOR_ID); + } + + if (ledger) { + const { default: TransportWebHID } = await import( + '@ledgerhq/hw-transport-webhid' + ); + const transport = await TransportWebHID.open(ledger); + return transport; + } + + throw new Error('No transport'); +}; + +export const openApp = async () => { + let transport: TransportWebHID | null = null; + + try { + transport = await connect(); + + await transport.send( + 0xe0, + 0xd8, + 0x00, + 0x00, + Buffer.from('Kadena', 'ascii'), + ); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } catch (error) { + // When the app is already open, the device returns 0x6e01 + if (!error.toString().includes('0x6e01')) { + console.error(error); + } + } finally { + if (transport) { + transport.close(); + } + } +}; diff --git a/packages/libs/ledger/example/src/main.tsx b/packages/libs/ledger/example/src/main.tsx new file mode 100644 index 0000000000..76bdb648f0 --- /dev/null +++ b/packages/libs/ledger/example/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './app.tsx'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/packages/libs/ledger/example/src/vite-env.d.ts b/packages/libs/ledger/example/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/libs/ledger/example/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/libs/ledger/example/tsconfig.json b/packages/libs/ledger/example/tsconfig.json new file mode 100644 index 0000000000..d7dc65f498 --- /dev/null +++ b/packages/libs/ledger/example/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "jsx": "react-jsx", + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/packages/libs/ledger/example/vite.config.ts b/packages/libs/ledger/example/vite.config.ts new file mode 100644 index 0000000000..4e7004ebc6 --- /dev/null +++ b/packages/libs/ledger/example/vite.config.ts @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/packages/libs/ledger/package.json b/packages/libs/ledger/package.json new file mode 100644 index 0000000000..01458063a1 --- /dev/null +++ b/packages/libs/ledger/package.json @@ -0,0 +1,35 @@ +{ + "name": "@kadena/ledger", + "version": "0.0.1", + "private": true, + "description": "", + "keywords": [], + "license": "BSD-3-Clause", + "author": "", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "unbuild", + "example": "cd example && pnpm dev" + }, + "dependencies": { + "blake2b": "^2.1.4", + "ledger-transport-hid": "^0.1.0" + }, + "devDependencies": { + "@kadena-dev/shared-config": "workspace:*", + "@types/blake2b": "^2.1.3", + "@types/w3c-web-hid": "^1.0.6", + "unbuild": "^2.0.0" + } +} diff --git a/packages/libs/ledger/src/constants.ts b/packages/libs/ledger/src/constants.ts new file mode 100644 index 0000000000..304e06af7b --- /dev/null +++ b/packages/libs/ledger/src/constants.ts @@ -0,0 +1,2 @@ +export const LEDGER_VENDOR_ID = 0x2c97; +export const KADENA_PATH = "m/44'/626'/{index}'/0/0"; diff --git a/packages/libs/ledger/src/index.ts b/packages/libs/ledger/src/index.ts new file mode 100644 index 0000000000..cb007171f0 --- /dev/null +++ b/packages/libs/ledger/src/index.ts @@ -0,0 +1,33 @@ +import { KadenaLedger } from './ledger'; +import { TransactionParams } from './transaction'; + +let ledger: KadenaLedger | null = null; + +async function getLedger(): Promise { + if (!ledger) ledger = await KadenaLedger.findDevice(); + return ledger; +} + +export async function openApp() { + const ledger = await getLedger(); + return await ledger.openApp(); +} + +export async function getPublicKey(index: number) { + const ledger = await getLedger(); + return await ledger.getPublicKey(index); +} + +export async function signTransaction( + index: number, + type: 'transfer' | 'cross-chain-transfer', + params: TransactionParams, +) { + const ledger = await getLedger(); + return await ledger.signTransaction(index, type, params); +} + +export async function getVersion() { + const ledger = await getLedger(); + return await ledger.getVersion(); +} diff --git a/packages/libs/ledger/src/ledger.ts b/packages/libs/ledger/src/ledger.ts new file mode 100644 index 0000000000..0873354612 --- /dev/null +++ b/packages/libs/ledger/src/ledger.ts @@ -0,0 +1,165 @@ +import { LedgerTransport, StatusCodes } from 'ledger-transport-hid'; +import { KADENA_PATH, LEDGER_VENDOR_ID } from './constants'; +import { TransactionParams, createTransaction } from './transaction'; +import { arrayBufferToHex, concatUint8Array, convertDecimal } from './utils'; + +const NOT_SUPPORTED_ERROR = + 'Your browser does not support connecting to hardware devices.'; + +export class KadenaLedger { + transport: LedgerTransport; + + constructor(device: HIDDevice) { + if (!KadenaLedger.isSupported()) { + throw new Error(NOT_SUPPORTED_ERROR); + } + + this.transport = new LedgerTransport(device); + } + + async openApp() { + await this.transport.send( + 0xe0, + 0xd8, + 0x00, + 0x00, + new TextEncoder().encode('Kadena'), + [StatusCodes.OK, 0x6e01], // 0x6e01 = already open + ); + // Opening the app makes the device reconnect, wait a moment for this + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + async getVersion(): Promise<{ + major: number; + minor: number; + patch: number; + appName: string; + }> { + const response = await this.transport.send( + 0x00, + 0x00, + 0x00, + 0x00, + new Uint8Array(230), + [StatusCodes.OK, 0x6e01], // 0x6e01 = already open + ); + + // App was not open, try to open it and try again + if (response.code === 0x6e01) { + await this.openApp(); + return this.getVersion(); + } + + const [major, minor, patch, ...appName] = response.data; + return { + major, + minor, + patch, + appName: new TextDecoder().decode(new Uint8Array(appName)), + }; + } + + private derivationPath(index: number) { + const path = KADENA_PATH.replace('{index}', index.toString()).split('/'); + + const derivationPath = path.reduce((acc, curr) => { + const hardened = curr.endsWith("'"); + const value = parseInt(curr, 10); + if (!isNaN(value)) + return acc.concat(hardened ? 0x80000000 + value : value); + return acc; + }, [] as number[]); + + const data = new DataView(new ArrayBuffer(1 + derivationPath.length * 4)); + data.setUint8(0, derivationPath.length); + for (let i = 0; i < derivationPath.length; i++) { + data.setUint32(1 + i * 4, derivationPath[i], true); + } + return data.buffer; + } + + async getPublicKey(index: number, prompt: boolean = false): Promise { + const response = await this.transport.send( + 0x00, + prompt ? 0x01 : 0x02, + 0x00, + 0x00, + this.derivationPath(index), + // status 0x6e01 means app is not open + [StatusCodes.OK, 0x6e01], + ); + + // App was not open, try to open it and try again + if (response.code === 0x6e01) { + await this.openApp(); + return this.getPublicKey(index, prompt); + } + + return arrayBufferToHex(response.data.slice(1)); + } + + async signTransaction( + index: number, + type: 'transfer' | 'cross-chain-transfer', + params: TransactionParams, + ) { + const textEncode = (text: string) => { + const encoder = new TextEncoder(); + const buffer = encoder.encode(text); + return new Uint8Array([buffer.byteLength, ...buffer]); + }; + + const payload = concatUint8Array( + this.derivationPath(index), + new Uint8Array([type === 'transfer' ? 0x01 : 0x02]), + textEncode(params.recipient), + textEncode(params.recipientChainId), + textEncode(params.networkId), + textEncode(convertDecimal(params.amount)), + textEncode(params.namespace_), + textEncode(params.module_), + textEncode(params.gasPrice), + textEncode(params.gasLimit), + textEncode(params.creationTime), + textEncode(params.chainId), + textEncode(params.nonce), + textEncode(params.ttl), + ); + const response = await this.transport.send(0x00, 0x10, 0x00, 0x00, payload); + const sig = arrayBufferToHex(response.data.slice(0, 64)); + const pubKey = arrayBufferToHex(response.data.slice(64, 96)); + const { cmd, hash } = createTransaction(params, pubKey); + return { + pubKey, + command: { + cmd, + hash, + sigs: [{ sig: sig }], + }, + }; + } + + static async findDevice() { + if (!KadenaLedger.isSupported()) { + throw new Error(NOT_SUPPORTED_ERROR); + } + try { + let devices = await navigator.hid.getDevices(); + const exists = devices.find((d) => d.vendorId === LEDGER_VENDOR_ID); + if (exists) { + return new KadenaLedger(exists); + } + const [ledger] = await navigator.hid.requestDevice({ + filters: [{ vendorId: LEDGER_VENDOR_ID }], + }); + return new KadenaLedger(ledger); + } catch (error) { + throw new Error('Failed to find and pair with ledger device'); + } + } + + static isSupported() { + return 'hid' in navigator; + } +} diff --git a/packages/libs/ledger/src/transaction.ts b/packages/libs/ledger/src/transaction.ts new file mode 100644 index 0000000000..a5cb210f47 --- /dev/null +++ b/packages/libs/ledger/src/transaction.ts @@ -0,0 +1,172 @@ +import blake2b from 'blake2b'; + +export type TransactionParams = { + type: 'transfer' | 'cross-chain-transfer'; + recipient: string; + recipientChainId: string; + networkId: string; + amount: number; + namespace_: string; + module_: string; + gasPrice: string; + gasLimit: string; + creationTime: string; + chainId: string; + nonce: string; + ttl: string; +}; + +/*** + * The ledger Kadena app by default does not just accept a hash and signs it. + * It accepts all parameters and builds the transaction itself + * hashes it, and provides the signature for that. + * + * This means to use the signature you need an exact match of the transaction. + * This is why @kadena/client can not be used, and we create the transaction here. + */ +export function createTransaction( + { + type, + recipient, + recipientChainId, + networkId, + amount, + namespace_, + module_, + gasPrice, + gasLimit, + creationTime, + chainId, + nonce, + ttl, + }: TransactionParams, + pubKey: string, +) { + // Build the JSON, exactly like the Ledger app + var cmd = '{"networkId":"' + networkId + '"'; + if (type === 'transfer') { + cmd += ',"payload":{"exec":{"data":{},"code":"'; + if (namespace_ === '') { + cmd += '(coin.transfer'; + } else { + cmd += '(' + namespace_ + '.' + module_ + '.transfer'; + } + cmd += ' \\"k:' + pubKey + '\\"'; + cmd += ' \\"k:' + recipient + '\\"'; + cmd += ' ' + amount + ')"}}'; + cmd += ',"signers":[{"pubKey":"' + pubKey + '"'; + cmd += + ',"clist":[{"args":["k:' + + pubKey + + '","k:' + + recipient + + '",' + + amount + + ']'; + if (namespace_ === '') { + cmd += ',"name":"coin.TRANSFER"},{"args":[],"name":"coin.GAS"}]}]'; + } else { + cmd += + ',"name":"' + + namespace_ + + '.' + + module_ + + '.TRANSFER"},{"args":[],"name":"coin.GAS"}]}]'; + } + } else if (type === 'cross-chain-transfer') { + cmd += ',"payload":{"exec":{"data":{'; + cmd += '"ks":{"pred":"keys-all","keys":["' + recipient + '"]}'; + cmd += '},"code":"'; + if (namespace_ === '') { + cmd += '(coin.transfer-create'; + } else { + cmd += '(' + namespace_ + '.' + module_ + '.transfer-create'; + } + cmd += ' \\"k:' + pubKey + '\\"'; + cmd += ' \\"k:' + recipient + '\\"'; + cmd += ' (read-keyset \\"ks\\")'; + cmd += ' ' + amount + ')"}}'; + cmd += ',"signers":[{"pubKey":"' + pubKey + '"'; + cmd += + ',"clist":[{"args":["k:' + + pubKey + + '","k:' + + recipient + + '",' + + amount + + ']'; + if (namespace_ === '') { + cmd += ',"name":"coin.TRANSFER"},{"args":[],"name":"coin.GAS"}]}]'; + } else { + cmd += + ',"name":"' + + namespace_ + + '.' + + module_ + + '.TRANSFER"},{"args":[],"name":"coin.GAS"}]}]'; + } + } else { + cmd += ',"payload":{"exec":{"data":{'; + cmd += '"ks":{"pred":"keys-all","keys":["' + recipient + '"]}'; + cmd += '},"code":"'; + if (namespace_ === '') { + cmd += '(coin.transfer-crosschain'; + } else { + cmd += '(' + namespace_ + '.' + module_ + '.transfer-crosschain'; + } + cmd += ' \\"k:' + pubKey + '\\"'; + cmd += ' \\"k:' + recipient + '\\"'; + cmd += ' (read-keyset \\"ks\\")'; + cmd += ' \\"' + recipientChainId + '\\"'; + cmd += ' ' + amount + ')"}}'; + cmd += ',"signers":[{"pubKey":"' + pubKey + '"'; + cmd += + ',"clist":[{"args":["k:' + + pubKey + + '","k:' + + recipient + + '",' + + amount + + ',"' + + recipientChainId + + '"]'; + if (namespace_ === '') { + cmd += ',"name":"coin.TRANSFER_XCHAIN"},{"args":[],"name":"coin.GAS"}]}]'; + } else { + cmd += + ',"name":"' + + namespace_ + + '.' + + module_ + + '.TRANSFER_XCHAIN"},{"args":[],"name":"coin.GAS"}]}]'; + } + } + cmd += ',"meta":{"creationTime":' + creationTime.toString(); + cmd += + ',"ttl":' + + ttl + + ',"gasLimit":' + + gasLimit + + ',"chainId":"' + + chainId + + '"'; + cmd += + ',"gasPrice":' + + gasPrice + + ',"sender":"k:' + + pubKey + + '"},"nonce":"' + + nonce + + '"}'; + + const hash_bytes = blake2b(32).update(new TextEncoder().encode(cmd)).digest(); + const hash = btoa(String.fromCharCode.apply(null, [...hash_bytes])) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); // base64url encode, remove padding + + return { + cmd, + hash, + }; +} diff --git a/packages/libs/ledger/src/utils.ts b/packages/libs/ledger/src/utils.ts new file mode 100644 index 0000000000..9e21d5f483 --- /dev/null +++ b/packages/libs/ledger/src/utils.ts @@ -0,0 +1,34 @@ +export function concatUint8Array(...buffers: ArrayBuffer[]): Uint8Array { + const totalLength = buffers.reduce( + (sum, buffer) => sum + buffer.byteLength, + 0, + ); + const result = new Uint8Array(totalLength); + let offset = 0; + + for (const buffer of buffers) { + result.set(new Uint8Array(buffer), offset); + offset += buffer.byteLength; + } + + return result; +} + +export function arrayBufferToHex(buffer: ArrayBuffer) { + return [...new Uint8Array(buffer)] + .map((x) => x.toString(16).padStart(2, '0')) + .join(''); +} + +export function convertDecimal(decimalNumber: number): string { + const decimalString = decimalNumber.toString(); + + if (decimalString.includes('.')) { + return decimalString; + } + if (decimalNumber / Math.floor(decimalNumber) === 1) { + return decimalString + '.0'; + } + + return decimalString; +} diff --git a/packages/libs/ledger/tsconfig.json b/packages/libs/ledger/tsconfig.json new file mode 100644 index 0000000000..5b83cfec59 --- /dev/null +++ b/packages/libs/ledger/tsconfig.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "declaration": true, + "sourceMap": true, + "declarationMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strict": true, + "useUnknownInCatchVariables": false, + "esModuleInterop": true, + "noEmitOnError": false, + "allowUnreachableCode": false, + "module": "commonjs", + "target": "es2019", + "lib": ["ES2020", "DOM"], + "skipLibCheck": true + } +}