@wopr-network/platform-core 1.67.0 → 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 -742
  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 -472
  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 -823
  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 -617
  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,823 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import type { ICryptoChargeRepository } from "../charge-store.js";
3
- import type { KeyServerDeps } from "../key-server.js";
4
- import { createKeyServerApp } from "../key-server.js";
5
- import type { IPaymentMethodStore } from "../payment-method-store.js";
6
- import type { PluginRegistry } from "../plugin/registry.js";
7
-
8
- /** Create a mock db that supports transaction() by passing itself to the callback. */
9
- function createMockDb() {
10
- const mockMethod = {
11
- id: "btc",
12
- type: "native",
13
- token: "BTC",
14
- chain: "bitcoin",
15
- xpub: "xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz",
16
- nextIndex: 1,
17
- decimals: 8,
18
- addressType: "bech32",
19
- encodingParams: '{"hrp":"bc"}',
20
- watcherType: "utxo",
21
- oracleAssetId: "bitcoin",
22
- confirmations: 6,
23
- };
24
-
25
- const db = {
26
- update: vi.fn().mockReturnValue({
27
- set: vi.fn().mockReturnValue({
28
- where: vi.fn().mockReturnValue({
29
- returning: vi.fn().mockResolvedValue([mockMethod]),
30
- }),
31
- }),
32
- }),
33
- insert: vi.fn().mockReturnValue({
34
- values: vi.fn().mockReturnValue({
35
- onConflictDoNothing: vi.fn().mockResolvedValue({ rowCount: 1 }),
36
- }),
37
- }),
38
- select: vi.fn().mockReturnValue({
39
- from: vi.fn().mockReturnValue({
40
- where: vi.fn().mockResolvedValue([]),
41
- }),
42
- }),
43
- // transaction() passes itself as tx — mocks work the same way
44
- transaction: vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db)),
45
- };
46
- return db;
47
- }
48
-
49
- /** Minimal mock deps for key server tests. */
50
- function mockDeps(): KeyServerDeps & {
51
- chargeStore: { [K in keyof ICryptoChargeRepository]: ReturnType<typeof vi.fn> };
52
- methodStore: { [K in keyof IPaymentMethodStore]: ReturnType<typeof vi.fn> };
53
- } {
54
- const chargeStore = {
55
- getByReferenceId: vi.fn().mockResolvedValue({
56
- referenceId: "btc:bc1q...",
57
- status: "New",
58
- depositAddress: "bc1q...",
59
- chain: "bitcoin",
60
- token: "BTC",
61
- amountUsdCents: 5000,
62
- creditedAt: null,
63
- }),
64
- createStablecoinCharge: vi.fn().mockResolvedValue(undefined),
65
- create: vi.fn(),
66
- updateStatus: vi.fn(),
67
- markCredited: vi.fn(),
68
- isCredited: vi.fn(),
69
- getByDepositAddress: vi.fn(),
70
- getNextDerivationIndex: vi.fn(),
71
- listActiveDepositAddresses: vi.fn(),
72
- };
73
- const methodStore = {
74
- listEnabled: vi.fn().mockResolvedValue([
75
- {
76
- id: "btc",
77
- token: "BTC",
78
- chain: "bitcoin",
79
- decimals: 8,
80
- displayName: "Bitcoin",
81
- contractAddress: null,
82
- confirmations: 6,
83
- iconUrl: null,
84
- },
85
- {
86
- id: "base-usdc",
87
- token: "USDC",
88
- chain: "base",
89
- decimals: 6,
90
- displayName: "USDC on Base",
91
- contractAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
92
- confirmations: 12,
93
- iconUrl: null,
94
- },
95
- ]),
96
- listAll: vi.fn(),
97
- getById: vi.fn().mockResolvedValue({
98
- id: "btc",
99
- type: "native",
100
- token: "BTC",
101
- chain: "bitcoin",
102
- decimals: 8,
103
- displayName: "Bitcoin",
104
- contractAddress: null,
105
- confirmations: 6,
106
- oracleAddress: "0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F",
107
- xpub: null,
108
- displayOrder: 0,
109
- iconUrl: null,
110
- enabled: true,
111
- rpcUrl: null,
112
- }),
113
- listByType: vi.fn(),
114
- upsert: vi.fn().mockResolvedValue(undefined),
115
- setEnabled: vi.fn().mockResolvedValue(undefined),
116
- patchMetadata: vi.fn().mockResolvedValue(true),
117
- };
118
- return {
119
- db: createMockDb() as never,
120
- chargeStore: chargeStore as never,
121
- methodStore: methodStore as never,
122
- oracle: { getPrice: vi.fn().mockResolvedValue({ priceMicros: 65_000_000_000, updatedAt: new Date() }) } as never,
123
- };
124
- }
125
-
126
- describe("key-server routes", () => {
127
- it("GET /chains returns enabled payment methods", async () => {
128
- const app = createKeyServerApp(mockDeps());
129
- const res = await app.request("/chains");
130
- expect(res.status).toBe(200);
131
- const body = await res.json();
132
- expect(body).toHaveLength(2);
133
- expect(body[0].token).toBe("BTC");
134
- expect(body[1].token).toBe("USDC");
135
- });
136
-
137
- it("POST /address requires chain", async () => {
138
- const app = createKeyServerApp(mockDeps());
139
- const res = await app.request("/address", {
140
- method: "POST",
141
- headers: { "Content-Type": "application/json" },
142
- body: JSON.stringify({}),
143
- });
144
- expect(res.status).toBe(400);
145
- });
146
-
147
- it("POST /address derives BTC address", async () => {
148
- const app = createKeyServerApp(mockDeps());
149
- const res = await app.request("/address", {
150
- method: "POST",
151
- headers: { "Content-Type": "application/json" },
152
- body: JSON.stringify({ chain: "btc" }),
153
- });
154
- expect(res.status).toBe(201);
155
- const body = await res.json();
156
- expect(body.address).toMatch(/^bc1q/);
157
- expect(body.index).toBe(0);
158
- expect(body.chain).toBe("bitcoin");
159
- expect(body.token).toBe("BTC");
160
- });
161
-
162
- it("GET /charges/:id returns charge status", async () => {
163
- const app = createKeyServerApp(mockDeps());
164
- const res = await app.request("/charges/btc:bc1q...");
165
- expect(res.status).toBe(200);
166
- const body = await res.json();
167
- expect(body.chargeId).toBe("btc:bc1q...");
168
- expect(body.status).toBe("New");
169
- });
170
-
171
- it("GET /charges/:id returns 404 for missing charge", async () => {
172
- const deps = mockDeps();
173
- (deps.chargeStore.getByReferenceId as ReturnType<typeof vi.fn>).mockResolvedValue(null);
174
- const app = createKeyServerApp(deps);
175
- const res = await app.request("/charges/nonexistent");
176
- expect(res.status).toBe(404);
177
- });
178
-
179
- it("POST /charges validates amountUsd", async () => {
180
- const app = createKeyServerApp(mockDeps());
181
- const res = await app.request("/charges", {
182
- method: "POST",
183
- headers: { "Content-Type": "application/json" },
184
- body: JSON.stringify({ chain: "btc", amountUsd: -10 }),
185
- });
186
- expect(res.status).toBe(400);
187
- });
188
-
189
- it("POST /address retries on shared-xpub address collision", async () => {
190
- const collision = Object.assign(new Error("unique_violation"), { code: "23505" });
191
- let callCount = 0;
192
-
193
- const mockMethod = {
194
- id: "eth",
195
- type: "native",
196
- token: "ETH",
197
- chain: "base",
198
- xpub: "xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz",
199
- nextIndex: 0,
200
- decimals: 18,
201
- addressType: "evm",
202
- encodingParams: "{}",
203
- watcherType: "evm",
204
- oracleAssetId: "ethereum",
205
- confirmations: 1,
206
- };
207
-
208
- const db = {
209
- // Each update call increments nextIndex
210
- update: vi.fn().mockImplementation(() => ({
211
- set: vi.fn().mockReturnValue({
212
- where: vi.fn().mockReturnValue({
213
- returning: vi.fn().mockImplementation(() => {
214
- callCount++;
215
- return Promise.resolve([{ ...mockMethod, nextIndex: callCount }]);
216
- }),
217
- }),
218
- }),
219
- })),
220
- insert: vi.fn().mockImplementation(() => ({
221
- values: vi.fn().mockImplementation(() => {
222
- // First insert collides, second succeeds
223
- if (callCount <= 1) throw collision;
224
- return { onConflictDoNothing: vi.fn().mockResolvedValue({ rowCount: 1 }) };
225
- }),
226
- })),
227
- select: vi.fn().mockReturnValue({
228
- from: vi.fn().mockReturnValue({
229
- where: vi.fn().mockResolvedValue([]),
230
- }),
231
- }),
232
- transaction: vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db)),
233
- };
234
-
235
- const deps = mockDeps();
236
- (deps as unknown as { db: unknown }).db = db;
237
- const app = createKeyServerApp(deps);
238
-
239
- const res = await app.request("/address", {
240
- method: "POST",
241
- headers: { "Content-Type": "application/json" },
242
- body: JSON.stringify({ chain: "eth" }),
243
- });
244
-
245
- expect(res.status).toBe(201);
246
- const body = await res.json();
247
- expect(body.address).toMatch(/^0x/);
248
- // Should have called update twice (first collision, then success)
249
- expect(callCount).toBe(2);
250
- expect(body.index).toBe(1); // skipped index 0
251
- });
252
-
253
- it("POST /address retries on Drizzle-wrapped collision error (cause.code)", async () => {
254
- // Drizzle wraps PG errors: err.code is undefined, err.cause.code has "23505"
255
- const pgError = Object.assign(new Error("unique_violation"), { code: "23505" });
256
- const drizzleError = Object.assign(new Error("DrizzleQueryError"), { cause: pgError });
257
- let callCount = 0;
258
-
259
- const mockMethod = {
260
- id: "eth",
261
- type: "native",
262
- token: "ETH",
263
- chain: "base",
264
- xpub: "xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz",
265
- nextIndex: 0,
266
- decimals: 18,
267
- addressType: "evm",
268
- encodingParams: "{}",
269
- watcherType: "evm",
270
- oracleAssetId: "ethereum",
271
- confirmations: 1,
272
- };
273
-
274
- const db = {
275
- update: vi.fn().mockImplementation(() => ({
276
- set: vi.fn().mockReturnValue({
277
- where: vi.fn().mockReturnValue({
278
- returning: vi.fn().mockImplementation(() => {
279
- callCount++;
280
- return Promise.resolve([{ ...mockMethod, nextIndex: callCount }]);
281
- }),
282
- }),
283
- }),
284
- })),
285
- insert: vi.fn().mockImplementation(() => ({
286
- values: vi.fn().mockImplementation(() => {
287
- if (callCount <= 1) throw drizzleError;
288
- return { onConflictDoNothing: vi.fn().mockResolvedValue({ rowCount: 1 }) };
289
- }),
290
- })),
291
- select: vi.fn().mockReturnValue({
292
- from: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
293
- }),
294
- transaction: vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db)),
295
- };
296
-
297
- const deps = mockDeps();
298
- (deps as unknown as { db: unknown }).db = db;
299
- const app = createKeyServerApp(deps);
300
-
301
- const res = await app.request("/address", {
302
- method: "POST",
303
- headers: { "Content-Type": "application/json" },
304
- body: JSON.stringify({ chain: "eth" }),
305
- });
306
-
307
- expect(res.status).toBe(201);
308
- const body = await res.json();
309
- expect(body.address).toMatch(/^0x/);
310
- expect(callCount).toBe(2);
311
- expect(body.index).toBe(1);
312
- });
313
-
314
- it("POST /charges creates a charge", async () => {
315
- const app = createKeyServerApp(mockDeps());
316
- const res = await app.request("/charges", {
317
- method: "POST",
318
- headers: { "Content-Type": "application/json" },
319
- body: JSON.stringify({ chain: "btc", amountUsd: 50 }),
320
- });
321
- expect(res.status).toBe(201);
322
- const body = await res.json();
323
- expect(body.address).toMatch(/^bc1q/);
324
- expect(body.amountUsd).toBe(50);
325
- expect(body.expiresAt).toBeTruthy();
326
- });
327
-
328
- it("GET /admin/next-path returns available path", async () => {
329
- const deps = mockDeps();
330
- deps.adminToken = "test-admin";
331
- const app = createKeyServerApp(deps);
332
- const res = await app.request("/admin/next-path?coin_type=0", {
333
- headers: { Authorization: "Bearer test-admin" },
334
- });
335
- expect(res.status).toBe(200);
336
- const body = await res.json();
337
- expect(body.path).toBe("m/44'/0'/0'");
338
- expect(body.status).toBe("available");
339
- });
340
-
341
- it("DELETE /admin/chains/:id disables chain", async () => {
342
- const deps = mockDeps();
343
- deps.adminToken = "test-admin";
344
- const app = createKeyServerApp(deps);
345
- const res = await app.request("/admin/chains/doge", {
346
- method: "DELETE",
347
- headers: { Authorization: "Bearer test-admin" },
348
- });
349
- expect(res.status).toBe(204);
350
- expect(deps.methodStore.setEnabled).toHaveBeenCalledWith("doge", false);
351
- });
352
- });
353
-
354
- describe("key-server pool endpoints", () => {
355
- /** Create a mock db that supports pool queries. */
356
- function createPoolMockDb(opts?: {
357
- keyRing?: { id: string; derivationMode: string } | null;
358
- poolEntries?: Array<{
359
- id: number;
360
- keyRingId: string;
361
- derivationIndex: number;
362
- publicKey: string;
363
- address: string;
364
- assignedTo: string | null;
365
- }>;
366
- allKeyRings?: Array<{ id: string }>;
367
- }) {
368
- const keyRing = opts?.keyRing ?? null;
369
- const poolEntries = opts?.poolEntries ?? [];
370
- const allKeyRings = opts?.allKeyRings ?? (keyRing ? [keyRing] : []);
371
-
372
- const db: Record<string, unknown> = {};
373
-
374
- // Track which table is being queried via from()
375
- db.select = vi.fn().mockReturnValue({
376
- from: vi.fn().mockImplementation((table: unknown) => {
377
- const tableName = (table as Record<symbol, string>)[Symbol.for("drizzle:Name")];
378
- if (tableName === "key_rings") {
379
- return {
380
- where: vi.fn().mockResolvedValue(keyRing ? [keyRing] : []),
381
- orderBy: vi.fn().mockResolvedValue(allKeyRings),
382
- };
383
- }
384
- if (tableName === "address_pool") {
385
- return {
386
- where: vi.fn().mockImplementation(() => ({
387
- orderBy: vi.fn().mockImplementation(() => ({
388
- limit: vi.fn().mockResolvedValue(poolEntries.filter((e) => e.assignedTo === null).slice(0, 1)),
389
- })),
390
- // For counting all pool entries for a key ring
391
- length: poolEntries.length,
392
- filter: (fn: (e: unknown) => boolean) => poolEntries.filter(fn),
393
- [Symbol.iterator]: function* () {
394
- yield* poolEntries;
395
- },
396
- })),
397
- };
398
- }
399
- // Default: payment_methods
400
- return {
401
- where: vi.fn().mockResolvedValue([]),
402
- };
403
- }),
404
- });
405
-
406
- db.insert = vi.fn().mockReturnValue({
407
- values: vi.fn().mockReturnValue({
408
- onConflictDoNothing: vi.fn().mockResolvedValue({ rowCount: 1 }),
409
- }),
410
- });
411
-
412
- db.update = vi.fn().mockReturnValue({
413
- set: vi.fn().mockReturnValue({
414
- where: vi.fn().mockReturnValue({
415
- returning: vi.fn().mockResolvedValue([]),
416
- }),
417
- }),
418
- });
419
-
420
- db.transaction = vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db));
421
-
422
- return db;
423
- }
424
-
425
- it("POST /admin/pool/replenish validates required fields", async () => {
426
- const deps = mockDeps();
427
- deps.adminToken = "test-admin";
428
- const app = createKeyServerApp(deps);
429
- const res = await app.request("/admin/pool/replenish", {
430
- method: "POST",
431
- headers: { "Content-Type": "application/json", Authorization: "Bearer test-admin" },
432
- body: JSON.stringify({}),
433
- });
434
- expect(res.status).toBe(400);
435
- });
436
-
437
- it("POST /admin/pool/replenish returns 404 for unknown key ring", async () => {
438
- const db = createPoolMockDb({ keyRing: null });
439
- const deps = mockDeps();
440
- deps.adminToken = "test-admin";
441
- (deps as unknown as { db: unknown }).db = db;
442
- const app = createKeyServerApp(deps);
443
- const res = await app.request("/admin/pool/replenish", {
444
- method: "POST",
445
- headers: { "Content-Type": "application/json", Authorization: "Bearer test-admin" },
446
- body: JSON.stringify({
447
- key_ring_id: "sol-main",
448
- plugin_id: "solana",
449
- encoding: "base58-solana",
450
- addresses: [{ index: 0, public_key: "abcd", address: "SolAddr1" }],
451
- }),
452
- });
453
- expect(res.status).toBe(404);
454
- });
455
-
456
- it("POST /admin/pool/replenish inserts validated addresses", async () => {
457
- const db = createPoolMockDb({
458
- keyRing: { id: "sol-main", derivationMode: "pre-derived" },
459
- poolEntries: [],
460
- });
461
-
462
- // Override select().from() to handle both keyRings query and the count query
463
- const selectMock = vi.fn().mockReturnValue({
464
- from: vi.fn().mockImplementation((table: unknown) => {
465
- const tableName = (table as Record<symbol, string>)[Symbol.for("drizzle:Name")];
466
- if (tableName === "key_rings") {
467
- return {
468
- where: vi.fn().mockResolvedValue([{ id: "sol-main", derivationMode: "pre-derived" }]),
469
- };
470
- }
471
- // address_pool count query (after insert)
472
- return {
473
- where: vi.fn().mockResolvedValue([
474
- {
475
- id: 1,
476
- keyRingId: "sol-main",
477
- derivationIndex: 0,
478
- publicKey: "ab",
479
- address: "SolAddr0",
480
- assignedTo: null,
481
- },
482
- {
483
- id: 2,
484
- keyRingId: "sol-main",
485
- derivationIndex: 1,
486
- publicKey: "cd",
487
- address: "SolAddr1",
488
- assignedTo: null,
489
- },
490
- ]),
491
- };
492
- }),
493
- });
494
- (db as Record<string, unknown>).select = selectMock;
495
-
496
- const deps = mockDeps();
497
- deps.adminToken = "test-admin";
498
- (deps as unknown as { db: unknown }).db = db;
499
- // No registry — skip re-encoding validation
500
- deps.registry = undefined;
501
-
502
- const app = createKeyServerApp(deps);
503
- const res = await app.request("/admin/pool/replenish", {
504
- method: "POST",
505
- headers: { "Content-Type": "application/json", Authorization: "Bearer test-admin" },
506
- body: JSON.stringify({
507
- key_ring_id: "sol-main",
508
- plugin_id: "solana",
509
- encoding: "base58-solana",
510
- addresses: [
511
- { index: 0, public_key: "ab", address: "SolAddr0" },
512
- { index: 1, public_key: "cd", address: "SolAddr1" },
513
- ],
514
- }),
515
- });
516
-
517
- expect(res.status).toBe(201);
518
- const body = await res.json();
519
- expect(body.inserted).toBe(2);
520
- expect(body.total).toBe(2);
521
- });
522
-
523
- it("POST /admin/pool/replenish rejects mismatched address when encoder present", async () => {
524
- const mockEncoder = {
525
- encode: vi.fn().mockReturnValue("CorrectAddress"),
526
- encodingType: vi.fn().mockReturnValue("base58-solana"),
527
- };
528
- const mockPlugin = {
529
- pluginId: "solana",
530
- supportedCurve: "ed25519" as const,
531
- encoders: { "base58-solana": mockEncoder },
532
- createWatcher: vi.fn(),
533
- createSweeper: vi.fn(),
534
- version: 1,
535
- };
536
- const mockRegistry = {
537
- get: vi.fn().mockReturnValue(mockPlugin),
538
- getOrThrow: vi.fn(),
539
- list: vi.fn(),
540
- register: vi.fn(),
541
- };
542
-
543
- const db = createPoolMockDb({
544
- keyRing: { id: "sol-main", derivationMode: "pre-derived" },
545
- });
546
-
547
- const deps = mockDeps();
548
- deps.adminToken = "test-admin";
549
- (deps as unknown as { db: unknown }).db = db;
550
- deps.registry = mockRegistry as unknown as PluginRegistry;
551
-
552
- const app = createKeyServerApp(deps);
553
- const res = await app.request("/admin/pool/replenish", {
554
- method: "POST",
555
- headers: { "Content-Type": "application/json", Authorization: "Bearer test-admin" },
556
- body: JSON.stringify({
557
- key_ring_id: "sol-main",
558
- plugin_id: "solana",
559
- encoding: "base58-solana",
560
- addresses: [{ index: 0, public_key: "abcd1234", address: "WrongAddress" }],
561
- }),
562
- });
563
-
564
- expect(res.status).toBe(400);
565
- const body = await res.json();
566
- expect(body.error).toContain("Address mismatch");
567
- });
568
-
569
- it("GET /admin/pool/status returns pool stats", async () => {
570
- const db = createPoolMockDb({
571
- allKeyRings: [{ id: "sol-main" }],
572
- poolEntries: [
573
- { id: 1, keyRingId: "sol-main", derivationIndex: 0, publicKey: "a", address: "A", assignedTo: null },
574
- { id: 2, keyRingId: "sol-main", derivationIndex: 1, publicKey: "b", address: "B", assignedTo: "tenant:1" },
575
- { id: 3, keyRingId: "sol-main", derivationIndex: 2, publicKey: "c", address: "C", assignedTo: null },
576
- ],
577
- });
578
-
579
- // Override select to handle the two different query patterns in pool/status
580
- let selectCallCount = 0;
581
- const poolEntries = [
582
- { id: 1, keyRingId: "sol-main", derivationIndex: 0, publicKey: "a", address: "A", assignedTo: null },
583
- { id: 2, keyRingId: "sol-main", derivationIndex: 1, publicKey: "b", address: "B", assignedTo: "tenant:1" },
584
- { id: 3, keyRingId: "sol-main", derivationIndex: 2, publicKey: "c", address: "C", assignedTo: null },
585
- ];
586
- (db as Record<string, unknown>).select = vi.fn().mockReturnValue({
587
- from: vi.fn().mockImplementation(() => {
588
- selectCallCount++;
589
- if (selectCallCount === 1) {
590
- // First call: select from keyRings (no where clause)
591
- return [{ id: "sol-main" }];
592
- }
593
- // Second call: select from addressPool where keyRingId = ring.id
594
- return {
595
- where: vi.fn().mockResolvedValue(poolEntries),
596
- };
597
- }),
598
- });
599
-
600
- const deps = mockDeps();
601
- deps.adminToken = "test-admin";
602
- (deps as unknown as { db: unknown }).db = db;
603
- const app = createKeyServerApp(deps);
604
-
605
- const res = await app.request("/admin/pool/status", {
606
- headers: { Authorization: "Bearer test-admin" },
607
- });
608
-
609
- expect(res.status).toBe(200);
610
- const body = await res.json();
611
- expect(body.pools).toHaveLength(1);
612
- expect(body.pools[0].key_ring_id).toBe("sol-main");
613
- expect(body.pools[0].total).toBe(3);
614
- expect(body.pools[0].available).toBe(2);
615
- expect(body.pools[0].assigned).toBe(1);
616
- });
617
-
618
- it("POST /address uses pool for pre-derived key ring", async () => {
619
- const poolEntry = {
620
- id: 1,
621
- keyRingId: "sol-main",
622
- derivationIndex: 7,
623
- publicKey: "deadbeef",
624
- address: "SolanaAddr7",
625
- assignedTo: null,
626
- createdAt: "2026-01-01",
627
- };
628
-
629
- const solMethod = {
630
- id: "sol",
631
- type: "native",
632
- token: "SOL",
633
- chain: "solana",
634
- xpub: null,
635
- keyRingId: "sol-main",
636
- nextIndex: 0,
637
- decimals: 9,
638
- addressType: "base58-solana",
639
- encodingParams: "{}",
640
- watcherType: "solana",
641
- oracleAssetId: "solana",
642
- confirmations: 1,
643
- pluginId: "solana",
644
- encoding: "base58-solana",
645
- };
646
-
647
- const keyRing = {
648
- id: "sol-main",
649
- curve: "ed25519",
650
- derivationScheme: "slip10",
651
- derivationMode: "pre-derived",
652
- keyMaterial: "{}",
653
- coinType: 501,
654
- accountIndex: 0,
655
- };
656
-
657
- // Build a mock that handles the pool flow:
658
- // 1. select from paymentMethods where id=sol -> solMethod
659
- // 2. select from keyRings where id=sol-main -> keyRing
660
- // 3. transaction: select from addressPool -> poolEntry, update addressPool, insert derivedAddresses
661
- let selectCallCount = 0;
662
- const db = {
663
- select: vi.fn().mockImplementation(() => ({
664
- from: vi.fn().mockImplementation(() => {
665
- selectCallCount++;
666
- if (selectCallCount === 1) {
667
- // paymentMethods lookup
668
- return { where: vi.fn().mockResolvedValue([solMethod]) };
669
- }
670
- if (selectCallCount === 2) {
671
- // keyRings lookup
672
- return { where: vi.fn().mockResolvedValue([keyRing]) };
673
- }
674
- // addressPool query inside transaction
675
- return {
676
- where: vi.fn().mockReturnValue({
677
- orderBy: vi.fn().mockReturnValue({
678
- limit: vi.fn().mockResolvedValue([poolEntry]),
679
- }),
680
- }),
681
- };
682
- }),
683
- })),
684
- update: vi.fn().mockReturnValue({
685
- set: vi.fn().mockReturnValue({
686
- where: vi.fn().mockResolvedValue(undefined),
687
- }),
688
- }),
689
- insert: vi.fn().mockReturnValue({
690
- values: vi.fn().mockResolvedValue(undefined),
691
- }),
692
- transaction: vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db)),
693
- };
694
-
695
- const deps = mockDeps();
696
- (deps as unknown as { db: unknown }).db = db;
697
- const app = createKeyServerApp(deps);
698
-
699
- const res = await app.request("/address", {
700
- method: "POST",
701
- headers: { "Content-Type": "application/json" },
702
- body: JSON.stringify({ chain: "sol" }),
703
- });
704
-
705
- expect(res.status).toBe(201);
706
- const body = await res.json();
707
- expect(body.address).toBe("SolanaAddr7");
708
- expect(body.index).toBe(7);
709
- expect(body.chain).toBe("solana");
710
- expect(body.token).toBe("SOL");
711
- });
712
-
713
- it("POST /address throws when pool is empty", async () => {
714
- const solMethod = {
715
- id: "sol",
716
- type: "native",
717
- token: "SOL",
718
- chain: "solana",
719
- xpub: null,
720
- keyRingId: "sol-main",
721
- nextIndex: 0,
722
- decimals: 9,
723
- addressType: "base58-solana",
724
- encodingParams: "{}",
725
- watcherType: "solana",
726
- oracleAssetId: "solana",
727
- confirmations: 1,
728
- pluginId: "solana",
729
- encoding: "base58-solana",
730
- };
731
-
732
- const keyRing = {
733
- id: "sol-main",
734
- curve: "ed25519",
735
- derivationScheme: "slip10",
736
- derivationMode: "pre-derived",
737
- keyMaterial: "{}",
738
- coinType: 501,
739
- accountIndex: 0,
740
- };
741
-
742
- let selectCallCount = 0;
743
- const db = {
744
- select: vi.fn().mockImplementation(() => ({
745
- from: vi.fn().mockImplementation(() => {
746
- selectCallCount++;
747
- if (selectCallCount === 1) {
748
- return { where: vi.fn().mockResolvedValue([solMethod]) };
749
- }
750
- if (selectCallCount === 2) {
751
- return { where: vi.fn().mockResolvedValue([keyRing]) };
752
- }
753
- // Empty pool
754
- return {
755
- where: vi.fn().mockReturnValue({
756
- orderBy: vi.fn().mockReturnValue({
757
- limit: vi.fn().mockResolvedValue([]),
758
- }),
759
- }),
760
- };
761
- }),
762
- })),
763
- transaction: vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db)),
764
- };
765
-
766
- const deps = mockDeps();
767
- (deps as unknown as { db: unknown }).db = db;
768
- const app = createKeyServerApp(deps);
769
-
770
- const res = await app.request("/address", {
771
- method: "POST",
772
- headers: { "Content-Type": "application/json" },
773
- body: JSON.stringify({ chain: "sol" }),
774
- });
775
-
776
- // Hono catches the thrown error and returns 500
777
- expect(res.status).toBe(500);
778
- });
779
- });
780
-
781
- describe("key-server auth", () => {
782
- it("rejects unauthenticated request when serviceKey is set", async () => {
783
- const deps = mockDeps();
784
- deps.serviceKey = "sk-test-secret";
785
- const app = createKeyServerApp(deps);
786
- const res = await app.request("/address", {
787
- method: "POST",
788
- headers: { "Content-Type": "application/json" },
789
- body: JSON.stringify({ chain: "btc" }),
790
- });
791
- expect(res.status).toBe(401);
792
- });
793
-
794
- it("allows authenticated request with correct serviceKey", async () => {
795
- const deps = mockDeps();
796
- deps.serviceKey = "sk-test-secret";
797
- const app = createKeyServerApp(deps);
798
- const res = await app.request("/address", {
799
- method: "POST",
800
- headers: { "Content-Type": "application/json", Authorization: "Bearer sk-test-secret" },
801
- body: JSON.stringify({ chain: "btc" }),
802
- });
803
- expect(res.status).toBe(201);
804
- });
805
-
806
- it("rejects admin route without adminToken", async () => {
807
- const deps = mockDeps();
808
- // no adminToken set — admin routes disabled
809
- const app = createKeyServerApp(deps);
810
- const res = await app.request("/admin/next-path?coin_type=0");
811
- expect(res.status).toBe(403);
812
- });
813
-
814
- it("allows admin route with correct adminToken", async () => {
815
- const deps = mockDeps();
816
- deps.adminToken = "admin-secret";
817
- const app = createKeyServerApp(deps);
818
- const res = await app.request("/admin/next-path?coin_type=0", {
819
- headers: { Authorization: "Bearer admin-secret" },
820
- });
821
- expect(res.status).toBe(200);
822
- });
823
- });