@wopr-network/platform-core 1.42.2 → 1.43.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.
@@ -0,0 +1,247 @@
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
+
7
+ /** Create a mock db that supports transaction() by passing itself to the callback. */
8
+ function createMockDb() {
9
+ const mockMethod = {
10
+ id: "btc",
11
+ type: "native",
12
+ token: "BTC",
13
+ chain: "bitcoin",
14
+ xpub: "xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz",
15
+ nextIndex: 1,
16
+ decimals: 8,
17
+ confirmations: 6,
18
+ };
19
+
20
+ const db = {
21
+ update: vi.fn().mockReturnValue({
22
+ set: vi.fn().mockReturnValue({
23
+ where: vi.fn().mockReturnValue({
24
+ returning: vi.fn().mockResolvedValue([mockMethod]),
25
+ }),
26
+ }),
27
+ }),
28
+ insert: vi.fn().mockReturnValue({
29
+ values: vi.fn().mockReturnValue({
30
+ onConflictDoNothing: vi.fn().mockResolvedValue({ rowCount: 1 }),
31
+ }),
32
+ }),
33
+ select: vi.fn().mockReturnValue({
34
+ from: vi.fn().mockReturnValue({
35
+ where: vi.fn().mockResolvedValue([]),
36
+ }),
37
+ }),
38
+ // transaction() passes itself as tx — mocks work the same way
39
+ transaction: vi.fn().mockImplementation(async (fn: (tx: unknown) => unknown) => fn(db)),
40
+ };
41
+ return db;
42
+ }
43
+
44
+ /** Minimal mock deps for key server tests. */
45
+ function mockDeps(): KeyServerDeps & {
46
+ chargeStore: { [K in keyof ICryptoChargeRepository]: ReturnType<typeof vi.fn> };
47
+ methodStore: { [K in keyof IPaymentMethodStore]: ReturnType<typeof vi.fn> };
48
+ } {
49
+ const chargeStore = {
50
+ getByReferenceId: vi.fn().mockResolvedValue({
51
+ referenceId: "btc:bc1q...",
52
+ status: "New",
53
+ depositAddress: "bc1q...",
54
+ chain: "bitcoin",
55
+ token: "BTC",
56
+ amountUsdCents: 5000,
57
+ creditedAt: null,
58
+ }),
59
+ createStablecoinCharge: vi.fn().mockResolvedValue(undefined),
60
+ create: vi.fn(),
61
+ updateStatus: vi.fn(),
62
+ markCredited: vi.fn(),
63
+ isCredited: vi.fn(),
64
+ getByDepositAddress: vi.fn(),
65
+ getNextDerivationIndex: vi.fn(),
66
+ listActiveDepositAddresses: vi.fn(),
67
+ };
68
+ const methodStore = {
69
+ listEnabled: vi.fn().mockResolvedValue([
70
+ {
71
+ id: "btc",
72
+ token: "BTC",
73
+ chain: "bitcoin",
74
+ decimals: 8,
75
+ displayName: "Bitcoin",
76
+ contractAddress: null,
77
+ confirmations: 6,
78
+ },
79
+ {
80
+ id: "base-usdc",
81
+ token: "USDC",
82
+ chain: "base",
83
+ decimals: 6,
84
+ displayName: "USDC on Base",
85
+ contractAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
86
+ confirmations: 12,
87
+ },
88
+ ]),
89
+ listAll: vi.fn(),
90
+ getById: vi.fn(),
91
+ listByType: vi.fn(),
92
+ upsert: vi.fn().mockResolvedValue(undefined),
93
+ setEnabled: vi.fn().mockResolvedValue(undefined),
94
+ };
95
+ return {
96
+ db: createMockDb() as never,
97
+ chargeStore: chargeStore as never,
98
+ methodStore: methodStore as never,
99
+ };
100
+ }
101
+
102
+ describe("key-server routes", () => {
103
+ it("GET /chains returns enabled payment methods", async () => {
104
+ const app = createKeyServerApp(mockDeps());
105
+ const res = await app.request("/chains");
106
+ expect(res.status).toBe(200);
107
+ const body = await res.json();
108
+ expect(body).toHaveLength(2);
109
+ expect(body[0].token).toBe("BTC");
110
+ expect(body[1].token).toBe("USDC");
111
+ });
112
+
113
+ it("POST /address requires chain", async () => {
114
+ const app = createKeyServerApp(mockDeps());
115
+ const res = await app.request("/address", {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify({}),
119
+ });
120
+ expect(res.status).toBe(400);
121
+ });
122
+
123
+ it("POST /address derives BTC address", async () => {
124
+ const app = createKeyServerApp(mockDeps());
125
+ const res = await app.request("/address", {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({ chain: "btc" }),
129
+ });
130
+ expect(res.status).toBe(201);
131
+ const body = await res.json();
132
+ expect(body.address).toMatch(/^bc1q/);
133
+ expect(body.index).toBe(0);
134
+ expect(body.chain).toBe("bitcoin");
135
+ expect(body.token).toBe("BTC");
136
+ });
137
+
138
+ it("GET /charges/:id returns charge status", async () => {
139
+ const app = createKeyServerApp(mockDeps());
140
+ const res = await app.request("/charges/btc:bc1q...");
141
+ expect(res.status).toBe(200);
142
+ const body = await res.json();
143
+ expect(body.chargeId).toBe("btc:bc1q...");
144
+ expect(body.status).toBe("New");
145
+ });
146
+
147
+ it("GET /charges/:id returns 404 for missing charge", async () => {
148
+ const deps = mockDeps();
149
+ (deps.chargeStore.getByReferenceId as ReturnType<typeof vi.fn>).mockResolvedValue(null);
150
+ const app = createKeyServerApp(deps);
151
+ const res = await app.request("/charges/nonexistent");
152
+ expect(res.status).toBe(404);
153
+ });
154
+
155
+ it("POST /charges validates amountUsd", async () => {
156
+ const app = createKeyServerApp(mockDeps());
157
+ const res = await app.request("/charges", {
158
+ method: "POST",
159
+ headers: { "Content-Type": "application/json" },
160
+ body: JSON.stringify({ chain: "btc", amountUsd: -10 }),
161
+ });
162
+ expect(res.status).toBe(400);
163
+ });
164
+
165
+ it("POST /charges creates a charge", async () => {
166
+ const app = createKeyServerApp(mockDeps());
167
+ const res = await app.request("/charges", {
168
+ method: "POST",
169
+ headers: { "Content-Type": "application/json" },
170
+ body: JSON.stringify({ chain: "btc", amountUsd: 50 }),
171
+ });
172
+ expect(res.status).toBe(201);
173
+ const body = await res.json();
174
+ expect(body.address).toMatch(/^bc1q/);
175
+ expect(body.amountUsd).toBe(50);
176
+ expect(body.expiresAt).toBeTruthy();
177
+ });
178
+
179
+ it("GET /admin/next-path returns available path", async () => {
180
+ const deps = mockDeps();
181
+ deps.adminToken = "test-admin";
182
+ const app = createKeyServerApp(deps);
183
+ const res = await app.request("/admin/next-path?coin_type=0", {
184
+ headers: { Authorization: "Bearer test-admin" },
185
+ });
186
+ expect(res.status).toBe(200);
187
+ const body = await res.json();
188
+ expect(body.path).toBe("m/44'/0'/0'");
189
+ expect(body.status).toBe("available");
190
+ });
191
+
192
+ it("DELETE /admin/chains/:id disables chain", async () => {
193
+ const deps = mockDeps();
194
+ deps.adminToken = "test-admin";
195
+ const app = createKeyServerApp(deps);
196
+ const res = await app.request("/admin/chains/doge", {
197
+ method: "DELETE",
198
+ headers: { Authorization: "Bearer test-admin" },
199
+ });
200
+ expect(res.status).toBe(204);
201
+ expect(deps.methodStore.setEnabled).toHaveBeenCalledWith("doge", false);
202
+ });
203
+ });
204
+
205
+ describe("key-server auth", () => {
206
+ it("rejects unauthenticated request when serviceKey is set", async () => {
207
+ const deps = mockDeps();
208
+ deps.serviceKey = "sk-test-secret";
209
+ const app = createKeyServerApp(deps);
210
+ const res = await app.request("/address", {
211
+ method: "POST",
212
+ headers: { "Content-Type": "application/json" },
213
+ body: JSON.stringify({ chain: "btc" }),
214
+ });
215
+ expect(res.status).toBe(401);
216
+ });
217
+
218
+ it("allows authenticated request with correct serviceKey", async () => {
219
+ const deps = mockDeps();
220
+ deps.serviceKey = "sk-test-secret";
221
+ const app = createKeyServerApp(deps);
222
+ const res = await app.request("/address", {
223
+ method: "POST",
224
+ headers: { "Content-Type": "application/json", Authorization: "Bearer sk-test-secret" },
225
+ body: JSON.stringify({ chain: "btc" }),
226
+ });
227
+ expect(res.status).toBe(201);
228
+ });
229
+
230
+ it("rejects admin route without adminToken", async () => {
231
+ const deps = mockDeps();
232
+ // no adminToken set — admin routes disabled
233
+ const app = createKeyServerApp(deps);
234
+ const res = await app.request("/admin/next-path?coin_type=0");
235
+ expect(res.status).toBe(403);
236
+ });
237
+
238
+ it("allows admin route with correct adminToken", async () => {
239
+ const deps = mockDeps();
240
+ deps.adminToken = "admin-secret";
241
+ const app = createKeyServerApp(deps);
242
+ const res = await app.request("/admin/next-path?coin_type=0", {
243
+ headers: { Authorization: "Bearer admin-secret" },
244
+ });
245
+ expect(res.status).toBe(200);
246
+ });
247
+ });
@@ -1,132 +1,116 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { BTCPayClient, loadCryptoConfig } from "./client.js";
3
-
4
- describe("BTCPayClient", () => {
5
- it("createInvoice sends correct request and returns id + checkoutLink", async () => {
6
- const mockResponse = { id: "inv-001", checkoutLink: "https://btcpay.example.com/i/inv-001" };
7
- const fetchSpy = vi
8
- .spyOn(globalThis, "fetch")
9
- .mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
10
-
11
- const client = new BTCPayClient({
12
- apiKey: "test-key",
13
- baseUrl: "https://btcpay.example.com",
14
- storeId: "store-abc",
15
- });
16
-
17
- const result = await client.createInvoice({
18
- amountUsd: 25,
19
- orderId: "order-123",
20
- buyerEmail: "test@example.com",
21
- });
2
+ import { BTCPayClient, CryptoServiceClient, loadCryptoConfig } from "./client.js";
22
3
 
23
- expect(result.id).toBe("inv-001");
24
- expect(result.checkoutLink).toBe("https://btcpay.example.com/i/inv-001");
4
+ describe("CryptoServiceClient", () => {
5
+ afterEach(() => vi.restoreAllMocks());
25
6
 
26
- expect(fetchSpy).toHaveBeenCalledOnce();
27
- const [url, opts] = fetchSpy.mock.calls[0];
28
- expect(url).toBe("https://btcpay.example.com/api/v1/stores/store-abc/invoices");
29
- expect(opts?.method).toBe("POST");
7
+ it("deriveAddress sends POST /address with chain", async () => {
8
+ const mockResponse = { address: "bc1q...", index: 42, chain: "bitcoin", token: "BTC" };
9
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 201 }));
30
10
 
31
- const headers = opts?.headers as Record<string, string>;
32
- expect(headers.Authorization).toBe("token test-key");
33
- expect(headers["Content-Type"]).toBe("application/json");
11
+ const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
12
+ const result = await client.deriveAddress("btc");
34
13
 
35
- const body = JSON.parse(opts?.body as string);
36
- expect(body.amount).toBe("25");
37
- expect(body.currency).toBe("USD");
38
- expect(body.metadata.orderId).toBe("order-123");
39
- expect(body.metadata.buyerEmail).toBe("test@example.com");
40
- expect(body.checkout.speedPolicy).toBe("MediumSpeed");
14
+ expect(result.address).toBe("bc1q...");
15
+ expect(result.index).toBe(42);
41
16
 
42
- fetchSpy.mockRestore();
17
+ const [url, opts] = vi.mocked(fetch).mock.calls[0];
18
+ expect(url).toBe("http://localhost:3100/address");
19
+ expect(opts?.method).toBe("POST");
20
+ expect(JSON.parse(opts?.body as string)).toEqual({ chain: "btc" });
43
21
  });
44
22
 
45
- it("createInvoice includes redirectURL when provided", async () => {
46
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
47
- new Response(JSON.stringify({ id: "inv-002", checkoutLink: "https://btcpay.example.com/i/inv-002" }), {
48
- status: 200,
49
- }),
50
- );
51
-
52
- const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://btcpay.example.com", storeId: "s" });
53
- await client.createInvoice({ amountUsd: 10, orderId: "o", redirectURL: "https://app.example.com/success" });
23
+ it("createCharge sends POST /charges", async () => {
24
+ const mockResponse = {
25
+ chargeId: "btc:bc1q...",
26
+ address: "bc1q...",
27
+ chain: "btc",
28
+ token: "BTC",
29
+ amountUsd: 50,
30
+ derivationIndex: 42,
31
+ expiresAt: "2026-03-20T04:00:00Z",
32
+ };
33
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 201 }));
34
+
35
+ const client = new CryptoServiceClient({
36
+ baseUrl: "http://localhost:3100",
37
+ serviceKey: "sk-test",
38
+ tenantId: "tenant-1",
39
+ });
40
+ const result = await client.createCharge({ chain: "btc", amountUsd: 50 });
54
41
 
55
- const body = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string);
56
- expect(body.checkout.redirectURL).toBe("https://app.example.com/success");
42
+ expect(result.chargeId).toBe("btc:bc1q...");
43
+ expect(result.address).toBe("bc1q...");
57
44
 
58
- fetchSpy.mockRestore();
45
+ const [, opts] = vi.mocked(fetch).mock.calls[0];
46
+ const headers = opts?.headers as Record<string, string>;
47
+ expect(headers.Authorization).toBe("Bearer sk-test");
48
+ expect(headers["X-Tenant-Id"]).toBe("tenant-1");
59
49
  });
60
50
 
61
- it("createInvoice throws on non-ok response", async () => {
62
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Unauthorized", { status: 401 }));
51
+ it("getCharge sends GET /charges/:id", async () => {
52
+ const mockResponse = { chargeId: "btc:bc1q...", status: "confirmed" };
53
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
63
54
 
64
- const client = new BTCPayClient({ apiKey: "bad-key", baseUrl: "https://btcpay.example.com", storeId: "s" });
65
- await expect(client.createInvoice({ amountUsd: 10, orderId: "o" })).rejects.toThrow(
66
- "BTCPay createInvoice failed (401)",
67
- );
55
+ const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
56
+ const result = await client.getCharge("btc:bc1q...");
68
57
 
69
- fetchSpy.mockRestore();
58
+ expect(result.status).toBe("confirmed");
59
+ expect(vi.mocked(fetch).mock.calls[0][0]).toBe("http://localhost:3100/charges/btc%3Abc1q...");
70
60
  });
71
61
 
72
- it("getInvoice sends correct request", async () => {
73
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
74
- new Response(JSON.stringify({ id: "inv-001", status: "Settled", amount: "25", currency: "USD" }), {
75
- status: 200,
76
- }),
77
- );
62
+ it("listChains sends GET /chains", async () => {
63
+ const mockResponse = [{ id: "btc", token: "BTC", chain: "bitcoin", decimals: 8 }];
64
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
78
65
 
79
- const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://btcpay.example.com", storeId: "store-abc" });
80
- const result = await client.getInvoice("inv-001");
66
+ const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
67
+ const result = await client.listChains();
81
68
 
82
- expect(result.status).toBe("Settled");
83
- expect(fetchSpy.mock.calls[0][0]).toBe("https://btcpay.example.com/api/v1/stores/store-abc/invoices/inv-001");
84
-
85
- fetchSpy.mockRestore();
69
+ expect(result).toHaveLength(1);
70
+ expect(result[0].token).toBe("BTC");
86
71
  });
87
- });
88
72
 
89
- describe("loadCryptoConfig", () => {
90
- beforeEach(() => {
91
- delete process.env.BTCPAY_API_KEY;
92
- delete process.env.BTCPAY_BASE_URL;
93
- delete process.env.BTCPAY_STORE_ID;
94
- });
73
+ it("throws on non-ok response", async () => {
74
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Not found", { status: 404 }));
95
75
 
96
- afterEach(() => {
97
- vi.unstubAllEnvs();
76
+ const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
77
+ await expect(client.getCharge("missing")).rejects.toThrow("CryptoService getCharge failed (404)");
98
78
  });
79
+ });
99
80
 
100
- it("returns null when BTCPAY_API_KEY is missing", () => {
101
- vi.stubEnv("BTCPAY_BASE_URL", "https://btcpay.test");
102
- vi.stubEnv("BTCPAY_STORE_ID", "store-1");
103
- expect(loadCryptoConfig()).toBeNull();
81
+ describe("BTCPayClient (deprecated)", () => {
82
+ it("throws on createInvoice", async () => {
83
+ const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://example.com", storeId: "s" });
84
+ await expect(client.createInvoice({ amountUsd: 10, orderId: "o" })).rejects.toThrow("deprecated");
104
85
  });
105
86
 
106
- it("returns null when BTCPAY_BASE_URL is missing", () => {
107
- vi.stubEnv("BTCPAY_API_KEY", "test-key");
108
- vi.stubEnv("BTCPAY_STORE_ID", "store-1");
109
- expect(loadCryptoConfig()).toBeNull();
87
+ it("throws on getInvoice", async () => {
88
+ const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://example.com", storeId: "s" });
89
+ await expect(client.getInvoice("inv-1")).rejects.toThrow("deprecated");
110
90
  });
91
+ });
111
92
 
112
- it("returns null when BTCPAY_STORE_ID is missing", () => {
113
- vi.stubEnv("BTCPAY_API_KEY", "test-key");
114
- vi.stubEnv("BTCPAY_BASE_URL", "https://btcpay.test");
115
- expect(loadCryptoConfig()).toBeNull();
93
+ describe("loadCryptoConfig", () => {
94
+ beforeEach(() => {
95
+ delete process.env.CRYPTO_SERVICE_URL;
96
+ delete process.env.CRYPTO_SERVICE_KEY;
97
+ delete process.env.TENANT_ID;
116
98
  });
117
99
 
118
- it("returns config when all env vars are set", () => {
119
- vi.stubEnv("BTCPAY_API_KEY", "test-key");
120
- vi.stubEnv("BTCPAY_BASE_URL", "https://btcpay.test");
121
- vi.stubEnv("BTCPAY_STORE_ID", "store-1");
100
+ afterEach(() => vi.unstubAllEnvs());
101
+
102
+ it("returns config when CRYPTO_SERVICE_URL is set", () => {
103
+ vi.stubEnv("CRYPTO_SERVICE_URL", "http://10.120.0.5:3100");
104
+ vi.stubEnv("CRYPTO_SERVICE_KEY", "sk-test");
105
+ vi.stubEnv("TENANT_ID", "tenant-1");
122
106
  expect(loadCryptoConfig()).toEqual({
123
- apiKey: "test-key",
124
- baseUrl: "https://btcpay.test",
125
- storeId: "store-1",
107
+ baseUrl: "http://10.120.0.5:3100",
108
+ serviceKey: "sk-test",
109
+ tenantId: "tenant-1",
126
110
  });
127
111
  });
128
112
 
129
- it("returns null when all env vars are missing", () => {
113
+ it("returns null when CRYPTO_SERVICE_URL is missing", () => {
130
114
  expect(loadCryptoConfig()).toBeNull();
131
115
  });
132
116
  });