@ledgerhq/coin-tester-bitcoin 1.1.0-nightly.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.
Files changed (81) hide show
  1. package/.env.example +7 -0
  2. package/.eslintrc.js +23 -0
  3. package/.turbo/turbo-build.log +4 -0
  4. package/.unimportedrc.json +8 -0
  5. package/CHANGELOG.md +18 -0
  6. package/LICENSE.txt +21 -0
  7. package/README.md +30 -0
  8. package/jest.config.ts +19 -0
  9. package/lib/src/assert.d.ts +6 -0
  10. package/lib/src/assert.d.ts.map +1 -0
  11. package/lib/src/assert.js +29 -0
  12. package/lib/src/assert.js.map +1 -0
  13. package/lib/src/atlas.d.ts +3 -0
  14. package/lib/src/atlas.d.ts.map +1 -0
  15. package/lib/src/atlas.js +84 -0
  16. package/lib/src/atlas.js.map +1 -0
  17. package/lib/src/constants.d.ts +5 -0
  18. package/lib/src/constants.d.ts.map +1 -0
  19. package/lib/src/constants.js +5 -0
  20. package/lib/src/constants.js.map +1 -0
  21. package/lib/src/fixtures.d.ts +5 -0
  22. package/lib/src/fixtures.d.ts.map +1 -0
  23. package/lib/src/fixtures.js +61 -0
  24. package/lib/src/fixtures.js.map +1 -0
  25. package/lib/src/helpers.d.ts +11 -0
  26. package/lib/src/helpers.d.ts.map +1 -0
  27. package/lib/src/helpers.js +339 -0
  28. package/lib/src/helpers.js.map +1 -0
  29. package/lib/src/scenarii/bitcoin.d.ts +4 -0
  30. package/lib/src/scenarii/bitcoin.d.ts.map +1 -0
  31. package/lib/src/scenarii/bitcoin.js +258 -0
  32. package/lib/src/scenarii/bitcoin.js.map +1 -0
  33. package/lib/src/utils.d.ts +5 -0
  34. package/lib/src/utils.d.ts.map +1 -0
  35. package/lib/src/utils.js +48 -0
  36. package/lib/src/utils.js.map +1 -0
  37. package/lib/tsconfig.tsbuildinfo +1 -0
  38. package/lib-es/src/assert.d.ts +6 -0
  39. package/lib-es/src/assert.d.ts.map +1 -0
  40. package/lib-es/src/assert.js +23 -0
  41. package/lib-es/src/assert.js.map +1 -0
  42. package/lib-es/src/atlas.d.ts +3 -0
  43. package/lib-es/src/atlas.d.ts.map +1 -0
  44. package/lib-es/src/atlas.js +43 -0
  45. package/lib-es/src/atlas.js.map +1 -0
  46. package/lib-es/src/constants.d.ts +5 -0
  47. package/lib-es/src/constants.d.ts.map +1 -0
  48. package/lib-es/src/constants.js +2 -0
  49. package/lib-es/src/constants.js.map +1 -0
  50. package/lib-es/src/fixtures.d.ts +5 -0
  51. package/lib-es/src/fixtures.d.ts.map +1 -0
  52. package/lib-es/src/fixtures.js +54 -0
  53. package/lib-es/src/fixtures.js.map +1 -0
  54. package/lib-es/src/helpers.d.ts +11 -0
  55. package/lib-es/src/helpers.d.ts.map +1 -0
  56. package/lib-es/src/helpers.js +323 -0
  57. package/lib-es/src/helpers.js.map +1 -0
  58. package/lib-es/src/scenarii/bitcoin.d.ts +4 -0
  59. package/lib-es/src/scenarii/bitcoin.d.ts.map +1 -0
  60. package/lib-es/src/scenarii/bitcoin.js +252 -0
  61. package/lib-es/src/scenarii/bitcoin.js.map +1 -0
  62. package/lib-es/src/utils.d.ts +5 -0
  63. package/lib-es/src/utils.d.ts.map +1 -0
  64. package/lib-es/src/utils.js +40 -0
  65. package/lib-es/src/utils.js.map +1 -0
  66. package/lib-es/tsconfig.tsbuildinfo +1 -0
  67. package/package.json +83 -0
  68. package/src/assert.ts +34 -0
  69. package/src/atlas.ts +52 -0
  70. package/src/constants.ts +1 -0
  71. package/src/docker/atlas/bitcoin.conf +56 -0
  72. package/src/docker/atlas/pending.conf +36 -0
  73. package/src/docker/docker-compose.yml +76 -0
  74. package/src/docker/nginx/default.conf +31 -0
  75. package/src/docker/postgres/create-db.sh +9 -0
  76. package/src/fixtures.ts +66 -0
  77. package/src/helpers.ts +372 -0
  78. package/src/scenarii/bitcoin.ts +306 -0
  79. package/src/scenarii.test.ts +27 -0
  80. package/src/utils.ts +52 -0
  81. package/tsconfig.json +14 -0
@@ -0,0 +1,306 @@
1
+ import { Scenario, ScenarioTransaction } from "@ledgerhq/coin-tester/main";
2
+ import { killSpeculos, spawnSpeculos } from "@ledgerhq/coin-tester/signers/speculos";
3
+ import Btc from "@ledgerhq/hw-app-btc";
4
+ import { BitcoinAccount, Transaction as BtcTransaction } from "@ledgerhq/coin-bitcoin/types";
5
+ import { createBridges } from "@ledgerhq/coin-bitcoin/bridge/js";
6
+ import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
7
+ import resolver from "@ledgerhq/coin-bitcoin/hw-getAddress";
8
+ import { LiveConfig } from "@ledgerhq/live-config/LiveConfig";
9
+ import { SignerContext } from "@ledgerhq/coin-bitcoin/signer";
10
+ import { BitcoinConfigInfo, setCoinConfig } from "@ledgerhq/coin-bitcoin/config";
11
+ import { BigNumber } from "bignumber.js";
12
+ import { defaultNanoApp } from "../constants";
13
+ import { loadWallet, mineToWalletAddress, sendTo } from "../helpers";
14
+ import { makeAccount } from "../fixtures";
15
+ import { killAtlas, spawnAtlas } from "../atlas";
16
+ import { findNewUtxo, waitForExplorerSync } from "../utils";
17
+ import {
18
+ assertCommonTxProperties,
19
+ assertUtxoExcluded,
20
+ assertUtxoSpent,
21
+ getNewChangeUtxos,
22
+ } from "../assert";
23
+
24
+ type BitcoinCoinConfig = {
25
+ info: BitcoinConfigInfo;
26
+ };
27
+ type BitcoinScenarioTransaction = ScenarioTransaction<BtcTransaction, BitcoinAccount>;
28
+
29
+ let firstUtxoHash = "";
30
+ let firstUtxoOutputIndex = 0;
31
+ let secondUtxoHash = "";
32
+ let secondUtxoOutputIndex = 0;
33
+
34
+ const makeScenarioTransactions = (): BitcoinScenarioTransaction[] => {
35
+ const scenarioExcludeTwoUtxos: BitcoinScenarioTransaction = {
36
+ name: "Send BTC excluding two UTXOs",
37
+ rbf: true,
38
+ utxoStrategy: {
39
+ strategy: 1, // Optimize size, for exemple
40
+ excludeUTXOs: [
41
+ { hash: firstUtxoHash, outputIndex: firstUtxoOutputIndex },
42
+ { hash: secondUtxoHash, outputIndex: secondUtxoOutputIndex },
43
+ ],
44
+ },
45
+ amount: new BigNumber(1e8),
46
+ recipient: "bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz",
47
+ expect: (previousAccount, currentAccount) => {
48
+ assertCommonTxProperties(previousAccount, currentAccount);
49
+
50
+ assertUtxoExcluded(currentAccount, firstUtxoHash, firstUtxoOutputIndex);
51
+ assertUtxoExcluded(currentAccount, secondUtxoHash, secondUtxoOutputIndex);
52
+
53
+ const newChangeUtxo = findNewUtxo(previousAccount, currentAccount);
54
+ expect(newChangeUtxo).toBeDefined();
55
+ },
56
+ };
57
+ const scenarioExcludeOneUtxo: BitcoinScenarioTransaction = {
58
+ name: "Send BTC excluding second UTXO",
59
+ rbf: true,
60
+ utxoStrategy: {
61
+ strategy: 1,
62
+ excludeUTXOs: [{ hash: secondUtxoHash, outputIndex: secondUtxoOutputIndex }],
63
+ },
64
+ useAllAmount: true,
65
+ recipient: "bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz",
66
+ expect: (previousAccount, currentAccount) => {
67
+ const latestOperation = assertCommonTxProperties(previousAccount, currentAccount);
68
+
69
+ // Excluded UTXO must still be there
70
+ assertUtxoExcluded(currentAccount, secondUtxoHash, secondUtxoOutputIndex);
71
+
72
+ // First UTXO must be spent
73
+ assertUtxoSpent(previousAccount, currentAccount, firstUtxoHash, firstUtxoOutputIndex);
74
+
75
+ // No change expected when using all amount
76
+ const changeUtxos = getNewChangeUtxos(previousAccount, currentAccount);
77
+ expect(changeUtxos.length).toBe(0);
78
+
79
+ const spentUtxos = previousAccount.bitcoinResources.utxos.filter(
80
+ utxo => !(utxo.hash === secondUtxoHash && utxo.outputIndex === secondUtxoOutputIndex),
81
+ );
82
+
83
+ const totalSpent = spentUtxos.reduce((sum, utxo) => sum.plus(utxo.value), new BigNumber(0));
84
+ const totalChange = changeUtxos.reduce((sum, utxo) => sum.plus(utxo.value), new BigNumber(0));
85
+ const expectedChange = totalSpent.minus(latestOperation.value);
86
+ expect(totalChange.toFixed()).toBe(expectedChange.toFixed());
87
+ },
88
+ };
89
+ const scenarioSendBtcTransaction: BitcoinScenarioTransaction = {
90
+ name: "Send 1 BTC",
91
+ rbf: false,
92
+ amount: new BigNumber(1e8),
93
+ recipient: "bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz",
94
+ expect: (previousAccount, currentAccount) => {
95
+ const [latestOperation] = currentAccount.operations;
96
+
97
+ expect(currentAccount.operations.length - previousAccount.operations.length).toBe(1);
98
+ expect(latestOperation.type).toBe("OUT");
99
+ expect(latestOperation.value.toFixed()).toBe(latestOperation.fee.plus(1e8).toFixed());
100
+ expect(currentAccount.balance.toFixed()).toBe(
101
+ previousAccount.balance.minus(latestOperation.value).toFixed(),
102
+ );
103
+ },
104
+ };
105
+ const scenarioSendFastFeesStrategyBtcTransaction: BitcoinScenarioTransaction = {
106
+ name: "Send Fast Fees Strategy BTC",
107
+ rbf: false,
108
+ feesStrategy: "fast",
109
+ amount: new BigNumber(1e6),
110
+ recipient: "bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz",
111
+ expect: (previousAccount, currentAccount) => {
112
+ const [latestOperation] = currentAccount.operations;
113
+
114
+ expect(currentAccount.operations.length - previousAccount.operations.length).toBe(1);
115
+ expect(latestOperation.type).toBe("OUT");
116
+ expect(currentAccount.balance.toFixed()).toBe(
117
+ previousAccount.balance.minus(latestOperation.value).toFixed(),
118
+ );
119
+ },
120
+ };
121
+ const scenarioSendSlowFeesStrategyBtcTransaction: BitcoinScenarioTransaction = {
122
+ name: "Send Slow Fees Strategy BTC",
123
+ rbf: false,
124
+ feesStrategy: "slow",
125
+ amount: new BigNumber(1e6),
126
+ recipient: "bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz",
127
+ expect: (previousAccount, currentAccount) => {
128
+ const [latestOperation] = currentAccount.operations;
129
+
130
+ expect(currentAccount.operations.length - previousAccount.operations.length).toBe(1);
131
+ expect(latestOperation.type).toBe("OUT");
132
+ expect(currentAccount.balance.toFixed()).toBe(
133
+ previousAccount.balance.minus(latestOperation.value).toFixed(),
134
+ );
135
+ },
136
+ };
137
+ const scenarioSendFIFOBtcTransaction: BitcoinScenarioTransaction = {
138
+ name: "Send FIFO BTC",
139
+ rbf: true,
140
+ utxoStrategy: { strategy: 0, excludeUTXOs: [] },
141
+ amount: new BigNumber(1e6),
142
+ recipient: "bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz",
143
+ expect: (previousAccount, currentAccount) => {
144
+ const [latestOperation] = currentAccount.operations;
145
+
146
+ expect(currentAccount.operations.length - previousAccount.operations.length).toBe(1);
147
+ expect(latestOperation.type).toBe("OUT");
148
+ expect(currentAccount.balance.toFixed()).toBe(
149
+ previousAccount.balance.minus(latestOperation.value).toFixed(),
150
+ );
151
+ },
152
+ };
153
+ const scenarioSendOptimizeSizeBtcTransaction: BitcoinScenarioTransaction = {
154
+ name: "Send Optimize Size BTC",
155
+ rbf: true,
156
+ utxoStrategy: {
157
+ strategy: 1,
158
+ excludeUTXOs: [],
159
+ },
160
+ amount: new BigNumber(1e6),
161
+ recipient: "bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz",
162
+ expect: (previousAccount, currentAccount) => {
163
+ const [latestOperation] = currentAccount.operations;
164
+
165
+ expect(currentAccount.operations.length - previousAccount.operations.length).toBe(1);
166
+ expect(latestOperation.type).toBe("OUT");
167
+ expect(currentAccount.balance.toFixed()).toBe(
168
+ previousAccount.balance.minus(latestOperation.value).toFixed(),
169
+ );
170
+ },
171
+ };
172
+ const scenarioSendMergeCoinsBtcTransaction: BitcoinScenarioTransaction = {
173
+ name: "Send Merge Coins BTC",
174
+ rbf: true,
175
+ utxoStrategy: { strategy: 2, excludeUTXOs: [] },
176
+ amount: new BigNumber(1e6),
177
+ recipient: "bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz",
178
+ expect: (previousAccount, currentAccount) => {
179
+ const [latestOperation] = currentAccount.operations;
180
+
181
+ expect(currentAccount.operations.length - previousAccount.operations.length).toBe(1);
182
+ expect(latestOperation.type).toBe("OUT");
183
+ expect(currentAccount.balance.toFixed()).toBe(
184
+ previousAccount.balance.minus(latestOperation.value).toFixed(),
185
+ );
186
+ },
187
+ };
188
+ const scenarioSendMaxBtcTransaction: BitcoinScenarioTransaction = {
189
+ name: "Send Max BTC",
190
+ rbf: false,
191
+ useAllAmount: true,
192
+ recipient: "bcrt1qajglhjtctn88f5l6rajzz52fy78fhxspjajjwz",
193
+ expect: (previousAccount, currentAccount) => {
194
+ const [latestOperation] = currentAccount.operations;
195
+
196
+ expect(currentAccount.operations.length - previousAccount.operations.length).toBe(1);
197
+ expect(latestOperation.type).toBe("OUT");
198
+ expect(currentAccount.balance.toFixed()).toBe(
199
+ previousAccount.balance.minus(latestOperation.value).toFixed(),
200
+ );
201
+ },
202
+ };
203
+ return [
204
+ scenarioExcludeTwoUtxos,
205
+ scenarioExcludeOneUtxo,
206
+ scenarioSendBtcTransaction,
207
+ scenarioSendFIFOBtcTransaction,
208
+ scenarioSendOptimizeSizeBtcTransaction,
209
+ scenarioSendMergeCoinsBtcTransaction,
210
+ scenarioSendFastFeesStrategyBtcTransaction,
211
+ scenarioSendSlowFeesStrategyBtcTransaction,
212
+ scenarioSendMaxBtcTransaction,
213
+ ];
214
+ };
215
+
216
+ export const scenarioBitcoin: Scenario<BtcTransaction, BitcoinAccount> = {
217
+ name: "Ledger Live Basic Bitcoin Transactions",
218
+ setup: async () => {
219
+ const [{ getOnSpeculosConfirmation, transport }] = await Promise.all([
220
+ spawnSpeculos(`/${defaultNanoApp.firmware}/BitcoinTest/app_${defaultNanoApp.version}.elf`),
221
+ spawnAtlas(),
222
+ ]);
223
+
224
+ const signerContext: SignerContext = (_, crypto, fn) =>
225
+ fn(new Btc({ transport, currency: BITCOIN.id }));
226
+
227
+ const coinConfig: BitcoinCoinConfig = {
228
+ info: {
229
+ status: {
230
+ type: "active",
231
+ },
232
+ },
233
+ };
234
+ setCoinConfig(() => ({ ...coinConfig }));
235
+ LiveConfig.setConfig({
236
+ config_currency_bitcoin_regtest: {
237
+ type: "object",
238
+ default: {
239
+ status: {
240
+ type: "active",
241
+ },
242
+ },
243
+ },
244
+ });
245
+
246
+ const onSignerConfirmation = getOnSpeculosConfirmation("Sign transaction");
247
+ const { accountBridge, currencyBridge } = createBridges(signerContext, () => coinConfig);
248
+ await currencyBridge.preload();
249
+ const BITCOIN = getCryptoCurrencyById("bitcoin_regtest");
250
+ const getAddress = resolver(signerContext);
251
+ // Can also test LEGACY here
252
+ const { address, publicKey } = await getAddress("", {
253
+ path: "49'/1'/0'/0/0",
254
+ currency: BITCOIN,
255
+ derivationMode: "segwit",
256
+ });
257
+ const { bitcoinLikeInfo } = BITCOIN;
258
+ const { XPUBVersion: xpubVersion } = bitcoinLikeInfo as {
259
+ XPUBVersion: number;
260
+ };
261
+
262
+ const xpub = await signerContext("", BITCOIN, signer =>
263
+ signer.getWalletXpub({
264
+ path: "49'/1'/0'",
265
+ xpubVersion,
266
+ }),
267
+ );
268
+ const scenarioAccount = makeAccount(xpub, publicKey, address, BITCOIN, "segwit");
269
+ await loadWallet("coinTester");
270
+ // Need to wait 100 blocks to be able to spend coinbase UTXOs
271
+ await mineToWalletAddress("101");
272
+ // Fund it with 7 BTC in three send to have 3 UTXOs
273
+ await sendTo(address, 2);
274
+ await sendTo(address, 3);
275
+ await sendTo(address, 2);
276
+ return {
277
+ accountBridge,
278
+ currencyBridge,
279
+ account: scenarioAccount,
280
+ onSignerConfirmation,
281
+ retryLimit: 0,
282
+ };
283
+ },
284
+ getTransactions: () => makeScenarioTransactions(),
285
+ beforeAll: async account => {
286
+ firstUtxoHash = (account as BitcoinAccount).bitcoinResources.utxos[0].hash;
287
+ firstUtxoOutputIndex = (account as BitcoinAccount).bitcoinResources.utxos[0].outputIndex;
288
+ secondUtxoHash = (account as BitcoinAccount).bitcoinResources.utxos[1].hash;
289
+ secondUtxoOutputIndex = (account as BitcoinAccount).bitcoinResources.utxos[1].outputIndex;
290
+ },
291
+ afterEach: async () => {
292
+ // Mine 2 blocks after each transaction to confirm it
293
+ await mineToWalletAddress("2");
294
+ },
295
+ beforeEach: async () => {
296
+ // Make sure explorer is in sync before each transaction
297
+ await waitForExplorerSync();
298
+ },
299
+ beforeSync: async () => {
300
+ // Make sure explorer is in sync before sync
301
+ await waitForExplorerSync();
302
+ },
303
+ teardown: async () => {
304
+ await Promise.all([killSpeculos(), killAtlas()]);
305
+ },
306
+ };
@@ -0,0 +1,27 @@
1
+ import console from "console";
2
+ import { executeScenario } from "@ledgerhq/coin-tester/main";
3
+ import { killSpeculos } from "@ledgerhq/coin-tester/signers/speculos";
4
+ import { scenarioBitcoin } from "./scenarii/bitcoin";
5
+ import { killAtlas } from "./atlas";
6
+
7
+ global.console = console;
8
+ jest.setTimeout(300_000);
9
+
10
+ describe("Bitcoin Deterministic Tester", () => {
11
+ it("scenario Bitcoin", async () => {
12
+ try {
13
+ await executeScenario(scenarioBitcoin);
14
+ } catch (e) {
15
+ if (e != "done") {
16
+ await Promise.all([killSpeculos(), killAtlas()]);
17
+ throw e;
18
+ }
19
+ }
20
+ });
21
+ });
22
+
23
+ ["exit", "SIGINT", "SIGQUIT", "SIGTERM", "SIGUSR1", "SIGUSR2", "uncaughtException"].map(e =>
24
+ process.on(e, async () => {
25
+ await Promise.all([killSpeculos(), killAtlas()]);
26
+ }),
27
+ );
package/src/utils.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types";
2
+ import { getCurrentBlock } from "./helpers";
3
+ import network from "@ledgerhq/live-network/network";
4
+
5
+ function sleep(ms: number) {
6
+ return new Promise(resolve => setTimeout(resolve, ms));
7
+ }
8
+
9
+ export async function waitForExplorerSync(
10
+ url: string = "http://localhost:9876/blockchain/v4/btc_regtest/block/current",
11
+ pollInterval: number = 2000,
12
+ ): Promise<void> {
13
+ let explorerBlock = 0;
14
+ let nodeBlock = await getCurrentBlock();
15
+ let i = 0;
16
+
17
+ console.log(`🕓 FIRST Waiting for explorer to sync... ${i} ${explorerBlock}/${nodeBlock}`);
18
+
19
+ while (explorerBlock < nodeBlock) {
20
+ try {
21
+ const { data } = await network({
22
+ method: "GET",
23
+ url,
24
+ });
25
+ explorerBlock = data?.height ?? 0;
26
+ } catch (error) {
27
+ console.error("❌ Error fetching explorer block:", error);
28
+ }
29
+
30
+ nodeBlock = await getCurrentBlock();
31
+ console.log(`🔁 Waiting for explorer to sync... ${i} ${explorerBlock}/${nodeBlock}`);
32
+ await sleep(pollInterval);
33
+ if (i > 100) {
34
+ throw new Error("Explorer sync timeout");
35
+ }
36
+ i++;
37
+ }
38
+
39
+ console.log(`✅ Explorer is synced at block ${explorerBlock}`);
40
+ }
41
+
42
+ export function findUtxo(account: BitcoinAccount, hash: string, index: number) {
43
+ return account.bitcoinResources.utxos.find(
44
+ utxo => utxo.hash === hash && utxo.outputIndex === index,
45
+ );
46
+ }
47
+
48
+ // Utility to find UTXO that didn’t exist before (i.e., new change output)
49
+ export function findNewUtxo(previous: BitcoinAccount, current: BitcoinAccount) {
50
+ const prevSet = new Set(previous.bitcoinResources.utxos.map(u => `${u.hash}:${u.outputIndex}`));
51
+ return current.bitcoinResources.utxos.find(u => !prevSet.has(`${u.hash}:${u.outputIndex}`));
52
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../../tsconfig.base",
3
+ "compilerOptions": {
4
+ "lib": ["es2020", "dom"],
5
+ "outDir": "lib",
6
+ "composite": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "downlevelIteration": true,
10
+ "sourceMap": true,
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src/**/*", "src/**/*.json"]
14
+ }