@wopr-network/platform-core 1.25.0 → 1.27.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.
Files changed (48) hide show
  1. package/dist/billing/crypto/index.d.ts +4 -0
  2. package/dist/billing/crypto/index.js +2 -0
  3. package/dist/billing/crypto/payment-method-store.d.ts +38 -0
  4. package/dist/billing/crypto/payment-method-store.js +82 -0
  5. package/dist/billing/crypto/unified-checkout.d.ts +35 -0
  6. package/dist/billing/crypto/unified-checkout.js +128 -0
  7. package/dist/db/schema/crypto.d.ts +216 -0
  8. package/dist/db/schema/crypto.js +20 -1
  9. package/dist/db/schema/index.d.ts +1 -0
  10. package/dist/db/schema/index.js +1 -0
  11. package/dist/db/schema/tenant-update-configs.d.ts +79 -0
  12. package/dist/db/schema/tenant-update-configs.js +15 -0
  13. package/dist/fleet/__tests__/tenant-update-config-repository.test.d.ts +1 -0
  14. package/dist/fleet/__tests__/tenant-update-config-repository.test.js +55 -0
  15. package/dist/fleet/drizzle-tenant-update-config-repository.d.ts +10 -0
  16. package/dist/fleet/drizzle-tenant-update-config-repository.js +44 -0
  17. package/dist/fleet/fleet-event-emitter.d.ts +1 -1
  18. package/dist/fleet/index.d.ts +2 -0
  19. package/dist/fleet/index.js +2 -0
  20. package/dist/fleet/init-fleet-updater.d.ts +6 -0
  21. package/dist/fleet/init-fleet-updater.js +29 -4
  22. package/dist/fleet/services.d.ts +2 -0
  23. package/dist/fleet/services.js +10 -0
  24. package/dist/fleet/tenant-update-config-repository.d.ts +11 -0
  25. package/dist/fleet/tenant-update-config-repository.js +1 -0
  26. package/dist/trpc/fleet-update-config-router.d.ts +24 -0
  27. package/dist/trpc/fleet-update-config-router.js +26 -0
  28. package/dist/trpc/index.d.ts +1 -0
  29. package/dist/trpc/index.js +1 -0
  30. package/drizzle/migrations/0008_payment_methods.sql +22 -0
  31. package/drizzle/migrations/0009_tenant_update_configs.sql +6 -0
  32. package/drizzle/migrations/meta/_journal.json +14 -0
  33. package/package.json +1 -1
  34. package/src/billing/crypto/index.ts +4 -0
  35. package/src/billing/crypto/payment-method-store.ts +117 -0
  36. package/src/billing/crypto/unified-checkout.ts +190 -0
  37. package/src/db/schema/crypto.ts +21 -1
  38. package/src/db/schema/index.ts +1 -0
  39. package/src/db/schema/tenant-update-configs.ts +16 -0
  40. package/src/fleet/__tests__/tenant-update-config-repository.test.ts +69 -0
  41. package/src/fleet/drizzle-tenant-update-config-repository.ts +48 -0
  42. package/src/fleet/fleet-event-emitter.ts +8 -1
  43. package/src/fleet/index.ts +2 -0
  44. package/src/fleet/init-fleet-updater.ts +38 -3
  45. package/src/fleet/services.ts +13 -0
  46. package/src/fleet/tenant-update-config-repository.ts +12 -0
  47. package/src/trpc/fleet-update-config-router.ts +31 -0
  48. package/src/trpc/index.ts +1 -0
@@ -0,0 +1,55 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
2
+ import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../../test/db.js";
3
+ import { DrizzleTenantUpdateConfigRepository } from "../drizzle-tenant-update-config-repository.js";
4
+ describe("DrizzleTenantUpdateConfigRepository", () => {
5
+ let db;
6
+ let pool;
7
+ let repo;
8
+ beforeAll(async () => {
9
+ ({ db, pool } = await createTestDb());
10
+ await beginTestTransaction(pool);
11
+ });
12
+ afterAll(async () => {
13
+ await endTestTransaction(pool);
14
+ await pool.close();
15
+ });
16
+ beforeEach(async () => {
17
+ await rollbackTestTransaction(pool);
18
+ repo = new DrizzleTenantUpdateConfigRepository(db);
19
+ });
20
+ it("get returns null for unknown tenant", async () => {
21
+ expect(await repo.get("nonexistent")).toBeNull();
22
+ });
23
+ it("upsert creates a new config", async () => {
24
+ await repo.upsert("tenant-1", { mode: "auto", preferredHourUtc: 5 });
25
+ const config = await repo.get("tenant-1");
26
+ expect(config).not.toBeNull();
27
+ expect(config?.tenantId).toBe("tenant-1");
28
+ expect(config?.mode).toBe("auto");
29
+ expect(config?.preferredHourUtc).toBe(5);
30
+ expect(config?.updatedAt).toBeGreaterThan(0);
31
+ });
32
+ it("upsert updates an existing config", async () => {
33
+ await repo.upsert("tenant-1", { mode: "auto", preferredHourUtc: 5 });
34
+ const before = await repo.get("tenant-1");
35
+ await repo.upsert("tenant-1", { mode: "manual", preferredHourUtc: 12 });
36
+ const after = await repo.get("tenant-1");
37
+ expect(after?.mode).toBe("manual");
38
+ expect(after?.preferredHourUtc).toBe(12);
39
+ expect(after?.updatedAt).toBeGreaterThanOrEqual(before?.updatedAt ?? 0);
40
+ });
41
+ it("listAutoEnabled returns only auto-mode tenants", async () => {
42
+ await repo.upsert("tenant-auto-1", { mode: "auto", preferredHourUtc: 3 });
43
+ await repo.upsert("tenant-auto-2", { mode: "auto", preferredHourUtc: 8 });
44
+ await repo.upsert("tenant-manual", { mode: "manual", preferredHourUtc: 0 });
45
+ const autoConfigs = await repo.listAutoEnabled();
46
+ expect(autoConfigs).toHaveLength(2);
47
+ expect(autoConfigs.every((c) => c.mode === "auto")).toBe(true);
48
+ expect(autoConfigs.map((c) => c.tenantId).sort()).toEqual(["tenant-auto-1", "tenant-auto-2"]);
49
+ });
50
+ it("listAutoEnabled returns empty array when none enabled", async () => {
51
+ await repo.upsert("tenant-manual", { mode: "manual", preferredHourUtc: 0 });
52
+ const autoConfigs = await repo.listAutoEnabled();
53
+ expect(autoConfigs).toEqual([]);
54
+ });
55
+ });
@@ -0,0 +1,10 @@
1
+ import type { DrizzleDb } from "../db/index.js";
2
+ import type { ITenantUpdateConfigRepository, TenantUpdateConfig } from "./tenant-update-config-repository.js";
3
+ /** Drizzle-backed implementation of ITenantUpdateConfigRepository. */
4
+ export declare class DrizzleTenantUpdateConfigRepository implements ITenantUpdateConfigRepository {
5
+ private readonly db;
6
+ constructor(db: DrizzleDb);
7
+ get(tenantId: string): Promise<TenantUpdateConfig | null>;
8
+ upsert(tenantId: string, config: Omit<TenantUpdateConfig, "tenantId" | "updatedAt">): Promise<void>;
9
+ listAutoEnabled(): Promise<TenantUpdateConfig[]>;
10
+ }
@@ -0,0 +1,44 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { tenantUpdateConfigs } from "../db/schema/index.js";
3
+ /** Drizzle-backed implementation of ITenantUpdateConfigRepository. */
4
+ export class DrizzleTenantUpdateConfigRepository {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ async get(tenantId) {
10
+ const rows = await this.db.select().from(tenantUpdateConfigs).where(eq(tenantUpdateConfigs.tenantId, tenantId));
11
+ return rows[0] ? toConfig(rows[0]) : null;
12
+ }
13
+ async upsert(tenantId, config) {
14
+ const now = Date.now();
15
+ await this.db
16
+ .insert(tenantUpdateConfigs)
17
+ .values({
18
+ tenantId,
19
+ mode: config.mode,
20
+ preferredHourUtc: config.preferredHourUtc,
21
+ updatedAt: now,
22
+ })
23
+ .onConflictDoUpdate({
24
+ target: tenantUpdateConfigs.tenantId,
25
+ set: {
26
+ mode: config.mode,
27
+ preferredHourUtc: config.preferredHourUtc,
28
+ updatedAt: now,
29
+ },
30
+ });
31
+ }
32
+ async listAutoEnabled() {
33
+ const rows = await this.db.select().from(tenantUpdateConfigs).where(eq(tenantUpdateConfigs.mode, "auto"));
34
+ return rows.map(toConfig);
35
+ }
36
+ }
37
+ function toConfig(row) {
38
+ return {
39
+ tenantId: row.tenantId,
40
+ mode: row.mode,
41
+ preferredHourUtc: row.preferredHourUtc,
42
+ updatedAt: row.updatedAt,
43
+ };
44
+ }
@@ -1,5 +1,5 @@
1
1
  import type { IFleetEventRepository } from "./fleet-event-repository.js";
2
- export type BotEventType = "bot.started" | "bot.stopped" | "bot.created" | "bot.removed" | "bot.restarted";
2
+ export type BotEventType = "bot.started" | "bot.stopped" | "bot.created" | "bot.removed" | "bot.restarted" | "bot.updated" | "bot.update_failed";
3
3
  export type NodeEventType = "node.provisioned" | "node.registered" | "node.draining" | "node.drained" | "node.deprovisioned" | "node.heartbeat_lost" | "node.returned";
4
4
  export type FleetEventType = BotEventType | NodeEventType;
5
5
  export interface BotFleetEvent {
@@ -1,7 +1,9 @@
1
+ export * from "./drizzle-tenant-update-config-repository.js";
1
2
  export * from "./init-fleet-updater.js";
2
3
  export * from "./repository-types.js";
3
4
  export * from "./rollout-orchestrator.js";
4
5
  export * from "./rollout-strategy.js";
5
6
  export * from "./services.js";
7
+ export * from "./tenant-update-config-repository.js";
6
8
  export * from "./types.js";
7
9
  export * from "./volume-snapshot-manager.js";
@@ -1,7 +1,9 @@
1
+ export * from "./drizzle-tenant-update-config-repository.js";
1
2
  export * from "./init-fleet-updater.js";
2
3
  export * from "./repository-types.js";
3
4
  export * from "./rollout-orchestrator.js";
4
5
  export * from "./rollout-strategy.js";
5
6
  export * from "./services.js";
7
+ export * from "./tenant-update-config-repository.js";
6
8
  export * from "./types.js";
7
9
  export * from "./volume-snapshot-manager.js";
@@ -12,11 +12,13 @@
12
12
  */
13
13
  import type Docker from "dockerode";
14
14
  import type { IBotProfileRepository } from "./bot-profile-repository.js";
15
+ import type { FleetEventEmitter } from "./fleet-event-emitter.js";
15
16
  import type { FleetManager } from "./fleet-manager.js";
16
17
  import { ImagePoller } from "./image-poller.js";
17
18
  import type { IProfileStore } from "./profile-store.js";
18
19
  import { RolloutOrchestrator, type RolloutResult } from "./rollout-orchestrator.js";
19
20
  import { type RollingWaveOptions } from "./rollout-strategy.js";
21
+ import type { ITenantUpdateConfigRepository } from "./tenant-update-config-repository.js";
20
22
  import { ContainerUpdater } from "./updater.js";
21
23
  import { VolumeSnapshotManager } from "./volume-snapshot-manager.js";
22
24
  export interface FleetUpdaterConfig {
@@ -34,6 +36,10 @@ export interface FleetUpdaterConfig {
34
36
  }) => void;
35
37
  /** Called when a rollout completes */
36
38
  onRolloutComplete?: (result: RolloutResult) => void;
39
+ /** Optional per-tenant update config repository. When provided, tenants with mode=manual are excluded. */
40
+ configRepo?: ITenantUpdateConfigRepository;
41
+ /** Optional fleet event emitter. When provided, bot.updated / bot.update_failed events are emitted. */
42
+ eventEmitter?: FleetEventEmitter;
37
43
  }
38
44
  export interface FleetUpdaterHandle {
39
45
  poller: ImagePoller;
@@ -29,7 +29,7 @@ import { VolumeSnapshotManager } from "./volume-snapshot-manager.js";
29
29
  * @param config - Optional pipeline configuration
30
30
  */
31
31
  export function initFleetUpdater(docker, fleet, profileStore, profileRepo, config = {}) {
32
- const { strategy: strategyType = "rolling-wave", strategyOptions, snapshotDir = "/data/fleet/snapshots", onBotUpdated, onRolloutComplete, } = config;
32
+ const { strategy: strategyType = "rolling-wave", strategyOptions, snapshotDir = "/data/fleet/snapshots", onBotUpdated, onRolloutComplete, configRepo, eventEmitter, } = config;
33
33
  const poller = new ImagePoller(docker, profileStore);
34
34
  const updater = new ContainerUpdater(docker, profileStore, fleet, poller);
35
35
  const snapshotManager = new VolumeSnapshotManager(docker, snapshotDir);
@@ -40,10 +40,35 @@ export function initFleetUpdater(docker, fleet, profileStore, profileRepo, confi
40
40
  strategy,
41
41
  getUpdatableProfiles: async () => {
42
42
  const profiles = await profileRepo.list();
43
- return profiles.filter((p) => p.updatePolicy !== "manual");
43
+ const nonManualPolicy = profiles.filter((p) => p.updatePolicy !== "manual");
44
+ if (!configRepo)
45
+ return nonManualPolicy;
46
+ // Filter out tenants whose per-tenant config is set to manual
47
+ const results = await Promise.all(nonManualPolicy.map(async (p) => {
48
+ const tenantCfg = await configRepo.get(p.tenantId);
49
+ // If tenant has an explicit config with mode=manual, exclude
50
+ if (tenantCfg && tenantCfg.mode === "manual")
51
+ return null;
52
+ return p;
53
+ }));
54
+ return results.filter((p) => p !== null);
55
+ },
56
+ onBotUpdated: (result) => {
57
+ // Emit fleet events if emitter is provided
58
+ if (eventEmitter) {
59
+ eventEmitter.emit({
60
+ type: result.success ? "bot.updated" : "bot.update_failed",
61
+ botId: result.botId,
62
+ tenantId: "",
63
+ timestamp: new Date().toISOString(),
64
+ });
65
+ }
66
+ // Chain user-provided callback
67
+ onBotUpdated?.(result);
68
+ },
69
+ onRolloutComplete: (result) => {
70
+ onRolloutComplete?.(result);
44
71
  },
45
- onBotUpdated,
46
- onRolloutComplete,
47
72
  });
48
73
  // Wire the detection → orchestration pipeline.
49
74
  // Any digest change triggers a fleet-wide rollout because the managed image
@@ -42,6 +42,7 @@ import { OrphanCleaner } from "./orphan-cleaner.js";
42
42
  import { RecoveryOrchestrator } from "./recovery-orchestrator.js";
43
43
  import type { IRecoveryRepository } from "./recovery-repository.js";
44
44
  import type { IRegistrationTokenRepository } from "./registration-token-store.js";
45
+ import type { ITenantUpdateConfigRepository } from "./tenant-update-config-repository.js";
45
46
  import type { IVpsRepository } from "./vps-repository.js";
46
47
  export declare function getPool(): Pool;
47
48
  export declare function getDb(): DrizzleDb;
@@ -71,6 +72,7 @@ export declare function getMigrationOrchestrator(): MigrationOrchestrator;
71
72
  export declare function getNodeDrainer(): NodeDrainer;
72
73
  export declare function getFleetEventEmitter(): FleetEventEmitter;
73
74
  export declare function getFleetEventRepo(): IFleetEventRepository;
75
+ export declare function getTenantUpdateConfigRepo(): ITenantUpdateConfigRepository;
74
76
  export declare function getHeartbeatWatchdog(): HeartbeatWatchdog;
75
77
  export declare function getInferenceWatchdog(): InferenceWatchdog;
76
78
  export declare function getDOClient(): DOClient;
@@ -37,6 +37,7 @@ import { DrizzleBotProfileRepository } from "./drizzle-bot-profile-repository.js
37
37
  import { DrizzleFleetEventRepository } from "./drizzle-fleet-event-repository.js";
38
38
  import { DrizzleNodeRepository } from "./drizzle-node-repository.js";
39
39
  import { DrizzleRecoveryRepository } from "./drizzle-recovery-repository.js";
40
+ import { DrizzleTenantUpdateConfigRepository } from "./drizzle-tenant-update-config-repository.js";
40
41
  import { FleetEventEmitter } from "./fleet-event-emitter.js";
41
42
  import { DrizzleGpuAllocationRepository } from "./gpu-allocation-repository.js";
42
43
  import { DrizzleGpuConfigurationRepository } from "./gpu-configuration-repository.js";
@@ -95,6 +96,8 @@ let _inferenceWatchdog = null;
95
96
  let _fleetEventEmitter = null;
96
97
  // Fleet event repository
97
98
  let _fleetEventRepo = null;
99
+ // Tenant update config
100
+ let _tenantUpdateConfigRepo = null;
98
101
  // Infrastructure
99
102
  let _doClient = null;
100
103
  let _nodeProvider = null;
@@ -317,6 +320,12 @@ export function getFleetEventRepo() {
317
320
  }
318
321
  return _fleetEventRepo;
319
322
  }
323
+ export function getTenantUpdateConfigRepo() {
324
+ if (!_tenantUpdateConfigRepo) {
325
+ _tenantUpdateConfigRepo = new DrizzleTenantUpdateConfigRepository(getDb());
326
+ }
327
+ return _tenantUpdateConfigRepo;
328
+ }
320
329
  // ---------------------------------------------------------------------------
321
330
  // HeartbeatWatchdog
322
331
  // ---------------------------------------------------------------------------
@@ -694,6 +703,7 @@ export function _resetForTest() {
694
703
  _inferenceWatchdog = null;
695
704
  _fleetEventRepo = null;
696
705
  _fleetEventEmitter = null;
706
+ _tenantUpdateConfigRepo = null;
697
707
  _doClient = null;
698
708
  _nodeProvider = null;
699
709
  _nodeProvisioner = null;
@@ -0,0 +1,11 @@
1
+ export interface TenantUpdateConfig {
2
+ tenantId: string;
3
+ mode: "auto" | "manual";
4
+ preferredHourUtc: number;
5
+ updatedAt: number;
6
+ }
7
+ export interface ITenantUpdateConfigRepository {
8
+ get(tenantId: string): Promise<TenantUpdateConfig | null>;
9
+ upsert(tenantId: string, config: Omit<TenantUpdateConfig, "tenantId" | "updatedAt">): Promise<void>;
10
+ listAutoEnabled(): Promise<TenantUpdateConfig[]>;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { ITenantUpdateConfigRepository } from "../fleet/tenant-update-config-repository.js";
2
+ export declare function createFleetUpdateConfigRouter(configRepo: ITenantUpdateConfigRepository): import("@trpc/server").TRPCBuiltRouter<{
3
+ ctx: import("./init.js").TRPCContext;
4
+ meta: object;
5
+ errorShape: import("@trpc/server").TRPCDefaultErrorShape;
6
+ transformer: false;
7
+ }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
8
+ getUpdateConfig: import("@trpc/server").TRPCQueryProcedure<{
9
+ input: {
10
+ tenantId: string;
11
+ };
12
+ output: import("../fleet/tenant-update-config-repository.js").TenantUpdateConfig | null;
13
+ meta: object;
14
+ }>;
15
+ setUpdateConfig: import("@trpc/server").TRPCMutationProcedure<{
16
+ input: {
17
+ tenantId: string;
18
+ mode: "manual" | "auto";
19
+ preferredHourUtc?: number | undefined;
20
+ };
21
+ output: void;
22
+ meta: object;
23
+ }>;
24
+ }>>;
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { logger } from "../config/logger.js";
3
+ import { protectedProcedure, router } from "./init.js";
4
+ export function createFleetUpdateConfigRouter(configRepo) {
5
+ return router({
6
+ getUpdateConfig: protectedProcedure.input(z.object({ tenantId: z.string().min(1) })).query(async ({ input }) => {
7
+ return configRepo.get(input.tenantId);
8
+ }),
9
+ setUpdateConfig: protectedProcedure
10
+ .input(z.object({
11
+ tenantId: z.string().min(1),
12
+ mode: z.enum(["auto", "manual"]),
13
+ preferredHourUtc: z.number().int().min(0).max(23).optional(),
14
+ }))
15
+ .mutation(async ({ input }) => {
16
+ await configRepo.upsert(input.tenantId, {
17
+ mode: input.mode,
18
+ preferredHourUtc: input.preferredHourUtc ?? 3,
19
+ });
20
+ logger.info("Tenant update config changed", {
21
+ tenantId: input.tenantId,
22
+ mode: input.mode,
23
+ });
24
+ }),
25
+ });
26
+ }
@@ -1 +1,2 @@
1
+ export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
1
2
  export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, type TRPCContext, tenantProcedure, } from "./init.js";
@@ -1 +1,2 @@
1
+ export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
1
2
  export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, tenantProcedure, } from "./init.js";
@@ -0,0 +1,22 @@
1
+ CREATE TABLE IF NOT EXISTS "payment_methods" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "type" text NOT NULL,
4
+ "token" text NOT NULL,
5
+ "chain" text NOT NULL,
6
+ "contract_address" text,
7
+ "decimals" integer NOT NULL,
8
+ "display_name" text NOT NULL,
9
+ "enabled" boolean NOT NULL DEFAULT true,
10
+ "display_order" integer NOT NULL DEFAULT 0,
11
+ "rpc_url" text,
12
+ "confirmations" integer NOT NULL DEFAULT 1,
13
+ "created_at" text DEFAULT (now()) NOT NULL
14
+ );
15
+ --> statement-breakpoint
16
+ INSERT INTO "payment_methods" ("id", "type", "token", "chain", "contract_address", "decimals", "display_name", "display_order", "confirmations") VALUES
17
+ ('USDC:base', 'erc20', 'USDC', 'base', '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', 6, 'USDC on Base', 0, 1),
18
+ ('USDT:base', 'erc20', 'USDT', 'base', '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', 6, 'USDT on Base', 1, 1),
19
+ ('DAI:base', 'erc20', 'DAI', 'base', '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', 18, 'DAI on Base', 2, 1),
20
+ ('ETH:base', 'native', 'ETH', 'base', NULL, 18, 'ETH on Base', 3, 1),
21
+ ('BTC:mainnet', 'native', 'BTC', 'bitcoin', NULL, 8, 'Bitcoin', 10, 3)
22
+ ON CONFLICT ("id") DO NOTHING;
@@ -0,0 +1,6 @@
1
+ CREATE TABLE IF NOT EXISTS "tenant_update_configs" (
2
+ "tenant_id" text PRIMARY KEY NOT NULL,
3
+ "mode" text DEFAULT 'manual' NOT NULL,
4
+ "preferred_hour_utc" integer DEFAULT 3 NOT NULL,
5
+ "updated_at" bigint NOT NULL
6
+ );
@@ -57,6 +57,20 @@
57
57
  "when": 1742227200000,
58
58
  "tag": "0007_watcher_cursors",
59
59
  "breakpoints": true
60
+ },
61
+ {
62
+ "idx": 8,
63
+ "version": "7",
64
+ "when": 1742313600000,
65
+ "tag": "0008_payment_methods",
66
+ "breakpoints": true
67
+ },
68
+ {
69
+ "idx": 9,
70
+ "version": "7",
71
+ "when": 1742400000000,
72
+ "tag": "0009_tenant_update_configs",
73
+ "breakpoints": true
60
74
  }
61
75
  ]
62
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.25.0",
3
+ "version": "1.27.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,6 +8,8 @@ export type { IWatcherCursorStore } from "./cursor-store.js";
8
8
  export { DrizzleWatcherCursorStore } from "./cursor-store.js";
9
9
  export * from "./evm/index.js";
10
10
  export * from "./oracle/index.js";
11
+ export type { IPaymentMethodStore, PaymentMethodRecord } from "./payment-method-store.js";
12
+ export { DrizzlePaymentMethodStore } from "./payment-method-store.js";
11
13
  export type {
12
14
  CryptoBillingConfig,
13
15
  CryptoCheckoutOpts,
@@ -16,5 +18,7 @@ export type {
16
18
  CryptoWebhookResult,
17
19
  } from "./types.js";
18
20
  export { mapBtcPayEventToStatus } from "./types.js";
21
+ export type { UnifiedCheckoutDeps, UnifiedCheckoutResult } from "./unified-checkout.js";
22
+ export { createUnifiedCheckout, MIN_CHECKOUT_USD } from "./unified-checkout.js";
19
23
  export type { CryptoWebhookDeps } from "./webhook.js";
20
24
  export { handleCryptoWebhook, verifyCryptoWebhookSignature } from "./webhook.js";
@@ -0,0 +1,117 @@
1
+ import { and, eq } from "drizzle-orm";
2
+ import type { PlatformDb } from "../../db/index.js";
3
+ import { paymentMethods } from "../../db/schema/crypto.js";
4
+
5
+ export interface PaymentMethodRecord {
6
+ id: string;
7
+ type: string;
8
+ token: string;
9
+ chain: string;
10
+ contractAddress: string | null;
11
+ decimals: number;
12
+ displayName: string;
13
+ enabled: boolean;
14
+ displayOrder: number;
15
+ rpcUrl: string | null;
16
+ confirmations: number;
17
+ }
18
+
19
+ export interface IPaymentMethodStore {
20
+ /** List all enabled payment methods, ordered by displayOrder. */
21
+ listEnabled(): Promise<PaymentMethodRecord[]>;
22
+ /** List all payment methods (including disabled). */
23
+ listAll(): Promise<PaymentMethodRecord[]>;
24
+ /** Get a specific payment method by id. */
25
+ getById(id: string): Promise<PaymentMethodRecord | null>;
26
+ /** Get enabled methods by type (stablecoin, eth, btc). */
27
+ listByType(type: string): Promise<PaymentMethodRecord[]>;
28
+ /** Upsert a payment method (admin). */
29
+ upsert(method: PaymentMethodRecord): Promise<void>;
30
+ /** Enable or disable a payment method (admin). */
31
+ setEnabled(id: string, enabled: boolean): Promise<void>;
32
+ }
33
+
34
+ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
35
+ constructor(private readonly db: PlatformDb) {}
36
+
37
+ async listEnabled(): Promise<PaymentMethodRecord[]> {
38
+ const rows = await this.db
39
+ .select()
40
+ .from(paymentMethods)
41
+ .where(eq(paymentMethods.enabled, true))
42
+ .orderBy(paymentMethods.displayOrder);
43
+ return rows.map(toRecord);
44
+ }
45
+
46
+ async listAll(): Promise<PaymentMethodRecord[]> {
47
+ const rows = await this.db.select().from(paymentMethods).orderBy(paymentMethods.displayOrder);
48
+ return rows.map(toRecord);
49
+ }
50
+
51
+ async getById(id: string): Promise<PaymentMethodRecord | null> {
52
+ const row = (await this.db.select().from(paymentMethods).where(eq(paymentMethods.id, id)))[0];
53
+ return row ? toRecord(row) : null;
54
+ }
55
+
56
+ async listByType(type: string): Promise<PaymentMethodRecord[]> {
57
+ const rows = await this.db
58
+ .select()
59
+ .from(paymentMethods)
60
+ .where(and(eq(paymentMethods.type, type), eq(paymentMethods.enabled, true)))
61
+ .orderBy(paymentMethods.displayOrder);
62
+ return rows.map(toRecord);
63
+ }
64
+
65
+ async upsert(method: PaymentMethodRecord): Promise<void> {
66
+ await this.db
67
+ .insert(paymentMethods)
68
+ .values({
69
+ id: method.id,
70
+ type: method.type,
71
+ token: method.token,
72
+ chain: method.chain,
73
+ contractAddress: method.contractAddress,
74
+ decimals: method.decimals,
75
+ displayName: method.displayName,
76
+ enabled: method.enabled,
77
+ displayOrder: method.displayOrder,
78
+ rpcUrl: method.rpcUrl,
79
+ confirmations: method.confirmations,
80
+ })
81
+ .onConflictDoUpdate({
82
+ target: paymentMethods.id,
83
+ set: {
84
+ type: method.type,
85
+ token: method.token,
86
+ chain: method.chain,
87
+ contractAddress: method.contractAddress,
88
+ decimals: method.decimals,
89
+ displayName: method.displayName,
90
+ enabled: method.enabled,
91
+ displayOrder: method.displayOrder,
92
+ rpcUrl: method.rpcUrl,
93
+ confirmations: method.confirmations,
94
+ },
95
+ });
96
+ }
97
+
98
+ async setEnabled(id: string, enabled: boolean): Promise<void> {
99
+ await this.db.update(paymentMethods).set({ enabled }).where(eq(paymentMethods.id, id));
100
+ }
101
+ }
102
+
103
+ function toRecord(row: typeof paymentMethods.$inferSelect): PaymentMethodRecord {
104
+ return {
105
+ id: row.id,
106
+ type: row.type,
107
+ token: row.token,
108
+ chain: row.chain,
109
+ contractAddress: row.contractAddress,
110
+ decimals: row.decimals,
111
+ displayName: row.displayName,
112
+ enabled: row.enabled,
113
+ displayOrder: row.displayOrder,
114
+ rpcUrl: row.rpcUrl,
115
+ confirmations: row.confirmations,
116
+ };
117
+ }