@ledgerhq/coin-tester-bitcoin 1.1.0-nightly.4 → 1.1.0

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-nightly.4",
3
+ "version": "1.1.0",
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-nightly.7",
52
- "@ledgerhq/coin-framework": "^6.8.0-nightly.7",
53
- "@ledgerhq/coin-tester": "^0.11.0-nightly.4",
54
- "@ledgerhq/cryptoassets": "^13.32.0-nightly.5",
51
+ "@ledgerhq/coin-bitcoin": "^0.24.0",
52
+ "@ledgerhq/coin-framework": "^6.8.0",
53
+ "@ledgerhq/coin-tester": "^0.11.0",
55
54
  "@ledgerhq/live-config": "^3.2.0",
56
- "@ledgerhq/live-network": "^2.1.0-nightly.1",
57
- "@ledgerhq/hw-app-btc": "^10.12.0-nightly.4",
58
- "@ledgerhq/types-cryptoassets": "^7.29.0",
59
- "@ledgerhq/types-live": "^6.88.0-nightly.3"
55
+ "@ledgerhq/live-network": "^2.1.0",
56
+ "@ledgerhq/cryptoassets": "^13.32.0",
57
+ "@ledgerhq/types-cryptoassets": "^7.30.0",
58
+ "@ledgerhq/hw-app-btc": "^10.12.0",
59
+ "@ledgerhq/types-live": "^6.88.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/jest": "^29.5.10",
package/src/helpers.ts CHANGED
@@ -15,6 +15,7 @@ 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 });
18
19
  return;
19
20
  } catch (error: any) {
20
21
  const message = error?.message || "Unknown error";
@@ -248,66 +249,113 @@ export const sendRaw = async (recipientAddress: string, amount: number, sequence
248
249
  }
249
250
  };
250
251
 
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
- }
264
-
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;
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
+ }
273
257
 
274
- // Try to estimate the original feerate
275
- let originalFeerate = 0.00001; // fallback (BTC/vB)
276
- let vsize = 200; // default guess
258
+ console.log(
259
+ `Sending replaceable transaction to ${recipientAddress} with amount ${amount} BTC...`,
260
+ );
277
261
 
278
262
  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.");
284
- }
263
+ // Step 1: Get an unspent UTXO
264
+ const unspent = await client.listUnspent({
265
+ query_options: { minimumSumAmount: amount },
266
+ });
285
267
 
286
- const newFeerate = originalFeerate * (1 + feeBumpPercent);
287
- const estimatedFee = newFeerate * vsize;
268
+ if (!unspent.length) {
269
+ console.error("No suitable UTXOs available.");
270
+ return;
271
+ }
288
272
 
289
- // Adjust amount by the new fee
290
- const adjustedAmount = Number(originalOutputValue - estimatedFee).toFixed(8);
273
+ const utxo = unspent[0];
291
274
 
292
- if (Number(adjustedAmount) <= 0) {
293
- throw new Error(`❌ Adjusted amount <= 0 (fee too large)`);
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
+ });
288
+
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
+ });
297
+
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);
294
304
  }
305
+ };
295
306
 
296
- const newRawTx = await client.createRawTransaction([{ txid: input.txid, vout: input.vout }], {
297
- [newRecipientAddress]: adjustedAmount,
298
- });
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
+ }
299
312
 
300
- const signed = await client.signRawTransactionWithWallet(newRawTx);
313
+ console.log(
314
+ `Sending replaceable transaction to ${recipientAddress} with amount ${amount} BTC...`,
315
+ );
301
316
 
302
- const replacementTxid = await client.sendRawTransaction(signed.hex, 0);
303
- console.log(`✅ Replaced TX: ${originalTxid} ${replacementTxid}`);
317
+ try {
318
+ // Step 1: Get an unspent UTXO
319
+ const unspent = await client.listUnspent({
320
+ query_options: { minimumSumAmount: amount },
321
+ });
304
322
 
305
- return replacementTxid;
306
- }
323
+ if (!unspent.length) {
324
+ console.error("No suitable UTXOs available.");
325
+ return;
326
+ }
327
+
328
+ const utxo = unspent[0];
329
+
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
+ });
343
+
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
+ });
307
352
 
308
- export const getRawMempool = async () => {
309
- const mempool = await client.getRawMempool();
310
- return mempool;
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
+ }
311
359
  };
312
360
 
313
361
  /*Available commands:
@@ -10,17 +10,10 @@ 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 {
14
- loadWallet,
15
- mineToWalletAddress,
16
- sendTo,
17
- sendTransaction,
18
- replaceTransaction,
19
- getRawMempool,
20
- } from "../helpers";
13
+ import { loadWallet, mineToWalletAddress, sendTo } from "../helpers";
21
14
  import { makeAccount } from "../fixtures";
22
15
  import { killAtlas, spawnAtlas } from "../atlas";
23
- import { findNewUtxo, waitForExplorerSync, waitForTxInMempool } from "../utils";
16
+ import { findNewUtxo, waitForExplorerSync } from "../utils";
24
17
  import {
25
18
  assertCommonTxProperties,
26
19
  assertUtxoExcluded,
@@ -220,46 +213,6 @@ const makeScenarioTransactions = (): BitcoinScenarioTransaction[] => {
220
213
  ];
221
214
  };
222
215
 
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
-
263
216
  export const scenarioBitcoin: Scenario<BtcTransaction, BitcoinAccount> = {
264
217
  name: "Ledger Live Basic Bitcoin Transactions",
265
218
  setup: async () => {
@@ -329,7 +282,6 @@ export const scenarioBitcoin: Scenario<BtcTransaction, BitcoinAccount> = {
329
282
  };
330
283
  },
331
284
  getTransactions: () => makeScenarioTransactions(),
332
- getInternalTransactions: () => makeInternalScenarioTransactions(),
333
285
  beforeAll: async account => {
334
286
  firstUtxoHash = (account as BitcoinAccount).bitcoinResources.utxos[0].hash;
335
287
  firstUtxoOutputIndex = (account as BitcoinAccount).bitcoinResources.utxos[0].outputIndex;
@@ -339,10 +291,6 @@ export const scenarioBitcoin: Scenario<BtcTransaction, BitcoinAccount> = {
339
291
  afterEach: async () => {
340
292
  // Mine 2 blocks after each transaction to confirm it
341
293
  await mineToWalletAddress("2");
342
- await waitForExplorerSync();
343
- },
344
- afterAll: async account => {
345
- expect(account.operations.length).toBeGreaterThanOrEqual(14);
346
294
  },
347
295
  beforeEach: async () => {
348
296
  // 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(500_000);
8
+ jest.setTimeout(300_000);
9
9
 
10
10
  describe("Bitcoin Deterministic Tester", () => {
11
11
  it("scenario Bitcoin", async () => {
package/src/utils.ts CHANGED
@@ -1,21 +1,11 @@
1
1
  import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types";
2
- import { getCurrentBlock, getRawMempool } from "./helpers";
2
+ import { getCurrentBlock } from "./helpers";
3
3
  import network from "@ledgerhq/live-network/network";
4
4
 
5
- export function sleep(ms: number) {
5
+ 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
-
19
9
  export async function waitForExplorerSync(
20
10
  url: string = "http://localhost:9876/blockchain/v4/btc_regtest/block/current",
21
11
  pollInterval: number = 2000,