@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,240 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createKeyServerApp } from "../key-server.js";
|
|
3
|
+
/** Create a mock db that supports transaction() by passing itself to the callback. */
|
|
4
|
+
function createMockDb() {
|
|
5
|
+
const mockMethod = {
|
|
6
|
+
id: "btc",
|
|
7
|
+
type: "native",
|
|
8
|
+
token: "BTC",
|
|
9
|
+
chain: "bitcoin",
|
|
10
|
+
xpub: "xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz",
|
|
11
|
+
nextIndex: 1,
|
|
12
|
+
decimals: 8,
|
|
13
|
+
confirmations: 6,
|
|
14
|
+
};
|
|
15
|
+
const db = {
|
|
16
|
+
update: vi.fn().mockReturnValue({
|
|
17
|
+
set: vi.fn().mockReturnValue({
|
|
18
|
+
where: vi.fn().mockReturnValue({
|
|
19
|
+
returning: vi.fn().mockResolvedValue([mockMethod]),
|
|
20
|
+
}),
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
insert: vi.fn().mockReturnValue({
|
|
24
|
+
values: vi.fn().mockReturnValue({
|
|
25
|
+
onConflictDoNothing: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
|
26
|
+
}),
|
|
27
|
+
}),
|
|
28
|
+
select: vi.fn().mockReturnValue({
|
|
29
|
+
from: vi.fn().mockReturnValue({
|
|
30
|
+
where: vi.fn().mockResolvedValue([]),
|
|
31
|
+
}),
|
|
32
|
+
}),
|
|
33
|
+
// transaction() passes itself as tx — mocks work the same way
|
|
34
|
+
transaction: vi.fn().mockImplementation(async (fn) => fn(db)),
|
|
35
|
+
};
|
|
36
|
+
return db;
|
|
37
|
+
}
|
|
38
|
+
/** Minimal mock deps for key server tests. */
|
|
39
|
+
function mockDeps() {
|
|
40
|
+
const chargeStore = {
|
|
41
|
+
getByReferenceId: vi.fn().mockResolvedValue({
|
|
42
|
+
referenceId: "btc:bc1q...",
|
|
43
|
+
status: "New",
|
|
44
|
+
depositAddress: "bc1q...",
|
|
45
|
+
chain: "bitcoin",
|
|
46
|
+
token: "BTC",
|
|
47
|
+
amountUsdCents: 5000,
|
|
48
|
+
creditedAt: null,
|
|
49
|
+
}),
|
|
50
|
+
createStablecoinCharge: vi.fn().mockResolvedValue(undefined),
|
|
51
|
+
create: vi.fn(),
|
|
52
|
+
updateStatus: vi.fn(),
|
|
53
|
+
markCredited: vi.fn(),
|
|
54
|
+
isCredited: vi.fn(),
|
|
55
|
+
getByDepositAddress: vi.fn(),
|
|
56
|
+
getNextDerivationIndex: vi.fn(),
|
|
57
|
+
listActiveDepositAddresses: vi.fn(),
|
|
58
|
+
};
|
|
59
|
+
const methodStore = {
|
|
60
|
+
listEnabled: vi.fn().mockResolvedValue([
|
|
61
|
+
{
|
|
62
|
+
id: "btc",
|
|
63
|
+
token: "BTC",
|
|
64
|
+
chain: "bitcoin",
|
|
65
|
+
decimals: 8,
|
|
66
|
+
displayName: "Bitcoin",
|
|
67
|
+
contractAddress: null,
|
|
68
|
+
confirmations: 6,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "base-usdc",
|
|
72
|
+
token: "USDC",
|
|
73
|
+
chain: "base",
|
|
74
|
+
decimals: 6,
|
|
75
|
+
displayName: "USDC on Base",
|
|
76
|
+
contractAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
77
|
+
confirmations: 12,
|
|
78
|
+
},
|
|
79
|
+
]),
|
|
80
|
+
listAll: vi.fn(),
|
|
81
|
+
getById: vi.fn().mockResolvedValue({
|
|
82
|
+
id: "btc",
|
|
83
|
+
type: "native",
|
|
84
|
+
token: "BTC",
|
|
85
|
+
chain: "bitcoin",
|
|
86
|
+
decimals: 8,
|
|
87
|
+
displayName: "Bitcoin",
|
|
88
|
+
contractAddress: null,
|
|
89
|
+
confirmations: 6,
|
|
90
|
+
oracleAddress: "0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F",
|
|
91
|
+
xpub: null,
|
|
92
|
+
displayOrder: 0,
|
|
93
|
+
enabled: true,
|
|
94
|
+
rpcUrl: null,
|
|
95
|
+
}),
|
|
96
|
+
listByType: vi.fn(),
|
|
97
|
+
upsert: vi.fn().mockResolvedValue(undefined),
|
|
98
|
+
setEnabled: vi.fn().mockResolvedValue(undefined),
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
db: createMockDb(),
|
|
102
|
+
chargeStore: chargeStore,
|
|
103
|
+
methodStore: methodStore,
|
|
104
|
+
oracle: { getPrice: vi.fn().mockResolvedValue({ priceCents: 6_500_000, updatedAt: new Date() }) },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
describe("key-server routes", () => {
|
|
108
|
+
it("GET /chains returns enabled payment methods", async () => {
|
|
109
|
+
const app = createKeyServerApp(mockDeps());
|
|
110
|
+
const res = await app.request("/chains");
|
|
111
|
+
expect(res.status).toBe(200);
|
|
112
|
+
const body = await res.json();
|
|
113
|
+
expect(body).toHaveLength(2);
|
|
114
|
+
expect(body[0].token).toBe("BTC");
|
|
115
|
+
expect(body[1].token).toBe("USDC");
|
|
116
|
+
});
|
|
117
|
+
it("POST /address requires chain", async () => {
|
|
118
|
+
const app = createKeyServerApp(mockDeps());
|
|
119
|
+
const res = await app.request("/address", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "Content-Type": "application/json" },
|
|
122
|
+
body: JSON.stringify({}),
|
|
123
|
+
});
|
|
124
|
+
expect(res.status).toBe(400);
|
|
125
|
+
});
|
|
126
|
+
it("POST /address derives BTC address", async () => {
|
|
127
|
+
const app = createKeyServerApp(mockDeps());
|
|
128
|
+
const res = await app.request("/address", {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json" },
|
|
131
|
+
body: JSON.stringify({ chain: "btc" }),
|
|
132
|
+
});
|
|
133
|
+
expect(res.status).toBe(201);
|
|
134
|
+
const body = await res.json();
|
|
135
|
+
expect(body.address).toMatch(/^bc1q/);
|
|
136
|
+
expect(body.index).toBe(0);
|
|
137
|
+
expect(body.chain).toBe("bitcoin");
|
|
138
|
+
expect(body.token).toBe("BTC");
|
|
139
|
+
});
|
|
140
|
+
it("GET /charges/:id returns charge status", async () => {
|
|
141
|
+
const app = createKeyServerApp(mockDeps());
|
|
142
|
+
const res = await app.request("/charges/btc:bc1q...");
|
|
143
|
+
expect(res.status).toBe(200);
|
|
144
|
+
const body = await res.json();
|
|
145
|
+
expect(body.chargeId).toBe("btc:bc1q...");
|
|
146
|
+
expect(body.status).toBe("New");
|
|
147
|
+
});
|
|
148
|
+
it("GET /charges/:id returns 404 for missing charge", async () => {
|
|
149
|
+
const deps = mockDeps();
|
|
150
|
+
deps.chargeStore.getByReferenceId.mockResolvedValue(null);
|
|
151
|
+
const app = createKeyServerApp(deps);
|
|
152
|
+
const res = await app.request("/charges/nonexistent");
|
|
153
|
+
expect(res.status).toBe(404);
|
|
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
|
+
it("POST /charges creates a charge", async () => {
|
|
165
|
+
const app = createKeyServerApp(mockDeps());
|
|
166
|
+
const res = await app.request("/charges", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "Content-Type": "application/json" },
|
|
169
|
+
body: JSON.stringify({ chain: "btc", amountUsd: 50 }),
|
|
170
|
+
});
|
|
171
|
+
expect(res.status).toBe(201);
|
|
172
|
+
const body = await res.json();
|
|
173
|
+
expect(body.address).toMatch(/^bc1q/);
|
|
174
|
+
expect(body.amountUsd).toBe(50);
|
|
175
|
+
expect(body.expiresAt).toBeTruthy();
|
|
176
|
+
});
|
|
177
|
+
it("GET /admin/next-path returns available path", async () => {
|
|
178
|
+
const deps = mockDeps();
|
|
179
|
+
deps.adminToken = "test-admin";
|
|
180
|
+
const app = createKeyServerApp(deps);
|
|
181
|
+
const res = await app.request("/admin/next-path?coin_type=0", {
|
|
182
|
+
headers: { Authorization: "Bearer test-admin" },
|
|
183
|
+
});
|
|
184
|
+
expect(res.status).toBe(200);
|
|
185
|
+
const body = await res.json();
|
|
186
|
+
expect(body.path).toBe("m/44'/0'/0'");
|
|
187
|
+
expect(body.status).toBe("available");
|
|
188
|
+
});
|
|
189
|
+
it("DELETE /admin/chains/:id disables chain", async () => {
|
|
190
|
+
const deps = mockDeps();
|
|
191
|
+
deps.adminToken = "test-admin";
|
|
192
|
+
const app = createKeyServerApp(deps);
|
|
193
|
+
const res = await app.request("/admin/chains/doge", {
|
|
194
|
+
method: "DELETE",
|
|
195
|
+
headers: { Authorization: "Bearer test-admin" },
|
|
196
|
+
});
|
|
197
|
+
expect(res.status).toBe(204);
|
|
198
|
+
expect(deps.methodStore.setEnabled).toHaveBeenCalledWith("doge", false);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
describe("key-server auth", () => {
|
|
202
|
+
it("rejects unauthenticated request when serviceKey is set", async () => {
|
|
203
|
+
const deps = mockDeps();
|
|
204
|
+
deps.serviceKey = "sk-test-secret";
|
|
205
|
+
const app = createKeyServerApp(deps);
|
|
206
|
+
const res = await app.request("/address", {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: { "Content-Type": "application/json" },
|
|
209
|
+
body: JSON.stringify({ chain: "btc" }),
|
|
210
|
+
});
|
|
211
|
+
expect(res.status).toBe(401);
|
|
212
|
+
});
|
|
213
|
+
it("allows authenticated request with correct serviceKey", async () => {
|
|
214
|
+
const deps = mockDeps();
|
|
215
|
+
deps.serviceKey = "sk-test-secret";
|
|
216
|
+
const app = createKeyServerApp(deps);
|
|
217
|
+
const res = await app.request("/address", {
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer sk-test-secret" },
|
|
220
|
+
body: JSON.stringify({ chain: "btc" }),
|
|
221
|
+
});
|
|
222
|
+
expect(res.status).toBe(201);
|
|
223
|
+
});
|
|
224
|
+
it("rejects admin route without adminToken", async () => {
|
|
225
|
+
const deps = mockDeps();
|
|
226
|
+
// no adminToken set — admin routes disabled
|
|
227
|
+
const app = createKeyServerApp(deps);
|
|
228
|
+
const res = await app.request("/admin/next-path?coin_type=0");
|
|
229
|
+
expect(res.status).toBe(403);
|
|
230
|
+
});
|
|
231
|
+
it("allows admin route with correct adminToken", async () => {
|
|
232
|
+
const deps = mockDeps();
|
|
233
|
+
deps.adminToken = "admin-secret";
|
|
234
|
+
const app = createKeyServerApp(deps);
|
|
235
|
+
const res = await app.request("/admin/next-path?coin_type=0", {
|
|
236
|
+
headers: { Authorization: "Bearer admin-secret" },
|
|
237
|
+
});
|
|
238
|
+
expect(res.status).toBe(200);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -12,6 +12,8 @@ export interface BtcWatcherOpts {
|
|
|
12
12
|
oracle: IPriceOracle;
|
|
13
13
|
/** Required — BTC has no block cursor, so txid dedup must be persisted. */
|
|
14
14
|
cursorStore: IWatcherCursorStore;
|
|
15
|
+
/** Override chain identity for cursor namespace (default: config.network). Prevents txid collisions across BTC/LTC/DOGE. */
|
|
16
|
+
chainId?: string;
|
|
15
17
|
}
|
|
16
18
|
export declare class BtcWatcher {
|
|
17
19
|
private readonly rpc;
|
|
@@ -13,7 +13,7 @@ export class BtcWatcher {
|
|
|
13
13
|
this.minConfirmations = opts.config.confirmations;
|
|
14
14
|
this.oracle = opts.oracle;
|
|
15
15
|
this.cursorStore = opts.cursorStore;
|
|
16
|
-
this.watcherId = `btc:${opts.config.network}`;
|
|
16
|
+
this.watcherId = `btc:${opts.chainId ?? opts.config.network}`;
|
|
17
17
|
}
|
|
18
18
|
/** Update the set of watched addresses. */
|
|
19
19
|
setWatchedAddresses(addresses) {
|
|
@@ -14,6 +14,9 @@ export interface CryptoChargeRecord {
|
|
|
14
14
|
token: string | null;
|
|
15
15
|
depositAddress: string | null;
|
|
16
16
|
derivationIndex: number | null;
|
|
17
|
+
callbackUrl: string | null;
|
|
18
|
+
expectedAmount: string | null;
|
|
19
|
+
receivedAmount: string | null;
|
|
17
20
|
}
|
|
18
21
|
export interface CryptoDepositChargeInput {
|
|
19
22
|
referenceId: string;
|
|
@@ -23,6 +26,9 @@ export interface CryptoDepositChargeInput {
|
|
|
23
26
|
token: string;
|
|
24
27
|
depositAddress: string;
|
|
25
28
|
derivationIndex: number;
|
|
29
|
+
callbackUrl?: string;
|
|
30
|
+
/** Expected crypto amount in native base units (sats for BTC, base units for ERC20). */
|
|
31
|
+
expectedAmount?: string;
|
|
26
32
|
}
|
|
27
33
|
export interface ICryptoChargeRepository {
|
|
28
34
|
create(referenceId: string, tenantId: string, amountUsdCents: number): Promise<void>;
|
|
@@ -42,7 +48,7 @@ export interface ICryptoChargeRepository {
|
|
|
42
48
|
/**
|
|
43
49
|
* Manages crypto charge records in PostgreSQL.
|
|
44
50
|
*
|
|
45
|
-
* Each charge maps a
|
|
51
|
+
* Each charge maps a deposit address to a tenant and tracks
|
|
46
52
|
* the payment lifecycle (New → Processing → Settled/Expired/Invalid).
|
|
47
53
|
*
|
|
48
54
|
* amountUsdCents stores the requested amount in USD cents (integer).
|
|
@@ -3,7 +3,7 @@ import { cryptoCharges } from "../../db/schema/crypto.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Manages crypto charge records in PostgreSQL.
|
|
5
5
|
*
|
|
6
|
-
* Each charge maps a
|
|
6
|
+
* Each charge maps a deposit address to a tenant and tracks
|
|
7
7
|
* the payment lifecycle (New → Processing → Settled/Expired/Invalid).
|
|
8
8
|
*
|
|
9
9
|
* amountUsdCents stores the requested amount in USD cents (integer).
|
|
@@ -46,6 +46,9 @@ export class DrizzleCryptoChargeRepository {
|
|
|
46
46
|
token: row.token ?? null,
|
|
47
47
|
depositAddress: row.depositAddress ?? null,
|
|
48
48
|
derivationIndex: row.derivationIndex ?? null,
|
|
49
|
+
callbackUrl: row.callbackUrl ?? null,
|
|
50
|
+
expectedAmount: row.expectedAmount ?? null,
|
|
51
|
+
receivedAmount: row.receivedAmount ?? null,
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
/** Update charge status and payment details from webhook. */
|
|
@@ -89,6 +92,9 @@ export class DrizzleCryptoChargeRepository {
|
|
|
89
92
|
token: input.token,
|
|
90
93
|
depositAddress: input.depositAddress.toLowerCase(),
|
|
91
94
|
derivationIndex: input.derivationIndex,
|
|
95
|
+
callbackUrl: input.callbackUrl,
|
|
96
|
+
expectedAmount: input.expectedAmount,
|
|
97
|
+
receivedAmount: "0",
|
|
92
98
|
});
|
|
93
99
|
}
|
|
94
100
|
/** Look up a charge by its deposit address. */
|
|
@@ -1,39 +1,77 @@
|
|
|
1
|
-
import type { CryptoBillingConfig } from "./types.js";
|
|
2
|
-
export type { CryptoBillingConfig as CryptoConfig };
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
2
|
+
* Crypto Key Server client — for products to call the shared service.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Replaces BTCPayClient. Products set CRYPTO_SERVICE_URL instead of
|
|
5
|
+
* BTCPAY_API_KEY + BTCPAY_BASE_URL + BTCPAY_STORE_ID.
|
|
8
6
|
*/
|
|
9
|
-
export
|
|
7
|
+
export interface CryptoServiceConfig {
|
|
8
|
+
/** Base URL of the crypto key server (e.g. http://10.120.0.5:3100) */
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/** Service key for auth (reuses gateway service key) */
|
|
11
|
+
serviceKey?: string;
|
|
12
|
+
/** Tenant ID header */
|
|
13
|
+
tenantId?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DeriveAddressResult {
|
|
16
|
+
address: string;
|
|
17
|
+
index: number;
|
|
18
|
+
chain: string;
|
|
19
|
+
token: string;
|
|
20
|
+
}
|
|
21
|
+
export interface CreateChargeResult {
|
|
22
|
+
chargeId: string;
|
|
23
|
+
address: string;
|
|
24
|
+
chain: string;
|
|
25
|
+
token: string;
|
|
26
|
+
amountUsd: number;
|
|
27
|
+
derivationIndex: number;
|
|
28
|
+
expiresAt: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ChargeStatus {
|
|
31
|
+
chargeId: string;
|
|
32
|
+
status: string;
|
|
33
|
+
address: string | null;
|
|
34
|
+
chain: string | null;
|
|
35
|
+
token: string | null;
|
|
36
|
+
amountUsdCents: number;
|
|
37
|
+
creditedAt: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface ChainInfo {
|
|
40
|
+
id: string;
|
|
41
|
+
token: string;
|
|
42
|
+
chain: string;
|
|
43
|
+
decimals: number;
|
|
44
|
+
displayName: string;
|
|
45
|
+
contractAddress: string | null;
|
|
46
|
+
confirmations: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Client for the shared crypto key server.
|
|
50
|
+
* Products use this instead of running local watchers + holding xpubs.
|
|
51
|
+
*/
|
|
52
|
+
export declare class CryptoServiceClient {
|
|
10
53
|
private readonly config;
|
|
11
|
-
constructor(config:
|
|
54
|
+
constructor(config: CryptoServiceConfig);
|
|
12
55
|
private headers;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
createInvoice(opts: {
|
|
56
|
+
/** Derive the next unused address for a chain. */
|
|
57
|
+
deriveAddress(chain: string): Promise<DeriveAddressResult>;
|
|
58
|
+
/** Create a payment charge — derives address, sets expiry, starts watching. */
|
|
59
|
+
createCharge(opts: {
|
|
60
|
+
chain: string;
|
|
19
61
|
amountUsd: number;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/** Get invoice status by ID. */
|
|
28
|
-
getInvoice(invoiceId: string): Promise<{
|
|
29
|
-
id: string;
|
|
30
|
-
status: string;
|
|
31
|
-
amount: string;
|
|
32
|
-
currency: string;
|
|
33
|
-
}>;
|
|
62
|
+
callbackUrl?: string;
|
|
63
|
+
metadata?: Record<string, unknown>;
|
|
64
|
+
}): Promise<CreateChargeResult>;
|
|
65
|
+
/** Check charge status. */
|
|
66
|
+
getCharge(chargeId: string): Promise<ChargeStatus>;
|
|
67
|
+
/** List all enabled payment methods (for checkout UI). */
|
|
68
|
+
listChains(): Promise<ChainInfo[]>;
|
|
34
69
|
}
|
|
35
70
|
/**
|
|
36
|
-
* Load
|
|
37
|
-
* Returns null if
|
|
71
|
+
* Load crypto service config from environment.
|
|
72
|
+
* Returns null if CRYPTO_SERVICE_URL is not set.
|
|
73
|
+
*
|
|
74
|
+
* Also supports legacy BTCPay env vars for backwards compat during migration.
|
|
38
75
|
*/
|
|
39
|
-
export declare function loadCryptoConfig():
|
|
76
|
+
export declare function loadCryptoConfig(): CryptoServiceConfig | null;
|
|
77
|
+
export type CryptoConfig = CryptoServiceConfig;
|
|
@@ -1,72 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Crypto Key Server client — for products to call the shared service.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Replaces BTCPayClient. Products set CRYPTO_SERVICE_URL instead of
|
|
5
|
+
* BTCPAY_API_KEY + BTCPAY_BASE_URL + BTCPAY_STORE_ID.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Client for the shared crypto key server.
|
|
9
|
+
* Products use this instead of running local watchers + holding xpubs.
|
|
10
|
+
*/
|
|
11
|
+
export class CryptoServiceClient {
|
|
8
12
|
config;
|
|
9
13
|
constructor(config) {
|
|
10
14
|
this.config = config;
|
|
11
15
|
}
|
|
12
16
|
headers() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Authorization
|
|
16
|
-
|
|
17
|
+
const h = { "Content-Type": "application/json" };
|
|
18
|
+
if (this.config.serviceKey)
|
|
19
|
+
h.Authorization = `Bearer ${this.config.serviceKey}`;
|
|
20
|
+
if (this.config.tenantId)
|
|
21
|
+
h["X-Tenant-Id"] = this.config.tenantId;
|
|
22
|
+
return h;
|
|
17
23
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* Returns the invoice ID and checkout link (URL to redirect the user).
|
|
22
|
-
*/
|
|
23
|
-
async createInvoice(opts) {
|
|
24
|
-
const url = `${this.config.baseUrl}/api/v1/stores/${this.config.storeId}/invoices`;
|
|
25
|
-
const body = {
|
|
26
|
-
amount: String(opts.amountUsd),
|
|
27
|
-
currency: "USD",
|
|
28
|
-
metadata: {
|
|
29
|
-
orderId: opts.orderId,
|
|
30
|
-
buyerEmail: opts.buyerEmail,
|
|
31
|
-
},
|
|
32
|
-
checkout: {
|
|
33
|
-
speedPolicy: "MediumSpeed",
|
|
34
|
-
expirationMinutes: 30,
|
|
35
|
-
...(opts.redirectURL ? { redirectURL: opts.redirectURL } : {}),
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
const res = await fetch(url, {
|
|
24
|
+
/** Derive the next unused address for a chain. */
|
|
25
|
+
async deriveAddress(chain) {
|
|
26
|
+
const res = await fetch(`${this.config.baseUrl}/address`, {
|
|
39
27
|
method: "POST",
|
|
40
28
|
headers: this.headers(),
|
|
41
|
-
body: JSON.stringify(
|
|
29
|
+
body: JSON.stringify({ chain }),
|
|
42
30
|
});
|
|
43
31
|
if (!res.ok) {
|
|
44
32
|
const text = await res.text().catch(() => "");
|
|
45
|
-
throw new Error(`
|
|
33
|
+
throw new Error(`CryptoService deriveAddress failed (${res.status}): ${text}`);
|
|
46
34
|
}
|
|
47
|
-
|
|
48
|
-
return { id: data.id, checkoutLink: data.checkoutLink };
|
|
35
|
+
return (await res.json());
|
|
49
36
|
}
|
|
50
|
-
/**
|
|
51
|
-
async
|
|
52
|
-
const
|
|
53
|
-
|
|
37
|
+
/** Create a payment charge — derives address, sets expiry, starts watching. */
|
|
38
|
+
async createCharge(opts) {
|
|
39
|
+
const res = await fetch(`${this.config.baseUrl}/charges`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: this.headers(),
|
|
42
|
+
body: JSON.stringify(opts),
|
|
43
|
+
});
|
|
54
44
|
if (!res.ok) {
|
|
55
45
|
const text = await res.text().catch(() => "");
|
|
56
|
-
throw new Error(`
|
|
46
|
+
throw new Error(`CryptoService createCharge failed (${res.status}): ${text}`);
|
|
47
|
+
}
|
|
48
|
+
return (await res.json());
|
|
49
|
+
}
|
|
50
|
+
/** Check charge status. */
|
|
51
|
+
async getCharge(chargeId) {
|
|
52
|
+
const res = await fetch(`${this.config.baseUrl}/charges/${encodeURIComponent(chargeId)}`, {
|
|
53
|
+
headers: this.headers(),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const text = await res.text().catch(() => "");
|
|
57
|
+
throw new Error(`CryptoService getCharge failed (${res.status}): ${text}`);
|
|
58
|
+
}
|
|
59
|
+
return (await res.json());
|
|
60
|
+
}
|
|
61
|
+
/** List all enabled payment methods (for checkout UI). */
|
|
62
|
+
async listChains() {
|
|
63
|
+
const res = await fetch(`${this.config.baseUrl}/chains`, {
|
|
64
|
+
headers: this.headers(),
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const text = await res.text().catch(() => "");
|
|
68
|
+
throw new Error(`CryptoService listChains failed (${res.status}): ${text}`);
|
|
57
69
|
}
|
|
58
70
|
return (await res.json());
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
/**
|
|
62
|
-
* Load
|
|
63
|
-
* Returns null if
|
|
74
|
+
* Load crypto service config from environment.
|
|
75
|
+
* Returns null if CRYPTO_SERVICE_URL is not set.
|
|
76
|
+
*
|
|
77
|
+
* Also supports legacy BTCPay env vars for backwards compat during migration.
|
|
64
78
|
*/
|
|
65
79
|
export function loadCryptoConfig() {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
const baseUrl = process.env.CRYPTO_SERVICE_URL;
|
|
81
|
+
if (baseUrl) {
|
|
82
|
+
return {
|
|
83
|
+
baseUrl,
|
|
84
|
+
serviceKey: process.env.CRYPTO_SERVICE_KEY,
|
|
85
|
+
tenantId: process.env.TENANT_ID,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
72
89
|
}
|