@wopr-network/platform-core 1.66.1 → 1.67.1

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 (136) hide show
  1. package/dist/billing/crypto/btc/checkout.d.ts +4 -0
  2. package/dist/billing/crypto/btc/checkout.js +1 -2
  3. package/dist/billing/crypto/btc/index.d.ts +0 -4
  4. package/dist/billing/crypto/btc/index.js +0 -2
  5. package/dist/billing/crypto/evm/__tests__/checkout.test.js +8 -11
  6. package/dist/billing/crypto/evm/__tests__/eth-checkout.test.js +15 -1
  7. package/dist/billing/crypto/evm/checkout.d.ts +2 -0
  8. package/dist/billing/crypto/evm/checkout.js +1 -2
  9. package/dist/billing/crypto/evm/eth-checkout.d.ts +13 -2
  10. package/dist/billing/crypto/evm/eth-checkout.js +2 -4
  11. package/dist/billing/crypto/evm/eth-settler.d.ts +1 -1
  12. package/dist/billing/crypto/evm/index.d.ts +2 -8
  13. package/dist/billing/crypto/evm/index.js +0 -3
  14. package/dist/billing/crypto/evm/types.d.ts +16 -0
  15. package/dist/billing/crypto/index.d.ts +1 -6
  16. package/dist/billing/crypto/index.js +2 -3
  17. package/dist/billing/crypto/types.d.ts +0 -43
  18. package/dist/billing/crypto/types.js +1 -24
  19. package/package.json +1 -5
  20. package/src/billing/crypto/btc/checkout.ts +3 -2
  21. package/src/billing/crypto/btc/index.ts +0 -4
  22. package/src/billing/crypto/evm/__tests__/checkout.test.ts +10 -12
  23. package/src/billing/crypto/evm/__tests__/eth-checkout.test.ts +17 -1
  24. package/src/billing/crypto/evm/__tests__/eth-settler.test.ts +1 -1
  25. package/src/billing/crypto/evm/checkout.ts +3 -2
  26. package/src/billing/crypto/evm/eth-checkout.ts +15 -6
  27. package/src/billing/crypto/evm/eth-settler.ts +1 -1
  28. package/src/billing/crypto/evm/index.ts +8 -7
  29. package/src/billing/crypto/evm/types.ts +17 -0
  30. package/src/billing/crypto/index.ts +14 -12
  31. package/src/billing/crypto/types.ts +0 -63
  32. package/dist/billing/crypto/__tests__/address-gen.test.d.ts +0 -1
  33. package/dist/billing/crypto/__tests__/address-gen.test.js +0 -219
  34. package/dist/billing/crypto/__tests__/key-server.test.d.ts +0 -1
  35. package/dist/billing/crypto/__tests__/key-server.test.js +0 -363
  36. package/dist/billing/crypto/__tests__/watcher-service.test.d.ts +0 -1
  37. package/dist/billing/crypto/__tests__/watcher-service.test.js +0 -174
  38. package/dist/billing/crypto/address-gen.d.ts +0 -24
  39. package/dist/billing/crypto/address-gen.js +0 -176
  40. package/dist/billing/crypto/btc/__tests__/watcher.test.d.ts +0 -1
  41. package/dist/billing/crypto/btc/__tests__/watcher.test.js +0 -170
  42. package/dist/billing/crypto/btc/watcher.d.ts +0 -44
  43. package/dist/billing/crypto/btc/watcher.js +0 -118
  44. package/dist/billing/crypto/evm/__tests__/eth-watcher.test.d.ts +0 -1
  45. package/dist/billing/crypto/evm/__tests__/eth-watcher.test.js +0 -167
  46. package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.d.ts +0 -1
  47. package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.js +0 -159
  48. package/dist/billing/crypto/evm/__tests__/watcher.test.d.ts +0 -1
  49. package/dist/billing/crypto/evm/__tests__/watcher.test.js +0 -145
  50. package/dist/billing/crypto/evm/eth-watcher.d.ts +0 -66
  51. package/dist/billing/crypto/evm/eth-watcher.js +0 -121
  52. package/dist/billing/crypto/evm/watcher.d.ts +0 -51
  53. package/dist/billing/crypto/evm/watcher.js +0 -156
  54. package/dist/billing/crypto/key-server-entry.d.ts +0 -1
  55. package/dist/billing/crypto/key-server-entry.js +0 -122
  56. package/dist/billing/crypto/key-server.d.ts +0 -32
  57. package/dist/billing/crypto/key-server.js +0 -348
  58. package/dist/billing/crypto/oracle/__tests__/chainlink.test.d.ts +0 -1
  59. package/dist/billing/crypto/oracle/__tests__/chainlink.test.js +0 -83
  60. package/dist/billing/crypto/oracle/__tests__/coingecko.test.d.ts +0 -1
  61. package/dist/billing/crypto/oracle/__tests__/coingecko.test.js +0 -65
  62. package/dist/billing/crypto/oracle/__tests__/composite.test.d.ts +0 -1
  63. package/dist/billing/crypto/oracle/__tests__/composite.test.js +0 -48
  64. package/dist/billing/crypto/oracle/__tests__/convert.test.d.ts +0 -1
  65. package/dist/billing/crypto/oracle/__tests__/convert.test.js +0 -61
  66. package/dist/billing/crypto/oracle/__tests__/fixed.test.d.ts +0 -1
  67. package/dist/billing/crypto/oracle/__tests__/fixed.test.js +0 -20
  68. package/dist/billing/crypto/oracle/chainlink.d.ts +0 -26
  69. package/dist/billing/crypto/oracle/chainlink.js +0 -62
  70. package/dist/billing/crypto/oracle/coingecko.d.ts +0 -22
  71. package/dist/billing/crypto/oracle/coingecko.js +0 -71
  72. package/dist/billing/crypto/oracle/composite.d.ts +0 -14
  73. package/dist/billing/crypto/oracle/composite.js +0 -34
  74. package/dist/billing/crypto/oracle/convert.d.ts +0 -30
  75. package/dist/billing/crypto/oracle/convert.js +0 -51
  76. package/dist/billing/crypto/oracle/fixed.d.ts +0 -10
  77. package/dist/billing/crypto/oracle/fixed.js +0 -22
  78. package/dist/billing/crypto/oracle/index.d.ts +0 -9
  79. package/dist/billing/crypto/oracle/index.js +0 -6
  80. package/dist/billing/crypto/oracle/types.d.ts +0 -22
  81. package/dist/billing/crypto/oracle/types.js +0 -7
  82. package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +0 -1
  83. package/dist/billing/crypto/plugin/__tests__/integration.test.js +0 -58
  84. package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +0 -1
  85. package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +0 -46
  86. package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +0 -1
  87. package/dist/billing/crypto/plugin/__tests__/registry.test.js +0 -49
  88. package/dist/billing/crypto/plugin/index.d.ts +0 -2
  89. package/dist/billing/crypto/plugin/index.js +0 -1
  90. package/dist/billing/crypto/plugin/interfaces.d.ts +0 -97
  91. package/dist/billing/crypto/plugin/interfaces.js +0 -2
  92. package/dist/billing/crypto/plugin/registry.d.ts +0 -8
  93. package/dist/billing/crypto/plugin/registry.js +0 -21
  94. package/dist/billing/crypto/plugin-watcher-service.d.ts +0 -32
  95. package/dist/billing/crypto/plugin-watcher-service.js +0 -113
  96. package/dist/billing/crypto/tron/__tests__/address-convert.test.d.ts +0 -1
  97. package/dist/billing/crypto/tron/__tests__/address-convert.test.js +0 -55
  98. package/dist/billing/crypto/tron/address-convert.d.ts +0 -14
  99. package/dist/billing/crypto/tron/address-convert.js +0 -93
  100. package/dist/billing/crypto/watcher-service.d.ts +0 -55
  101. package/dist/billing/crypto/watcher-service.js +0 -438
  102. package/src/billing/crypto/__tests__/address-gen.test.ts +0 -264
  103. package/src/billing/crypto/__tests__/key-server.test.ts +0 -395
  104. package/src/billing/crypto/__tests__/watcher-service.test.ts +0 -242
  105. package/src/billing/crypto/address-gen.ts +0 -185
  106. package/src/billing/crypto/btc/__tests__/watcher.test.ts +0 -201
  107. package/src/billing/crypto/btc/watcher.ts +0 -161
  108. package/src/billing/crypto/evm/__tests__/eth-watcher.test.ts +0 -190
  109. package/src/billing/crypto/evm/__tests__/watcher-confirmations.test.ts +0 -191
  110. package/src/billing/crypto/evm/__tests__/watcher.test.ts +0 -167
  111. package/src/billing/crypto/evm/eth-watcher.ts +0 -182
  112. package/src/billing/crypto/evm/watcher.ts +0 -204
  113. package/src/billing/crypto/key-server-entry.ts +0 -144
  114. package/src/billing/crypto/key-server.ts +0 -444
  115. package/src/billing/crypto/oracle/__tests__/chainlink.test.ts +0 -107
  116. package/src/billing/crypto/oracle/__tests__/coingecko.test.ts +0 -75
  117. package/src/billing/crypto/oracle/__tests__/composite.test.ts +0 -61
  118. package/src/billing/crypto/oracle/__tests__/convert.test.ts +0 -74
  119. package/src/billing/crypto/oracle/__tests__/fixed.test.ts +0 -23
  120. package/src/billing/crypto/oracle/chainlink.ts +0 -86
  121. package/src/billing/crypto/oracle/coingecko.ts +0 -96
  122. package/src/billing/crypto/oracle/composite.ts +0 -35
  123. package/src/billing/crypto/oracle/convert.ts +0 -53
  124. package/src/billing/crypto/oracle/fixed.ts +0 -25
  125. package/src/billing/crypto/oracle/index.ts +0 -9
  126. package/src/billing/crypto/oracle/types.ts +0 -28
  127. package/src/billing/crypto/plugin/__tests__/integration.test.ts +0 -64
  128. package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +0 -51
  129. package/src/billing/crypto/plugin/__tests__/registry.test.ts +0 -58
  130. package/src/billing/crypto/plugin/index.ts +0 -17
  131. package/src/billing/crypto/plugin/interfaces.ts +0 -106
  132. package/src/billing/crypto/plugin/registry.ts +0 -26
  133. package/src/billing/crypto/plugin-watcher-service.ts +0 -148
  134. package/src/billing/crypto/tron/__tests__/address-convert.test.ts +0 -67
  135. package/src/billing/crypto/tron/address-convert.ts +0 -89
  136. package/src/billing/crypto/watcher-service.ts +0 -549
@@ -1,176 +0,0 @@
1
- /**
2
- * Universal address derivation — one function for all secp256k1 chains.
3
- *
4
- * Encoding type and chain-specific params (HRP, version byte) are read from
5
- * the paymentMethods DB row, not hardcoded per chain. Adding a new chain
6
- * is a DB INSERT, not a code change.
7
- *
8
- * Supported encodings:
9
- * - bech32: BTC (bc1q...), LTC (ltc1q...) — params: { hrp }
10
- * - p2pkh: DOGE (D...), TRON (T...) — params: { version }
11
- * - evm: ETH, ERC-20 (0x...) — params: {}
12
- */
13
- import { secp256k1 } from "@noble/curves/secp256k1.js";
14
- import { ripemd160 } from "@noble/hashes/legacy.js";
15
- import { sha256 } from "@noble/hashes/sha2.js";
16
- import { keccak_256 } from "@noble/hashes/sha3.js";
17
- import { bech32 } from "@scure/base";
18
- import { HDKey } from "@scure/bip32";
19
- import { publicKeyToAddress } from "viem/accounts";
20
- // ---------- encoding helpers ----------
21
- const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
22
- function base58encode(data) {
23
- let num = 0n;
24
- for (const byte of data)
25
- num = num * 256n + BigInt(byte);
26
- let encoded = "";
27
- while (num > 0n) {
28
- encoded = BASE58_ALPHABET[Number(num % 58n)] + encoded;
29
- num = num / 58n;
30
- }
31
- for (const byte of data) {
32
- if (byte !== 0)
33
- break;
34
- encoded = `1${encoded}`;
35
- }
36
- return encoded;
37
- }
38
- function hash160(pubkey) {
39
- return ripemd160(sha256(pubkey));
40
- }
41
- function encodeBech32(pubkey, hrp) {
42
- const h = hash160(pubkey);
43
- const words = bech32.toWords(h);
44
- return bech32.encode(hrp, [0, ...words]);
45
- }
46
- function encodeP2pkh(pubkey, versionByte) {
47
- const h = hash160(pubkey);
48
- const payload = new Uint8Array(21);
49
- payload[0] = versionByte;
50
- payload.set(h, 1);
51
- const checksum = sha256(sha256(payload));
52
- const full = new Uint8Array(25);
53
- full.set(payload);
54
- full.set(checksum.slice(0, 4), 21);
55
- return base58encode(full);
56
- }
57
- function encodeEvm(pubkey) {
58
- // HDKey.publicKey is SEC1 compressed (33 bytes, 02/03 prefix).
59
- // Ethereum addresses = keccak256(uncompressed_pubkey[1:]).slice(-20).
60
- // viem's publicKeyToAddress expects uncompressed (65 bytes, 04 prefix).
61
- // Decompress via secp256k1 point recovery before hashing.
62
- const hexKey = Array.from(pubkey, (b) => b.toString(16).padStart(2, "0")).join("");
63
- const uncompressed = secp256k1.Point.fromHex(hexKey).toBytes(false);
64
- const hexPubKey = `0x${Array.from(uncompressed, (b) => b.toString(16).padStart(2, "0")).join("")}`;
65
- return publicKeyToAddress(hexPubKey);
66
- }
67
- /** Keccak256-based Base58Check (Tron-style): keccak256(uncompressed[1:]) → last 20 bytes → version + checksum → Base58 */
68
- function encodeKeccakB58(pubkey, versionByte) {
69
- const hexKey = Array.from(pubkey, (b) => b.toString(16).padStart(2, "0")).join("");
70
- const uncompressed = secp256k1.Point.fromHex(hexKey).toBytes(false);
71
- const hash = keccak_256(uncompressed.slice(1));
72
- const addressBytes = hash.slice(-20);
73
- const payload = new Uint8Array(21);
74
- payload[0] = versionByte;
75
- payload.set(addressBytes, 1);
76
- const checksum = sha256(sha256(payload));
77
- const full = new Uint8Array(25);
78
- full.set(payload);
79
- full.set(checksum.slice(0, 4), 21);
80
- return base58encode(full);
81
- }
82
- // ---------- public API ----------
83
- /**
84
- * Derive a deposit address from an xpub at a given BIP-44 index.
85
- * Path: xpub / 0 / index (external chain).
86
- * No private keys involved.
87
- *
88
- * @param xpub - Extended public key
89
- * @param index - Derivation index (0, 1, 2, ...)
90
- * @param addressType - Encoding type: "bech32", "p2pkh", "evm"
91
- * @param params - Chain-specific encoding params from DB (parsed JSON)
92
- */
93
- export function deriveAddress(xpub, index, addressType, params = {}) {
94
- if (!Number.isInteger(index) || index < 0)
95
- throw new Error(`Invalid derivation index: ${index}`);
96
- const master = HDKey.fromExtendedKey(xpub);
97
- const child = master.deriveChild(0).deriveChild(index);
98
- if (!child.publicKey)
99
- throw new Error("Failed to derive public key");
100
- switch (addressType) {
101
- case "bech32": {
102
- if (!params.hrp)
103
- throw new Error("bech32 encoding requires 'hrp' param");
104
- return encodeBech32(child.publicKey, params.hrp);
105
- }
106
- case "p2pkh": {
107
- if (!params.version)
108
- throw new Error("p2pkh encoding requires 'version' param");
109
- const versionByte = Number(params.version);
110
- if (!Number.isInteger(versionByte) || versionByte < 0 || versionByte > 255)
111
- throw new Error(`Invalid p2pkh version byte: ${params.version}`);
112
- return encodeP2pkh(child.publicKey, versionByte);
113
- }
114
- case "keccak-b58check": {
115
- if (!params.version)
116
- throw new Error("keccak-b58check encoding requires 'version' param");
117
- const versionByte = Number(params.version);
118
- if (!Number.isInteger(versionByte) || versionByte < 0 || versionByte > 255)
119
- throw new Error(`Invalid keccak-b58check version byte: ${params.version}`);
120
- return encodeKeccakB58(child.publicKey, versionByte);
121
- }
122
- case "evm":
123
- return encodeEvm(child.publicKey);
124
- default:
125
- throw new Error(`Unknown address type: ${addressType}`);
126
- }
127
- }
128
- /**
129
- * Derive the treasury address (internal chain, index 0).
130
- * Used for sweep destinations.
131
- */
132
- export function deriveTreasury(xpub, addressType, params = {}) {
133
- const master = HDKey.fromExtendedKey(xpub);
134
- const child = master.deriveChild(1).deriveChild(0); // internal chain
135
- if (!child.publicKey)
136
- throw new Error("Failed to derive public key");
137
- switch (addressType) {
138
- case "bech32": {
139
- if (!params.hrp)
140
- throw new Error("bech32 encoding requires 'hrp' param");
141
- return encodeBech32(child.publicKey, params.hrp);
142
- }
143
- case "p2pkh": {
144
- if (!params.version)
145
- throw new Error("p2pkh encoding requires 'version' param");
146
- const versionByte = Number(params.version);
147
- if (!Number.isInteger(versionByte) || versionByte < 0 || versionByte > 255)
148
- throw new Error(`Invalid p2pkh version byte: ${params.version}`);
149
- return encodeP2pkh(child.publicKey, versionByte);
150
- }
151
- case "keccak-b58check": {
152
- if (!params.version)
153
- throw new Error("keccak-b58check encoding requires 'version' param");
154
- const versionByte = Number(params.version);
155
- if (!Number.isInteger(versionByte) || versionByte < 0 || versionByte > 255)
156
- throw new Error(`Invalid keccak-b58check version byte: ${params.version}`);
157
- return encodeKeccakB58(child.publicKey, versionByte);
158
- }
159
- case "evm":
160
- return encodeEvm(child.publicKey);
161
- default:
162
- throw new Error(`Unknown address type: ${addressType}`);
163
- }
164
- }
165
- /** Validate that a string is an xpub (not xprv). */
166
- export function isValidXpub(key) {
167
- if (!key.startsWith("xpub"))
168
- return false;
169
- try {
170
- HDKey.fromExtendedKey(key);
171
- return true;
172
- }
173
- catch {
174
- return false;
175
- }
176
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,170 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { BtcWatcher } from "../watcher.js";
3
- function makeCursorStore() {
4
- const processed = new Set();
5
- const confirmationCounts = new Map();
6
- return {
7
- get: vi.fn().mockResolvedValue(null),
8
- save: vi.fn().mockResolvedValue(undefined),
9
- hasProcessedTx: vi.fn().mockImplementation(async (_, txId) => processed.has(txId)),
10
- markProcessedTx: vi.fn().mockImplementation(async (_, txId) => {
11
- processed.add(txId);
12
- }),
13
- getConfirmationCount: vi
14
- .fn()
15
- .mockImplementation(async (_, txId) => confirmationCounts.get(txId) ?? null),
16
- saveConfirmationCount: vi.fn().mockImplementation(async (_, txId, count) => {
17
- confirmationCounts.set(txId, count);
18
- }),
19
- _processed: processed,
20
- _confirmationCounts: confirmationCounts,
21
- };
22
- }
23
- function makeOracle() {
24
- return { getPrice: vi.fn().mockResolvedValue({ priceMicros: 65_000_000_000 }) };
25
- }
26
- describe("BtcWatcher — intermediate confirmations", () => {
27
- it("fires onPayment at 0 confirmations when tx first detected", async () => {
28
- const events = [];
29
- const cursorStore = makeCursorStore();
30
- const rpc = vi
31
- .fn()
32
- .mockResolvedValueOnce([{ address: "bc1qtest", amount: 0.0005, confirmations: 0, txids: ["tx1"] }])
33
- .mockResolvedValueOnce({
34
- details: [{ address: "bc1qtest", amount: 0.0005, category: "receive" }],
35
- confirmations: 0,
36
- });
37
- const watcher = new BtcWatcher({
38
- config: { rpcUrl: "http://localhost", rpcUser: "u", rpcPassword: "p", network: "regtest", confirmations: 3 },
39
- rpcCall: rpc,
40
- watchedAddresses: ["bc1qtest"],
41
- oracle: makeOracle(),
42
- cursorStore,
43
- onPayment: (evt) => {
44
- events.push(evt);
45
- },
46
- });
47
- await watcher.poll();
48
- expect(events).toHaveLength(1);
49
- expect(events[0].confirmations).toBe(0);
50
- expect(events[0].confirmationsRequired).toBe(3);
51
- });
52
- it("fires onPayment on each confirmation increment", async () => {
53
- const events = [];
54
- const cursorStore = makeCursorStore();
55
- cursorStore._confirmationCounts.set("tx1", 1);
56
- const rpc = vi
57
- .fn()
58
- .mockResolvedValueOnce([{ address: "bc1qtest", amount: 0.0005, confirmations: 2, txids: ["tx1"] }])
59
- .mockResolvedValueOnce({
60
- details: [{ address: "bc1qtest", amount: 0.0005, category: "receive" }],
61
- confirmations: 2,
62
- });
63
- const watcher = new BtcWatcher({
64
- config: { rpcUrl: "http://localhost", rpcUser: "u", rpcPassword: "p", network: "regtest", confirmations: 3 },
65
- rpcCall: rpc,
66
- watchedAddresses: ["bc1qtest"],
67
- oracle: makeOracle(),
68
- cursorStore,
69
- onPayment: (evt) => {
70
- events.push(evt);
71
- },
72
- });
73
- await watcher.poll();
74
- expect(events).toHaveLength(1);
75
- expect(events[0].confirmations).toBe(2);
76
- });
77
- it("does not fire when confirmation count unchanged", async () => {
78
- const events = [];
79
- const cursorStore = makeCursorStore();
80
- cursorStore._confirmationCounts.set("tx1", 2);
81
- const rpc = vi
82
- .fn()
83
- .mockResolvedValueOnce([{ address: "bc1qtest", amount: 0.0005, confirmations: 2, txids: ["tx1"] }])
84
- .mockResolvedValueOnce({
85
- details: [{ address: "bc1qtest", amount: 0.0005, category: "receive" }],
86
- confirmations: 2,
87
- });
88
- const watcher = new BtcWatcher({
89
- config: { rpcUrl: "http://localhost", rpcUser: "u", rpcPassword: "p", network: "regtest", confirmations: 3 },
90
- rpcCall: rpc,
91
- watchedAddresses: ["bc1qtest"],
92
- oracle: makeOracle(),
93
- cursorStore,
94
- onPayment: (evt) => {
95
- events.push(evt);
96
- },
97
- });
98
- await watcher.poll();
99
- expect(events).toHaveLength(0);
100
- });
101
- it("marks tx as processed once confirmations reach threshold", async () => {
102
- const events = [];
103
- const cursorStore = makeCursorStore();
104
- cursorStore._confirmationCounts.set("tx1", 2);
105
- const rpc = vi
106
- .fn()
107
- .mockResolvedValueOnce([{ address: "bc1qtest", amount: 0.0005, confirmations: 3, txids: ["tx1"] }])
108
- .mockResolvedValueOnce({
109
- details: [{ address: "bc1qtest", amount: 0.0005, category: "receive" }],
110
- confirmations: 3,
111
- });
112
- const watcher = new BtcWatcher({
113
- config: { rpcUrl: "http://localhost", rpcUser: "u", rpcPassword: "p", network: "regtest", confirmations: 3 },
114
- rpcCall: rpc,
115
- watchedAddresses: ["bc1qtest"],
116
- oracle: makeOracle(),
117
- cursorStore,
118
- onPayment: (evt) => {
119
- events.push(evt);
120
- },
121
- });
122
- await watcher.poll();
123
- expect(events).toHaveLength(1);
124
- expect(events[0].confirmations).toBe(3);
125
- expect(cursorStore.markProcessedTx).toHaveBeenCalledWith(expect.any(String), "tx1");
126
- });
127
- it("skips fully-processed txids", async () => {
128
- const events = [];
129
- const cursorStore = makeCursorStore();
130
- cursorStore._processed.add("tx1");
131
- const rpc = vi
132
- .fn()
133
- .mockResolvedValueOnce([{ address: "bc1qtest", amount: 0.0005, confirmations: 6, txids: ["tx1"] }]);
134
- const watcher = new BtcWatcher({
135
- config: { rpcUrl: "http://localhost", rpcUser: "u", rpcPassword: "p", network: "regtest", confirmations: 3 },
136
- rpcCall: rpc,
137
- watchedAddresses: ["bc1qtest"],
138
- oracle: makeOracle(),
139
- cursorStore,
140
- onPayment: (evt) => {
141
- events.push(evt);
142
- },
143
- });
144
- await watcher.poll();
145
- expect(events).toHaveLength(0);
146
- });
147
- it("includes confirmationsRequired in event", async () => {
148
- const events = [];
149
- const cursorStore = makeCursorStore();
150
- const rpc = vi
151
- .fn()
152
- .mockResolvedValueOnce([{ address: "bc1qtest", amount: 0.001, confirmations: 0, txids: ["txNew"] }])
153
- .mockResolvedValueOnce({
154
- details: [{ address: "bc1qtest", amount: 0.001, category: "receive" }],
155
- confirmations: 0,
156
- });
157
- const watcher = new BtcWatcher({
158
- config: { rpcUrl: "http://localhost", rpcUser: "u", rpcPassword: "p", network: "regtest", confirmations: 6 },
159
- rpcCall: rpc,
160
- watchedAddresses: ["bc1qtest"],
161
- oracle: makeOracle(),
162
- cursorStore,
163
- onPayment: (evt) => {
164
- events.push(evt);
165
- },
166
- });
167
- await watcher.poll();
168
- expect(events[0].confirmationsRequired).toBe(6);
169
- });
170
- });
@@ -1,44 +0,0 @@
1
- import type { IWatcherCursorStore } from "../cursor-store.js";
2
- import type { IPriceOracle } from "../oracle/types.js";
3
- import type { BitcoindConfig, BtcPaymentEvent } from "./types.js";
4
- type RpcCall = (method: string, params: unknown[]) => Promise<unknown>;
5
- export interface BtcWatcherOpts {
6
- config: BitcoindConfig;
7
- rpcCall: RpcCall;
8
- /** Addresses to watch (must be imported into bitcoind wallet first). */
9
- watchedAddresses: string[];
10
- onPayment: (event: BtcPaymentEvent) => void | Promise<void>;
11
- /** Price oracle for BTC/USD conversion. */
12
- oracle: IPriceOracle;
13
- /** Required — BTC has no block cursor, so txid dedup must be persisted. */
14
- cursorStore: IWatcherCursorStore;
15
- /** Override chain identity for cursor namespace (default: config.network). Prevents txid collisions across BTC/LTC/DOGE. */
16
- chainId?: string;
17
- }
18
- export declare class BtcWatcher {
19
- private readonly rpc;
20
- private readonly addresses;
21
- private readonly onPayment;
22
- private readonly minConfirmations;
23
- private readonly oracle;
24
- private readonly cursorStore;
25
- private readonly watcherId;
26
- constructor(opts: BtcWatcherOpts);
27
- /** Update the set of watched addresses. */
28
- setWatchedAddresses(addresses: string[]): void;
29
- /**
30
- * Import an address into bitcoind's wallet (watch-only).
31
- * Uses `importdescriptors` (modern bitcoind v24+) with fallback to legacy `importaddress`.
32
- */
33
- importAddress(address: string): Promise<void>;
34
- /**
35
- * Poll for payments to watched addresses, including unconfirmed txs.
36
- *
37
- * Fires onPayment on every confirmation increment (0, 1, 2, ... threshold).
38
- * Only marks a tx as fully processed once it reaches the confirmation threshold.
39
- */
40
- poll(): Promise<void>;
41
- }
42
- /** Create a bitcoind JSON-RPC caller with basic auth. */
43
- export declare function createBitcoindRpc(config: BitcoindConfig): RpcCall;
44
- export {};
@@ -1,118 +0,0 @@
1
- import { nativeToCents } from "../oracle/convert.js";
2
- export class BtcWatcher {
3
- rpc;
4
- addresses;
5
- onPayment;
6
- minConfirmations;
7
- oracle;
8
- cursorStore;
9
- watcherId;
10
- constructor(opts) {
11
- this.rpc = opts.rpcCall;
12
- this.addresses = new Set(opts.watchedAddresses);
13
- this.onPayment = opts.onPayment;
14
- this.minConfirmations = opts.config.confirmations;
15
- this.oracle = opts.oracle;
16
- this.cursorStore = opts.cursorStore;
17
- this.watcherId = `btc:${opts.chainId ?? opts.config.network}`;
18
- }
19
- /** Update the set of watched addresses. */
20
- setWatchedAddresses(addresses) {
21
- this.addresses.clear();
22
- for (const a of addresses)
23
- this.addresses.add(a);
24
- }
25
- /**
26
- * Import an address into bitcoind's wallet (watch-only).
27
- * Uses `importdescriptors` (modern bitcoind v24+) with fallback to legacy `importaddress`.
28
- */
29
- async importAddress(address) {
30
- try {
31
- // Modern bitcoind: get descriptor checksum, then import
32
- const info = (await this.rpc("getdescriptorinfo", [`addr(${address})`]));
33
- const result = (await this.rpc("importdescriptors", [[{ desc: info.descriptor, timestamp: 0 }]]));
34
- if (result[0] && !result[0].success) {
35
- throw new Error(result[0].error?.message ?? "importdescriptors failed");
36
- }
37
- }
38
- catch {
39
- // Fallback: legacy importaddress (bitcoind <v24)
40
- await this.rpc("importaddress", [address, "", false]);
41
- }
42
- this.addresses.add(address);
43
- }
44
- /**
45
- * Poll for payments to watched addresses, including unconfirmed txs.
46
- *
47
- * Fires onPayment on every confirmation increment (0, 1, 2, ... threshold).
48
- * Only marks a tx as fully processed once it reaches the confirmation threshold.
49
- */
50
- async poll() {
51
- if (this.addresses.size === 0)
52
- return;
53
- // Poll with minconf=0 to see unconfirmed txs
54
- const received = (await this.rpc("listreceivedbyaddress", [
55
- 0, // minconf=0: see ALL txs including unconfirmed
56
- false, // include_empty
57
- true, // include_watchonly
58
- ]));
59
- const { priceMicros } = await this.oracle.getPrice("BTC");
60
- for (const entry of received) {
61
- if (!this.addresses.has(entry.address))
62
- continue;
63
- for (const txid of entry.txids) {
64
- // Skip fully-processed txids (already reached threshold, persisted to DB)
65
- if (await this.cursorStore.hasProcessedTx(this.watcherId, txid))
66
- continue;
67
- // Get transaction details for the exact amount sent to this address
68
- const tx = (await this.rpc("gettransaction", [txid, true]));
69
- const detail = tx.details.find((d) => d.address === entry.address && d.category === "receive");
70
- if (!detail)
71
- continue;
72
- // Check if confirmations have increased since last seen
73
- const lastSeen = await this.cursorStore.getConfirmationCount(this.watcherId, txid);
74
- if (lastSeen !== null && tx.confirmations <= lastSeen)
75
- continue; // No change
76
- const amountSats = Math.round(detail.amount * 100_000_000);
77
- // priceMicros is microdollars per 1 BTC. Convert sats→USD cents via nativeToCents.
78
- const amountUsdCents = nativeToCents(BigInt(amountSats), priceMicros, 8);
79
- const event = {
80
- address: entry.address,
81
- txid,
82
- amountSats,
83
- amountUsdCents,
84
- confirmations: tx.confirmations,
85
- confirmationsRequired: this.minConfirmations,
86
- };
87
- await this.onPayment(event);
88
- // Persist confirmation count
89
- await this.cursorStore.saveConfirmationCount(this.watcherId, txid, tx.confirmations);
90
- // Mark as fully processed once we reach the threshold
91
- if (tx.confirmations >= this.minConfirmations) {
92
- await this.cursorStore.markProcessedTx(this.watcherId, txid);
93
- }
94
- }
95
- }
96
- }
97
- }
98
- /** Create a bitcoind JSON-RPC caller with basic auth. */
99
- export function createBitcoindRpc(config) {
100
- let id = 0;
101
- const auth = btoa(`${config.rpcUser}:${config.rpcPassword}`);
102
- return async (method, params) => {
103
- const res = await fetch(config.rpcUrl, {
104
- method: "POST",
105
- headers: {
106
- "Content-Type": "application/json",
107
- Authorization: `Basic ${auth}`,
108
- },
109
- body: JSON.stringify({ jsonrpc: "1.0", id: ++id, method, params }),
110
- });
111
- if (!res.ok)
112
- throw new Error(`bitcoind ${method} failed: ${res.status}`);
113
- const data = (await res.json());
114
- if (data.error)
115
- throw new Error(`bitcoind ${method}: ${data.error.message}`);
116
- return data.result;
117
- };
118
- }
@@ -1,167 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { EthWatcher } from "../eth-watcher.js";
3
- function makeRpc(responses) {
4
- return vi.fn(async (method) => responses[method]);
5
- }
6
- const mockOracle = { getPrice: vi.fn().mockResolvedValue({ priceMicros: 3_500_000_000, updatedAt: new Date() }) };
7
- describe("EthWatcher", () => {
8
- it("detects native ETH transfer to watched address", async () => {
9
- const onPayment = vi.fn();
10
- // latest = 0xa (10), fromBlock = 10 → scans exactly block 10
11
- const rpc = makeRpc({
12
- eth_blockNumber: "0xa",
13
- eth_getBlockByNumber: {
14
- transactions: [
15
- {
16
- hash: "0xabc",
17
- from: "0xsender",
18
- to: "0xdeposit",
19
- value: "0xDE0B6B3A7640000", // 1 ETH = 10^18 wei
20
- blockNumber: "0xa",
21
- },
22
- ],
23
- },
24
- });
25
- const watcher = new EthWatcher({
26
- chain: "base",
27
- rpcCall: rpc,
28
- oracle: mockOracle,
29
- fromBlock: 10,
30
- confirmations: 1,
31
- onPayment,
32
- watchedAddresses: ["0xDeposit"],
33
- });
34
- await watcher.poll();
35
- expect(onPayment).toHaveBeenCalledOnce();
36
- const event = onPayment.mock.calls[0][0];
37
- expect(event.to).toBe("0xdeposit");
38
- expect(event.valueWei).toBe("1000000000000000000");
39
- expect(event.amountUsdCents).toBe(350_000); // 1 ETH × $3,500 = $3,500 = 350,000 cents
40
- expect(event.txHash).toBe("0xabc");
41
- expect(event.confirmations).toBe(0);
42
- expect(event.confirmationsRequired).toBe(1);
43
- });
44
- it("skips transactions not to watched addresses", async () => {
45
- const onPayment = vi.fn();
46
- const rpc = makeRpc({
47
- eth_blockNumber: "0xb",
48
- eth_getBlockByNumber: {
49
- transactions: [{ hash: "0xabc", from: "0xa", to: "0xother", value: "0xDE0B6B3A7640000", blockNumber: "0xa" }],
50
- },
51
- });
52
- const watcher = new EthWatcher({
53
- chain: "base",
54
- rpcCall: rpc,
55
- oracle: mockOracle,
56
- fromBlock: 10,
57
- confirmations: 1,
58
- onPayment,
59
- watchedAddresses: ["0xDeposit"],
60
- });
61
- await watcher.poll();
62
- expect(onPayment).not.toHaveBeenCalled();
63
- });
64
- it("skips zero-value transactions", async () => {
65
- const onPayment = vi.fn();
66
- const rpc = makeRpc({
67
- eth_blockNumber: "0xb",
68
- eth_getBlockByNumber: {
69
- transactions: [{ hash: "0xabc", from: "0xa", to: "0xdeposit", value: "0x0", blockNumber: "0xa" }],
70
- },
71
- });
72
- const watcher = new EthWatcher({
73
- chain: "base",
74
- rpcCall: rpc,
75
- oracle: mockOracle,
76
- fromBlock: 10,
77
- confirmations: 1,
78
- onPayment,
79
- watchedAddresses: ["0xDeposit"],
80
- });
81
- await watcher.poll();
82
- expect(onPayment).not.toHaveBeenCalled();
83
- });
84
- it("does not double-process same txid", async () => {
85
- const onPayment = vi.fn();
86
- const confirmations = new Map();
87
- const cursorStore = {
88
- get: vi.fn().mockResolvedValue(null),
89
- save: vi.fn().mockResolvedValue(undefined),
90
- hasProcessedTx: vi.fn().mockResolvedValue(false),
91
- markProcessedTx: vi.fn().mockResolvedValue(undefined),
92
- getConfirmationCount: vi
93
- .fn()
94
- .mockImplementation(async (_, txId) => confirmations.get(txId) ?? null),
95
- saveConfirmationCount: vi.fn().mockImplementation(async (_, txId, count) => {
96
- confirmations.set(txId, count);
97
- }),
98
- };
99
- const rpc = makeRpc({
100
- eth_blockNumber: "0xb",
101
- eth_getBlockByNumber: {
102
- transactions: [{ hash: "0xabc", from: "0xa", to: "0xdeposit", value: "0xDE0B6B3A7640000", blockNumber: "0xa" }],
103
- },
104
- });
105
- const watcher = new EthWatcher({
106
- chain: "base",
107
- rpcCall: rpc,
108
- oracle: mockOracle,
109
- fromBlock: 10,
110
- confirmations: 1,
111
- onPayment,
112
- watchedAddresses: ["0xDeposit"],
113
- cursorStore,
114
- });
115
- await watcher.poll();
116
- // Second poll — same block, same confirmations → no duplicate emission
117
- await watcher.poll();
118
- expect(onPayment).toHaveBeenCalledOnce();
119
- });
120
- it("skips poll when no watched addresses", async () => {
121
- const onPayment = vi.fn();
122
- const rpc = vi.fn();
123
- const watcher = new EthWatcher({
124
- chain: "base",
125
- rpcCall: rpc,
126
- oracle: mockOracle,
127
- fromBlock: 10,
128
- confirmations: 1,
129
- onPayment,
130
- watchedAddresses: [],
131
- });
132
- await watcher.poll();
133
- expect(rpc).not.toHaveBeenCalled();
134
- });
135
- it("does not mark txid as processed if onPayment throws", async () => {
136
- const onPayment = vi.fn().mockRejectedValueOnce(new Error("db fail")).mockResolvedValueOnce(undefined);
137
- const cursorStore = {
138
- get: vi.fn().mockResolvedValue(null),
139
- save: vi.fn().mockResolvedValue(undefined),
140
- hasProcessedTx: vi.fn().mockResolvedValue(false),
141
- markProcessedTx: vi.fn().mockResolvedValue(undefined),
142
- getConfirmationCount: vi.fn().mockResolvedValue(null),
143
- saveConfirmationCount: vi.fn().mockResolvedValue(undefined),
144
- };
145
- // latest = 0xa (10) = fromBlock → exactly one block to scan
146
- const rpc = makeRpc({
147
- eth_blockNumber: "0xa",
148
- eth_getBlockByNumber: {
149
- transactions: [{ hash: "0xabc", from: "0xa", to: "0xdeposit", value: "0xDE0B6B3A7640000", blockNumber: "0xa" }],
150
- },
151
- });
152
- const watcher = new EthWatcher({
153
- chain: "base",
154
- rpcCall: rpc,
155
- oracle: mockOracle,
156
- fromBlock: 10,
157
- confirmations: 1,
158
- onPayment,
159
- watchedAddresses: ["0xDeposit"],
160
- cursorStore,
161
- });
162
- await expect(watcher.poll()).rejects.toThrow("db fail");
163
- // Retry — should process the same tx again since confirmationCount wasn't saved (error before save)
164
- await watcher.poll();
165
- expect(onPayment).toHaveBeenCalledTimes(2);
166
- });
167
- });