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 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
+
+
+
openApp().then(() => setOpenAppResult('success'))}
+ >
+ Open App
+
+
{openAppResult}
+
+
+
+
+ getPublicKey(0).then((x) => setGetPublicKeyResult(x))
+ }
+ >
+ Get public key
+
+
+ {JSON.stringify(getPublicKeyResult)}
+
+
+
+
+
getVersion().then((x) => setGetVersionResult(x))}
+ >
+ Get version
+
+
+ {JSON.stringify(getVersionResult)}
+
+
+
+
+
+ signTransaction(0, 'transfer', {
+ recipient:
+ '2017fee3fb15cfe840e5ed34bf101cc7d5579ffdd20dea09e32fd77c1757f946',
+ recipientChainId: '0',
+ networkId: 'testnet04',
+ amount: 0.1,
+ namespace_: '',
+ module_: '',
+ gasPrice: '1.0e-6',
+ gasLimit: '2300',
+ creationTime: '1723017869',
+ chainId: '0',
+ nonce: '',
+ ttl: '600',
+ type: 'transfer',
+ }).then((x) => setSignTransactionResult(x))
+ }
+ >
+ Sign transaction
+
+
+ {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
+ }
+}