@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.
- package/CHANGELOG.md +12 -10
- package/lib/bridge/js.d.ts +1 -1
- package/lib/bridge/js.d.ts.map +1 -1
- package/lib/bridge/js.js +2 -3
- package/lib/bridge/js.js.map +1 -1
- package/lib/buildOptimisticOperation.d.ts +13 -0
- package/lib/buildOptimisticOperation.d.ts.map +1 -0
- package/lib/buildOptimisticOperation.js +20 -0
- package/lib/buildOptimisticOperation.js.map +1 -0
- package/lib/explorer.d.ts.map +1 -1
- package/lib/explorer.js +0 -7
- package/lib/explorer.js.map +1 -1
- package/lib/observable.d.ts +4 -0
- package/lib/observable.d.ts.map +1 -0
- package/lib/observable.js +9 -0
- package/lib/observable.js.map +1 -0
- package/lib/psbtFees.d.ts +9 -0
- package/lib/psbtFees.d.ts.map +1 -0
- package/lib/psbtFees.js +73 -0
- package/lib/psbtFees.js.map +1 -0
- package/lib/signOperation.d.ts.map +1 -1
- package/lib/signOperation.js +94 -99
- package/lib/signOperation.js.map +1 -1
- package/lib/signRawOperation.d.ts +6 -0
- package/lib/signRawOperation.d.ts.map +1 -0
- package/lib/signRawOperation.js +70 -0
- package/lib/signRawOperation.js.map +1 -0
- package/lib/signer.d.ts +15 -0
- package/lib/signer.d.ts.map +1 -1
- package/lib/types.d.ts +2 -0
- package/lib/types.d.ts.map +1 -1
- package/lib-es/bridge/js.d.ts +1 -1
- package/lib-es/bridge/js.d.ts.map +1 -1
- package/lib-es/bridge/js.js +2 -3
- package/lib-es/bridge/js.js.map +1 -1
- package/lib-es/buildOptimisticOperation.d.ts +13 -0
- package/lib-es/buildOptimisticOperation.d.ts.map +1 -0
- package/lib-es/buildOptimisticOperation.js +16 -0
- package/lib-es/buildOptimisticOperation.js.map +1 -0
- package/lib-es/explorer.d.ts.map +1 -1
- package/lib-es/explorer.js +0 -7
- package/lib-es/explorer.js.map +1 -1
- package/lib-es/observable.d.ts +4 -0
- package/lib-es/observable.d.ts.map +1 -0
- package/lib-es/observable.js +5 -0
- package/lib-es/observable.js.map +1 -0
- package/lib-es/psbtFees.d.ts +9 -0
- package/lib-es/psbtFees.d.ts.map +1 -0
- package/lib-es/psbtFees.js +69 -0
- package/lib-es/psbtFees.js.map +1 -0
- package/lib-es/signOperation.d.ts.map +1 -1
- package/lib-es/signOperation.js +94 -99
- package/lib-es/signOperation.js.map +1 -1
- package/lib-es/signRawOperation.d.ts +6 -0
- package/lib-es/signRawOperation.d.ts.map +1 -0
- package/lib-es/signRawOperation.js +66 -0
- package/lib-es/signRawOperation.js.map +1 -0
- package/lib-es/signer.d.ts +15 -0
- package/lib-es/signer.d.ts.map +1 -1
- package/lib-es/types.d.ts +2 -0
- package/lib-es/types.d.ts.map +1 -1
- package/package.json +13 -12
- package/src/__tests__/fixtures/common.fixtures.ts +6 -0
- package/src/__tests__/unit/psbtFees.fromFixture.unit.test.ts +31 -0
- package/src/__tests__/unit/signOperation.test.ts +226 -0
- package/src/__tests__/unit/signRawOperation.test.ts +188 -0
- package/src/bridge/js.test.ts +30 -0
- package/src/bridge/js.ts +2 -3
- package/src/buildOptimisticOperation.ts +34 -0
- package/src/explorer.ts +0 -8
- package/src/hw-signMessage.test.ts +1 -0
- package/src/observable.ts +12 -0
- package/src/observable.unit.test.ts +27 -0
- package/src/psbtFees.ts +77 -0
- package/src/signOperation.ts +140 -126
- package/src/signRawOperation.ts +104 -0
- package/src/signer.ts +13 -0
- package/src/types.ts +2 -0
- package/src/wallet-btc/__tests__/fixtures/common.fixtures.ts +64 -1
- package/src/wallet-btc/__tests__/wallet.integration.test.ts +1 -1
- package/src/wallet-btc/__tests__/xpub.txs.dogecoin.integration.test.ts +1 -1
- package/src/wallet-btc/__tests__/xpub.txs.zcash.integration.test.ts +1 -1
- package/tsconfig.json +3 -12
- package/lib/descriptor.d.ts +0 -19
- package/lib/descriptor.d.ts.map +0 -1
- package/lib/descriptor.js +0 -127
- package/lib/descriptor.js.map +0 -1
- package/lib/mockBtcSigner.d.ts +0 -20
- package/lib/mockBtcSigner.d.ts.map +0 -1
- package/lib/mockBtcSigner.js +0 -50
- package/lib/mockBtcSigner.js.map +0 -1
- package/lib-es/descriptor.d.ts +0 -19
- package/lib-es/descriptor.d.ts.map +0 -1
- package/lib-es/descriptor.js +0 -118
- package/lib-es/descriptor.js.map +0 -1
- package/lib-es/mockBtcSigner.d.ts +0 -20
- package/lib-es/mockBtcSigner.d.ts.map +0 -1
- package/lib-es/mockBtcSigner.js +0 -45
- package/lib-es/mockBtcSigner.js.map +0 -1
- package/src/descriptor.test.ts +0 -76
- package/src/descriptor.ts +0 -204
- 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
|
}
|
|
@@ -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
|
+
});
|
package/src/psbtFees.ts
ADDED
|
@@ -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
|
+
}
|
package/src/signOperation.ts
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
10
|
+
import { MockBtcSigner } from "./fixtures/common.fixtures";
|
|
11
11
|
import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
|
|
12
12
|
|
|
13
13
|
describe("testing dogecoin transactions", () => {
|