Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions packages/hw-wallets/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,85 @@
# @enkryptcom/hw-wallets

## v0.0.12

## Hardware wallet manager for enkrypt

### Getting started

#### Minimum Node version

`node v20`

#### Installation

NPM: `npm install @enkryptcom/hw-wallets @enkryptcom/types`
Yarn: `yarn add @enkryptcom/hw-wallets @enkryptcom/types`
PNPM `pnpm add @enkryptcom/hw-wallets @enkryptcom/types`

### How to use

1. Create an instance of `HWwalletManager`

```
import HWwalletManager from @enkryptcom/hw-wallets

const hwManager = new HWwalletManager()
```

2. Call methods within class, passing network names and providers. Class automatically handles which wallet to use.

### API

#### `getAddress(options: getAddressRequest): Promise<AddressResponse>`

Returns wallet address based off of the path provided in the `getAddressRequest`.

#### `signPersonalMessage(options: SignMessageRequest): Promise<string>`

Signs personal message.

#### `signTransaction(options: SignTransactionRequest): Promise<string>`

Returns an RPC sign you can then add to a transaction object.

#### `getSupportedPaths(options: isConnectedRequest): Promise<PathType[]>`

Returns supported paths based on options provided.

#### `isNetworkSupported(networkName: NetworkNames): boolean`

Checks network name support.

#### `isConnected(options: isConnectedRequest): Promise<boolean>`

Checks connection status.

#### `close(): Promise<void>`

Closes all HW wallet connections

### Types

`NetworkNames`: https://github.com/enkryptcom/enKrypt/blob/main/packages/types/src/networks.ts#L1
`getAddressRequest`: https://github.com/enkryptcom/enKrypt/blob/main/packages/hw-wallets/src/types.ts#L74
`AddressResponse`: https://github.com/enkryptcom/enKrypt/blob/main/packages/hw-wallets/src/types.ts#L31
`SignMessageRequest`: https://github.com/enkryptcom/enKrypt/blob/main/packages/hw-wallets/src/types.ts#L54
`SignTransactionRequest`: https://github.com/enkryptcom/enKrypt/blob/main/packages/hw-wallets/src/types.ts#L65
`isConnectedRequest`: https://github.com/enkryptcom/enKrypt/blob/main/packages/hw-wallets/src/types.ts#L78

### Adding more paths

Navigate to `src/configs.ts` and add your new derivation path at the bottom.
Make sure to follow the configuration in that file.
Import path in the corresponding `Trezor` provider `config.ts`.
See `src/trezor/ethereum/configs.ts` for example.

NOTE: `Ledger` can't have any paths as each paths are defined by the corresponding app.

### Notes

Connection request to hardware wallet actually happens on `getAddress()` request.

#### For Vue devs

`ref/reactive` will mess with how Vue compiles these classes because of how Vue utilizes proxies. If you want to store an instance into a ref or reactive, use [`makeRaw`](https://vuejs.org/api/reactivity-advanced#markraw).
5 changes: 3 additions & 2 deletions packages/hw-wallets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"test": "vitest run"
},
"engines": {
"node": ">=14.15.0"
"node": ">=20"
},
Comment on lines 21 to 23
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Node 20 compatibility and transport package versions

echo "=== Checking package.json for transport dependencies ==="
cat packages/hw-wallets/package.json | grep -A 20 '"dependencies"'

echo -e "\n=== Checking for Node version config files ==="
cd packages/hw-wallets
for file in .nvmrc .node-version; do
  if [ -f "$file" ]; then
    echo "Found $file:"; cat "$file"
  fi
done

cd ../..
echo -e "\n=== Checking root .nvmrc or node version config ==="
for file in .nvmrc .node-version; do
  if [ -f "$file" ]; then
    echo "Found $file:"; cat "$file"
  fi
done

echo -e "\n=== Checking for CHANGELOG or migration docs ==="
fd -i changelog -i migration -i "release.*notes" packages/hw-wallets/ || echo "No changelog found in hw-wallets"

echo -e "\n=== Checking if Node version requirement is documented in README ==="
if [ -f "packages/hw-wallets/README.md" ]; then
  echo "Checking README.md:"; grep -i "node" packages/hw-wallets/README.md || echo "No Node version mentioned"
fi

echo -e "\n=== Checking git for any Node version changes ==="
git log --oneline --all -S ">=20" -- "packages/hw-wallets/package.json" 2>/dev/null | head -5 || echo "Git history not available"

echo -e "\n=== Checking for any CI/CD config mentioning Node version ==="
fd "\.github|\.gitlab|\.circleci|\.travis" -t d || echo "No CI configs found"

Repository: enkryptcom/enKrypt

Length of output: 1532


🏁 Script executed:

#!/bin/bash
# Check if hw-wallets .nvmrc is the issue and confirm package version compatibility

echo "=== Checking Node versions represented by .nvmrc files ==="
echo "lts/fermium = Node 14 (EOL)"
echo "v22.18.0 = Node 22 LTS"

echo -e "\n=== Verifying hw-wallets/.nvmrc content ==="
cat packages/hw-wallets/.nvmrc

echo -e "\n=== Checking @ledgerhq peer dependencies in lock file ==="
if [ -f "pnpm-lock.yaml" ]; then
  echo "Checking pnpm-lock.yaml for @ledgerhq transport conflicts..."
  rg -A 3 "@ledgerhq/hw-transport" pnpm-lock.yaml | head -40
elif [ -f "package-lock.json" ]; then
  echo "Checking package-lock.json..."
  grep -A 5 "@ledgerhq/hw-transport" package-lock.json | head -30
fi

echo -e "\n=== Checking if transport package versions have peer dependency constraints ==="
# Search for package metadata that might indicate version conflicts
rg "peerDependencies|engines" packages/hw-wallets/package.json

Repository: enkryptcom/enKrypt

Length of output: 382


Update hw-wallets/.nvmrc to align with Node >=20 requirement.

The package.json correctly specifies engines.node: ">=20" and this is documented in the README. However, packages/hw-wallets/.nvmrc contains lts/fermium (Node 14), which creates a developer experience issue—tools like nvm will override to Node 14 despite the package requiring Node >=20. Update this file to v20 or higher to match the enforced constraint.

🤖 Prompt for AI Agents
In packages/hw-wallets/.nvmrc (related to package.json lines 28-30) the .nvmrc
currently pins lts/fermium (Node 14) which conflicts with package.json's
engines.node ">=20"; update packages/hw-wallets/.nvmrc to "v20" (or a newer vX
where X>=20) so tools like nvm will use Node 20+, ensuring it matches the
enforced constraint and README.

"devDependencies": {
"@types/node": "^22.15.24",
Expand Down Expand Up @@ -56,8 +56,9 @@
"@ledgerhq/hw-app-eth": "^6.45.5",
"@ledgerhq/hw-app-solana": "^7.4.1",
"@ledgerhq/hw-transport": "^6.31.5",
"@ledgerhq/hw-transport-web-ble": "^6.29.5",
"@ledgerhq/hw-transport-webusb": "^6.29.5",
"@ledgerhq/live-common": "^34.20.0",
"@ledgerhq/live-common": "34.20.0",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
"@polkadot/types": "^16.1.1",
"@polkadot/util": "^13.5.1",
"@trezor/connect": "^9.5.5",
Expand Down
141 changes: 141 additions & 0 deletions packages/hw-wallets/src/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ const bip44Paths = {
basePath: "m/44'/137'/0'/0",
label: "Rootstock",
},
rootstockTestnet: {
path: "m/44'/37310'/0'/0/{index}",
basePath: "m/44'/37310'/0'/0",
label: 'Rootstock Testnet'
},
ethereumClassic: {
path: "m/44'/61'/0'/0/{index}",
basePath: "m/44'/61'/0'/0",
Expand Down Expand Up @@ -116,5 +121,141 @@ const bip44Paths = {
basePath: "m/44'/3'/0'/0",
label: "Dogecoin",
},
// Additional paths from MyEtherWallet
poaNetwork: {
path: "m/44'/60'/0'/0/{index}",
basePath: "m/44'/60'/0'/0",
label: 'POA network'
},
expanse: {
path: "m/44'/40'/0'/0/{index}",
basePath: "m/44'/40'/0'/0",
label: 'Expanse'
},
ubiq: {
path: "m/44'/108'/0'/0/{index}",
basePath: "m/44'/108'/0'/0",
label: 'Ubiq'
},
ellaism: {
path: "m/44'/163'/0'/0/{index}",
basePath: "m/44'/163'/0'/0",
label: 'Ellaism'
},
etherGem: {
path: "m/44'/1987'/0'/0/{index}",
basePath: "m/44'/1987'/0'/0",
label: 'EtherGem'
},
callisto: {
path: "m/44'/820'/0'/0/{index}",
basePath: "m/44'/820'/0'/0",
label: 'Callisto'
},
ethereumSocial: {
path: "m/44'/1128'/0'/0/{index}",
basePath: "m/44'/1128'/0'/0",
label: 'Ethereum Social'
},
musicoin: {
path: "m/44'/184'/0'/0/{index}",
basePath: "m/44'/184'/0'/0",
label: 'Musicoin'
},
goChain: {
path: "m/44'/6060'/0'/0/{index}",
basePath: "m/44'/6060'/0'/0",
label: 'GoChain'
},
eosClassic: {
path: "m/44'/2018'/0'/0/{index}",
basePath: "m/44'/2018'/0'/0",
label: 'EOS Classic'
},
akroma: {
path: "m/44'/200625'/0'/0/{index}",
basePath: "m/44'/200625'/0'/0",
label: 'Akroma'
},
etherSocialNetwork: {
path: "m/44'/31102'/0'/0/{index}",
basePath: "m/44'/31102'/0'/0",
label: 'EtherSocial Network'
},
pirl: {
path: "m/44'/164'/0'/0/{index}",
basePath: "m/44'/164'/0'/0",
label: 'PIRL'
},
ether1: {
path: "m/44'/1313114'/0'/0/{index}",
basePath: "m/44'/1313114'/0'/0",
label: 'Ether-1'
},
atheios: {
path: "m/44'/1620'/0'/0/{index}",
basePath: "m/44'/1620'/0'/0",
label: 'Atheios'
},
tomoChain: {
path: "m/44'/889'/0'/0/{index}",
basePath: "m/44'/889'/0'/0",
label: 'TomoChain'
},
mixBlockchain: {
path: "m/44'/76'/0'/0/{index}",
basePath: "m/44'/76'/0'/0",
label: 'Mix Blockchain'
},
iolite: {
path: "m/44'/1171337'/0'/0/{index}",
basePath: "m/44'/1171337'/0'/0",
label: 'Iolite'
},
thundercore: {
path: "m/44'/1001'/0'/0/{index}",
basePath: "m/44'/1001'/0'/0",
label: 'ThunderCore'
},
solidum: {
path: "m/44'/997'/0'/0/{index}",
basePath: "m/44'/997'/0'/0",
label: 'Solidum'
},
metadium: {
path: "m/44'/916'/0'/0/{index}",
basePath: "m/44'/916'/0'/0",
label: 'Metadium'
},
reoscChain: {
path: "m/44'/2894'/0'/0/{index}",
basePath: "m/44'/2894'/0'/0",
label: 'REOSC'
},
dexon: {
path: "m/44'/237'/0'/0/{index}",
basePath: "m/44'/237'/0'/0",
label: 'DEXON Network'
},
lightstreamsNetwork: {
path: "m/44'/60'/0'/{index}",
basePath: "m/44'/60'/0'",
label: 'Lightstreams Network'
},
mintmeComCoin: {
path: "m/44'/227'/0'/0/{index}",
basePath: "m/44'/227'/0'/0",
label: 'MintMe.com Coin'
},
ethercore: {
path: "m/44'/466'/0'/0/{index}",
basePath: "m/44'/466'/0'/0",
label: 'EtherCore'
},
binanceChain: {
path: "m/44'/714'/{index}",
basePath: "m/44'/714'",
label: 'Binance Chain'
},
Comment thread
gamalielhere marked this conversation as resolved.
};
export { walletConfigs, MessengerName, ledgerAppNames, bip44Paths };
10 changes: 10 additions & 0 deletions packages/hw-wallets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,13 @@ class HWwalletManager {
export default HWwalletManager;

export { ledgerAppNames };

export type {
AddressResponse,
getAddressRequest,
HWWalletProvider,
isConnectedRequest,
PathType,
SignMessageRequest,
SignTransactionRequest,
}
37 changes: 26 additions & 11 deletions packages/hw-wallets/src/ledger/ethereum/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type Transport from "@ledgerhq/hw-transport";
import webUsbTransport from "@ledgerhq/hw-transport-webusb";
import bleTransport from '@ledgerhq/hw-transport-web-ble';
import ledgerService from "@ledgerhq/hw-app-eth/lib/services/ledger";
import { HWwalletCapabilities, NetworkNames } from "@enkryptcom/types";
import EthApp from "@ledgerhq/hw-app-eth";
Expand Down Expand Up @@ -34,17 +35,31 @@ class LedgerEthereum implements HWWalletProvider {

async init(): Promise<boolean> {
if (!this.transport) {
const support = await webUsbTransport.isSupported();
if (support) {
this.transport = await webUsbTransport.openConnected().then((res) => {
if (!res) return webUsbTransport.create();
return res;
});
} else {
return Promise.reject(
new Error("ledger-ethereum: webusb is not supported"),
);
try {
const BLEsupport = await bleTransport.isSupported();
if (BLEsupport) {
const transport = await bleTransport.create();
transport.on("disconnect", () => { // connection wasnt succesful
this.transport = null;
})
this.transport = transport;
}

} catch {
const support = await webUsbTransport.isSupported();
if (support) {
this.transport = await webUsbTransport.openConnected().then((res) => {
if (!res) return webUsbTransport.create();
return res;
});

} else {
return Promise.reject(
new Error("ledger-ethereum: webusb is not supported"),
);
}
}
Comment thread
gamalielhere marked this conversation as resolved.
Outdated

}
return true;
}
Expand Down Expand Up @@ -142,7 +157,7 @@ class LedgerEthereum implements HWWalletProvider {

close(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return this.transport.close().catch(() => {});
return this.transport.close().catch(() => { });
}
Comment on lines 171 to 174
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Prevent close() from crashing after failed init

If transport setup fails and this.transport stays null, calling close() triggers a TypeError before the catch runs. That makes cleanup impossible after an init failure.

Guard the call (same pattern as other Ledger classes):

   close(): Promise<void> {
-    // eslint-disable-next-line @typescript-eslint/no-empty-function
-    return this.transport.close().catch(() => { });
+    if (!this.transport) return Promise.resolve();
+    return this.transport.close().catch(() => undefined);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
close(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return this.transport.close().catch(() => {});
return this.transport.close().catch(() => { });
}
close(): Promise<void> {
if (!this.transport) return Promise.resolve();
return this.transport.close().catch(() => undefined);
}
🤖 Prompt for AI Agents
In packages/hw-wallets/src/ledger/ethereum/index.ts around lines 155 to 158,
calling this.transport.close() can throw a TypeError when this.transport is null
after a failed init; update close() to first check if this.transport is non-null
and only call close().catch(...) when present, otherwise return a resolved
Promise<void> (matching the guard pattern used in other Ledger classes) so
cleanup never crashes when transport was not created.


isConnected(networkName: NetworkNames): Promise<boolean> {
Expand Down
39 changes: 37 additions & 2 deletions packages/hw-wallets/src/trezor/ethereum/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NetworkNames } from "@enkryptcom/types";
import { bip44Paths } from "../../configs";

const DEFAULT_PATHS = [bip44Paths.ethereum];
const supportedPaths = {
const networkBasedSupportedPaths = {
[NetworkNames.Ethereum]: DEFAULT_PATHS,
[NetworkNames.Matic]: DEFAULT_PATHS,
[NetworkNames.Avalanche]: DEFAULT_PATHS,
Expand All @@ -28,4 +28,39 @@ const supportedPaths = {
[NetworkNames.Telos]: DEFAULT_PATHS,
[NetworkNames.Blast]: DEFAULT_PATHS,
};
export { supportedPaths };
const ALL_SUPPORTED_PATHS = [
bip44Paths.ethereum,
bip44Paths.ethereumTestnet,
bip44Paths.ethereumClassic,
bip44Paths.rootstock,
Object.assign({}, bip44Paths.ethereumLedger, { label: "Ethereum - Ledger" }), // specify ledger
bip44Paths.ethereumLedgerLive,
bip44Paths.poaNetwork,
bip44Paths.expanse,
bip44Paths.ubiq,
bip44Paths.ellaism,
bip44Paths.etherGem,
bip44Paths.callisto,
bip44Paths.ethereumSocial,
bip44Paths.musicoin,
bip44Paths.goChain,
bip44Paths.eosClassic,
bip44Paths.akroma,
bip44Paths.etherSocialNetwork,
bip44Paths.pirl,
bip44Paths.ether1,
bip44Paths.atheios,
bip44Paths.tomoChain,
bip44Paths.mixBlockchain,
bip44Paths.iolite,
bip44Paths.thundercore,
bip44Paths.solidum,
bip44Paths.metadium,
bip44Paths.reoscChain,
bip44Paths.dexon,
bip44Paths.lightstreamsNetwork,
bip44Paths.mintmeComCoin,
bip44Paths.ethercore,
bip44Paths.binanceChain,
];
export { networkBasedSupportedPaths, ALL_SUPPORTED_PATHS };
Loading
Loading