@ledgerhq/coin-tester-bitcoin 1.1.0 → 1.2.0-nightly.20251107095716

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/coin-tester-bitcoin",
3
- "version": "1.1.0",
3
+ "version": "1.2.0-nightly.20251107095716",
4
4
  "description": "Ledger BTC Coin Tester",
5
5
  "main": "src/scenarii.test.ts",
6
6
  "keywords": [
@@ -48,15 +48,15 @@
48
48
  "bignumber.js": "^9.1.2",
49
49
  "bitcoin-core": "^5.0.0",
50
50
  "docker-compose": "^1.1.0",
51
- "@ledgerhq/coin-bitcoin": "^0.24.0",
52
- "@ledgerhq/coin-framework": "^6.8.0",
53
- "@ledgerhq/coin-tester": "^0.11.0",
51
+ "@ledgerhq/coin-bitcoin": "^0.25.0-nightly.20251107095716",
52
+ "@ledgerhq/coin-framework": "^6.9.0-nightly.20251107095716",
53
+ "@ledgerhq/coin-tester": "^0.12.0-nightly.20251107095716",
54
54
  "@ledgerhq/live-config": "^3.2.0",
55
+ "@ledgerhq/cryptoassets": "^13.33.0-nightly.20251107095716",
55
56
  "@ledgerhq/live-network": "^2.1.0",
56
- "@ledgerhq/cryptoassets": "^13.32.0",
57
- "@ledgerhq/types-cryptoassets": "^7.30.0",
58
57
  "@ledgerhq/hw-app-btc": "^10.12.0",
59
- "@ledgerhq/types-live": "^6.88.0"
58
+ "@ledgerhq/types-cryptoassets": "^7.30.0",
59
+ "@ledgerhq/types-live": "^6.89.0-nightly.20251107095716"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/jest": "^29.5.10",
package/src/helpers.ts CHANGED
@@ -15,7 +15,6 @@ export async function loadWallet(name: string): Promise<void> {
15
15
  console.log(`✅ Wallet "${name}" created and loaded:`, res);
16
16
 
17
17
  // Optionally verify that the wallet is accessible
18
- await client.getBalance({ minconf: 0 });
19
18
  return;
20
19
  } catch (error: any) {
21
20
  const message = error?.message || "Unknown error";
@@ -249,113 +248,66 @@ export const sendRaw = async (recipientAddress: string, amount: number, sequence
249
248
  }
250
249
  };
251
250
 
252
- export const sendToReplaceCurrentTx = async (recipientAddress: string, amount: number) => {
253
- if (!recipientAddress || amount <= 0) {
254
- console.error("Invalid parameters: Provide a valid address and positive amount.");
255
- return;
256
- }
257
-
258
- console.log(
259
- `Sending replaceable transaction to ${recipientAddress} with amount ${amount} BTC...`,
260
- );
261
-
262
- try {
263
- // Step 1: Get an unspent UTXO
264
- const unspent = await client.listUnspent({
265
- query_options: { minimumSumAmount: amount },
266
- });
267
-
268
- if (!unspent.length) {
269
- console.error("No suitable UTXOs available.");
270
- return;
271
- }
272
-
273
- const utxo = unspent[0];
251
+ export async function sendTransaction(
252
+ recipientAddress: string,
253
+ amount: number,
254
+ rbf: boolean = true,
255
+ ): Promise<string> {
256
+ // Send an RBF-enabled transaction
257
+ const txid = await client.sendToAddress(recipientAddress, amount, "", "", rbf);
258
+
259
+ // Log details
260
+ const mempoolEntry = await client.getMempoolEntry(txid);
261
+ console.log("BIP125 replaceable:", mempoolEntry["bip125-replaceable"]);
262
+ return txid;
263
+ }
274
264
 
275
- // Step 2: Create the replaceable transaction (RBF enabled)
276
- const rawTx1 = await client.createRawTransaction({
277
- inputs: [
278
- {
279
- txid: utxo.txid,
280
- vout: utxo.vout,
281
- sequence: 4294967293, // RBF enabled
282
- },
283
- ],
284
- outputs: {
285
- [recipientAddress]: amount,
286
- },
287
- });
265
+ export async function replaceTransaction(
266
+ originalTxid: string,
267
+ newRecipientAddress: string,
268
+ feeBumpPercent: number = 0.1, // 10% bump
269
+ ): Promise<string> {
270
+ const rawTx = await client.getRawTransaction(originalTxid, true);
271
+ const input = rawTx.vin[0];
272
+ const originalOutputValue = rawTx.vout[0].value;
288
273
 
289
- // Step 3: Fund & Sign the transaction
290
- const fundedTx1 = await client.fundRawTransaction({
291
- hexstring: rawTx1,
292
- options: { feeRate: 0.0004 }, // Increase fee rate
293
- });
294
- const signedTx1 = await client.signRawTransactionWithWallet({
295
- hexstring: fundedTx1.hex,
296
- });
274
+ // Try to estimate the original feerate
275
+ let originalFeerate = 0.00001; // fallback (BTC/vB)
276
+ let vsize = 200; // default guess
297
277
 
298
- // Step 4: Broadcast the transaction
299
- const txId1 = await client.sendRawTransaction({ hexstring: signedTx1.hex });
300
- console.log(`Transaction sent (TXID: ${txId1}), waiting before replacing...`);
301
- console.log(`If you need to, make a tx that sends funds to ${recipientAddress}`);
302
- } catch (error: any) {
303
- console.error("Error in sendReplaceableTransaction:", error.message);
278
+ try {
279
+ const entry = await client.getMempoolEntry(originalTxid);
280
+ originalFeerate = entry.fees.base / entry.vsize;
281
+ vsize = entry.vsize;
282
+ } catch {
283
+ console.warn("⚠️ Could not get mempool entry, using fallback feerate.");
304
284
  }
305
- };
306
285
 
307
- export const sendReplaceableTransaction = async (recipientAddress: string, amount: number) => {
308
- if (!recipientAddress || amount <= 0) {
309
- console.error("Invalid parameters: Provide a valid address and positive amount.");
310
- return;
311
- }
286
+ const newFeerate = originalFeerate * (1 + feeBumpPercent);
287
+ const estimatedFee = newFeerate * vsize;
312
288
 
313
- console.log(
314
- `Sending replaceable transaction to ${recipientAddress} with amount ${amount} BTC...`,
315
- );
289
+ // Adjust amount by the new fee
290
+ const adjustedAmount = Number(originalOutputValue - estimatedFee).toFixed(8);
316
291
 
317
- try {
318
- // Step 1: Get an unspent UTXO
319
- const unspent = await client.listUnspent({
320
- query_options: { minimumSumAmount: amount },
321
- });
292
+ if (Number(adjustedAmount) <= 0) {
293
+ throw new Error(`❌ Adjusted amount <= 0 (fee too large)`);
294
+ }
322
295
 
323
- if (!unspent.length) {
324
- console.error("No suitable UTXOs available.");
325
- return;
326
- }
296
+ const newRawTx = await client.createRawTransaction([{ txid: input.txid, vout: input.vout }], {
297
+ [newRecipientAddress]: adjustedAmount,
298
+ });
327
299
 
328
- const utxo = unspent[0];
300
+ const signed = await client.signRawTransactionWithWallet(newRawTx);
329
301
 
330
- // Step 2: Create the first replaceable transaction (RBF enabled)
331
- const rawTx1 = await client.createRawTransaction({
332
- inputs: [
333
- {
334
- txid: utxo.txid,
335
- vout: utxo.vout,
336
- sequence: 4294967293, // RBF enabled
337
- },
338
- ],
339
- outputs: {
340
- [recipientAddress]: amount,
341
- },
342
- });
302
+ const replacementTxid = await client.sendRawTransaction(signed.hex, 0);
303
+ console.log(`✅ Replaced TX: ${originalTxid} → ${replacementTxid}`);
343
304
 
344
- // Step 3: Fund & Sign the first transaction
345
- const fundedTx1 = await client.fundRawTransaction({
346
- hexstring: rawTx1,
347
- options: { feeRate: 0.0002 }, // Increase fee rate
348
- });
349
- const signedTx1 = await client.signRawTransactionWithWallet({
350
- hexstring: fundedTx1.hex,
351
- });
305
+ return replacementTxid;
306
+ }
352
307
 
353
- // Step 4: Broadcast the first transaction
354
- const txId1 = await client.sendRawTransaction({ hexstring: signedTx1.hex });
355
- console.log(`First transaction sent (TXID: ${txId1}), waiting before replacing...`);
356
- } catch (error: any) {
357
- console.error("Error in sendReplaceableTransaction:", error.message);
358
- }
308
+ export const getRawMempool = async () => {
309
+ const mempool = await client.getRawMempool();
310
+ return mempool;
359
311
  };
360
312
 
361
313
  /*Available commands:
@@ -10,10 +10,17 @@ import { SignerContext } from "@ledgerhq/coin-bitcoin/signer";
10
10
  import { BitcoinConfigInfo, setCoinConfig } from "@ledgerhq/coin-bitcoin/config";
11
11
  import { BigNumber } from "bignumber.js";
12
12
  import { defaultNanoApp } from "../constants";
13
- import { loadWallet, mineToWalletAddress, sendTo } from "../helpers";
13
+ import {
14
+ loadWallet,
15
+ mineToWalletAddress,
16
+ sendTo,
17
+ sendTransaction,
18
+ replaceTransaction,
19
+ getRawMempool,
20
+ } from "../helpers";
14
21
  import { makeAccount } from "../fixtures";
15
22
  import { killAtlas, spawnAtlas } from "../atlas";
16
- import { findNewUtxo, waitForExplorerSync } from "../utils";
23
+ import { findNewUtxo, waitForExplorerSync, waitForTxInMempool } from "../utils";
17
24
  import {
18
25
  assertCommonTxProperties,
19
26
  assertUtxoExcluded,
@@ -213,6 +220,46 @@ const makeScenarioTransactions = (): BitcoinScenarioTransaction[] => {
213
220
  ];
214
221
  };
215
222
 
223
+ // TODO: RBF and CTFP scenarios are not supported on Ledger Live yet That is why they are made using regtest for now
224
+ // TODO: once RBF and CTFP are supported natively in Ledger Live, we can move these scenarios to use bitcoin module instead of regtest helpers
225
+ const makeInternalScenarioTransactions = async () => {
226
+ const scenarioReplaceBtcTransaction: BitcoinScenarioTransaction = {
227
+ name: "Send Replace BTC transaction",
228
+ expect: async (previousAccount, currentAccount) => {
229
+ const txId = await sendTransaction((currentAccount as BitcoinAccount).freshAddress, 0.003);
230
+ // Waiting a bit before replacing...
231
+ await waitForTxInMempool(txId, 10000); // waits up to 10s
232
+
233
+ const replacementTxid = await replaceTransaction(
234
+ txId,
235
+ (currentAccount as BitcoinAccount).freshAddress,
236
+ );
237
+ const mempool = await getRawMempool();
238
+ expect(mempool.includes(txId)).toBe(false);
239
+ expect(mempool.includes(replacementTxid)).toBe(true);
240
+ },
241
+ };
242
+ const scenarioCancelBtcTransaction: BitcoinScenarioTransaction = {
243
+ name: "Send Cancel BTC transaction",
244
+ expect: async (previousAccount, currentAccount) => {
245
+ await mineToWalletAddress("2");
246
+ const txId = await sendTransaction("bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz", 0.003);
247
+ // Waiting a bit before replacing...
248
+ await waitForTxInMempool(txId, 10000); // waits up to 10s
249
+
250
+ const replacementTxid = await replaceTransaction(
251
+ txId,
252
+ (currentAccount as BitcoinAccount).freshAddress,
253
+ );
254
+ const mempool = await getRawMempool();
255
+ expect(mempool.includes(txId)).toBe(false);
256
+ expect(mempool.includes(replacementTxid)).toBe(true);
257
+ },
258
+ };
259
+
260
+ return [scenarioReplaceBtcTransaction, scenarioCancelBtcTransaction];
261
+ };
262
+
216
263
  export const scenarioBitcoin: Scenario<BtcTransaction, BitcoinAccount> = {
217
264
  name: "Ledger Live Basic Bitcoin Transactions",
218
265
  setup: async () => {
@@ -282,6 +329,7 @@ export const scenarioBitcoin: Scenario<BtcTransaction, BitcoinAccount> = {
282
329
  };
283
330
  },
284
331
  getTransactions: () => makeScenarioTransactions(),
332
+ getInternalTransactions: () => makeInternalScenarioTransactions(),
285
333
  beforeAll: async account => {
286
334
  firstUtxoHash = (account as BitcoinAccount).bitcoinResources.utxos[0].hash;
287
335
  firstUtxoOutputIndex = (account as BitcoinAccount).bitcoinResources.utxos[0].outputIndex;
@@ -291,6 +339,10 @@ export const scenarioBitcoin: Scenario<BtcTransaction, BitcoinAccount> = {
291
339
  afterEach: async () => {
292
340
  // Mine 2 blocks after each transaction to confirm it
293
341
  await mineToWalletAddress("2");
342
+ await waitForExplorerSync();
343
+ },
344
+ afterAll: async account => {
345
+ expect(account.operations.length).toBeGreaterThanOrEqual(14);
294
346
  },
295
347
  beforeEach: async () => {
296
348
  // Make sure explorer is in sync before each transaction
@@ -5,7 +5,7 @@ import { scenarioBitcoin } from "./scenarii/bitcoin";
5
5
  import { killAtlas } from "./atlas";
6
6
 
7
7
  global.console = console;
8
- jest.setTimeout(300_000);
8
+ jest.setTimeout(500_000);
9
9
 
10
10
  describe("Bitcoin Deterministic Tester", () => {
11
11
  it("scenario Bitcoin", async () => {
package/src/utils.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types";
2
- import { getCurrentBlock } from "./helpers";
2
+ import { getCurrentBlock, getRawMempool } from "./helpers";
3
3
  import network from "@ledgerhq/live-network/network";
4
4
 
5
- function sleep(ms: number) {
5
+ export function sleep(ms: number) {
6
6
  return new Promise(resolve => setTimeout(resolve, ms));
7
7
  }
8
8
 
9
+ export async function waitForTxInMempool(txId: string, timeout = 10000) {
10
+ const start = Date.now();
11
+ while (Date.now() - start < timeout) {
12
+ const mempool = await getRawMempool();
13
+ if (mempool.includes(txId)) return true;
14
+ await sleep(500);
15
+ }
16
+ throw new Error(`Transaction ${txId} not found in mempool after ${timeout}ms`);
17
+ }
18
+
9
19
  export async function waitForExplorerSync(
10
20
  url: string = "http://localhost:9876/blockchain/v4/btc_regtest/block/current",
11
21
  pollInterval: number = 2000,