@swapkit/wallet-hardware 4.9.9 → 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
@@ -18,6 +18,7 @@ import {
18
18
  assertDerivationIndex,
19
19
  compileMemo,
20
20
  createHDWalletHelpers,
21
+ getNetworkForChain,
21
22
  getUTXOAccountIndexFromPath,
22
23
  getUTXOAccountPath,
23
24
  getUTXOAddressPath,
@@ -26,7 +27,7 @@ import {
26
27
  type UTXOForMultiAddressTransfer,
27
28
  } from "@swapkit/toolboxes/utxo";
28
29
  import type { Transaction } from "@swapkit/utxo-signer";
29
- import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core";
30
+ import { createWallet, getWalletSupportedChains, type HardwareExtendedPublicKeyInfo } from "@swapkit/wallet-core";
30
31
  import { getLedgerAddress, getLedgerClient } from "./helpers";
31
32
 
32
33
  /**
@@ -37,21 +38,21 @@ import { getLedgerAddress, getLedgerClient } from "./helpers";
37
38
  * client and will NOT recreate it on `forceReconnect`. When omitted, the
38
39
  * default browser flow (WebHID / WebUSB via `navigator.usb`) is used.
39
40
  */
40
- export type ConnectLedgerOptions = { transport?: Transport };
41
+ export type ConnectLedgerOptions = { address?: string; transport?: Transport };
41
42
 
42
43
  export const ledgerWallet = createWallet({
43
44
  connect: ({ addChain, supportedChains, walletType }) =>
44
45
  async function connectLedger(
45
46
  chains: Chain[],
46
47
  derivationPath?: DerivationPathArray,
47
- { transport }: ConnectLedgerOptions = {},
48
+ { address, transport }: ConnectLedgerOptions = {},
48
49
  ) {
49
50
  const [chain] = filterSupportedChains({ chains, supportedChains, walletType });
50
51
 
51
52
  if (!chain) return false;
52
53
 
53
54
  const resolvedPath = derivationPath ?? (NetworkDerivationPath[chain] as DerivationPathArray | undefined);
54
- const walletMethods = await getWalletMethods({ chain, derivationPath: resolvedPath, transport });
55
+ const walletMethods = await getWalletMethods({ address, chain, derivationPath: resolvedPath, transport });
55
56
 
56
57
  addChain({ ...walletMethods, chain, walletType: WalletOption.LEDGER });
57
58
 
@@ -83,6 +84,7 @@ export const ledgerWallet = createWallet({
83
84
  [Chain.XLayer]: true,
84
85
  // ZEC: still on bespoke signPCZT path
85
86
  },
87
+ getExtendedPublicKey: getLedgerExtendedPublicKey,
86
88
  name: "connectLedger",
87
89
  supportedChains: [
88
90
  Chain.Arbitrum,
@@ -124,7 +126,30 @@ function reduceMemo(memo?: string, affiliateAddress = "t") {
124
126
  return removedAffiliate?.substring(0, removedAffiliate.lastIndexOf(":"));
125
127
  }
126
128
 
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 });
136
+ }
137
+
138
+ const utxoChain = chain as UTXOChain;
139
+ const signer = await getLedgerClient({ chain: utxoChain, derivationPath });
140
+ if (!signer.getExtendedPublicKey) return undefined;
141
+
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);
147
+
148
+ return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub };
149
+ }
150
+
127
151
  async function getWalletMethods({
152
+ address: providedAddress,
128
153
  chain,
129
154
  derivationPath,
130
155
  transport,
@@ -141,31 +166,41 @@ async function getWalletMethods({
141
166
 
142
167
  const signer = await getLedgerClient({ chain, derivationPath, transport });
143
168
 
144
- const address = await getLedgerAddress({ chain, ledgerClient: signer });
169
+ const address = providedAddress ?? (await getLedgerAddress({ chain, ledgerClient: signer }));
145
170
 
146
171
  // V3 toolbox signer:
147
- // - BTC/LTC use the modern `ledger-bitcoin` AppClient with native PSBT signing.
148
- // - 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
149
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.
150
177
  // - ZEC stays on the bespoke `signPCZT` flow for now.
151
178
  let toolboxSigner:
152
179
  | { getAddress: () => Promise<string>; signTransaction: (tx: Transaction) => Promise<Transaction> }
153
180
  | undefined;
154
- if (chain === Chain.Bitcoin || chain === Chain.Litecoin) {
155
- const { BitcoinPsbtLedger, LitecoinPsbtLedger } = await import("./clients/utxo-psbt");
156
- const psbtClient =
157
- chain === Chain.Bitcoin
158
- ? BitcoinPsbtLedger(derivationPath, transport)
159
- : 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);
160
185
  toolboxSigner = { getAddress: psbtClient.getAddress, signTransaction: psbtClient.signTransaction };
161
- } else if (chain === Chain.BitcoinCash || chain === Chain.Dogecoin || chain === Chain.Dash) {
162
- 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");
163
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
+ };
164
198
  }
165
199
 
166
200
  const toolbox = toolboxSigner
167
201
  ? await getUtxoToolbox(utxoChain, { signer: toolboxSigner })
168
202
  : getUtxoToolbox(utxoChain);
203
+ const signAndBroadcastTransaction = signAndBroadcastLegacyPsbtTransaction ?? toolbox.signAndBroadcastTransaction;
169
204
 
170
205
  const transfer = async (params: UTXOBuildTxParams) => {
171
206
  const feeRate = params.feeRate || (await toolbox.getFeeRates())[FeeOption.Average];
@@ -179,8 +214,8 @@ async function getWalletMethods({
179
214
  sender: address,
180
215
  });
181
216
 
182
- // Cast tx to Transaction - signTransaction handles both Transaction and ZcashTransaction
183
- // 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.
184
219
  const txHex = await signer.signTransaction(tx as Transaction, inputs);
185
220
  const txHash = await toolbox.broadcastTx(txHex);
186
221
 
@@ -193,7 +228,8 @@ async function getWalletMethods({
193
228
  const accountPath = getUTXOAccountPath({ accountIndex, chain: utxoChain, derivationPath });
194
229
  const path = derivationPathToString(accountPath);
195
230
  const ledgerPath = chain === Chain.Bitcoin || chain === Chain.Litecoin ? path : path.replace(/^m\//, "");
196
- const xpub = await signer.getExtendedPublicKey(ledgerPath);
231
+ const xpubVersion = getNetworkForChain(utxoChain).bip32.public;
232
+ const xpub = await signer.getExtendedPublicKey(ledgerPath, xpubVersion);
197
233
 
198
234
  return { accountIndex: getUTXOAccountIndexFromPath(accountPath), path, xpub };
199
235
  }
@@ -352,6 +388,7 @@ async function getWalletMethods({
352
388
  deriveAddresses,
353
389
  getExtendedPublicKey,
354
390
  getExtendedPublicKeyInfo,
391
+ signAndBroadcastTransaction,
355
392
  transfer,
356
393
  transferFromMultipleAddresses,
357
394
  };