@ledgerhq/coin-tester-bitcoin 1.1.0-nightly.3 → 1.1.0-nightly.4
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +11 -0
- package/lib/src/helpers.d.ts +3 -2
- package/lib/src/helpers.d.ts.map +1 -1
- package/lib/src/helpers.js +41 -90
- package/lib/src/helpers.js.map +1 -1
- package/lib/src/scenarii/bitcoin.d.ts.map +1 -1
- package/lib/src/scenarii/bitcoin.js +35 -0
- package/lib/src/scenarii/bitcoin.js.map +1 -1
- package/lib/src/utils.d.ts +2 -0
- package/lib/src/utils.d.ts.map +1 -1
- package/lib/src/utils.js +12 -0
- package/lib/src/utils.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib-es/src/helpers.d.ts +3 -2
- package/lib-es/src/helpers.d.ts.map +1 -1
- package/lib-es/src/helpers.js +37 -87
- package/lib-es/src/helpers.js.map +1 -1
- package/lib-es/src/scenarii/bitcoin.d.ts.map +1 -1
- package/lib-es/src/scenarii/bitcoin.js +37 -2
- package/lib-es/src/scenarii/bitcoin.js.map +1 -1
- package/lib-es/src/utils.d.ts +2 -0
- package/lib-es/src/utils.d.ts.map +1 -1
- package/lib-es/src/utils.js +12 -2
- package/lib-es/src/utils.js.map +1 -1
- package/lib-es/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/helpers.ts +48 -96
- package/src/scenarii/bitcoin.ts +54 -2
- package/src/scenarii.test.ts +1 -1
- package/src/utils.ts +12 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ledgerhq/coin-tester-bitcoin",
|
|
3
|
-
"version": "1.1.0-nightly.
|
|
3
|
+
"version": "1.1.0-nightly.4",
|
|
4
4
|
"description": "Ledger BTC Coin Tester",
|
|
5
5
|
"main": "src/scenarii.test.ts",
|
|
6
6
|
"keywords": [
|
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
"docker-compose": "^1.1.0",
|
|
51
51
|
"@ledgerhq/coin-bitcoin": "^0.24.0-nightly.7",
|
|
52
52
|
"@ledgerhq/coin-framework": "^6.8.0-nightly.7",
|
|
53
|
+
"@ledgerhq/coin-tester": "^0.11.0-nightly.4",
|
|
53
54
|
"@ledgerhq/cryptoassets": "^13.32.0-nightly.5",
|
|
54
55
|
"@ledgerhq/live-config": "^3.2.0",
|
|
55
56
|
"@ledgerhq/live-network": "^2.1.0-nightly.1",
|
|
56
57
|
"@ledgerhq/hw-app-btc": "^10.12.0-nightly.4",
|
|
57
58
|
"@ledgerhq/types-cryptoassets": "^7.29.0",
|
|
58
|
-
"@ledgerhq/types-live": "^6.88.0-nightly.3"
|
|
59
|
-
"@ledgerhq/coin-tester": "^0.11.0-nightly.3"
|
|
59
|
+
"@ledgerhq/types-live": "^6.88.0-nightly.3"
|
|
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
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
} catch
|
|
303
|
-
console.
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
);
|
|
289
|
+
// Adjust amount by the new fee
|
|
290
|
+
const adjustedAmount = Number(originalOutputValue - estimatedFee).toFixed(8);
|
|
316
291
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
296
|
+
const newRawTx = await client.createRawTransaction([{ txid: input.txid, vout: input.vout }], {
|
|
297
|
+
[newRecipientAddress]: adjustedAmount,
|
|
298
|
+
});
|
|
327
299
|
|
|
328
|
-
|
|
300
|
+
const signed = await client.signRawTransactionWithWallet(newRawTx);
|
|
329
301
|
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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:
|
package/src/scenarii/bitcoin.ts
CHANGED
|
@@ -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 {
|
|
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
|
package/src/scenarii.test.ts
CHANGED
|
@@ -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(
|
|
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,
|