@wopr-network/platform-core 1.42.2 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -300,6 +300,9 @@ export declare const watcherCursors: import("drizzle-orm/pg-core").PgTableWithCo
300
300
  * Payment method registry — runtime-configurable tokens/chains.
301
301
  * Admin inserts a row to enable a new payment method. No deploy needed.
302
302
  * Contract addresses are immutable on-chain but configurable here.
303
+ *
304
+ * nextIndex is an atomic counter for HD derivation — never reuses an index.
305
+ * Increment via UPDATE ... SET next_index = next_index + 1 RETURNING next_index.
303
306
  */
304
307
  export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithColumns<{
305
308
  name: "payment_methods";
@@ -373,6 +376,23 @@ export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithCo
373
376
  identity: undefined;
374
377
  generated: undefined;
375
378
  }, {}, {}>;
379
+ network: import("drizzle-orm/pg-core").PgColumn<{
380
+ name: "network";
381
+ tableName: "payment_methods";
382
+ dataType: "string";
383
+ columnType: "PgText";
384
+ data: string;
385
+ driverParam: string;
386
+ notNull: true;
387
+ hasDefault: true;
388
+ isPrimaryKey: false;
389
+ isAutoincrement: false;
390
+ hasRuntimeDefault: false;
391
+ enumValues: [string, ...string[]];
392
+ baseColumn: never;
393
+ identity: undefined;
394
+ generated: undefined;
395
+ }, {}, {}>;
376
396
  contractAddress: import("drizzle-orm/pg-core").PgColumn<{
377
397
  name: "contract_address";
378
398
  tableName: "payment_methods";
@@ -526,6 +546,23 @@ export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithCo
526
546
  identity: undefined;
527
547
  generated: undefined;
528
548
  }, {}, {}>;
549
+ nextIndex: import("drizzle-orm/pg-core").PgColumn<{
550
+ name: "next_index";
551
+ tableName: "payment_methods";
552
+ dataType: "number";
553
+ columnType: "PgInteger";
554
+ data: number;
555
+ driverParam: string | number;
556
+ notNull: true;
557
+ hasDefault: true;
558
+ isPrimaryKey: false;
559
+ isAutoincrement: false;
560
+ hasRuntimeDefault: false;
561
+ enumValues: undefined;
562
+ baseColumn: never;
563
+ identity: undefined;
564
+ generated: undefined;
565
+ }, {}, {}>;
529
566
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
530
567
  name: "created_at";
531
568
  tableName: "payment_methods";
@@ -546,6 +583,216 @@ export declare const paymentMethods: import("drizzle-orm/pg-core").PgTableWithCo
546
583
  };
547
584
  dialect: "pg";
548
585
  }>;
586
+ /**
587
+ * BIP-44 path allocation registry — tracks which derivation paths are in use.
588
+ * The server knows which paths are allocated so you never collide.
589
+ * The seed phrase never touches the server — only xpubs.
590
+ */
591
+ export declare const pathAllocations: import("drizzle-orm/pg-core").PgTableWithColumns<{
592
+ name: "path_allocations";
593
+ schema: undefined;
594
+ columns: {
595
+ coinType: import("drizzle-orm/pg-core").PgColumn<{
596
+ name: "coin_type";
597
+ tableName: "path_allocations";
598
+ dataType: "number";
599
+ columnType: "PgInteger";
600
+ data: number;
601
+ driverParam: string | number;
602
+ notNull: true;
603
+ hasDefault: false;
604
+ isPrimaryKey: false;
605
+ isAutoincrement: false;
606
+ hasRuntimeDefault: false;
607
+ enumValues: undefined;
608
+ baseColumn: never;
609
+ identity: undefined;
610
+ generated: undefined;
611
+ }, {}, {}>;
612
+ accountIndex: import("drizzle-orm/pg-core").PgColumn<{
613
+ name: "account_index";
614
+ tableName: "path_allocations";
615
+ dataType: "number";
616
+ columnType: "PgInteger";
617
+ data: number;
618
+ driverParam: string | number;
619
+ notNull: true;
620
+ hasDefault: false;
621
+ isPrimaryKey: false;
622
+ isAutoincrement: false;
623
+ hasRuntimeDefault: false;
624
+ enumValues: undefined;
625
+ baseColumn: never;
626
+ identity: undefined;
627
+ generated: undefined;
628
+ }, {}, {}>;
629
+ chainId: import("drizzle-orm/pg-core").PgColumn<{
630
+ name: "chain_id";
631
+ tableName: "path_allocations";
632
+ dataType: "string";
633
+ columnType: "PgText";
634
+ data: string;
635
+ driverParam: string;
636
+ notNull: false;
637
+ hasDefault: false;
638
+ isPrimaryKey: false;
639
+ isAutoincrement: false;
640
+ hasRuntimeDefault: false;
641
+ enumValues: [string, ...string[]];
642
+ baseColumn: never;
643
+ identity: undefined;
644
+ generated: undefined;
645
+ }, {}, {}>;
646
+ xpub: import("drizzle-orm/pg-core").PgColumn<{
647
+ name: "xpub";
648
+ tableName: "path_allocations";
649
+ dataType: "string";
650
+ columnType: "PgText";
651
+ data: string;
652
+ driverParam: string;
653
+ notNull: true;
654
+ hasDefault: false;
655
+ isPrimaryKey: false;
656
+ isAutoincrement: false;
657
+ hasRuntimeDefault: false;
658
+ enumValues: [string, ...string[]];
659
+ baseColumn: never;
660
+ identity: undefined;
661
+ generated: undefined;
662
+ }, {}, {}>;
663
+ allocatedAt: import("drizzle-orm/pg-core").PgColumn<{
664
+ name: "allocated_at";
665
+ tableName: "path_allocations";
666
+ dataType: "string";
667
+ columnType: "PgText";
668
+ data: string;
669
+ driverParam: string;
670
+ notNull: true;
671
+ hasDefault: true;
672
+ isPrimaryKey: false;
673
+ isAutoincrement: false;
674
+ hasRuntimeDefault: false;
675
+ enumValues: [string, ...string[]];
676
+ baseColumn: never;
677
+ identity: undefined;
678
+ generated: undefined;
679
+ }, {}, {}>;
680
+ };
681
+ dialect: "pg";
682
+ }>;
683
+ /**
684
+ * Every address ever derived — immutable append-only log.
685
+ * Used for auditing and ensuring no address is ever reused.
686
+ */
687
+ export declare const derivedAddresses: import("drizzle-orm/pg-core").PgTableWithColumns<{
688
+ name: "derived_addresses";
689
+ schema: undefined;
690
+ columns: {
691
+ id: import("drizzle-orm/pg-core").PgColumn<{
692
+ name: "id";
693
+ tableName: "derived_addresses";
694
+ dataType: "number";
695
+ columnType: "PgInteger";
696
+ data: number;
697
+ driverParam: string | number;
698
+ notNull: true;
699
+ hasDefault: true;
700
+ isPrimaryKey: true;
701
+ isAutoincrement: false;
702
+ hasRuntimeDefault: false;
703
+ enumValues: undefined;
704
+ baseColumn: never;
705
+ identity: "always";
706
+ generated: undefined;
707
+ }, {}, {}>;
708
+ chainId: import("drizzle-orm/pg-core").PgColumn<{
709
+ name: "chain_id";
710
+ tableName: "derived_addresses";
711
+ dataType: "string";
712
+ columnType: "PgText";
713
+ data: string;
714
+ driverParam: string;
715
+ notNull: true;
716
+ hasDefault: false;
717
+ isPrimaryKey: false;
718
+ isAutoincrement: false;
719
+ hasRuntimeDefault: false;
720
+ enumValues: [string, ...string[]];
721
+ baseColumn: never;
722
+ identity: undefined;
723
+ generated: undefined;
724
+ }, {}, {}>;
725
+ derivationIndex: import("drizzle-orm/pg-core").PgColumn<{
726
+ name: "derivation_index";
727
+ tableName: "derived_addresses";
728
+ dataType: "number";
729
+ columnType: "PgInteger";
730
+ data: number;
731
+ driverParam: string | number;
732
+ notNull: true;
733
+ hasDefault: false;
734
+ isPrimaryKey: false;
735
+ isAutoincrement: false;
736
+ hasRuntimeDefault: false;
737
+ enumValues: undefined;
738
+ baseColumn: never;
739
+ identity: undefined;
740
+ generated: undefined;
741
+ }, {}, {}>;
742
+ address: import("drizzle-orm/pg-core").PgColumn<{
743
+ name: "address";
744
+ tableName: "derived_addresses";
745
+ dataType: "string";
746
+ columnType: "PgText";
747
+ data: string;
748
+ driverParam: string;
749
+ notNull: true;
750
+ hasDefault: false;
751
+ isPrimaryKey: false;
752
+ isAutoincrement: false;
753
+ hasRuntimeDefault: false;
754
+ enumValues: [string, ...string[]];
755
+ baseColumn: never;
756
+ identity: undefined;
757
+ generated: undefined;
758
+ }, {}, {}>;
759
+ tenantId: import("drizzle-orm/pg-core").PgColumn<{
760
+ name: "tenant_id";
761
+ tableName: "derived_addresses";
762
+ dataType: "string";
763
+ columnType: "PgText";
764
+ data: string;
765
+ driverParam: string;
766
+ notNull: false;
767
+ hasDefault: false;
768
+ isPrimaryKey: false;
769
+ isAutoincrement: false;
770
+ hasRuntimeDefault: false;
771
+ enumValues: [string, ...string[]];
772
+ baseColumn: never;
773
+ identity: undefined;
774
+ generated: undefined;
775
+ }, {}, {}>;
776
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
777
+ name: "created_at";
778
+ tableName: "derived_addresses";
779
+ dataType: "string";
780
+ columnType: "PgText";
781
+ data: string;
782
+ driverParam: string;
783
+ notNull: true;
784
+ hasDefault: true;
785
+ isPrimaryKey: false;
786
+ isAutoincrement: false;
787
+ hasRuntimeDefault: false;
788
+ enumValues: [string, ...string[]];
789
+ baseColumn: never;
790
+ identity: undefined;
791
+ generated: undefined;
792
+ }, {}, {}>;
793
+ };
794
+ dialect: "pg";
795
+ }>;
549
796
  /** Processed transaction IDs for watchers without block cursors (e.g. BTC). */
550
797
  export declare const watcherProcessed: import("drizzle-orm/pg-core").PgTableWithColumns<{
551
798
  name: "watcher_processed";
@@ -43,12 +43,16 @@ export const watcherCursors = pgTable("watcher_cursors", {
43
43
  * Payment method registry — runtime-configurable tokens/chains.
44
44
  * Admin inserts a row to enable a new payment method. No deploy needed.
45
45
  * Contract addresses are immutable on-chain but configurable here.
46
+ *
47
+ * nextIndex is an atomic counter for HD derivation — never reuses an index.
48
+ * Increment via UPDATE ... SET next_index = next_index + 1 RETURNING next_index.
46
49
  */
47
50
  export const paymentMethods = pgTable("payment_methods", {
48
- id: text("id").primaryKey(), // "USDC:base", "ETH:base", "BTC:mainnet"
49
- type: text("type").notNull(), // "stablecoin", "eth", "btc"
50
- token: text("token").notNull(), // "USDC", "ETH", "BTC"
51
- chain: text("chain").notNull(), // "base", "ethereum", "bitcoin"
51
+ id: text("id").primaryKey(), // "btc", "base-usdc", "arb-usdc", "doge"
52
+ type: text("type").notNull(), // "erc20", "native", "btc"
53
+ token: text("token").notNull(), // "USDC", "ETH", "BTC", "DOGE"
54
+ chain: text("chain").notNull(), // "base", "ethereum", "bitcoin", "arbitrum"
55
+ network: text("network").notNull().default("mainnet"), // "mainnet", "base", "arbitrum"
52
56
  contractAddress: text("contract_address"), // null for native (ETH, BTC)
53
57
  decimals: integer("decimals").notNull(),
54
58
  displayName: text("display_name").notNull(),
@@ -58,8 +62,35 @@ export const paymentMethods = pgTable("payment_methods", {
58
62
  oracleAddress: text("oracle_address"), // Chainlink feed address for price (null = 1:1 stablecoin)
59
63
  xpub: text("xpub"), // HD wallet extended public key for deposit address derivation
60
64
  confirmations: integer("confirmations").notNull().default(1),
65
+ nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
61
66
  createdAt: text("created_at").notNull().default(sql `(now())`),
62
67
  });
68
+ /**
69
+ * BIP-44 path allocation registry — tracks which derivation paths are in use.
70
+ * The server knows which paths are allocated so you never collide.
71
+ * The seed phrase never touches the server — only xpubs.
72
+ */
73
+ export const pathAllocations = pgTable("path_allocations", {
74
+ coinType: integer("coin_type").notNull(), // BIP44 coin type (0=BTC, 60=ETH, 3=DOGE, 501=SOL)
75
+ accountIndex: integer("account_index").notNull(), // m/44'/{coin_type}'/{index}'
76
+ chainId: text("chain_id").references(() => paymentMethods.id),
77
+ xpub: text("xpub").notNull(),
78
+ allocatedAt: text("allocated_at").notNull().default(sql `(now())`),
79
+ }, (table) => [primaryKey({ columns: [table.coinType, table.accountIndex] })]);
80
+ /**
81
+ * Every address ever derived — immutable append-only log.
82
+ * Used for auditing and ensuring no address is ever reused.
83
+ */
84
+ export const derivedAddresses = pgTable("derived_addresses", {
85
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
86
+ chainId: text("chain_id")
87
+ .notNull()
88
+ .references(() => paymentMethods.id),
89
+ derivationIndex: integer("derivation_index").notNull(),
90
+ address: text("address").notNull().unique(),
91
+ tenantId: text("tenant_id"),
92
+ createdAt: text("created_at").notNull().default(sql `(now())`),
93
+ }, (table) => [index("idx_derived_addresses_chain").on(table.chainId)]);
63
94
  /** Processed transaction IDs for watchers without block cursors (e.g. BTC). */
64
95
  export const watcherProcessed = pgTable("watcher_processed", {
65
96
  watcherId: text("watcher_id").notNull(),
@@ -39,6 +39,8 @@ export declare class Instance {
39
39
  /** Simple per-instance mutex to serialize start/stop/restart/remove. */
40
40
  private lockPromise;
41
41
  constructor(deps: InstanceDeps);
42
+ /** Serialize to a plain object safe for JSON.stringify / tRPC responses. */
43
+ toJSON(): Record<string, unknown>;
42
44
  /**
43
45
  * Remote instances have containerId like "remote:node-3".
44
46
  * Local Docker operations are not supported — callers (e.g. wopr-platform)
@@ -25,6 +25,21 @@ export class Instance {
25
25
  this.eventEmitter = deps.eventEmitter;
26
26
  this.botMetricsTracker = deps.botMetricsTracker;
27
27
  }
28
+ /** Serialize to a plain object safe for JSON.stringify / tRPC responses. */
29
+ toJSON() {
30
+ return {
31
+ id: this.id,
32
+ containerId: this.containerId,
33
+ containerName: this.containerName,
34
+ url: this.url,
35
+ name: this.profile.name,
36
+ image: this.profile.image,
37
+ tenantId: this.profile.tenantId,
38
+ env: this.profile.env,
39
+ restartPolicy: this.profile.restartPolicy,
40
+ nodeId: this.profile.nodeId,
41
+ };
42
+ }
28
43
  /**
29
44
  * Remote instances have containerId like "remote:node-3".
30
45
  * Local Docker operations are not supported — callers (e.g. wopr-platform)
@@ -0,0 +1,60 @@
1
+ -- Crypto Key Server schema additions.
2
+ -- Adds atomic derivation counter + path registry + address log.
3
+
4
+ -- 1. Add network column to payment_methods (parallel to chain)
5
+ ALTER TABLE "payment_methods" ADD COLUMN "network" text NOT NULL DEFAULT 'mainnet';
6
+ --> statement-breakpoint
7
+
8
+ -- 2. Add next_index atomic counter to payment_methods
9
+ ALTER TABLE "payment_methods" ADD COLUMN "next_index" integer NOT NULL DEFAULT 0;
10
+ --> statement-breakpoint
11
+
12
+ -- 3. BIP-44 path allocation registry
13
+ CREATE TABLE IF NOT EXISTS "path_allocations" (
14
+ "coin_type" integer NOT NULL,
15
+ "account_index" integer NOT NULL,
16
+ "chain_id" text REFERENCES "payment_methods"("id"),
17
+ "xpub" text NOT NULL,
18
+ "allocated_at" text NOT NULL DEFAULT (now()),
19
+ CONSTRAINT "path_allocations_pkey" PRIMARY KEY("coin_type","account_index")
20
+ );
21
+ --> statement-breakpoint
22
+
23
+ -- 4. Immutable derived address log
24
+ CREATE TABLE IF NOT EXISTS "derived_addresses" (
25
+ "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
26
+ "chain_id" text NOT NULL REFERENCES "payment_methods"("id"),
27
+ "derivation_index" integer NOT NULL,
28
+ "address" text NOT NULL UNIQUE,
29
+ "tenant_id" text,
30
+ "created_at" text NOT NULL DEFAULT (now())
31
+ );
32
+ --> statement-breakpoint
33
+
34
+ CREATE INDEX IF NOT EXISTS "idx_derived_addresses_chain" ON "derived_addresses" ("chain_id");
35
+ --> statement-breakpoint
36
+
37
+ -- 5. Backfill next_index + derived_addresses from existing crypto_charges.
38
+ -- Guarded: only runs if crypto_charges table exists (fresh deploys won't have it).
39
+ DO $$
40
+ BEGIN
41
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'crypto_charges') THEN
42
+ UPDATE "payment_methods" pm
43
+ SET "next_index" = sub.max_idx + 1
44
+ FROM (
45
+ SELECT "chain", MAX("derivation_index") AS max_idx
46
+ FROM "crypto_charges"
47
+ WHERE "derivation_index" IS NOT NULL
48
+ GROUP BY "chain"
49
+ ) sub
50
+ WHERE pm."chain" = sub."chain" AND sub.max_idx >= pm."next_index";
51
+
52
+ INSERT INTO "derived_addresses" ("chain_id", "derivation_index", "address", "tenant_id", "created_at")
53
+ SELECT cc."chain", cc."derivation_index", cc."deposit_address", cc."tenant_id", cc."created_at"
54
+ FROM "crypto_charges" cc
55
+ WHERE cc."deposit_address" IS NOT NULL
56
+ AND cc."derivation_index" IS NOT NULL
57
+ AND cc."chain" IS NOT NULL
58
+ ON CONFLICT ("address") DO NOTHING;
59
+ END IF;
60
+ END $$;
@@ -85,6 +85,27 @@
85
85
  "when": 1742572800000,
86
86
  "tag": "0011_notification_templates",
87
87
  "breakpoints": true
88
+ },
89
+ {
90
+ "idx": 12,
91
+ "version": "7",
92
+ "when": 1742659200000,
93
+ "tag": "0012_seed_popular_tokens",
94
+ "breakpoints": true
95
+ },
96
+ {
97
+ "idx": 13,
98
+ "version": "7",
99
+ "when": 1742745600000,
100
+ "tag": "0013_platform_service_tenant_type",
101
+ "breakpoints": true
102
+ },
103
+ {
104
+ "idx": 14,
105
+ "version": "7",
106
+ "when": 1742832000000,
107
+ "tag": "0014_crypto_key_server",
108
+ "breakpoints": true
88
109
  }
89
110
  ]
90
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.42.2",
3
+ "version": "1.43.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -128,6 +128,7 @@
128
128
  },
129
129
  "packageManager": "pnpm@10.31.0",
130
130
  "dependencies": {
131
+ "@hono/node-server": "^1.19.11",
131
132
  "@noble/hashes": "^2.0.1",
132
133
  "@scure/base": "^2.0.0",
133
134
  "@scure/bip32": "^2.0.1",