@wopr-network/platform-core 1.43.0 → 1.44.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 (59) hide show
  1. package/.env.example +20 -0
  2. package/dist/billing/crypto/__tests__/key-server.test.js +16 -1
  3. package/dist/billing/crypto/btc/watcher.d.ts +2 -0
  4. package/dist/billing/crypto/btc/watcher.js +1 -1
  5. package/dist/billing/crypto/charge-store.d.ts +7 -1
  6. package/dist/billing/crypto/charge-store.js +7 -1
  7. package/dist/billing/crypto/client.d.ts +0 -26
  8. package/dist/billing/crypto/client.js +0 -13
  9. package/dist/billing/crypto/client.test.js +1 -11
  10. package/dist/billing/crypto/index.d.ts +5 -7
  11. package/dist/billing/crypto/index.js +3 -5
  12. package/dist/billing/crypto/key-server-entry.js +43 -2
  13. package/dist/billing/crypto/key-server-webhook.d.ts +33 -0
  14. package/dist/billing/crypto/key-server-webhook.js +73 -0
  15. package/dist/billing/crypto/key-server.d.ts +2 -0
  16. package/dist/billing/crypto/key-server.js +25 -1
  17. package/dist/billing/crypto/watcher-service.d.ts +33 -0
  18. package/dist/billing/crypto/watcher-service.js +295 -0
  19. package/dist/billing/index.js +1 -1
  20. package/dist/db/schema/crypto.d.ts +217 -2
  21. package/dist/db/schema/crypto.js +25 -2
  22. package/dist/monetization/crypto/__tests__/webhook.test.js +57 -92
  23. package/dist/monetization/crypto/index.d.ts +4 -4
  24. package/dist/monetization/crypto/index.js +2 -2
  25. package/dist/monetization/crypto/webhook.d.ts +13 -14
  26. package/dist/monetization/crypto/webhook.js +12 -83
  27. package/dist/monetization/index.d.ts +2 -2
  28. package/dist/monetization/index.js +1 -1
  29. package/drizzle/migrations/0015_callback_url.sql +32 -0
  30. package/drizzle/migrations/meta/_journal.json +7 -0
  31. package/package.json +1 -1
  32. package/src/billing/crypto/__tests__/key-server.test.ts +16 -1
  33. package/src/billing/crypto/btc/watcher.ts +3 -1
  34. package/src/billing/crypto/charge-store.ts +13 -1
  35. package/src/billing/crypto/client.test.ts +1 -13
  36. package/src/billing/crypto/client.ts +0 -21
  37. package/src/billing/crypto/index.ts +9 -13
  38. package/src/billing/crypto/key-server-entry.ts +46 -2
  39. package/src/billing/crypto/key-server-webhook.ts +119 -0
  40. package/src/billing/crypto/key-server.ts +29 -1
  41. package/src/billing/crypto/watcher-service.ts +381 -0
  42. package/src/billing/index.ts +1 -1
  43. package/src/db/schema/crypto.ts +30 -2
  44. package/src/monetization/crypto/__tests__/webhook.test.ts +61 -104
  45. package/src/monetization/crypto/index.ts +9 -11
  46. package/src/monetization/crypto/webhook.ts +25 -99
  47. package/src/monetization/index.ts +3 -7
  48. package/dist/billing/crypto/checkout.d.ts +0 -18
  49. package/dist/billing/crypto/checkout.js +0 -35
  50. package/dist/billing/crypto/checkout.test.d.ts +0 -1
  51. package/dist/billing/crypto/checkout.test.js +0 -71
  52. package/dist/billing/crypto/webhook.d.ts +0 -34
  53. package/dist/billing/crypto/webhook.js +0 -107
  54. package/dist/billing/crypto/webhook.test.d.ts +0 -1
  55. package/dist/billing/crypto/webhook.test.js +0 -266
  56. package/src/billing/crypto/checkout.test.ts +0 -93
  57. package/src/billing/crypto/checkout.ts +0 -48
  58. package/src/billing/crypto/webhook.test.ts +0 -340
  59. package/src/billing/crypto/webhook.ts +0 -136
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { BTCPayClient, CryptoServiceClient, loadCryptoConfig } from "./client.js";
2
+ import { CryptoServiceClient, loadCryptoConfig } from "./client.js";
3
3
 
4
4
  describe("CryptoServiceClient", () => {
5
5
  afterEach(() => vi.restoreAllMocks());
@@ -78,18 +78,6 @@ describe("CryptoServiceClient", () => {
78
78
  });
79
79
  });
80
80
 
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");
85
- });
86
-
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");
90
- });
91
- });
92
-
93
81
  describe("loadCryptoConfig", () => {
94
82
  beforeEach(() => {
95
83
  delete process.env.CRYPTO_SERVICE_URL;
@@ -143,24 +143,3 @@ export function loadCryptoConfig(): CryptoServiceConfig | null {
143
143
 
144
144
  // Legacy type alias for backwards compat
145
145
  export type CryptoConfig = CryptoServiceConfig;
146
-
147
- /**
148
- * @deprecated Use CryptoServiceClient instead. BTCPay is replaced by the crypto key server.
149
- * Kept for backwards compat — products still import BTCPayClient during migration.
150
- */
151
- export class BTCPayClient {
152
- constructor(_config: { apiKey: string; baseUrl: string; storeId: string }) {}
153
-
154
- async createInvoice(_opts: {
155
- amountUsd: number;
156
- orderId: string;
157
- buyerEmail?: string;
158
- redirectURL?: string;
159
- }): Promise<{ id: string; checkoutLink: string }> {
160
- throw new Error("BTCPayClient is deprecated — migrate to CryptoServiceClient");
161
- }
162
-
163
- async getInvoice(_invoiceId: string): Promise<{ id: string; status: string; amount: string; currency: string }> {
164
- throw new Error("BTCPayClient is deprecated — migrate to CryptoServiceClient");
165
- }
166
- }
@@ -1,7 +1,6 @@
1
1
  export * from "./btc/index.js";
2
2
  export type { CryptoChargeRecord, CryptoDepositChargeInput, ICryptoChargeRepository } from "./charge-store.js";
3
3
  export { CryptoChargeRepository, DrizzleCryptoChargeRepository } from "./charge-store.js";
4
- export { createCryptoCheckout, MIN_PAYMENT_USD } from "./checkout.js";
5
4
  export type {
6
5
  ChainInfo,
7
6
  ChargeStatus,
@@ -10,24 +9,21 @@ export type {
10
9
  CryptoServiceConfig,
11
10
  DeriveAddressResult,
12
11
  } from "./client.js";
13
- export { BTCPayClient, CryptoServiceClient, loadCryptoConfig } from "./client.js";
12
+ export { CryptoServiceClient, loadCryptoConfig } from "./client.js";
14
13
  export type { IWatcherCursorStore } from "./cursor-store.js";
15
14
  export { DrizzleWatcherCursorStore } from "./cursor-store.js";
16
15
  export * from "./evm/index.js";
17
16
  export type { KeyServerDeps } from "./key-server.js";
18
17
  export { createKeyServerApp } from "./key-server.js";
18
+ export type {
19
+ KeyServerWebhookDeps as CryptoWebhookDeps,
20
+ KeyServerWebhookPayload as CryptoWebhookPayload,
21
+ KeyServerWebhookResult as CryptoWebhookResult,
22
+ } from "./key-server-webhook.js";
23
+ export { handleKeyServerWebhook, handleKeyServerWebhook as handleCryptoWebhook } from "./key-server-webhook.js";
19
24
  export * from "./oracle/index.js";
20
25
  export type { IPaymentMethodStore, PaymentMethodRecord } from "./payment-method-store.js";
21
26
  export { DrizzlePaymentMethodStore } from "./payment-method-store.js";
22
- export type {
23
- CryptoBillingConfig,
24
- CryptoCheckoutOpts,
25
- CryptoPaymentState,
26
- CryptoWebhookPayload,
27
- CryptoWebhookResult,
28
- } from "./types.js";
29
- export { mapBtcPayEventToStatus } from "./types.js";
27
+ export type { CryptoPaymentState } from "./types.js";
30
28
  export type { UnifiedCheckoutDeps, UnifiedCheckoutResult } from "./unified-checkout.js";
31
- export { createUnifiedCheckout, MIN_CHECKOUT_USD } from "./unified-checkout.js";
32
- export type { CryptoWebhookDeps } from "./webhook.js";
33
- export { handleCryptoWebhook, verifyCryptoWebhookSignature } from "./webhook.js";
29
+ export { createUnifiedCheckout, MIN_CHECKOUT_USD as MIN_PAYMENT_USD, MIN_CHECKOUT_USD } from "./unified-checkout.js";
@@ -13,13 +13,21 @@ import { migrate } from "drizzle-orm/node-postgres/migrator";
13
13
  import pg from "pg";
14
14
  import * as schema from "../../db/schema/index.js";
15
15
  import { DrizzleCryptoChargeRepository } from "./charge-store.js";
16
+ import { DrizzleWatcherCursorStore } from "./cursor-store.js";
17
+ import { createRpcCaller } from "./evm/watcher.js";
16
18
  import { createKeyServerApp } from "./key-server.js";
19
+ import { ChainlinkOracle } from "./oracle/chainlink.js";
20
+ import { FixedPriceOracle } from "./oracle/fixed.js";
17
21
  import { DrizzlePaymentMethodStore } from "./payment-method-store.js";
22
+ import { startWatchers } from "./watcher-service.js";
18
23
 
19
24
  const PORT = Number(process.env.PORT ?? "3100");
20
25
  const DATABASE_URL = process.env.DATABASE_URL;
21
26
  const SERVICE_KEY = process.env.SERVICE_KEY;
22
27
  const ADMIN_TOKEN = process.env.ADMIN_TOKEN;
28
+ const BITCOIND_USER = process.env.BITCOIND_USER ?? "btcpay";
29
+ const BITCOIND_PASSWORD = process.env.BITCOIND_PASSWORD ?? "";
30
+ const BASE_RPC_URL = process.env.BASE_RPC_URL ?? "https://mainnet.base.org";
23
31
 
24
32
  if (!DATABASE_URL) {
25
33
  console.error("DATABASE_URL is required");
@@ -40,10 +48,46 @@ async function main(): Promise<void> {
40
48
  const chargeStore = new DrizzleCryptoChargeRepository(db);
41
49
  const methodStore = new DrizzlePaymentMethodStore(db);
42
50
 
43
- const app = createKeyServerApp({ db, chargeStore, methodStore, serviceKey: SERVICE_KEY, adminToken: ADMIN_TOKEN });
51
+ // Chainlink on-chain oracle for volatile assets (BTC, ETH).
52
+ const oracle = BASE_RPC_URL
53
+ ? new ChainlinkOracle({ rpcCall: createRpcCaller(BASE_RPC_URL) })
54
+ : new FixedPriceOracle();
44
55
 
56
+ const app = createKeyServerApp({
57
+ db,
58
+ chargeStore,
59
+ methodStore,
60
+ oracle,
61
+ serviceKey: SERVICE_KEY,
62
+ adminToken: ADMIN_TOKEN,
63
+ });
64
+
65
+ // Boot watchers (BTC + EVM) — polls for payments, sends webhooks
66
+ const cursorStore = new DrizzleWatcherCursorStore(db);
67
+ const stopWatchers = await startWatchers({
68
+ db,
69
+ chargeStore,
70
+ methodStore,
71
+ cursorStore,
72
+ oracle,
73
+ bitcoindUser: BITCOIND_USER,
74
+ bitcoindPassword: BITCOIND_PASSWORD,
75
+ log: (msg, meta) => console.log(`[watcher] ${msg}`, meta ?? ""),
76
+ });
77
+
78
+ const server = serve({ fetch: app.fetch, port: PORT });
45
79
  console.log(`[crypto-key-server] Listening on :${PORT}`);
46
- serve({ fetch: app.fetch, port: PORT });
80
+
81
+ // Graceful shutdown — stop accepting requests, drain watchers, close pool
82
+ const shutdown = async () => {
83
+ console.log("[crypto-key-server] Shutting down...");
84
+ stopWatchers();
85
+ server.close();
86
+ await pool.end();
87
+ process.exit(0);
88
+ };
89
+ process.on("SIGTERM", shutdown);
90
+ process.on("SIGINT", shutdown);
47
91
  }
48
92
 
49
93
  main().catch((err) => {
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Key Server webhook handler — processes payment confirmations from the
3
+ * centralized crypto key server.
4
+ *
5
+ * Payload shape (from watcher-service.ts):
6
+ * {
7
+ * chargeId: "btc:bc1q...",
8
+ * chain: "bitcoin",
9
+ * address: "bc1q...",
10
+ * amountUsdCents: 5000,
11
+ * status: "confirmed",
12
+ * txHash: "abc123...",
13
+ * amountReceived: "50000 sats",
14
+ * confirmations: 6
15
+ * }
16
+ *
17
+ * Replaces handleCryptoWebhook() for products using the key server.
18
+ */
19
+ import { Credit } from "../../credits/credit.js";
20
+ import type { ILedger } from "../../credits/ledger.js";
21
+ import type { IWebhookSeenRepository } from "../webhook-seen-repository.js";
22
+ import type { ICryptoChargeRepository } from "./charge-store.js";
23
+
24
+ export interface KeyServerWebhookPayload {
25
+ chargeId: string;
26
+ chain: string;
27
+ address: string;
28
+ amountUsdCents: number;
29
+ status: string;
30
+ txHash?: string;
31
+ amountReceived?: string;
32
+ confirmations?: number;
33
+ }
34
+
35
+ export interface KeyServerWebhookDeps {
36
+ chargeStore: ICryptoChargeRepository;
37
+ creditLedger: ILedger;
38
+ replayGuard: IWebhookSeenRepository;
39
+ onCreditsPurchased?: (tenantId: string, ledger: ILedger) => Promise<string[]>;
40
+ }
41
+
42
+ export interface KeyServerWebhookResult {
43
+ handled: boolean;
44
+ duplicate?: boolean;
45
+ tenant?: string;
46
+ creditedCents?: number;
47
+ reactivatedBots?: string[];
48
+ }
49
+
50
+ /**
51
+ * Process a payment confirmation from the crypto key server.
52
+ *
53
+ * Credits the ledger when status is "confirmed".
54
+ * Idempotency: ledger referenceId + replay guard (same pattern as Stripe handler).
55
+ */
56
+ export async function handleKeyServerWebhook(
57
+ deps: KeyServerWebhookDeps,
58
+ payload: KeyServerWebhookPayload,
59
+ ): Promise<KeyServerWebhookResult> {
60
+ const { chargeStore, creditLedger } = deps;
61
+
62
+ // Replay guard: deduplicate by chargeId
63
+ const dedupeKey = `ks:${payload.chargeId}`;
64
+ if (await deps.replayGuard.isDuplicate(dedupeKey, "crypto")) {
65
+ return { handled: true, duplicate: true };
66
+ }
67
+
68
+ // Look up the charge to find the tenant + amount
69
+ const charge = await chargeStore.getByReferenceId(payload.chargeId);
70
+ if (!charge) {
71
+ return { handled: false };
72
+ }
73
+
74
+ if (payload.status === "confirmed") {
75
+ // Only settle when payment is confirmed
76
+ await chargeStore.updateStatus(payload.chargeId, "Settled", charge.token ?? undefined, payload.amountReceived);
77
+
78
+ // Idempotency: check ledger referenceId (atomic, same as BTCPay handler)
79
+ const creditRef = `crypto:${payload.chargeId}`;
80
+ if (await creditLedger.hasReferenceId(creditRef)) {
81
+ await deps.replayGuard.markSeen(dedupeKey, "crypto");
82
+ return { handled: true, duplicate: true, tenant: charge.tenantId };
83
+ }
84
+
85
+ // Credit the original USD amount requested.
86
+ // charge.amountUsdCents is integer cents. Credit.fromCents() → nanodollars.
87
+ await creditLedger.credit(charge.tenantId, Credit.fromCents(charge.amountUsdCents), "purchase", {
88
+ description: `Crypto payment confirmed (${payload.chain}, tx: ${payload.txHash ?? "unknown"})`,
89
+ referenceId: creditRef,
90
+ fundingSource: "crypto",
91
+ });
92
+
93
+ await chargeStore.markCredited(payload.chargeId);
94
+
95
+ let reactivatedBots: string[] | undefined;
96
+ if (deps.onCreditsPurchased) {
97
+ reactivatedBots = await deps.onCreditsPurchased(charge.tenantId, creditLedger);
98
+ if (reactivatedBots.length === 0) reactivatedBots = undefined;
99
+ }
100
+
101
+ await deps.replayGuard.markSeen(dedupeKey, "crypto");
102
+ return {
103
+ handled: true,
104
+ tenant: charge.tenantId,
105
+ creditedCents: charge.amountUsdCents,
106
+ reactivatedBots,
107
+ };
108
+ }
109
+
110
+ // Non-confirmed status — update status but don't settle or credit
111
+ await chargeStore.updateStatus(
112
+ payload.chargeId,
113
+ payload.status as "Processing",
114
+ charge.token ?? undefined,
115
+ payload.amountReceived,
116
+ );
117
+ await deps.replayGuard.markSeen(dedupeKey, "crypto");
118
+ return { handled: true, tenant: charge.tenantId };
119
+ }
@@ -14,12 +14,15 @@ import { derivedAddresses, pathAllocations, paymentMethods } from "../../db/sche
14
14
  import { deriveAddress, deriveP2pkhAddress } from "./btc/address-gen.js";
15
15
  import type { ICryptoChargeRepository } from "./charge-store.js";
16
16
  import { deriveDepositAddress } from "./evm/address-gen.js";
17
+ import { centsToNative } from "./oracle/convert.js";
18
+ import type { IPriceOracle } from "./oracle/types.js";
17
19
  import type { IPaymentMethodStore } from "./payment-method-store.js";
18
20
 
19
21
  export interface KeyServerDeps {
20
22
  db: DrizzleDb;
21
23
  chargeStore: ICryptoChargeRepository;
22
24
  methodStore: IPaymentMethodStore;
25
+ oracle: IPriceOracle;
23
26
  /** Bearer token for product API routes. If unset, auth is disabled. */
24
27
  serviceKey?: string;
25
28
  /** Bearer token for admin routes. If unset, admin routes are disabled. */
@@ -145,7 +148,24 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
145
148
  const tenantId = c.req.header("X-Tenant-Id") ?? "unknown";
146
149
  const { address, index, chain, token } = await deriveNextAddress(deps.db, body.chain, tenantId);
147
150
 
151
+ // Look up payment method for decimals + oracle config
152
+ const method = await deps.methodStore.getById(body.chain);
153
+ if (!method) return c.json({ error: `Unknown chain: ${body.chain}` }, 400);
154
+
148
155
  const amountUsdCents = Math.round(body.amountUsd * 100);
156
+
157
+ // Compute expected crypto amount in native base units.
158
+ // Price is locked NOW — this is what the user must send.
159
+ let expectedAmount: bigint;
160
+ if (method.oracleAddress) {
161
+ // Volatile asset (BTC, ETH, DOGE) — oracle-priced
162
+ const { priceCents } = await deps.oracle.getPrice(token);
163
+ expectedAmount = centsToNative(amountUsdCents, priceCents, method.decimals);
164
+ } else {
165
+ // Stablecoin (1:1 USD) — e.g. $50 USDC = 50_000_000 base units (6 decimals)
166
+ expectedAmount = (BigInt(amountUsdCents) * 10n ** BigInt(method.decimals)) / 100n;
167
+ }
168
+
149
169
  const referenceId = `${token.toLowerCase()}:${address.toLowerCase()}`;
150
170
 
151
171
  await deps.chargeStore.createStablecoinCharge({
@@ -156,15 +176,23 @@ export function createKeyServerApp(deps: KeyServerDeps): Hono {
156
176
  token,
157
177
  depositAddress: address,
158
178
  derivationIndex: index,
179
+ callbackUrl: body.callbackUrl,
180
+ expectedAmount: expectedAmount.toString(),
159
181
  });
160
182
 
183
+ // Format display amount for the client
184
+ const divisor = 10 ** method.decimals;
185
+ const displayAmount = `${(Number(expectedAmount) / divisor).toFixed(Math.min(method.decimals, 8))} ${token}`;
186
+
161
187
  return c.json(
162
188
  {
163
189
  chargeId: referenceId,
164
190
  address,
165
- chain: body.chain,
191
+ chain,
166
192
  token,
167
193
  amountUsd: body.amountUsd,
194
+ expectedAmount: expectedAmount.toString(),
195
+ displayAmount,
168
196
  derivationIndex: index,
169
197
  expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // 30 min
170
198
  },