@swapkit/wallet-hardware 4.9.8 → 4.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { hex } from "@scure/base";
2
- import type { UTXOChain } from "@swapkit/helpers";
2
+ import { SwapKitError, type UTXOChain } from "@swapkit/helpers";
3
3
  import type { UTXOType } from "@swapkit/toolboxes/utxo";
4
4
  import type { Transaction } from "@swapkit/utxo-signer";
5
5
 
@@ -7,15 +7,15 @@ import type { Transaction } from "@swapkit/utxo-signer";
7
7
  * Extract per-input metadata from a V3 PSBT in the shape the legacy
8
8
  * `@ledgerhq/hw-app-btc.createPaymentTransaction` adapter expects.
9
9
  *
10
- * For segwit inputs the SwapKit V3 API populates `witnessUtxo`; for legacy
11
- * (BCH/DOGE/DASH) it populates `nonWitnessUtxo` with the full prior-tx bytes.
12
- * We re-encode the parsed `nonWitnessUtxo` back to hex via `RawTx.encode` so
13
- * `btcApp.splitTransaction(hex)` can consume it.
10
+ * The legacy Ledger signer still needs the full previous tx hex for
11
+ * `btcApp.splitTransaction(hex)`. Some V3 PSBTs only include `witnessUtxo`,
12
+ * so we fetch the previous raw tx when `nonWitnessUtxo` is absent.
14
13
  *
15
14
  * Single-address account assumption: all inputs share our derivation path.
16
15
  */
17
- export async function extractInputsFromPsbt(tx: Transaction): Promise<UTXOType[]> {
16
+ export async function extractInputsFromPsbt(tx: Transaction, chain: UTXOChain): Promise<UTXOType[]> {
18
17
  const { RawTx } = await import("@swapkit/utxo-signer");
18
+ const { getUtxoApi } = await import("@swapkit/toolboxes/utxo");
19
19
  const inputs: UTXOType[] = [];
20
20
 
21
21
  for (let i = 0; i < tx.inputsLength; i++) {
@@ -25,31 +25,53 @@ export async function extractInputsFromPsbt(tx: Transaction): Promise<UTXOType[]
25
25
  throw new Error(`PSBT input ${i} is missing txid/index`);
26
26
  }
27
27
 
28
- const txHex = input.nonWitnessUtxo ? hex.encode(RawTx.encode(input.nonWitnessUtxo)) : "";
28
+ const txid = hex.encode(input.txid);
29
+ const txHex = input.nonWitnessUtxo
30
+ ? hex.encode(RawTx.encode(input.nonWitnessUtxo))
31
+ : await getUtxoApi(chain).getRawTx(txid);
32
+ if (!txHex) {
33
+ throw new SwapKitError("wallet_ledger_invalid_params", {
34
+ chain,
35
+ inputIndex: i,
36
+ reason: "Unable to resolve previous transaction hex for Ledger signing",
37
+ txid,
38
+ });
39
+ }
29
40
  const witnessUtxo = input.witnessUtxo
30
41
  ? { script: input.witnessUtxo.script, value: Number(input.witnessUtxo.amount) }
31
42
  : undefined;
43
+ const nonWitnessPrevout = input.index !== undefined ? input.nonWitnessUtxo?.outputs?.[input.index] : undefined;
44
+ const value = witnessUtxo?.value ?? (nonWitnessPrevout ? Number(nonWitnessPrevout.amount) : 0);
32
45
 
33
- inputs.push({
34
- hash: hex.encode(input.txid),
35
- index: input.index,
36
- txHex,
37
- value: witnessUtxo?.value ?? 0,
38
- witnessUtxo,
39
- } as UTXOType);
46
+ inputs.push({ hash: txid, index: input.index, txHex, value, witnessUtxo } as UTXOType);
40
47
  }
41
48
 
42
49
  return inputs;
43
50
  }
44
51
 
52
+ export async function signLegacyPsbtTransaction({
53
+ legacyClient,
54
+ chain,
55
+ tx,
56
+ }: {
57
+ legacyClient: { signTransaction: (tx: Transaction, inputUtxos: UTXOType[]) => Promise<string> };
58
+ chain: UTXOChain;
59
+ tx: Transaction;
60
+ }): Promise<string> {
61
+ const inputUtxos = await extractInputsFromPsbt(tx, chain);
62
+ return legacyClient.signTransaction(tx, inputUtxos);
63
+ }
64
+
45
65
  /**
46
66
  * Build a toolbox-compatible signer from the existing legacy Ledger UTXO
47
- * client. The toolbox synthesizes `signAndBroadcastTransaction` on top of
48
- * `signer.signTransaction(tx) Transaction`.
67
+ * client. Callers that need sign-and-broadcast should broadcast the raw hex
68
+ * from `signLegacyPsbtTransaction` directly; the legacy Ledger app returns an
69
+ * already-finalized transaction that should not be passed back to toolbox
70
+ * finalization.
49
71
  */
50
72
  export function createLegacyPsbtSigner({
51
73
  legacyClient,
52
- chain: _chain,
74
+ chain,
53
75
  address,
54
76
  }: {
55
77
  legacyClient: { signTransaction: (tx: Transaction, inputUtxos: UTXOType[]) => Promise<string> };
@@ -59,8 +81,7 @@ export function createLegacyPsbtSigner({
59
81
  return {
60
82
  getAddress: async () => address,
61
83
  signTransaction: async (tx: Transaction): Promise<Transaction> => {
62
- const inputUtxos = await extractInputsFromPsbt(tx);
63
- const signedTxHex = await legacyClient.signTransaction(tx, inputUtxos);
84
+ const signedTxHex = await signLegacyPsbtTransaction({ chain, legacyClient, tx });
64
85
 
65
86
  const { Transaction: TxClass } = await import("@swapkit/utxo-signer");
66
87
  // `Transaction.fromRaw` parses a serialised tx (no PSBT envelope) — exactly
@@ -10,16 +10,15 @@ import {
10
10
  getRPCUrl,
11
11
  NetworkDerivationPath,
12
12
  SwapKitError,
13
- THORConfig,
14
13
  type UTXOChain,
15
14
  WalletOption,
16
15
  } from "@swapkit/helpers";
17
- import type { ThorchainDepositParams } from "@swapkit/toolboxes/cosmos";
18
16
  import {
19
17
  addInputsAndOutputs,
20
18
  assertDerivationIndex,
21
19
  compileMemo,
22
20
  createHDWalletHelpers,
21
+ getNetworkForChain,
23
22
  getUTXOAccountIndexFromPath,
24
23
  getUTXOAccountPath,
25
24
  getUTXOAddressPath,
@@ -28,7 +27,7 @@ import {
28
27
  type UTXOForMultiAddressTransfer,
29
28
  } from "@swapkit/toolboxes/utxo";
30
29
  import type { Transaction } from "@swapkit/utxo-signer";
31
- import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core";
30
+ import { createWallet, getWalletSupportedChains, type HardwareExtendedPublicKeyInfo } from "@swapkit/wallet-core";
32
31
  import { getLedgerAddress, getLedgerClient } from "./helpers";
33
32
 
34
33
  /**
@@ -39,21 +38,21 @@ import { getLedgerAddress, getLedgerClient } from "./helpers";
39
38
  * client and will NOT recreate it on `forceReconnect`. When omitted, the
40
39
  * default browser flow (WebHID / WebUSB via `navigator.usb`) is used.
41
40
  */
42
- export type ConnectLedgerOptions = { transport?: Transport };
41
+ export type ConnectLedgerOptions = { address?: string; transport?: Transport };
43
42
 
44
43
  export const ledgerWallet = createWallet({
45
44
  connect: ({ addChain, supportedChains, walletType }) =>
46
45
  async function connectLedger(
47
46
  chains: Chain[],
48
47
  derivationPath?: DerivationPathArray,
49
- { transport }: ConnectLedgerOptions = {},
48
+ { address, transport }: ConnectLedgerOptions = {},
50
49
  ) {
51
50
  const [chain] = filterSupportedChains({ chains, supportedChains, walletType });
52
51
 
53
52
  if (!chain) return false;
54
53
 
55
54
  const resolvedPath = derivationPath ?? (NetworkDerivationPath[chain] as DerivationPathArray | undefined);
56
- const walletMethods = await getWalletMethods({ chain, derivationPath: resolvedPath, transport });
55
+ const walletMethods = await getWalletMethods({ address, chain, derivationPath: resolvedPath, transport });
57
56
 
58
57
  addChain({ ...walletMethods, chain, walletType: WalletOption.LEDGER });
59
58
 
@@ -80,11 +79,12 @@ export const ledgerWallet = createWallet({
80
79
  [Chain.Polygon]: true,
81
80
  [Chain.Ripple]: true,
82
81
  [Chain.Sui]: true,
82
+ [Chain.THORChain]: true,
83
83
  [Chain.Tron]: true,
84
84
  [Chain.XLayer]: true,
85
85
  // ZEC: still on bespoke signPCZT path
86
- // THORChain: needs signAmino added to THORChainLedger (V3 plan PR)
87
86
  },
87
+ getExtendedPublicKey: getLedgerExtendedPublicKey,
88
88
  name: "connectLedger",
89
89
  supportedChains: [
90
90
  Chain.Arbitrum,
@@ -126,33 +126,30 @@ function reduceMemo(memo?: string, affiliateAddress = "t") {
126
126
  return removedAffiliate?.substring(0, removedAffiliate.lastIndexOf(":"));
127
127
  }
128
128
 
129
- function recursivelyOrderKeys(unordered: any) {
130
- // If it's an array - recursively order any
131
- // dictionary items within the array
132
- if (Array.isArray(unordered)) {
133
- unordered.forEach((item, index) => {
134
- unordered[index] = recursivelyOrderKeys(item);
135
- });
136
- return unordered;
129
+ export async function getLedgerExtendedPublicKey(
130
+ chain: Chain,
131
+ derivationPath?: DerivationPathArray,
132
+ { accountIndex }: { accountIndex?: number } = {},
133
+ ): Promise<HardwareExtendedPublicKeyInfo | undefined> {
134
+ if (![Chain.BitcoinCash, Chain.Bitcoin, Chain.Dash, Chain.Dogecoin, Chain.Litecoin, Chain.Zcash].includes(chain)) {
135
+ throw new SwapKitError("wallet_chain_not_supported", { chain, wallet: WalletOption.LEDGER });
137
136
  }
138
137
 
139
- // If it's an object - let's order the keys
140
- if (typeof unordered !== "object") return unordered;
141
- const ordered: any = {};
142
- const sortedKeys = Object.keys(unordered).sort();
138
+ const utxoChain = chain as UTXOChain;
139
+ const signer = await getLedgerClient({ chain: utxoChain, derivationPath });
140
+ if (!signer.getExtendedPublicKey) return undefined;
143
141
 
144
- for (const key of sortedKeys) {
145
- ordered[key] = recursivelyOrderKeys(unordered[key]);
146
- }
147
-
148
- return ordered;
149
- }
142
+ const accountPath = getUTXOAccountPath({ accountIndex, chain: utxoChain, derivationPath });
143
+ const path = derivationPathToString(accountPath);
144
+ const ledgerPath = chain === Chain.Bitcoin || chain === Chain.Litecoin ? path : path.replace(/^m\//, "");
145
+ const xpubVersion = getNetworkForChain(utxoChain).bip32.public;
146
+ const xpub = await signer.getExtendedPublicKey(ledgerPath, xpubVersion);
150
147
 
151
- function stringifyKeysInOrder(data: any) {
152
- return JSON.stringify(recursivelyOrderKeys(data));
148
+ return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub };
153
149
  }
154
150
 
155
151
  async function getWalletMethods({
152
+ address: providedAddress,
156
153
  chain,
157
154
  derivationPath,
158
155
  transport,
@@ -169,31 +166,41 @@ async function getWalletMethods({
169
166
 
170
167
  const signer = await getLedgerClient({ chain, derivationPath, transport });
171
168
 
172
- const address = await getLedgerAddress({ chain, ledgerClient: signer });
169
+ const address = providedAddress ?? (await getLedgerAddress({ chain, ledgerClient: signer }));
173
170
 
174
171
  // V3 toolbox signer:
175
- // - BTC/LTC use the modern `ledger-bitcoin` AppClient with native PSBT signing.
176
- // - BCH/DOGE/DASH use the legacy `hw-app-btc` adapter that pulls
172
+ // - BTC uses the modern `ledger-bitcoin` AppClient with native PSBT signing.
173
+ // - LTC/BCH/DOGE/DASH use the legacy `hw-app-btc` adapter that pulls
177
174
  // `nonWitnessUtxo` (full prev-tx hex) out of the API PSBT.
175
+ // The Litecoin Ledger app does not support the `ledger-bitcoin`
176
+ // policy APDUs and returns CLA_NOT_SUPPORTED.
178
177
  // - ZEC stays on the bespoke `signPCZT` flow for now.
179
178
  let toolboxSigner:
180
179
  | { getAddress: () => Promise<string>; signTransaction: (tx: Transaction) => Promise<Transaction> }
181
180
  | undefined;
182
- if (chain === Chain.Bitcoin || chain === Chain.Litecoin) {
183
- const { BitcoinPsbtLedger, LitecoinPsbtLedger } = await import("./clients/utxo-psbt");
184
- const psbtClient =
185
- chain === Chain.Bitcoin
186
- ? BitcoinPsbtLedger(derivationPath, transport)
187
- : LitecoinPsbtLedger(derivationPath, transport);
181
+ let signAndBroadcastLegacyPsbtTransaction: ((tx: Transaction) => Promise<string>) | undefined;
182
+ if (chain === Chain.Bitcoin) {
183
+ const { BitcoinPsbtLedger } = await import("./clients/utxo-psbt");
184
+ const psbtClient = BitcoinPsbtLedger(derivationPath, transport);
188
185
  toolboxSigner = { getAddress: psbtClient.getAddress, signTransaction: psbtClient.signTransaction };
189
- } else if (chain === Chain.BitcoinCash || chain === Chain.Dogecoin || chain === Chain.Dash) {
190
- const { createLegacyPsbtSigner } = await import("./clients/utxo-legacy-adapter");
186
+ } else if (
187
+ chain === Chain.BitcoinCash ||
188
+ chain === Chain.Dogecoin ||
189
+ chain === Chain.Dash ||
190
+ chain === Chain.Litecoin
191
+ ) {
192
+ const { createLegacyPsbtSigner, signLegacyPsbtTransaction } = await import("./clients/utxo-legacy-adapter");
191
193
  toolboxSigner = createLegacyPsbtSigner({ address, chain: utxoChain, legacyClient: signer });
194
+ signAndBroadcastLegacyPsbtTransaction = async (tx) => {
195
+ const signedTxHex = await signLegacyPsbtTransaction({ chain: utxoChain, legacyClient: signer, tx });
196
+ return toolbox.broadcastTx(signedTxHex);
197
+ };
192
198
  }
193
199
 
194
200
  const toolbox = toolboxSigner
195
201
  ? await getUtxoToolbox(utxoChain, { signer: toolboxSigner })
196
202
  : getUtxoToolbox(utxoChain);
203
+ const signAndBroadcastTransaction = signAndBroadcastLegacyPsbtTransaction ?? toolbox.signAndBroadcastTransaction;
197
204
 
198
205
  const transfer = async (params: UTXOBuildTxParams) => {
199
206
  const feeRate = params.feeRate || (await toolbox.getFeeRates())[FeeOption.Average];
@@ -207,8 +214,8 @@ async function getWalletMethods({
207
214
  sender: address,
208
215
  });
209
216
 
210
- // Cast tx to Transaction - signTransaction handles both Transaction and ZcashTransaction
211
- // via tx.unsignedTx which exists on both types
217
+ // Legacy Ledger UTXO signing returns finalized raw tx hex, so transfer
218
+ // broadcasts directly instead of routing through toolbox finalization.
212
219
  const txHex = await signer.signTransaction(tx as Transaction, inputs);
213
220
  const txHash = await toolbox.broadcastTx(txHex);
214
221
 
@@ -221,7 +228,8 @@ async function getWalletMethods({
221
228
  const accountPath = getUTXOAccountPath({ accountIndex, chain: utxoChain, derivationPath });
222
229
  const path = derivationPathToString(accountPath);
223
230
  const ledgerPath = chain === Chain.Bitcoin || chain === Chain.Litecoin ? path : path.replace(/^m\//, "");
224
- const xpub = await signer.getExtendedPublicKey(ledgerPath);
231
+ const xpubVersion = getNetworkForChain(utxoChain).bip32.public;
232
+ const xpub = await signer.getExtendedPublicKey(ledgerPath, xpubVersion);
225
233
 
226
234
  return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub };
227
235
  }
@@ -380,6 +388,7 @@ async function getWalletMethods({
380
388
  deriveAddresses,
381
389
  getExtendedPublicKey,
382
390
  getExtendedPublicKeyInfo,
391
+ signAndBroadcastTransaction,
383
392
  transfer,
384
393
  transferFromMultipleAddresses,
385
394
  };
@@ -444,85 +453,13 @@ async function getWalletMethods({
444
453
  }
445
454
 
446
455
  case Chain.THORChain: {
447
- const { SignMode } = await import("cosmjs-types/cosmos/tx/signing/v1beta1/signing.js");
448
- const { TxRaw } = await import("cosmjs-types/cosmos/tx/v1beta1/tx.js");
449
- const importedSigning = await import("@cosmjs/proto-signing");
450
- const encodePubkey = importedSigning.encodePubkey ?? importedSigning.default?.encodePubkey;
451
- const makeAuthInfoBytes = importedSigning.makeAuthInfoBytes ?? importedSigning.default?.makeAuthInfoBytes;
452
- const {
453
- createStargateClient,
454
- buildEncodedTxBody,
455
- getCosmosToolbox,
456
- buildAminoMsg,
457
- getDefaultChainFee,
458
- fromBase64,
459
- parseAminoMessageForDirectSigning,
460
- } = await import("@swapkit/toolboxes/cosmos");
461
- const toolbox = getCosmosToolbox(chain);
462
456
  const signer = await getLedgerClient({ chain, derivationPath, transport });
457
+ const { getCosmosToolbox } = await import("@swapkit/toolboxes/cosmos");
458
+ const toolbox = getCosmosToolbox(chain, { signer });
463
459
  const address = await getLedgerAddress({ chain, ledgerClient: signer });
460
+ const { sign: signMessage } = signer;
464
461
 
465
- const fee = getDefaultChainFee(chain);
466
- const { pubkey: value, signTransaction, sign: signMessage } = signer;
467
-
468
- // ANCHOR (@Chillios): Same parts in methods + can extract StargateClient init to toolbox
469
- const thorchainTransfer = async ({
470
- memo = "",
471
- assetValue,
472
- ...rest
473
- }: GenericTransferParams | ThorchainDepositParams) => {
474
- const account = await toolbox.getAccount(address);
475
- if (!account) throw new SwapKitError("wallet_ledger_invalid_account");
476
- if (!assetValue) throw new SwapKitError("wallet_ledger_invalid_asset");
477
- if (!value) throw new SwapKitError("wallet_ledger_pubkey_not_found");
478
-
479
- const { accountNumber, sequence: sequenceNumber } = account;
480
- const sequence = (sequenceNumber || 0).toString();
481
-
482
- const orderedMessages = recursivelyOrderKeys([buildAminoMsg({ assetValue, memo, sender: address, ...rest })]);
483
-
484
- // get tx signing msg
485
- const rawSendTx = stringifyKeysInOrder({
486
- account_number: accountNumber?.toString(),
487
- chain_id: THORConfig.chainId,
488
- fee,
489
- memo,
490
- msgs: orderedMessages,
491
- sequence,
492
- });
493
-
494
- const signatures = await signTransaction(rawSendTx, sequence);
495
- if (!signatures) throw new SwapKitError("wallet_ledger_signing_error");
496
-
497
- const pubkey = encodePubkey({ type: "tendermint/PubKeySecp256k1", value });
498
- const msgs = orderedMessages.map(parseAminoMessageForDirectSigning);
499
- const bodyBytes = await buildEncodedTxBody({ chain, memo, msgs });
500
-
501
- const authInfoBytes = makeAuthInfoBytes(
502
- [{ pubkey, sequence: Number(sequence) }],
503
- fee.amount,
504
- Number.parseInt(fee.gas, 10),
505
- undefined,
506
- undefined,
507
- SignMode.SIGN_MODE_LEGACY_AMINO_JSON,
508
- );
509
-
510
- const signature = signatures?.[0]?.signature ? fromBase64(signatures[0].signature) : Uint8Array.from([]);
511
-
512
- const txRaw = TxRaw.fromPartial({ authInfoBytes, bodyBytes, signatures: [signature] });
513
- const txBytes = TxRaw.encode(txRaw).finish();
514
- const rpcUrl = await getRPCUrl(Chain.THORChain);
515
-
516
- const broadcaster = await createStargateClient(rpcUrl);
517
- const { transactionHash } = await broadcaster.broadcastTx(txBytes);
518
-
519
- return transactionHash;
520
- };
521
-
522
- const transfer = (params: GenericTransferParams) => thorchainTransfer(params);
523
- const deposit = (params: ThorchainDepositParams) => thorchainTransfer(params);
524
-
525
- return { ...toolbox, address, deposit, signMessage, transfer };
462
+ return { ...toolbox, address, signMessage };
526
463
  }
527
464
 
528
465
  case Chain.Near: {