@wopr-network/platform-core 1.67.0 → 1.68.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 (140) hide show
  1. package/dist/auth/better-auth.js +7 -0
  2. package/dist/billing/crypto/btc/checkout.d.ts +4 -0
  3. package/dist/billing/crypto/btc/checkout.js +1 -2
  4. package/dist/billing/crypto/btc/index.d.ts +0 -4
  5. package/dist/billing/crypto/btc/index.js +0 -2
  6. package/dist/billing/crypto/evm/__tests__/checkout.test.js +8 -11
  7. package/dist/billing/crypto/evm/__tests__/eth-checkout.test.js +15 -1
  8. package/dist/billing/crypto/evm/checkout.d.ts +2 -0
  9. package/dist/billing/crypto/evm/checkout.js +1 -2
  10. package/dist/billing/crypto/evm/eth-checkout.d.ts +13 -2
  11. package/dist/billing/crypto/evm/eth-checkout.js +2 -4
  12. package/dist/billing/crypto/evm/eth-settler.d.ts +1 -1
  13. package/dist/billing/crypto/evm/index.d.ts +2 -8
  14. package/dist/billing/crypto/evm/index.js +0 -3
  15. package/dist/billing/crypto/evm/types.d.ts +16 -0
  16. package/dist/billing/crypto/index.d.ts +1 -6
  17. package/dist/billing/crypto/index.js +2 -3
  18. package/dist/billing/crypto/types.d.ts +0 -43
  19. package/dist/billing/crypto/types.js +1 -24
  20. package/dist/email/client.js +16 -0
  21. package/package.json +4 -7
  22. package/src/auth/better-auth.ts +8 -0
  23. package/src/billing/crypto/btc/checkout.ts +3 -2
  24. package/src/billing/crypto/btc/index.ts +0 -4
  25. package/src/billing/crypto/evm/__tests__/checkout.test.ts +10 -12
  26. package/src/billing/crypto/evm/__tests__/eth-checkout.test.ts +17 -1
  27. package/src/billing/crypto/evm/__tests__/eth-settler.test.ts +1 -1
  28. package/src/billing/crypto/evm/checkout.ts +3 -2
  29. package/src/billing/crypto/evm/eth-checkout.ts +15 -6
  30. package/src/billing/crypto/evm/eth-settler.ts +1 -1
  31. package/src/billing/crypto/evm/index.ts +8 -7
  32. package/src/billing/crypto/evm/types.ts +17 -0
  33. package/src/billing/crypto/index.ts +14 -12
  34. package/src/billing/crypto/types.ts +0 -63
  35. package/src/email/client.ts +18 -0
  36. package/dist/billing/crypto/__tests__/address-gen.test.d.ts +0 -1
  37. package/dist/billing/crypto/__tests__/address-gen.test.js +0 -219
  38. package/dist/billing/crypto/__tests__/key-server.test.d.ts +0 -1
  39. package/dist/billing/crypto/__tests__/key-server.test.js +0 -742
  40. package/dist/billing/crypto/__tests__/watcher-service.test.d.ts +0 -1
  41. package/dist/billing/crypto/__tests__/watcher-service.test.js +0 -174
  42. package/dist/billing/crypto/address-gen.d.ts +0 -24
  43. package/dist/billing/crypto/address-gen.js +0 -176
  44. package/dist/billing/crypto/btc/__tests__/watcher.test.d.ts +0 -1
  45. package/dist/billing/crypto/btc/__tests__/watcher.test.js +0 -170
  46. package/dist/billing/crypto/btc/watcher.d.ts +0 -44
  47. package/dist/billing/crypto/btc/watcher.js +0 -118
  48. package/dist/billing/crypto/evm/__tests__/eth-watcher.test.d.ts +0 -1
  49. package/dist/billing/crypto/evm/__tests__/eth-watcher.test.js +0 -167
  50. package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.d.ts +0 -1
  51. package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.js +0 -159
  52. package/dist/billing/crypto/evm/__tests__/watcher.test.d.ts +0 -1
  53. package/dist/billing/crypto/evm/__tests__/watcher.test.js +0 -145
  54. package/dist/billing/crypto/evm/eth-watcher.d.ts +0 -66
  55. package/dist/billing/crypto/evm/eth-watcher.js +0 -121
  56. package/dist/billing/crypto/evm/watcher.d.ts +0 -51
  57. package/dist/billing/crypto/evm/watcher.js +0 -156
  58. package/dist/billing/crypto/key-server-entry.d.ts +0 -1
  59. package/dist/billing/crypto/key-server-entry.js +0 -122
  60. package/dist/billing/crypto/key-server.d.ts +0 -32
  61. package/dist/billing/crypto/key-server.js +0 -472
  62. package/dist/billing/crypto/oracle/__tests__/chainlink.test.d.ts +0 -1
  63. package/dist/billing/crypto/oracle/__tests__/chainlink.test.js +0 -83
  64. package/dist/billing/crypto/oracle/__tests__/coingecko.test.d.ts +0 -1
  65. package/dist/billing/crypto/oracle/__tests__/coingecko.test.js +0 -65
  66. package/dist/billing/crypto/oracle/__tests__/composite.test.d.ts +0 -1
  67. package/dist/billing/crypto/oracle/__tests__/composite.test.js +0 -48
  68. package/dist/billing/crypto/oracle/__tests__/convert.test.d.ts +0 -1
  69. package/dist/billing/crypto/oracle/__tests__/convert.test.js +0 -61
  70. package/dist/billing/crypto/oracle/__tests__/fixed.test.d.ts +0 -1
  71. package/dist/billing/crypto/oracle/__tests__/fixed.test.js +0 -20
  72. package/dist/billing/crypto/oracle/chainlink.d.ts +0 -26
  73. package/dist/billing/crypto/oracle/chainlink.js +0 -62
  74. package/dist/billing/crypto/oracle/coingecko.d.ts +0 -22
  75. package/dist/billing/crypto/oracle/coingecko.js +0 -71
  76. package/dist/billing/crypto/oracle/composite.d.ts +0 -14
  77. package/dist/billing/crypto/oracle/composite.js +0 -34
  78. package/dist/billing/crypto/oracle/convert.d.ts +0 -30
  79. package/dist/billing/crypto/oracle/convert.js +0 -51
  80. package/dist/billing/crypto/oracle/fixed.d.ts +0 -10
  81. package/dist/billing/crypto/oracle/fixed.js +0 -22
  82. package/dist/billing/crypto/oracle/index.d.ts +0 -9
  83. package/dist/billing/crypto/oracle/index.js +0 -6
  84. package/dist/billing/crypto/oracle/types.d.ts +0 -22
  85. package/dist/billing/crypto/oracle/types.js +0 -7
  86. package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +0 -1
  87. package/dist/billing/crypto/plugin/__tests__/integration.test.js +0 -58
  88. package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +0 -1
  89. package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +0 -46
  90. package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +0 -1
  91. package/dist/billing/crypto/plugin/__tests__/registry.test.js +0 -49
  92. package/dist/billing/crypto/plugin/index.d.ts +0 -2
  93. package/dist/billing/crypto/plugin/index.js +0 -1
  94. package/dist/billing/crypto/plugin/interfaces.d.ts +0 -97
  95. package/dist/billing/crypto/plugin/interfaces.js +0 -2
  96. package/dist/billing/crypto/plugin/registry.d.ts +0 -8
  97. package/dist/billing/crypto/plugin/registry.js +0 -21
  98. package/dist/billing/crypto/plugin-watcher-service.d.ts +0 -32
  99. package/dist/billing/crypto/plugin-watcher-service.js +0 -113
  100. package/dist/billing/crypto/tron/__tests__/address-convert.test.d.ts +0 -1
  101. package/dist/billing/crypto/tron/__tests__/address-convert.test.js +0 -55
  102. package/dist/billing/crypto/tron/address-convert.d.ts +0 -14
  103. package/dist/billing/crypto/tron/address-convert.js +0 -93
  104. package/dist/billing/crypto/watcher-service.d.ts +0 -55
  105. package/dist/billing/crypto/watcher-service.js +0 -438
  106. package/src/billing/crypto/__tests__/address-gen.test.ts +0 -264
  107. package/src/billing/crypto/__tests__/key-server.test.ts +0 -823
  108. package/src/billing/crypto/__tests__/watcher-service.test.ts +0 -242
  109. package/src/billing/crypto/address-gen.ts +0 -185
  110. package/src/billing/crypto/btc/__tests__/watcher.test.ts +0 -201
  111. package/src/billing/crypto/btc/watcher.ts +0 -161
  112. package/src/billing/crypto/evm/__tests__/eth-watcher.test.ts +0 -190
  113. package/src/billing/crypto/evm/__tests__/watcher-confirmations.test.ts +0 -191
  114. package/src/billing/crypto/evm/__tests__/watcher.test.ts +0 -167
  115. package/src/billing/crypto/evm/eth-watcher.ts +0 -182
  116. package/src/billing/crypto/evm/watcher.ts +0 -204
  117. package/src/billing/crypto/key-server-entry.ts +0 -144
  118. package/src/billing/crypto/key-server.ts +0 -617
  119. package/src/billing/crypto/oracle/__tests__/chainlink.test.ts +0 -107
  120. package/src/billing/crypto/oracle/__tests__/coingecko.test.ts +0 -75
  121. package/src/billing/crypto/oracle/__tests__/composite.test.ts +0 -61
  122. package/src/billing/crypto/oracle/__tests__/convert.test.ts +0 -74
  123. package/src/billing/crypto/oracle/__tests__/fixed.test.ts +0 -23
  124. package/src/billing/crypto/oracle/chainlink.ts +0 -86
  125. package/src/billing/crypto/oracle/coingecko.ts +0 -96
  126. package/src/billing/crypto/oracle/composite.ts +0 -35
  127. package/src/billing/crypto/oracle/convert.ts +0 -53
  128. package/src/billing/crypto/oracle/fixed.ts +0 -25
  129. package/src/billing/crypto/oracle/index.ts +0 -9
  130. package/src/billing/crypto/oracle/types.ts +0 -28
  131. package/src/billing/crypto/plugin/__tests__/integration.test.ts +0 -64
  132. package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +0 -51
  133. package/src/billing/crypto/plugin/__tests__/registry.test.ts +0 -58
  134. package/src/billing/crypto/plugin/index.ts +0 -17
  135. package/src/billing/crypto/plugin/interfaces.ts +0 -106
  136. package/src/billing/crypto/plugin/registry.ts +0 -26
  137. package/src/billing/crypto/plugin-watcher-service.ts +0 -148
  138. package/src/billing/crypto/tron/__tests__/address-convert.test.ts +0 -67
  139. package/src/billing/crypto/tron/address-convert.ts +0 -89
  140. package/src/billing/crypto/watcher-service.ts +0 -549
@@ -1,174 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { handlePayment } from "../watcher-service.js";
3
- function mockChargeStore(overrides = {}) {
4
- return {
5
- getByDepositAddress: vi.fn().mockResolvedValue({
6
- referenceId: "btc:test",
7
- tenantId: "t1",
8
- amountUsdCents: 5000,
9
- creditedAt: null,
10
- chain: "bitcoin",
11
- depositAddress: "bc1qtest",
12
- token: "BTC",
13
- callbackUrl: "https://example.com/hook",
14
- expectedAmount: "50000",
15
- receivedAmount: "0",
16
- confirmations: 0,
17
- confirmationsRequired: 6,
18
- ...overrides,
19
- }),
20
- updateProgress: vi.fn().mockResolvedValue(undefined),
21
- updateStatus: vi.fn().mockResolvedValue(undefined),
22
- markCredited: vi.fn().mockResolvedValue(undefined),
23
- };
24
- }
25
- function mockDb() {
26
- return {
27
- insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
28
- update: vi.fn().mockReturnValue({
29
- set: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) }),
30
- }),
31
- };
32
- }
33
- const noop = () => { };
34
- describe("handlePayment", () => {
35
- it("fires webhook with confirmations: 0 on first tx detection", async () => {
36
- const chargeStore = mockChargeStore();
37
- const db = mockDb();
38
- const enqueuedPayloads = [];
39
- db.insert = vi.fn().mockReturnValue({
40
- values: vi.fn().mockImplementation((val) => {
41
- if (val.payload)
42
- enqueuedPayloads.push(JSON.parse(val.payload));
43
- return Promise.resolve(undefined);
44
- }),
45
- });
46
- await handlePayment(db, chargeStore, "bc1qtest", "50000", {
47
- txHash: "abc123",
48
- confirmations: 0,
49
- confirmationsRequired: 6,
50
- amountReceivedCents: 5000,
51
- }, noop);
52
- expect(enqueuedPayloads).toHaveLength(1);
53
- expect(enqueuedPayloads[0]).toMatchObject({
54
- chargeId: "btc:test",
55
- status: "partial",
56
- confirmations: 0,
57
- confirmationsRequired: 6,
58
- });
59
- });
60
- it("fires webhook on each confirmation increment", async () => {
61
- const chargeStore = mockChargeStore({ confirmations: 2 });
62
- const db = mockDb();
63
- const enqueuedPayloads = [];
64
- db.insert = vi.fn().mockReturnValue({
65
- values: vi.fn().mockImplementation((val) => {
66
- if (val.payload)
67
- enqueuedPayloads.push(JSON.parse(val.payload));
68
- return Promise.resolve(undefined);
69
- }),
70
- });
71
- await handlePayment(db, chargeStore, "bc1qtest", "0", // no additional payment, just confirmation update
72
- {
73
- txHash: "abc123",
74
- confirmations: 3,
75
- confirmationsRequired: 6,
76
- amountReceivedCents: 5000,
77
- }, noop);
78
- expect(enqueuedPayloads).toHaveLength(1);
79
- expect(enqueuedPayloads[0]).toMatchObject({
80
- status: "partial",
81
- confirmations: 3,
82
- confirmationsRequired: 6,
83
- });
84
- });
85
- it("fires final webhook with status confirmed at threshold", async () => {
86
- const chargeStore = mockChargeStore({
87
- receivedAmount: "50000",
88
- confirmations: 5,
89
- });
90
- const db = mockDb();
91
- const enqueuedPayloads = [];
92
- db.insert = vi.fn().mockReturnValue({
93
- values: vi.fn().mockImplementation((val) => {
94
- if (val.payload)
95
- enqueuedPayloads.push(JSON.parse(val.payload));
96
- return Promise.resolve(undefined);
97
- }),
98
- });
99
- await handlePayment(db, chargeStore, "bc1qtest", "0", {
100
- txHash: "abc123",
101
- confirmations: 6,
102
- confirmationsRequired: 6,
103
- amountReceivedCents: 5000,
104
- }, noop);
105
- expect(enqueuedPayloads).toHaveLength(1);
106
- expect(enqueuedPayloads[0]).toMatchObject({
107
- status: "confirmed",
108
- confirmations: 6,
109
- confirmationsRequired: 6,
110
- });
111
- expect(chargeStore.markCredited).toHaveBeenCalledOnce();
112
- });
113
- it("all webhooks use canonical status values only", async () => {
114
- const chargeStore = mockChargeStore();
115
- const db = mockDb();
116
- const enqueuedPayloads = [];
117
- db.insert = vi.fn().mockReturnValue({
118
- values: vi.fn().mockImplementation((val) => {
119
- if (val.payload)
120
- enqueuedPayloads.push(JSON.parse(val.payload));
121
- return Promise.resolve(undefined);
122
- }),
123
- });
124
- await handlePayment(db, chargeStore, "bc1qtest", "50000", {
125
- txHash: "abc123",
126
- confirmations: 0,
127
- confirmationsRequired: 6,
128
- amountReceivedCents: 5000,
129
- }, noop);
130
- const validStatuses = ["pending", "partial", "confirmed", "expired", "failed"];
131
- for (const payload of enqueuedPayloads) {
132
- expect(validStatuses).toContain(payload.status);
133
- }
134
- // Must NEVER contain legacy statuses
135
- for (const payload of enqueuedPayloads) {
136
- expect(payload.status).not.toBe("Settled");
137
- expect(payload.status).not.toBe("Processing");
138
- expect(payload.status).not.toBe("New");
139
- }
140
- });
141
- it("updates charge progress via updateProgress()", async () => {
142
- const chargeStore = mockChargeStore();
143
- const db = mockDb();
144
- db.insert = vi.fn().mockReturnValue({
145
- values: vi.fn().mockResolvedValue(undefined),
146
- });
147
- await handlePayment(db, chargeStore, "bc1qtest", "25000", {
148
- txHash: "abc123",
149
- confirmations: 2,
150
- confirmationsRequired: 6,
151
- amountReceivedCents: 2500,
152
- }, noop);
153
- expect(chargeStore.updateProgress).toHaveBeenCalledWith("btc:test", {
154
- status: "partial",
155
- amountReceivedCents: 2500,
156
- confirmations: 2,
157
- confirmationsRequired: 6,
158
- txHash: "abc123",
159
- });
160
- });
161
- it("skips already-credited charges", async () => {
162
- const chargeStore = mockChargeStore({ creditedAt: "2026-01-01" });
163
- const db = mockDb();
164
- await handlePayment(db, chargeStore, "bc1qtest", "50000", { txHash: "abc123", confirmations: 6, confirmationsRequired: 6, amountReceivedCents: 5000 }, noop);
165
- expect(chargeStore.updateProgress).not.toHaveBeenCalled();
166
- });
167
- it("skips unknown addresses", async () => {
168
- const chargeStore = mockChargeStore();
169
- chargeStore.getByDepositAddress.mockResolvedValue(null);
170
- const db = mockDb();
171
- await handlePayment(db, chargeStore, "bc1qunknown", "50000", { txHash: "abc123", confirmations: 0, confirmationsRequired: 6, amountReceivedCents: 5000 }, noop);
172
- expect(chargeStore.updateProgress).not.toHaveBeenCalled();
173
- });
174
- });
@@ -1,24 +0,0 @@
1
- export interface EncodingParams {
2
- /** Bech32 human-readable prefix (e.g. "bc", "ltc", "tb"). */
3
- hrp?: string;
4
- /** Base58Check version byte as hex string (e.g. "0x1e" for DOGE, "0x41" for TRON). */
5
- version?: string;
6
- }
7
- /**
8
- * Derive a deposit address from an xpub at a given BIP-44 index.
9
- * Path: xpub / 0 / index (external chain).
10
- * No private keys involved.
11
- *
12
- * @param xpub - Extended public key
13
- * @param index - Derivation index (0, 1, 2, ...)
14
- * @param addressType - Encoding type: "bech32", "p2pkh", "evm"
15
- * @param params - Chain-specific encoding params from DB (parsed JSON)
16
- */
17
- export declare function deriveAddress(xpub: string, index: number, addressType: string, params?: EncodingParams): string;
18
- /**
19
- * Derive the treasury address (internal chain, index 0).
20
- * Used for sweep destinations.
21
- */
22
- export declare function deriveTreasury(xpub: string, addressType: string, params?: EncodingParams): string;
23
- /** Validate that a string is an xpub (not xprv). */
24
- export declare function isValidXpub(key: string): boolean;
@@ -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 {};