@ledgerhq/coin-bitcoin 0.29.0-nightly.20260115024415 → 0.29.0-nightly.20260116124336

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +12 -10
  2. package/lib/bridge/js.d.ts +1 -1
  3. package/lib/bridge/js.d.ts.map +1 -1
  4. package/lib/bridge/js.js +2 -3
  5. package/lib/bridge/js.js.map +1 -1
  6. package/lib/buildOptimisticOperation.d.ts +13 -0
  7. package/lib/buildOptimisticOperation.d.ts.map +1 -0
  8. package/lib/buildOptimisticOperation.js +20 -0
  9. package/lib/buildOptimisticOperation.js.map +1 -0
  10. package/lib/explorer.d.ts.map +1 -1
  11. package/lib/explorer.js +0 -7
  12. package/lib/explorer.js.map +1 -1
  13. package/lib/observable.d.ts +4 -0
  14. package/lib/observable.d.ts.map +1 -0
  15. package/lib/observable.js +9 -0
  16. package/lib/observable.js.map +1 -0
  17. package/lib/psbtFees.d.ts +9 -0
  18. package/lib/psbtFees.d.ts.map +1 -0
  19. package/lib/psbtFees.js +73 -0
  20. package/lib/psbtFees.js.map +1 -0
  21. package/lib/signOperation.d.ts.map +1 -1
  22. package/lib/signOperation.js +94 -99
  23. package/lib/signOperation.js.map +1 -1
  24. package/lib/signRawOperation.d.ts +6 -0
  25. package/lib/signRawOperation.d.ts.map +1 -0
  26. package/lib/signRawOperation.js +70 -0
  27. package/lib/signRawOperation.js.map +1 -0
  28. package/lib/signer.d.ts +15 -0
  29. package/lib/signer.d.ts.map +1 -1
  30. package/lib/types.d.ts +2 -0
  31. package/lib/types.d.ts.map +1 -1
  32. package/lib-es/bridge/js.d.ts +1 -1
  33. package/lib-es/bridge/js.d.ts.map +1 -1
  34. package/lib-es/bridge/js.js +2 -3
  35. package/lib-es/bridge/js.js.map +1 -1
  36. package/lib-es/buildOptimisticOperation.d.ts +13 -0
  37. package/lib-es/buildOptimisticOperation.d.ts.map +1 -0
  38. package/lib-es/buildOptimisticOperation.js +16 -0
  39. package/lib-es/buildOptimisticOperation.js.map +1 -0
  40. package/lib-es/explorer.d.ts.map +1 -1
  41. package/lib-es/explorer.js +0 -7
  42. package/lib-es/explorer.js.map +1 -1
  43. package/lib-es/observable.d.ts +4 -0
  44. package/lib-es/observable.d.ts.map +1 -0
  45. package/lib-es/observable.js +5 -0
  46. package/lib-es/observable.js.map +1 -0
  47. package/lib-es/psbtFees.d.ts +9 -0
  48. package/lib-es/psbtFees.d.ts.map +1 -0
  49. package/lib-es/psbtFees.js +69 -0
  50. package/lib-es/psbtFees.js.map +1 -0
  51. package/lib-es/signOperation.d.ts.map +1 -1
  52. package/lib-es/signOperation.js +94 -99
  53. package/lib-es/signOperation.js.map +1 -1
  54. package/lib-es/signRawOperation.d.ts +6 -0
  55. package/lib-es/signRawOperation.d.ts.map +1 -0
  56. package/lib-es/signRawOperation.js +66 -0
  57. package/lib-es/signRawOperation.js.map +1 -0
  58. package/lib-es/signer.d.ts +15 -0
  59. package/lib-es/signer.d.ts.map +1 -1
  60. package/lib-es/types.d.ts +2 -0
  61. package/lib-es/types.d.ts.map +1 -1
  62. package/package.json +13 -12
  63. package/src/__tests__/fixtures/common.fixtures.ts +6 -0
  64. package/src/__tests__/unit/psbtFees.fromFixture.unit.test.ts +31 -0
  65. package/src/__tests__/unit/signOperation.test.ts +226 -0
  66. package/src/__tests__/unit/signRawOperation.test.ts +188 -0
  67. package/src/bridge/js.test.ts +30 -0
  68. package/src/bridge/js.ts +2 -3
  69. package/src/buildOptimisticOperation.ts +34 -0
  70. package/src/explorer.ts +0 -8
  71. package/src/hw-signMessage.test.ts +1 -0
  72. package/src/observable.ts +12 -0
  73. package/src/observable.unit.test.ts +27 -0
  74. package/src/psbtFees.ts +77 -0
  75. package/src/signOperation.ts +140 -126
  76. package/src/signRawOperation.ts +104 -0
  77. package/src/signer.ts +13 -0
  78. package/src/types.ts +2 -0
  79. package/src/wallet-btc/__tests__/fixtures/common.fixtures.ts +64 -1
  80. package/src/wallet-btc/__tests__/wallet.integration.test.ts +1 -1
  81. package/src/wallet-btc/__tests__/xpub.txs.dogecoin.integration.test.ts +1 -1
  82. package/src/wallet-btc/__tests__/xpub.txs.zcash.integration.test.ts +1 -1
  83. package/tsconfig.json +3 -12
  84. package/lib/descriptor.d.ts +0 -19
  85. package/lib/descriptor.d.ts.map +0 -1
  86. package/lib/descriptor.js +0 -127
  87. package/lib/descriptor.js.map +0 -1
  88. package/lib/mockBtcSigner.d.ts +0 -20
  89. package/lib/mockBtcSigner.d.ts.map +0 -1
  90. package/lib/mockBtcSigner.js +0 -50
  91. package/lib/mockBtcSigner.js.map +0 -1
  92. package/lib-es/descriptor.d.ts +0 -19
  93. package/lib-es/descriptor.d.ts.map +0 -1
  94. package/lib-es/descriptor.js +0 -118
  95. package/lib-es/descriptor.js.map +0 -1
  96. package/lib-es/mockBtcSigner.d.ts +0 -20
  97. package/lib-es/mockBtcSigner.d.ts.map +0 -1
  98. package/lib-es/mockBtcSigner.js +0 -45
  99. package/lib-es/mockBtcSigner.js.map +0 -1
  100. package/src/descriptor.test.ts +0 -76
  101. package/src/descriptor.ts +0 -204
  102. package/src/mockBtcSigner.ts +0 -65
@@ -0,0 +1,34 @@
1
+ import { BigNumber } from "bignumber.js";
2
+ import type { Operation } from "@ledgerhq/types-live";
3
+ import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
4
+
5
+ type BuildOptimisticOperationParams = {
6
+ accountId: string;
7
+ fee: BigNumber;
8
+ value?: BigNumber;
9
+ senders?: string[];
10
+ recipients?: string[];
11
+ extra?: Record<string, unknown>;
12
+ };
13
+
14
+ export const buildOptimisticOperation = ({
15
+ accountId,
16
+ fee,
17
+ value,
18
+ senders = [],
19
+ recipients = [],
20
+ extra = {},
21
+ }: BuildOptimisticOperationParams): Operation => ({
22
+ id: encodeOperationId(accountId, "", "OUT"),
23
+ hash: "",
24
+ type: "OUT",
25
+ value: value ?? fee,
26
+ fee,
27
+ blockHash: null,
28
+ blockHeight: null,
29
+ senders,
30
+ recipients,
31
+ accountId,
32
+ date: new Date(),
33
+ extra,
34
+ });
package/src/explorer.ts CHANGED
@@ -9,14 +9,6 @@ type LedgerExplorer = {
9
9
  };
10
10
 
11
11
  const findCurrencyExplorer = (currency: CryptoCurrency): LedgerExplorer | null | undefined => {
12
- if (getEnv("SATSTACK") && currency.id === "bitcoin") {
13
- return {
14
- endpoint: getEnv("EXPLORER_SATSTACK"),
15
- id: "btc",
16
- version: "v3",
17
- };
18
- }
19
-
20
12
  if (!currency.explorerId) {
21
13
  console.warn("no explorerId for", currency.id);
22
14
  }
@@ -15,6 +15,7 @@ describe("signMessage", () => {
15
15
  }),
16
16
  splitTransaction: jest.fn(),
17
17
  createPaymentTransaction: jest.fn(),
18
+ signPsbtBuffer: jest.fn(),
18
19
  };
19
20
  const signerContext = <T>(
20
21
  _deviceId: string,
@@ -0,0 +1,12 @@
1
+ import { Observable } from "rxjs";
2
+ import type { Observer } from "rxjs";
3
+
4
+ export const fromAsyncOperation = <T>(
5
+ main: (observer: Observer<T>) => Promise<void>,
6
+ ): Observable<T> =>
7
+ new Observable<T>(observer => {
8
+ main(observer).then(
9
+ () => observer.complete(),
10
+ error => observer.error(error),
11
+ );
12
+ });
@@ -0,0 +1,27 @@
1
+ import { firstValueFrom, toArray } from "rxjs";
2
+ import { fromAsyncOperation } from "./observable";
3
+
4
+ describe("fromAsyncOperation", () => {
5
+ test("emits values and completes when the async function resolves", async () => {
6
+ const values = await firstValueFrom(
7
+ fromAsyncOperation<number>(async observer => {
8
+ observer.next(1);
9
+ observer.next(2);
10
+ }).pipe(toArray()),
11
+ );
12
+
13
+ expect(values).toEqual([1, 2]);
14
+ });
15
+
16
+ test("propagates errors when the async function rejects", async () => {
17
+ const error = new Error("boom");
18
+
19
+ await expect(
20
+ firstValueFrom(
21
+ fromAsyncOperation(async () => {
22
+ throw error;
23
+ }),
24
+ ),
25
+ ).rejects.toThrow("boom");
26
+ });
27
+ });
@@ -0,0 +1,77 @@
1
+ import { BigNumber } from "bignumber.js";
2
+ import { Transaction } from "bitcoinjs-lib";
3
+ import { PsbtV2 } from "@ledgerhq/psbtv2";
4
+
5
+ /** read uint64 little-endian into JS number (safe for BTC amounts) */
6
+ function readUInt64LE(buf: Buffer): bigint {
7
+ const lo = buf.readUInt32LE(0);
8
+ const hi = buf.readUInt32LE(4);
9
+ const loBig = BigInt(lo);
10
+ const hiBig = BigInt(hi);
11
+ const value = loBig + (hiBig << 32n);
12
+
13
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
14
+ throw new Error("readUInt64LE: value exceeds Number.MAX_SAFE_INTEGER");
15
+ }
16
+
17
+ return value;
18
+ }
19
+
20
+ /**
21
+ * Compute fee from a PSBT (v2 preferred).
22
+ * Returns:
23
+ * - BigNumber(fee) on success
24
+ * - null if cannot parse (caller should fallback to calculateFees()).
25
+ */
26
+ export function feeFromPsbt(buf: Buffer): BigNumber | null {
27
+ if (!buf || buf.length < 5) return null;
28
+
29
+ try {
30
+ // Check PSBT version and use appropriate deserialization method
31
+ const psbtVersion = PsbtV2.getPsbtVersionNumber(buf);
32
+ const psbt = psbtVersion === 2 ? new PsbtV2() : PsbtV2.fromV0(buf, true);
33
+
34
+ if (psbtVersion === 2) {
35
+ psbt.deserialize(buf);
36
+ }
37
+
38
+ // Sum inputs (prefer WITNESS_UTXO; fall back to NON_WITNESS_UTXO)
39
+ let inSum = 0n;
40
+ const nIn: number = psbt.getGlobalInputCount();
41
+ for (let i = 0; i < nIn; i++) {
42
+ const w = psbt.getInputWitnessUtxo(i);
43
+ if (w) {
44
+ inSum += readUInt64LE(w.amount);
45
+ continue;
46
+ }
47
+ const nonWitness = psbt.getInputNonWitnessUtxo(i);
48
+ if (!nonWitness) {
49
+ return null;
50
+ }
51
+
52
+ // NON_WITNESS_UTXO is the full previous transaction. Use the PSBT
53
+ // input's referenced output index to locate the amount.
54
+ const prevTx = Transaction.fromBuffer(nonWitness);
55
+ const prevOutIndex = psbt.getInputOutputIndex(i);
56
+
57
+ const prevOut = prevTx.outs[prevOutIndex];
58
+ if (!prevOut) {
59
+ return null;
60
+ }
61
+
62
+ inSum += BigInt(prevOut.value);
63
+ }
64
+
65
+ // Sum outputs
66
+ let outSum = 0n;
67
+ const nOut: number = psbt.getGlobalOutputCount();
68
+ for (let i = 0; i < nOut; i++) {
69
+ outSum += BigInt(psbt.getOutputAmount(i)); // number
70
+ }
71
+
72
+ if (inSum < outSum) return null;
73
+ return new BigNumber((inSum - outSum).toString());
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
@@ -1,142 +1,156 @@
1
1
  import { BigNumber } from "bignumber.js";
2
- import { Observable } from "rxjs";
3
2
  import { log } from "@ledgerhq/logs";
4
3
  import { isSegwitDerivationMode } from "@ledgerhq/coin-framework/derivation";
5
- import type { AccountBridge, DerivationMode, Operation } from "@ledgerhq/types-live";
6
- import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
4
+ import type { Account, AccountBridge, Operation } from "@ledgerhq/types-live";
5
+ import type { Observer } from "rxjs";
7
6
  import type { Transaction } from "./types";
8
7
  import { getNetworkParameters } from "./networks";
8
+ import { buildOptimisticOperation } from "./buildOptimisticOperation";
9
9
  import { buildTransaction } from "./buildTransaction";
10
10
  import { calculateFees } from "./cache";
11
11
  import wallet, { getWalletAccount } from "./wallet-btc";
12
12
  import { perCoinLogic } from "./logic";
13
13
  import { SignerContext } from "./signer";
14
+ import { fromAsyncOperation } from "./observable";
15
+
16
+ type SignOperationObserverEvent =
17
+ | { type: "device-signature-granted" }
18
+ | { type: "device-signature-requested" }
19
+ | { type: "device-streaming"; progress: number; index: number; total: number }
20
+ | { type: "signed"; signedOperation: { operation: Operation; signature: string } };
21
+
22
+ function buildAdditionals(
23
+ currencyId: string,
24
+ derivationMode: string,
25
+ transaction: Transaction,
26
+ ): string[] {
27
+ const perCoin = perCoinLogic[currencyId];
28
+ let additionals: string[] = [currencyId];
29
+
30
+ if (derivationMode === "native_segwit") {
31
+ additionals.push("bech32");
32
+ }
33
+
34
+ if (derivationMode === "taproot") {
35
+ additionals.push("bech32m");
36
+ }
37
+
38
+ if (perCoin?.getAdditionals) {
39
+ additionals = additionals.concat(
40
+ perCoin.getAdditionals({
41
+ transaction,
42
+ }),
43
+ );
44
+ }
45
+
46
+ return additionals;
47
+ }
48
+
49
+ async function executeSignOperation(
50
+ o: Observer<SignOperationObserverEvent>,
51
+ account: Account,
52
+ deviceId: string,
53
+ transaction: Transaction,
54
+ signerContext: SignerContext,
55
+ ): Promise<void> {
56
+ const { currency } = account;
57
+ const walletAccount = getWalletAccount(account);
58
+
59
+ log("hw", `signTransaction ${currency.id} for account ${account.id}`);
60
+ const txInfo = await buildTransaction(account, transaction);
61
+
62
+ // Maybe better not re-calculate these fields here, instead include them
63
+ // in Transaction type and set them in prepareTransaction?
64
+ const res = await calculateFees({
65
+ account,
66
+ transaction,
67
+ });
68
+
69
+ const senders = res.txInputs.reduce((acc, i) => {
70
+ if (i.address) acc.add(i.address);
71
+ return acc;
72
+ }, new Set<string>());
73
+
74
+ const recipients = res.txOutputs.reduce<string[]>((acc, o) => {
75
+ if (!o.isChange && o.address) acc.push(o.address);
76
+ return acc;
77
+ }, []);
78
+
79
+ const fee = res.fees;
80
+
81
+ let lockTime: number | undefined;
82
+
83
+ // (legacy) Set lockTime for Komodo to enable reward claiming on UTXOs created by
84
+ // Ledger Live. We should only set this if the currency is Komodo and
85
+ // lockTime isn't already defined.
86
+ if (currency.id === "komodo" && lockTime === undefined) {
87
+ const unixtime = Math.floor(Date.now() / 1000);
88
+ lockTime = unixtime - 777;
89
+ }
90
+
91
+ const networkParams = getNetworkParameters(currency.id);
92
+ const sigHashType = networkParams.sigHash;
93
+ if (isNaN(sigHashType)) {
94
+ throw new Error("sigHashType should not be NaN");
95
+ }
96
+
97
+ const segwit = isSegwitDerivationMode(account.derivationMode);
98
+ const additionals = buildAdditionals(currency.id, account.derivationMode, transaction);
99
+
100
+ const perCoin = perCoinLogic[currency.id];
101
+ const expiryHeight = perCoin?.hasExpiryHeight ? Buffer.from([0x00, 0x00, 0x00, 0x00]) : undefined;
102
+
103
+ const hasExtraData = perCoin?.hasExtraData || false;
104
+
105
+ const signature: string = await signerContext(deviceId, currency, signer =>
106
+ wallet.signAccountTx({
107
+ btc: signer,
108
+ fromAccount: walletAccount,
109
+ txInfo,
110
+ lockTime,
111
+ sigHashType,
112
+ segwit,
113
+ additionals,
114
+ expiryHeight,
115
+ hasExtraData,
116
+ onDeviceSignatureGranted: () =>
117
+ o.next({
118
+ type: "device-signature-granted",
119
+ }),
120
+ onDeviceSignatureRequested: () =>
121
+ o.next({
122
+ type: "device-signature-requested",
123
+ }),
124
+ onDeviceStreaming: ({ progress, index, total }) =>
125
+ o.next({
126
+ type: "device-streaming",
127
+ progress,
128
+ index,
129
+ total,
130
+ }),
131
+ }),
132
+ );
133
+
134
+ const operation = buildOptimisticOperation({
135
+ accountId: account.id,
136
+ fee,
137
+ value: new BigNumber(transaction.amount).plus(fee),
138
+ senders: Array.from(senders),
139
+ recipients,
140
+ });
141
+
142
+ o.next({
143
+ type: "signed",
144
+ signedOperation: {
145
+ operation,
146
+ signature,
147
+ },
148
+ });
149
+ }
14
150
 
15
151
  export const buildSignOperation =
16
152
  (signerContext: SignerContext): AccountBridge<Transaction>["signOperation"] =>
17
153
  ({ account, deviceId, transaction }) =>
18
- new Observable(o => {
19
- async function main() {
20
- const { currency } = account;
21
- const walletAccount = getWalletAccount(account);
22
-
23
- log("hw", `signTransaction ${currency.id} for account ${account.id}`);
24
- const txInfo = await buildTransaction(account, transaction);
25
- let senders = new Set<string>();
26
- let recipients: string[] = [];
27
- let fee = new BigNumber(0);
28
- // Maybe better not re-calculate these fields here, instead include them
29
- // in Transaction type and set them in prepareTransaction?
30
- await calculateFees({
31
- account,
32
- transaction,
33
- }).then(res => {
34
- senders = new Set(res.txInputs.map(i => i.address).filter(Boolean) as string[]);
35
- recipients = res.txOutputs
36
- .filter(o => o.address && !o.isChange)
37
- .map(o => o.address) as string[];
38
- fee = res.fees;
39
- });
40
-
41
- let lockTime: number | undefined;
42
-
43
- // (legacy) Set lockTime for Komodo to enable reward claiming on UTXOs created by
44
- // Ledger Live. We should only set this if the currency is Komodo and
45
- // lockTime isn't already defined.
46
- if (currency.id === "komodo" && lockTime === undefined) {
47
- const unixtime = Math.floor(Date.now() / 1000);
48
- lockTime = unixtime - 777;
49
- }
50
-
51
- const networkParams = getNetworkParameters(currency.id);
52
- const sigHashType = networkParams.sigHash;
53
- if (isNaN(sigHashType)) {
54
- throw new Error("sigHashType should not be NaN");
55
- }
56
-
57
- const segwit = isSegwitDerivationMode(account.derivationMode as DerivationMode);
58
-
59
- const perCoin = perCoinLogic[currency.id];
60
- let additionals: string[] = [currency.id];
61
-
62
- if (account.derivationMode === "native_segwit") {
63
- additionals.push("bech32");
64
- }
65
-
66
- if (account.derivationMode === "taproot") {
67
- additionals.push("bech32m");
68
- }
69
-
70
- if (perCoin?.getAdditionals) {
71
- additionals = additionals.concat(
72
- perCoin.getAdditionals({
73
- transaction,
74
- }),
75
- );
76
- }
77
-
78
- const expiryHeight = perCoin?.hasExpiryHeight
79
- ? Buffer.from([0x00, 0x00, 0x00, 0x00])
80
- : undefined;
81
-
82
- const hasExtraData = perCoin?.hasExtraData || false;
83
-
84
- const signature: string = await signerContext(deviceId, currency, signer =>
85
- wallet.signAccountTx({
86
- btc: signer,
87
- fromAccount: walletAccount,
88
- txInfo,
89
- lockTime,
90
- sigHashType,
91
- segwit,
92
- additionals,
93
- expiryHeight,
94
- hasExtraData,
95
- onDeviceSignatureGranted: () =>
96
- o.next({
97
- type: "device-signature-granted",
98
- }),
99
- onDeviceSignatureRequested: () =>
100
- o.next({
101
- type: "device-signature-requested",
102
- }),
103
- onDeviceStreaming: ({ progress, index, total }) =>
104
- o.next({
105
- type: "device-streaming",
106
- progress,
107
- index,
108
- total,
109
- }),
110
- }),
111
- );
112
- // Build the optimistic operation
113
- const operation: Operation = {
114
- id: encodeOperationId(account.id, "", "OUT"),
115
- hash: "", // Will be resolved in broadcast()
116
- type: "OUT",
117
- value: new BigNumber(transaction.amount).plus(fee),
118
- fee,
119
- blockHash: null,
120
- blockHeight: null,
121
- senders: Array.from(senders),
122
- recipients,
123
- accountId: account.id,
124
- date: new Date(),
125
- extra: {},
126
- };
127
- o.next({
128
- type: "signed",
129
- signedOperation: {
130
- operation,
131
- signature,
132
- },
133
- });
134
- }
135
-
136
- main().then(
137
- () => o.complete(),
138
- e => o.error(e),
139
- );
140
- });
154
+ fromAsyncOperation(o => executeSignOperation(o, account, deviceId, transaction, signerContext));
141
155
 
142
156
  export default buildSignOperation;
@@ -0,0 +1,104 @@
1
+ import { BigNumber } from "bignumber.js";
2
+ import { log } from "@ledgerhq/logs";
3
+ import { getAddressFormatDerivationMode } from "@ledgerhq/coin-framework/derivation";
4
+ import type { AccountBridge } from "@ledgerhq/types-live";
5
+ import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
6
+ import { parsePsbt } from "@ledgerhq/psbtv2";
7
+ import type { Transaction } from "./types";
8
+ import { getNetworkParameters } from "./networks";
9
+ import { buildOptimisticOperation } from "./buildOptimisticOperation";
10
+ import { getWalletAccount } from "./wallet-btc";
11
+ import { AddressFormat, SignerContext } from "./signer";
12
+ import { feeFromPsbt } from "./psbtFees";
13
+ import { fromAsyncOperation } from "./observable";
14
+
15
+ type SignPsbtOptions = {
16
+ accountPath: string;
17
+ addressFormat: string;
18
+ onDeviceSignatureRequested?: () => void;
19
+ onDeviceSignatureGranted?: () => void;
20
+ onDeviceStreaming?: (arg: { progress: number; total: number; index: number }) => void;
21
+ };
22
+
23
+ const signPsbtWithDevice = async (
24
+ signerContext: SignerContext,
25
+ deviceId: string,
26
+ currency: CryptoCurrency,
27
+ psbtBuffer: Buffer,
28
+ options: SignPsbtOptions,
29
+ ) =>
30
+ signerContext(deviceId, currency, signer => {
31
+ if (!signer.signPsbtBuffer) {
32
+ throw new Error("signPsbtBuffer not available");
33
+ }
34
+
35
+ return signer.signPsbtBuffer(psbtBuffer, {
36
+ accountPath: options.accountPath,
37
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
38
+ addressFormat: options.addressFormat as AddressFormat,
39
+ finalizePsbt: true,
40
+ onDeviceSignatureRequested: options.onDeviceSignatureRequested,
41
+ onDeviceSignatureGranted: options.onDeviceSignatureGranted,
42
+ onDeviceStreaming: options.onDeviceStreaming,
43
+ });
44
+ });
45
+
46
+ export const buildSignRawOperation =
47
+ (signerContext: SignerContext): AccountBridge<Transaction>["signRawOperation"] =>
48
+ ({ account, deviceId, transaction: psbt }) =>
49
+ fromAsyncOperation(async o => {
50
+ const { currency } = account;
51
+ const walletAccount = getWalletAccount(account);
52
+
53
+ log("hw", `signRawTransaction ${currency.id} for account ${account.id}`);
54
+
55
+ const networkParams = getNetworkParameters(currency.id);
56
+ const sigHashType = networkParams.sigHash;
57
+ if (Number.isNaN(sigHashType)) {
58
+ throw new TypeError("sigHashType should not be NaN");
59
+ }
60
+
61
+ const psbtBuffer = parsePsbt(psbt);
62
+
63
+ const psbtResult = await signPsbtWithDevice(signerContext, deviceId, currency, psbtBuffer, {
64
+ accountPath: `${walletAccount.params.path}/${walletAccount.params.index}'`,
65
+ addressFormat: getAddressFormatDerivationMode(account.derivationMode),
66
+ onDeviceSignatureRequested: () => o.next({ type: "device-signature-requested" }),
67
+ onDeviceSignatureGranted: () => o.next({ type: "device-signature-granted" }),
68
+ onDeviceStreaming: arg => o.next({ type: "device-streaming", ...arg }),
69
+ });
70
+
71
+ if (!psbtResult) {
72
+ throw new Error(
73
+ `PSBT signing failed: no result from device for account ${account.id} (${currency.id})`,
74
+ );
75
+ }
76
+
77
+ const parsedPsbtFee = feeFromPsbt(psbtBuffer);
78
+ if (!parsedPsbtFee) {
79
+ log(
80
+ "hw",
81
+ `Failed to extract fee from PSBT for account ${account.id} (${currency.id}); falling back to fee=0`,
82
+ );
83
+ }
84
+ const psbtFee = parsedPsbtFee || BigNumber(0);
85
+
86
+ // Optimistic operation for PSBT (we don't know recipients/amount here)
87
+ const operation = buildOptimisticOperation({
88
+ accountId: account.id,
89
+ fee: psbtFee,
90
+ extra: { psbt: true },
91
+ });
92
+
93
+ o.next({
94
+ type: "signed",
95
+ signedOperation: {
96
+ operation,
97
+ // Ensure non-empty signature: if not finalized, fall back to the PSBT (base64)
98
+ signature: psbtResult.tx || psbtResult.psbt.toString("base64"),
99
+ rawData: { psbtSigned: psbtResult.psbt.toString("base64") },
100
+ },
101
+ });
102
+ });
103
+
104
+ export default buildSignRawOperation;
package/src/signer.ts CHANGED
@@ -31,6 +31,19 @@ export interface BitcoinSigner {
31
31
  additionals: Array<string> | null | undefined,
32
32
  ): SignerTransaction;
33
33
  createPaymentTransaction(arg: CreateTransaction): Promise<string>;
34
+ signPsbtBuffer?(
35
+ psbtBuffer: Buffer,
36
+ options?: {
37
+ finalizePsbt?: boolean;
38
+ accountPath?: string;
39
+ addressFormat?: AddressFormat;
40
+ onDeviceSignatureRequested: (() => void) | undefined;
41
+ onDeviceSignatureGranted: (() => void) | undefined;
42
+ onDeviceStreaming:
43
+ | ((arg: { progress: number; total: number; index: number }) => void)
44
+ | undefined;
45
+ },
46
+ ): Promise<{ psbt: Buffer; tx: string }>;
34
47
  }
35
48
 
36
49
  export type SignerResult = BitcoinXPub | BitcoinAddress | BitcoinSignature;
package/src/types.ts CHANGED
@@ -146,6 +146,8 @@ export type Transaction = TransactionCommon & {
146
146
  networkInfo: NetworkInfo | null | undefined;
147
147
  opReturnData?: Buffer | undefined;
148
148
  changeAddress?: string | undefined;
149
+ psbt?: string;
150
+ finalizePsbt?: boolean;
149
151
  };
150
152
 
151
153
  export type TransactionRaw = TransactionCommonRaw & {
@@ -1,6 +1,14 @@
1
1
  import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
2
2
  import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
3
-
3
+ import hash from "object-hash";
4
+ import type {
5
+ AddressFormat,
6
+ BitcoinSignature,
7
+ BitcoinSigner,
8
+ BitcoinXPub,
9
+ CreateTransaction,
10
+ SignerTransaction,
11
+ } from "../../../signer";
4
12
  import { Account } from "../../account";
5
13
  import { ICrypto } from "../../crypto/types";
6
14
  import Xpub from "../../xpub";
@@ -84,3 +92,58 @@ export const mockStorage = {
84
92
  exportSync: jest.fn(),
85
93
  loadSync: jest.fn(),
86
94
  } as unknown as jest.Mocked<IStorage>;
95
+
96
+ export class MockBtcSigner implements BitcoinSigner {
97
+ // eslint-disable-next-line class-methods-use-this
98
+ async getWalletPublicKey(
99
+ path: string,
100
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
101
+ _opts: { verify?: boolean; format?: AddressFormat },
102
+ ) {
103
+ switch (path) {
104
+ case "44'/0'":
105
+ return {
106
+ publicKey:
107
+ "04c621f37493d99f39ca12fb02ba7fe1687b1650b875dcb6733f386a98958e6556fc95dcecb6ac41af0a5296965751b1598aa475a537474bab5b316fcdc1196568",
108
+ chainCode: "a45d311c31a80bf06cc38d8ed7934bd1e8a7b2d48b2868a70258a86e094bacfb",
109
+ bitcoinAddress: "1BKWjmA9swxRKMH9NgXpSz8YZfVMnWWU9D",
110
+ };
111
+ case "44'/0'/0'":
112
+ return {
113
+ publicKey:
114
+ "04d52d1ad9311c5a3d542fa652fbd7d7b0be70109e329d359704d9f2946f8eb52a829c23f8b980c5f7b6c51bf446b21f3dc80c865095243c9215dbf9f3cb6403b8",
115
+ chainCode: "0bd3e45edca4d8a466f523a2c4094c412d25c36d5298b2d3a29938151a8d37fe",
116
+ bitcoinAddress: "1FHa4cuKdea21ByTngP9vz3KYDqqQe9SsA",
117
+ };
118
+ default:
119
+ throw new Error("not supported");
120
+ }
121
+ }
122
+
123
+ async createPaymentTransaction(arg: CreateTransaction) {
124
+ return hash(arg);
125
+ }
126
+
127
+ getWalletXpub(_arg: { path: string; xpubVersion: number }): Promise<BitcoinXPub> {
128
+ return Promise.reject(new Error("not implemented"));
129
+ }
130
+ signMessage(_path: string, _messageHex: string): Promise<BitcoinSignature> {
131
+ return Promise.reject(new Error("not implemented"));
132
+ }
133
+ signPsbtBuffer(_psbtBuffer: Buffer): Promise<{ psbt: Buffer; tx: string }> {
134
+ return Promise.reject(new Error("not implemented"));
135
+ }
136
+ splitTransaction(
137
+ _transactionHex: string,
138
+ _isSegwitSupported: boolean | null | undefined,
139
+ _hasExtraData: boolean | null | undefined,
140
+ _additionals: Array<string> | null | undefined,
141
+ ): SignerTransaction {
142
+ // Stub
143
+ return {
144
+ version: Buffer.from(""),
145
+ inputs: [],
146
+ outputs: [],
147
+ };
148
+ }
149
+ }
@@ -4,7 +4,7 @@ import { DerivationModes } from "../types";
4
4
  import BitcoinLikeWallet from "../wallet";
5
5
  import { Account } from "../account";
6
6
  import { Merge } from "../pickingstrategies/Merge";
7
- import MockBtcSigner from "../../mockBtcSigner";
7
+ import { MockBtcSigner } from "./fixtures/common.fixtures";
8
8
  import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
9
9
 
10
10
  jest.setTimeout(180000);
@@ -7,7 +7,7 @@ import BitcoinLikeExplorer from "../explorer";
7
7
  import BitcoinLikeStorage from "../storage";
8
8
  import { Merge } from "../pickingstrategies/Merge";
9
9
  import BitcoinLikeWallet from "../wallet";
10
- import MockBtcSigner from "../../mockBtcSigner";
10
+ import { MockBtcSigner } from "./fixtures/common.fixtures";
11
11
  import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
12
12
 
13
13
  describe("testing dogecoin transactions", () => {