@wopr-network/platform-core 1.42.3 → 1.44.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.
- package/.github/workflows/key-server-image.yml +35 -0
- package/Dockerfile.key-server +20 -0
- package/GATEWAY_BILLING_RESEARCH.md +430 -0
- package/biome.json +2 -9
- package/dist/billing/crypto/__tests__/key-server.test.js +240 -0
- package/dist/billing/crypto/btc/watcher.d.ts +2 -0
- package/dist/billing/crypto/btc/watcher.js +1 -1
- package/dist/billing/crypto/charge-store.d.ts +7 -1
- package/dist/billing/crypto/charge-store.js +7 -1
- package/dist/billing/crypto/client.d.ts +68 -30
- package/dist/billing/crypto/client.js +63 -46
- package/dist/billing/crypto/client.test.js +66 -83
- package/dist/billing/crypto/index.d.ts +8 -8
- package/dist/billing/crypto/index.js +4 -5
- package/dist/billing/crypto/key-server-entry.js +84 -0
- package/dist/billing/crypto/key-server-webhook.d.ts +33 -0
- package/dist/billing/crypto/key-server-webhook.js +73 -0
- package/dist/billing/crypto/key-server.d.ts +20 -0
- package/dist/billing/crypto/key-server.js +263 -0
- package/dist/billing/crypto/watcher-service.d.ts +33 -0
- package/dist/billing/crypto/watcher-service.js +295 -0
- package/dist/billing/index.js +1 -1
- package/dist/db/schema/crypto.d.ts +464 -2
- package/dist/db/schema/crypto.js +60 -6
- package/dist/monetization/crypto/__tests__/webhook.test.js +57 -92
- package/dist/monetization/crypto/index.d.ts +4 -4
- package/dist/monetization/crypto/index.js +2 -2
- package/dist/monetization/crypto/webhook.d.ts +13 -14
- package/dist/monetization/crypto/webhook.js +12 -83
- package/dist/monetization/index.d.ts +2 -2
- package/dist/monetization/index.js +1 -1
- package/drizzle/migrations/0014_crypto_key_server.sql +60 -0
- package/drizzle/migrations/0015_callback_url.sql +32 -0
- package/drizzle/migrations/meta/_journal.json +28 -0
- package/package.json +2 -1
- package/src/billing/crypto/__tests__/key-server.test.ts +262 -0
- package/src/billing/crypto/btc/watcher.ts +3 -1
- package/src/billing/crypto/charge-store.ts +13 -1
- package/src/billing/crypto/client.test.ts +70 -98
- package/src/billing/crypto/client.ts +118 -59
- package/src/billing/crypto/index.ts +19 -14
- package/src/billing/crypto/key-server-entry.ts +96 -0
- package/src/billing/crypto/key-server-webhook.ts +119 -0
- package/src/billing/crypto/key-server.ts +343 -0
- package/src/billing/crypto/watcher-service.ts +381 -0
- package/src/billing/index.ts +1 -1
- package/src/db/schema/crypto.ts +75 -6
- package/src/monetization/crypto/__tests__/webhook.test.ts +61 -104
- package/src/monetization/crypto/index.ts +9 -11
- package/src/monetization/crypto/webhook.ts +25 -99
- package/src/monetization/index.ts +3 -7
- package/dist/billing/crypto/checkout.d.ts +0 -18
- package/dist/billing/crypto/checkout.js +0 -35
- package/dist/billing/crypto/checkout.test.js +0 -71
- package/dist/billing/crypto/webhook.d.ts +0 -34
- package/dist/billing/crypto/webhook.js +0 -107
- package/dist/billing/crypto/webhook.test.js +0 -266
- package/src/billing/crypto/checkout.test.ts +0 -93
- package/src/billing/crypto/checkout.ts +0 -48
- package/src/billing/crypto/webhook.test.ts +0 -340
- package/src/billing/crypto/webhook.ts +0 -136
- /package/dist/billing/crypto/{checkout.test.d.ts → __tests__/key-server.test.d.ts} +0 -0
- /package/dist/billing/crypto/{webhook.test.d.ts → key-server-entry.d.ts} +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
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().mockResolvedValue({
|
|
91
|
+
id: "btc",
|
|
92
|
+
type: "native",
|
|
93
|
+
token: "BTC",
|
|
94
|
+
chain: "bitcoin",
|
|
95
|
+
decimals: 8,
|
|
96
|
+
displayName: "Bitcoin",
|
|
97
|
+
contractAddress: null,
|
|
98
|
+
confirmations: 6,
|
|
99
|
+
oracleAddress: "0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F",
|
|
100
|
+
xpub: null,
|
|
101
|
+
displayOrder: 0,
|
|
102
|
+
enabled: true,
|
|
103
|
+
rpcUrl: null,
|
|
104
|
+
}),
|
|
105
|
+
listByType: vi.fn(),
|
|
106
|
+
upsert: vi.fn().mockResolvedValue(undefined),
|
|
107
|
+
setEnabled: vi.fn().mockResolvedValue(undefined),
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
db: createMockDb() as never,
|
|
111
|
+
chargeStore: chargeStore as never,
|
|
112
|
+
methodStore: methodStore as never,
|
|
113
|
+
oracle: { getPrice: vi.fn().mockResolvedValue({ priceCents: 6_500_000, updatedAt: new Date() }) } as never,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe("key-server routes", () => {
|
|
118
|
+
it("GET /chains returns enabled payment methods", async () => {
|
|
119
|
+
const app = createKeyServerApp(mockDeps());
|
|
120
|
+
const res = await app.request("/chains");
|
|
121
|
+
expect(res.status).toBe(200);
|
|
122
|
+
const body = await res.json();
|
|
123
|
+
expect(body).toHaveLength(2);
|
|
124
|
+
expect(body[0].token).toBe("BTC");
|
|
125
|
+
expect(body[1].token).toBe("USDC");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("POST /address requires chain", async () => {
|
|
129
|
+
const app = createKeyServerApp(mockDeps());
|
|
130
|
+
const res = await app.request("/address", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { "Content-Type": "application/json" },
|
|
133
|
+
body: JSON.stringify({}),
|
|
134
|
+
});
|
|
135
|
+
expect(res.status).toBe(400);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("POST /address derives BTC address", async () => {
|
|
139
|
+
const app = createKeyServerApp(mockDeps());
|
|
140
|
+
const res = await app.request("/address", {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: { "Content-Type": "application/json" },
|
|
143
|
+
body: JSON.stringify({ chain: "btc" }),
|
|
144
|
+
});
|
|
145
|
+
expect(res.status).toBe(201);
|
|
146
|
+
const body = await res.json();
|
|
147
|
+
expect(body.address).toMatch(/^bc1q/);
|
|
148
|
+
expect(body.index).toBe(0);
|
|
149
|
+
expect(body.chain).toBe("bitcoin");
|
|
150
|
+
expect(body.token).toBe("BTC");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("GET /charges/:id returns charge status", async () => {
|
|
154
|
+
const app = createKeyServerApp(mockDeps());
|
|
155
|
+
const res = await app.request("/charges/btc:bc1q...");
|
|
156
|
+
expect(res.status).toBe(200);
|
|
157
|
+
const body = await res.json();
|
|
158
|
+
expect(body.chargeId).toBe("btc:bc1q...");
|
|
159
|
+
expect(body.status).toBe("New");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("GET /charges/:id returns 404 for missing charge", async () => {
|
|
163
|
+
const deps = mockDeps();
|
|
164
|
+
(deps.chargeStore.getByReferenceId as ReturnType<typeof vi.fn>).mockResolvedValue(null);
|
|
165
|
+
const app = createKeyServerApp(deps);
|
|
166
|
+
const res = await app.request("/charges/nonexistent");
|
|
167
|
+
expect(res.status).toBe(404);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("POST /charges validates amountUsd", async () => {
|
|
171
|
+
const app = createKeyServerApp(mockDeps());
|
|
172
|
+
const res = await app.request("/charges", {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
body: JSON.stringify({ chain: "btc", amountUsd: -10 }),
|
|
176
|
+
});
|
|
177
|
+
expect(res.status).toBe(400);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("POST /charges creates a charge", async () => {
|
|
181
|
+
const app = createKeyServerApp(mockDeps());
|
|
182
|
+
const res = await app.request("/charges", {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "Content-Type": "application/json" },
|
|
185
|
+
body: JSON.stringify({ chain: "btc", amountUsd: 50 }),
|
|
186
|
+
});
|
|
187
|
+
expect(res.status).toBe(201);
|
|
188
|
+
const body = await res.json();
|
|
189
|
+
expect(body.address).toMatch(/^bc1q/);
|
|
190
|
+
expect(body.amountUsd).toBe(50);
|
|
191
|
+
expect(body.expiresAt).toBeTruthy();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("GET /admin/next-path returns available path", async () => {
|
|
195
|
+
const deps = mockDeps();
|
|
196
|
+
deps.adminToken = "test-admin";
|
|
197
|
+
const app = createKeyServerApp(deps);
|
|
198
|
+
const res = await app.request("/admin/next-path?coin_type=0", {
|
|
199
|
+
headers: { Authorization: "Bearer test-admin" },
|
|
200
|
+
});
|
|
201
|
+
expect(res.status).toBe(200);
|
|
202
|
+
const body = await res.json();
|
|
203
|
+
expect(body.path).toBe("m/44'/0'/0'");
|
|
204
|
+
expect(body.status).toBe("available");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("DELETE /admin/chains/:id disables chain", async () => {
|
|
208
|
+
const deps = mockDeps();
|
|
209
|
+
deps.adminToken = "test-admin";
|
|
210
|
+
const app = createKeyServerApp(deps);
|
|
211
|
+
const res = await app.request("/admin/chains/doge", {
|
|
212
|
+
method: "DELETE",
|
|
213
|
+
headers: { Authorization: "Bearer test-admin" },
|
|
214
|
+
});
|
|
215
|
+
expect(res.status).toBe(204);
|
|
216
|
+
expect(deps.methodStore.setEnabled).toHaveBeenCalledWith("doge", false);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("key-server auth", () => {
|
|
221
|
+
it("rejects unauthenticated request when serviceKey is set", async () => {
|
|
222
|
+
const deps = mockDeps();
|
|
223
|
+
deps.serviceKey = "sk-test-secret";
|
|
224
|
+
const app = createKeyServerApp(deps);
|
|
225
|
+
const res = await app.request("/address", {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "Content-Type": "application/json" },
|
|
228
|
+
body: JSON.stringify({ chain: "btc" }),
|
|
229
|
+
});
|
|
230
|
+
expect(res.status).toBe(401);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("allows authenticated request with correct serviceKey", async () => {
|
|
234
|
+
const deps = mockDeps();
|
|
235
|
+
deps.serviceKey = "sk-test-secret";
|
|
236
|
+
const app = createKeyServerApp(deps);
|
|
237
|
+
const res = await app.request("/address", {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer sk-test-secret" },
|
|
240
|
+
body: JSON.stringify({ chain: "btc" }),
|
|
241
|
+
});
|
|
242
|
+
expect(res.status).toBe(201);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("rejects admin route without adminToken", async () => {
|
|
246
|
+
const deps = mockDeps();
|
|
247
|
+
// no adminToken set — admin routes disabled
|
|
248
|
+
const app = createKeyServerApp(deps);
|
|
249
|
+
const res = await app.request("/admin/next-path?coin_type=0");
|
|
250
|
+
expect(res.status).toBe(403);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("allows admin route with correct adminToken", async () => {
|
|
254
|
+
const deps = mockDeps();
|
|
255
|
+
deps.adminToken = "admin-secret";
|
|
256
|
+
const app = createKeyServerApp(deps);
|
|
257
|
+
const res = await app.request("/admin/next-path?coin_type=0", {
|
|
258
|
+
headers: { Authorization: "Bearer admin-secret" },
|
|
259
|
+
});
|
|
260
|
+
expect(res.status).toBe(200);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -14,6 +14,8 @@ export interface BtcWatcherOpts {
|
|
|
14
14
|
oracle: IPriceOracle;
|
|
15
15
|
/** Required — BTC has no block cursor, so txid dedup must be persisted. */
|
|
16
16
|
cursorStore: IWatcherCursorStore;
|
|
17
|
+
/** Override chain identity for cursor namespace (default: config.network). Prevents txid collisions across BTC/LTC/DOGE. */
|
|
18
|
+
chainId?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
interface ReceivedByAddress {
|
|
@@ -39,7 +41,7 @@ export class BtcWatcher {
|
|
|
39
41
|
this.minConfirmations = opts.config.confirmations;
|
|
40
42
|
this.oracle = opts.oracle;
|
|
41
43
|
this.cursorStore = opts.cursorStore;
|
|
42
|
-
this.watcherId = `btc:${opts.config.network}`;
|
|
44
|
+
this.watcherId = `btc:${opts.chainId ?? opts.config.network}`;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/** Update the set of watched addresses. */
|
|
@@ -17,6 +17,9 @@ export interface CryptoChargeRecord {
|
|
|
17
17
|
token: string | null;
|
|
18
18
|
depositAddress: string | null;
|
|
19
19
|
derivationIndex: number | null;
|
|
20
|
+
callbackUrl: string | null;
|
|
21
|
+
expectedAmount: string | null;
|
|
22
|
+
receivedAmount: string | null;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export interface CryptoDepositChargeInput {
|
|
@@ -27,6 +30,9 @@ export interface CryptoDepositChargeInput {
|
|
|
27
30
|
token: string;
|
|
28
31
|
depositAddress: string;
|
|
29
32
|
derivationIndex: number;
|
|
33
|
+
callbackUrl?: string;
|
|
34
|
+
/** Expected crypto amount in native base units (sats for BTC, base units for ERC20). */
|
|
35
|
+
expectedAmount?: string;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export interface ICryptoChargeRepository {
|
|
@@ -50,7 +56,7 @@ export interface ICryptoChargeRepository {
|
|
|
50
56
|
/**
|
|
51
57
|
* Manages crypto charge records in PostgreSQL.
|
|
52
58
|
*
|
|
53
|
-
* Each charge maps a
|
|
59
|
+
* Each charge maps a deposit address to a tenant and tracks
|
|
54
60
|
* the payment lifecycle (New → Processing → Settled/Expired/Invalid).
|
|
55
61
|
*
|
|
56
62
|
* amountUsdCents stores the requested amount in USD cents (integer).
|
|
@@ -92,6 +98,9 @@ export class DrizzleCryptoChargeRepository implements ICryptoChargeRepository {
|
|
|
92
98
|
token: row.token ?? null,
|
|
93
99
|
depositAddress: row.depositAddress ?? null,
|
|
94
100
|
derivationIndex: row.derivationIndex ?? null,
|
|
101
|
+
callbackUrl: row.callbackUrl ?? null,
|
|
102
|
+
expectedAmount: row.expectedAmount ?? null,
|
|
103
|
+
receivedAmount: row.receivedAmount ?? null,
|
|
95
104
|
};
|
|
96
105
|
}
|
|
97
106
|
|
|
@@ -146,6 +155,9 @@ export class DrizzleCryptoChargeRepository implements ICryptoChargeRepository {
|
|
|
146
155
|
token: input.token,
|
|
147
156
|
depositAddress: input.depositAddress.toLowerCase(),
|
|
148
157
|
derivationIndex: input.derivationIndex,
|
|
158
|
+
callbackUrl: input.callbackUrl,
|
|
159
|
+
expectedAmount: input.expectedAmount,
|
|
160
|
+
receivedAmount: "0",
|
|
149
161
|
});
|
|
150
162
|
}
|
|
151
163
|
|
|
@@ -1,132 +1,104 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
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 { CryptoServiceClient, loadCryptoConfig } from "./client.js";
|
|
22
3
|
|
|
23
|
-
|
|
24
|
-
|
|
4
|
+
describe("CryptoServiceClient", () => {
|
|
5
|
+
afterEach(() => vi.restoreAllMocks());
|
|
25
6
|
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
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
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
expect(
|
|
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
|
-
|
|
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("
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
expect(
|
|
42
|
+
expect(result.chargeId).toBe("btc:bc1q...");
|
|
43
|
+
expect(result.address).toBe("bc1q...");
|
|
57
44
|
|
|
58
|
-
|
|
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("
|
|
62
|
-
const
|
|
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
|
|
65
|
-
await
|
|
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
|
-
|
|
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("
|
|
73
|
-
const
|
|
74
|
-
|
|
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
|
|
80
|
-
const result = await client.
|
|
66
|
+
const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
|
|
67
|
+
const result = await client.listChains();
|
|
68
|
+
|
|
69
|
+
expect(result).toHaveLength(1);
|
|
70
|
+
expect(result[0].token).toBe("BTC");
|
|
71
|
+
});
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
it("throws on non-ok response", async () => {
|
|
74
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Not found", { status: 404 }));
|
|
84
75
|
|
|
85
|
-
|
|
76
|
+
const client = new CryptoServiceClient({ baseUrl: "http://localhost:3100" });
|
|
77
|
+
await expect(client.getCharge("missing")).rejects.toThrow("CryptoService getCharge failed (404)");
|
|
86
78
|
});
|
|
87
79
|
});
|
|
88
80
|
|
|
89
81
|
describe("loadCryptoConfig", () => {
|
|
90
82
|
beforeEach(() => {
|
|
91
|
-
delete process.env.
|
|
92
|
-
delete process.env.
|
|
93
|
-
delete process.env.
|
|
83
|
+
delete process.env.CRYPTO_SERVICE_URL;
|
|
84
|
+
delete process.env.CRYPTO_SERVICE_KEY;
|
|
85
|
+
delete process.env.TENANT_ID;
|
|
94
86
|
});
|
|
95
87
|
|
|
96
|
-
afterEach(() =>
|
|
97
|
-
vi.unstubAllEnvs();
|
|
98
|
-
});
|
|
99
|
-
|
|
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();
|
|
104
|
-
});
|
|
105
|
-
|
|
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();
|
|
110
|
-
});
|
|
111
|
-
|
|
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();
|
|
116
|
-
});
|
|
88
|
+
afterEach(() => vi.unstubAllEnvs());
|
|
117
89
|
|
|
118
|
-
it("returns config when
|
|
119
|
-
vi.stubEnv("
|
|
120
|
-
vi.stubEnv("
|
|
121
|
-
vi.stubEnv("
|
|
90
|
+
it("returns config when CRYPTO_SERVICE_URL is set", () => {
|
|
91
|
+
vi.stubEnv("CRYPTO_SERVICE_URL", "http://10.120.0.5:3100");
|
|
92
|
+
vi.stubEnv("CRYPTO_SERVICE_KEY", "sk-test");
|
|
93
|
+
vi.stubEnv("TENANT_ID", "tenant-1");
|
|
122
94
|
expect(loadCryptoConfig()).toEqual({
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
95
|
+
baseUrl: "http://10.120.0.5:3100",
|
|
96
|
+
serviceKey: "sk-test",
|
|
97
|
+
tenantId: "tenant-1",
|
|
126
98
|
});
|
|
127
99
|
});
|
|
128
100
|
|
|
129
|
-
it("returns null when
|
|
101
|
+
it("returns null when CRYPTO_SERVICE_URL is missing", () => {
|
|
130
102
|
expect(loadCryptoConfig()).toBeNull();
|
|
131
103
|
});
|
|
132
104
|
});
|