@wopr-network/platform-core 1.63.2 → 1.65.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 (47) hide show
  1. package/dist/billing/crypto/__tests__/address-gen.test.js +191 -90
  2. package/dist/billing/crypto/address-gen.js +32 -0
  3. package/dist/billing/crypto/evm/eth-watcher.js +52 -41
  4. package/dist/billing/crypto/evm/watcher.js +5 -11
  5. package/dist/billing/crypto/index.d.ts +2 -0
  6. package/dist/billing/crypto/index.js +1 -0
  7. package/dist/billing/crypto/key-server.js +4 -0
  8. package/dist/billing/crypto/payment-method-store.d.ts +4 -0
  9. package/dist/billing/crypto/payment-method-store.js +11 -0
  10. package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +1 -0
  11. package/dist/billing/crypto/plugin/__tests__/integration.test.js +58 -0
  12. package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +1 -0
  13. package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +46 -0
  14. package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +1 -0
  15. package/dist/billing/crypto/plugin/__tests__/registry.test.js +49 -0
  16. package/dist/billing/crypto/plugin/index.d.ts +2 -0
  17. package/dist/billing/crypto/plugin/index.js +1 -0
  18. package/dist/billing/crypto/plugin/interfaces.d.ts +97 -0
  19. package/dist/billing/crypto/plugin/interfaces.js +2 -0
  20. package/dist/billing/crypto/plugin/registry.d.ts +8 -0
  21. package/dist/billing/crypto/plugin/registry.js +21 -0
  22. package/dist/billing/crypto/watcher-service.js +4 -4
  23. package/dist/db/schema/crypto.d.ts +345 -0
  24. package/dist/db/schema/crypto.js +34 -1
  25. package/dist/db/schema/snapshots.d.ts +1 -1
  26. package/docs/superpowers/plans/2026-03-24-crypto-plugin-phase1.md +697 -0
  27. package/docs/superpowers/specs/2026-03-24-crypto-plugin-architecture-design.md +309 -0
  28. package/drizzle/migrations/0022_rpc_headers_column.sql +1 -0
  29. package/drizzle/migrations/0023_key_rings_table.sql +35 -0
  30. package/drizzle/migrations/0024_backfill_key_rings.sql +75 -0
  31. package/drizzle/migrations/meta/_journal.json +14 -0
  32. package/package.json +5 -1
  33. package/src/billing/crypto/__tests__/address-gen.test.ts +207 -88
  34. package/src/billing/crypto/address-gen.ts +31 -0
  35. package/src/billing/crypto/evm/eth-watcher.ts +64 -47
  36. package/src/billing/crypto/evm/watcher.ts +8 -9
  37. package/src/billing/crypto/index.ts +9 -0
  38. package/src/billing/crypto/key-server.ts +5 -0
  39. package/src/billing/crypto/payment-method-store.ts +15 -0
  40. package/src/billing/crypto/plugin/__tests__/integration.test.ts +64 -0
  41. package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +51 -0
  42. package/src/billing/crypto/plugin/__tests__/registry.test.ts +58 -0
  43. package/src/billing/crypto/plugin/index.ts +17 -0
  44. package/src/billing/crypto/plugin/interfaces.ts +106 -0
  45. package/src/billing/crypto/plugin/registry.ts +26 -0
  46. package/src/billing/crypto/watcher-service.ts +4 -4
  47. package/src/db/schema/crypto.ts +44 -1
@@ -14,6 +14,7 @@ export interface PaymentMethodRecord {
14
14
  displayOrder: number;
15
15
  iconUrl: string | null;
16
16
  rpcUrl: string | null;
17
+ rpcHeaders: string;
17
18
  oracleAddress: string | null;
18
19
  xpub: string | null;
19
20
  addressType: string;
@@ -21,6 +22,9 @@ export interface PaymentMethodRecord {
21
22
  watcherType: string;
22
23
  oracleAssetId: string | null;
23
24
  confirmations: number;
25
+ keyRingId: string | null;
26
+ encoding: string | null;
27
+ pluginId: string | null;
24
28
  }
25
29
 
26
30
  export interface IPaymentMethodStore {
@@ -89,6 +93,7 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
89
93
  displayOrder: method.displayOrder,
90
94
  iconUrl: method.iconUrl,
91
95
  rpcUrl: method.rpcUrl,
96
+ rpcHeaders: method.rpcHeaders ?? "{}",
92
97
  oracleAddress: method.oracleAddress,
93
98
  xpub: method.xpub,
94
99
  addressType: method.addressType,
@@ -96,6 +101,9 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
96
101
  watcherType: method.watcherType,
97
102
  oracleAssetId: method.oracleAssetId,
98
103
  confirmations: method.confirmations,
104
+ keyRingId: method.keyRingId,
105
+ encoding: method.encoding,
106
+ pluginId: method.pluginId,
99
107
  })
100
108
  .onConflictDoUpdate({
101
109
  target: paymentMethods.id,
@@ -117,6 +125,9 @@ export class DrizzlePaymentMethodStore implements IPaymentMethodStore {
117
125
  watcherType: method.watcherType,
118
126
  oracleAssetId: method.oracleAssetId,
119
127
  confirmations: method.confirmations,
128
+ keyRingId: method.keyRingId,
129
+ encoding: method.encoding,
130
+ pluginId: method.pluginId,
120
131
  },
121
132
  });
122
133
  }
@@ -154,6 +165,7 @@ function toRecord(row: typeof paymentMethods.$inferSelect): PaymentMethodRecord
154
165
  displayOrder: row.displayOrder,
155
166
  iconUrl: row.iconUrl,
156
167
  rpcUrl: row.rpcUrl,
168
+ rpcHeaders: row.rpcHeaders ?? "{}",
157
169
  oracleAddress: row.oracleAddress,
158
170
  xpub: row.xpub,
159
171
  addressType: row.addressType,
@@ -161,5 +173,8 @@ function toRecord(row: typeof paymentMethods.$inferSelect): PaymentMethodRecord
161
173
  watcherType: row.watcherType,
162
174
  oracleAssetId: row.oracleAssetId,
163
175
  confirmations: row.confirmations,
176
+ keyRingId: row.keyRingId,
177
+ encoding: row.encoding,
178
+ pluginId: row.pluginId,
164
179
  };
165
180
  }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { IChainPlugin, PaymentEvent, WatcherOpts } from "../interfaces.js";
3
+ import { PluginRegistry } from "../registry.js";
4
+
5
+ describe("plugin integration — registry → watcher → events", () => {
6
+ it("full lifecycle: register → create watcher → poll → events", async () => {
7
+ const mockEvent: PaymentEvent = {
8
+ chain: "test",
9
+ token: "TEST",
10
+ from: "0xsender",
11
+ to: "0xreceiver",
12
+ rawAmount: "1000",
13
+ amountUsdCents: 100,
14
+ txHash: "0xhash",
15
+ blockNumber: 42,
16
+ confirmations: 6,
17
+ confirmationsRequired: 6,
18
+ };
19
+
20
+ const plugin: IChainPlugin = {
21
+ pluginId: "test",
22
+ supportedCurve: "secp256k1",
23
+ encoders: {},
24
+ createWatcher: (_opts: WatcherOpts) => ({
25
+ init: async () => {},
26
+ poll: async () => [mockEvent],
27
+ setWatchedAddresses: () => {},
28
+ getCursor: () => 42,
29
+ stop: () => {},
30
+ }),
31
+ createSweeper: () => ({ scan: async () => [], sweep: async () => [] }),
32
+ version: 1,
33
+ };
34
+
35
+ const registry = new PluginRegistry();
36
+ registry.register(plugin);
37
+
38
+ const resolved = registry.getOrThrow("test");
39
+ const watcher = resolved.createWatcher({
40
+ rpcUrl: "http://localhost:8545",
41
+ rpcHeaders: {},
42
+ oracle: {
43
+ getPrice: async () => ({ priceMicros: 3500_000000 }),
44
+ },
45
+ cursorStore: {
46
+ get: async () => null,
47
+ save: async () => {},
48
+ getConfirmationCount: async () => null,
49
+ saveConfirmationCount: async () => {},
50
+ },
51
+ token: "TEST",
52
+ chain: "test",
53
+ decimals: 18,
54
+ confirmations: 6,
55
+ });
56
+
57
+ await watcher.init();
58
+ const events = await watcher.poll();
59
+ expect(events).toHaveLength(1);
60
+ expect(events[0].txHash).toBe("0xhash");
61
+ expect(watcher.getCursor()).toBe(42);
62
+ watcher.stop();
63
+ });
64
+ });
@@ -0,0 +1,51 @@
1
+ // src/billing/crypto/plugin/__tests__/interfaces.test.ts
2
+ import { describe, expect, it } from "vitest";
3
+ import type { EncodingParams, IAddressEncoder, IChainWatcher, ICurveDeriver, PaymentEvent } from "../interfaces.js";
4
+
5
+ describe("plugin interfaces — type contracts", () => {
6
+ it("PaymentEvent has required fields", () => {
7
+ const event: PaymentEvent = {
8
+ chain: "ethereum",
9
+ token: "ETH",
10
+ from: "0xabc",
11
+ to: "0xdef",
12
+ rawAmount: "1000000000000000000",
13
+ amountUsdCents: 350000,
14
+ txHash: "0x123",
15
+ blockNumber: 100,
16
+ confirmations: 6,
17
+ confirmationsRequired: 6,
18
+ };
19
+ expect(event.chain).toBe("ethereum");
20
+ expect(event.amountUsdCents).toBe(350000);
21
+ });
22
+
23
+ it("ICurveDeriver contract is satisfiable", () => {
24
+ const deriver: ICurveDeriver = {
25
+ derivePublicKey: (_chain: number, _index: number) => new Uint8Array(33),
26
+ getCurve: () => "secp256k1",
27
+ };
28
+ expect(deriver.getCurve()).toBe("secp256k1");
29
+ expect(deriver.derivePublicKey(0, 0)).toBeInstanceOf(Uint8Array);
30
+ });
31
+
32
+ it("IAddressEncoder contract is satisfiable", () => {
33
+ const encoder: IAddressEncoder = {
34
+ encode: (_pk: Uint8Array, _params: EncodingParams) => "bc1qtest",
35
+ encodingType: () => "bech32",
36
+ };
37
+ expect(encoder.encodingType()).toBe("bech32");
38
+ expect(encoder.encode(new Uint8Array(33), { hrp: "bc" })).toBe("bc1qtest");
39
+ });
40
+
41
+ it("IChainWatcher contract is satisfiable", () => {
42
+ const watcher: IChainWatcher = {
43
+ init: async () => {},
44
+ poll: async () => [],
45
+ setWatchedAddresses: () => {},
46
+ getCursor: () => 0,
47
+ stop: () => {},
48
+ };
49
+ expect(watcher.getCursor()).toBe(0);
50
+ });
51
+ });
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { IChainPlugin } from "../interfaces.js";
3
+ import { PluginRegistry } from "../registry.js";
4
+
5
+ function mockPlugin(id: string, curve: "secp256k1" | "ed25519" = "secp256k1"): IChainPlugin {
6
+ return {
7
+ pluginId: id,
8
+ supportedCurve: curve,
9
+ encoders: {},
10
+ createWatcher: () => ({
11
+ init: async () => {},
12
+ poll: async () => [],
13
+ setWatchedAddresses: () => {},
14
+ getCursor: () => 0,
15
+ stop: () => {},
16
+ }),
17
+ createSweeper: () => ({ scan: async () => [], sweep: async () => [] }),
18
+ version: 1,
19
+ };
20
+ }
21
+
22
+ describe("PluginRegistry", () => {
23
+ it("registers and retrieves a plugin", () => {
24
+ const reg = new PluginRegistry();
25
+ reg.register(mockPlugin("evm"));
26
+ expect(reg.get("evm")).toBeDefined();
27
+ expect(reg.get("evm")?.pluginId).toBe("evm");
28
+ });
29
+
30
+ it("throws on duplicate registration", () => {
31
+ const reg = new PluginRegistry();
32
+ reg.register(mockPlugin("evm"));
33
+ expect(() => reg.register(mockPlugin("evm"))).toThrow("already registered");
34
+ });
35
+
36
+ it("returns undefined for unknown plugin", () => {
37
+ const reg = new PluginRegistry();
38
+ expect(reg.get("unknown")).toBeUndefined();
39
+ });
40
+
41
+ it("lists all registered plugins", () => {
42
+ const reg = new PluginRegistry();
43
+ reg.register(mockPlugin("evm"));
44
+ reg.register(mockPlugin("solana", "ed25519"));
45
+ expect(reg.list()).toHaveLength(2);
46
+ expect(
47
+ reg
48
+ .list()
49
+ .map((p) => p.pluginId)
50
+ .sort(),
51
+ ).toEqual(["evm", "solana"]);
52
+ });
53
+
54
+ it("getOrThrow throws for unknown plugin", () => {
55
+ const reg = new PluginRegistry();
56
+ expect(() => reg.getOrThrow("nope")).toThrow("not registered");
57
+ });
58
+ });
@@ -0,0 +1,17 @@
1
+ export type {
2
+ DepositInfo,
3
+ EncodingParams,
4
+ IAddressEncoder,
5
+ IChainPlugin,
6
+ IChainWatcher,
7
+ ICurveDeriver,
8
+ IPriceOracle,
9
+ ISweepStrategy,
10
+ IWatcherCursorStore,
11
+ KeyPair,
12
+ PaymentEvent,
13
+ SweeperOpts,
14
+ SweepResult,
15
+ WatcherOpts,
16
+ } from "./interfaces.js";
17
+ export { PluginRegistry } from "./registry.js";
@@ -0,0 +1,106 @@
1
+ // src/billing/crypto/plugin/interfaces.ts
2
+
3
+ export interface PaymentEvent {
4
+ chain: string;
5
+ token: string;
6
+ from: string;
7
+ to: string;
8
+ rawAmount: string;
9
+ amountUsdCents: number;
10
+ txHash: string;
11
+ blockNumber: number;
12
+ confirmations: number;
13
+ confirmationsRequired: number;
14
+ }
15
+
16
+ export interface ICurveDeriver {
17
+ derivePublicKey(chainIndex: number, addressIndex: number): Uint8Array;
18
+ getCurve(): "secp256k1" | "ed25519";
19
+ }
20
+
21
+ export interface EncodingParams {
22
+ hrp?: string;
23
+ version?: string;
24
+ [key: string]: string | undefined;
25
+ }
26
+
27
+ export interface IAddressEncoder {
28
+ encode(publicKey: Uint8Array, params: EncodingParams): string;
29
+ encodingType(): string;
30
+ }
31
+
32
+ export interface KeyPair {
33
+ privateKey: Uint8Array;
34
+ publicKey: Uint8Array;
35
+ address: string;
36
+ index: number;
37
+ }
38
+
39
+ export interface DepositInfo {
40
+ index: number;
41
+ address: string;
42
+ nativeBalance: bigint;
43
+ tokenBalances: Array<{ token: string; balance: bigint; decimals: number }>;
44
+ }
45
+
46
+ export interface SweepResult {
47
+ index: number;
48
+ address: string;
49
+ token: string;
50
+ amount: string;
51
+ txHash: string;
52
+ }
53
+
54
+ export interface ISweepStrategy {
55
+ scan(keys: KeyPair[], treasury: string): Promise<DepositInfo[]>;
56
+ sweep(keys: KeyPair[], treasury: string, dryRun: boolean): Promise<SweepResult[]>;
57
+ }
58
+
59
+ export interface IPriceOracle {
60
+ getPrice(token: string, feedAddress?: string): Promise<{ priceMicros: number }>;
61
+ }
62
+
63
+ export interface IWatcherCursorStore {
64
+ get(watcherId: string): Promise<number | null>;
65
+ save(watcherId: string, cursor: number): Promise<void>;
66
+ getConfirmationCount(watcherId: string, txKey: string): Promise<number | null>;
67
+ saveConfirmationCount(watcherId: string, txKey: string, count: number): Promise<void>;
68
+ }
69
+
70
+ export interface WatcherOpts {
71
+ rpcUrl: string;
72
+ rpcHeaders: Record<string, string>;
73
+ oracle: IPriceOracle;
74
+ cursorStore: IWatcherCursorStore;
75
+ token: string;
76
+ chain: string;
77
+ contractAddress?: string;
78
+ decimals: number;
79
+ confirmations: number;
80
+ }
81
+
82
+ export interface SweeperOpts {
83
+ rpcUrl: string;
84
+ rpcHeaders: Record<string, string>;
85
+ token: string;
86
+ chain: string;
87
+ contractAddress?: string;
88
+ decimals: number;
89
+ }
90
+
91
+ export interface IChainWatcher {
92
+ init(): Promise<void>;
93
+ poll(): Promise<PaymentEvent[]>;
94
+ setWatchedAddresses(addresses: string[]): void;
95
+ getCursor(): number;
96
+ stop(): void;
97
+ }
98
+
99
+ export interface IChainPlugin {
100
+ pluginId: string;
101
+ supportedCurve: "secp256k1" | "ed25519";
102
+ encoders: Record<string, IAddressEncoder>;
103
+ createWatcher(opts: WatcherOpts): IChainWatcher;
104
+ createSweeper(opts: SweeperOpts): ISweepStrategy;
105
+ version: number;
106
+ }
@@ -0,0 +1,26 @@
1
+ import type { IChainPlugin } from "./interfaces.js";
2
+
3
+ export class PluginRegistry {
4
+ private plugins = new Map<string, IChainPlugin>();
5
+
6
+ register(plugin: IChainPlugin): void {
7
+ if (this.plugins.has(plugin.pluginId)) {
8
+ throw new Error(`Plugin "${plugin.pluginId}" is already registered`);
9
+ }
10
+ this.plugins.set(plugin.pluginId, plugin);
11
+ }
12
+
13
+ get(pluginId: string): IChainPlugin | undefined {
14
+ return this.plugins.get(pluginId);
15
+ }
16
+
17
+ getOrThrow(pluginId: string): IChainPlugin {
18
+ const plugin = this.plugins.get(pluginId);
19
+ if (!plugin) throw new Error(`Plugin "${pluginId}" is not registered`);
20
+ return plugin;
21
+ }
22
+
23
+ list(): IChainPlugin[] {
24
+ return [...this.plugins.values()];
25
+ }
26
+ }
@@ -364,7 +364,7 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
364
364
  // Only applies to chains routed through the EVM watcher but storing non-hex addresses.
365
365
  // UTXO chains (DOGE p2pkh) never enter this path — they use the UTXO watcher.
366
366
  const isTronMethod = (method: { addressType: string; chain: string }): boolean =>
367
- method.addressType === "p2pkh" && method.chain === "tron";
367
+ (method.addressType === "p2pkh" || method.addressType === "keccak-b58check") && method.chain === "tron";
368
368
  const toWatcherAddr = (addr: string, method: { addressType: string; chain: string }): string =>
369
369
  isTronMethod(method) && isTronAddress(addr) ? tronToHex(addr) : addr;
370
370
  const fromWatcherAddr = (addr: string, method: { addressType: string; chain: string }): string =>
@@ -373,7 +373,7 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
373
373
  for (const method of nativeEvmMethods) {
374
374
  if (!method.rpcUrl) continue;
375
375
 
376
- const rpcCall = createRpcCaller(method.rpcUrl);
376
+ const rpcCall = createRpcCaller(method.rpcUrl, JSON.parse(method.rpcHeaders ?? "{}"));
377
377
  let latestBlock: number;
378
378
  try {
379
379
  const latestHex = (await rpcCall("eth_blockNumber", [])) as string;
@@ -452,7 +452,7 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
452
452
  for (const method of erc20Methods) {
453
453
  if (!method.rpcUrl || !method.contractAddress) continue;
454
454
 
455
- const rpcCall = createRpcCaller(method.rpcUrl);
455
+ const rpcCall = createRpcCaller(method.rpcUrl, JSON.parse(method.rpcHeaders ?? "{}"));
456
456
  let latestBlock: number;
457
457
  try {
458
458
  const latestHex = (await rpcCall("eth_blockNumber", [])) as string;
@@ -471,7 +471,7 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
471
471
  rpcCall,
472
472
  fromBlock: latestBlock,
473
473
  watchedAddresses: chainAddresses.map((a) => toWatcherAddr(a, method)),
474
- contractAddress: method.contractAddress,
474
+ contractAddress: toWatcherAddr(method.contractAddress, method),
475
475
  decimals: method.decimals,
476
476
  confirmations: method.confirmations,
477
477
  cursorStore,
@@ -1,5 +1,5 @@
1
1
  import { sql } from "drizzle-orm";
2
- import { boolean, index, integer, pgTable, primaryKey, text } from "drizzle-orm/pg-core";
2
+ import { boolean, index, integer, pgTable, primaryKey, serial, text, uniqueIndex } from "drizzle-orm/pg-core";
3
3
 
4
4
  /**
5
5
  * Crypto payment charges — tracks the lifecycle of each payment.
@@ -80,6 +80,7 @@ export const paymentMethods = pgTable("payment_methods", {
80
80
  displayOrder: integer("display_order").notNull().default(0),
81
81
  iconUrl: text("icon_url"),
82
82
  rpcUrl: text("rpc_url"), // chain node RPC endpoint
83
+ rpcHeaders: text("rpc_headers").notNull().default("{}"), // JSON: extra headers for RPC calls (e.g. {"TRON-PRO-API-KEY":"xxx"})
83
84
  oracleAddress: text("oracle_address"), // Chainlink feed address for price (null = 1:1 stablecoin)
84
85
  xpub: text("xpub"), // HD wallet extended public key for deposit address derivation
85
86
  addressType: text("address_type").notNull().default("evm"), // "bech32" (BTC/LTC), "p2pkh" (DOGE/TRX), "evm" (ETH/ERC20)
@@ -88,6 +89,9 @@ export const paymentMethods = pgTable("payment_methods", {
88
89
  oracleAssetId: text("oracle_asset_id"), // CoinGecko slug (e.g. "bitcoin", "tron"). Null = stablecoin (1:1 USD) or use token symbol fallback.
89
90
  confirmations: integer("confirmations").notNull().default(1),
90
91
  nextIndex: integer("next_index").notNull().default(0), // atomic derivation counter, never reuses
92
+ keyRingId: text("key_ring_id"), // FK to key_rings.id (nullable during migration)
93
+ encoding: text("encoding"), // address encoding override (e.g. "bech32", "p2pkh", "evm")
94
+ pluginId: text("plugin_id"), // plugin identifier (e.g. "evm", "utxo", "solana")
91
95
  createdAt: text("created_at").notNull().default(sql`(now())`),
92
96
  });
93
97
 
@@ -160,3 +164,42 @@ export const watcherProcessed = pgTable(
160
164
  },
161
165
  (table) => [primaryKey({ columns: [table.watcherId, table.txId] })],
162
166
  );
167
+
168
+ /**
169
+ * Key rings — decouples key material (xpub/seed) from payment methods.
170
+ * Each key ring maps to a BIP-44 coin type + account index.
171
+ */
172
+ export const keyRings = pgTable(
173
+ "key_rings",
174
+ {
175
+ id: text("id").primaryKey(),
176
+ curve: text("curve").notNull(), // "secp256k1" | "ed25519"
177
+ derivationScheme: text("derivation_scheme").notNull(), // "bip32" | "slip10" | "ed25519-hd"
178
+ derivationMode: text("derivation_mode").notNull().default("on-demand"), // "on-demand" | "pre-derived"
179
+ keyMaterial: text("key_material").notNull().default("{}"), // JSON: { xpub: "..." }
180
+ coinType: integer("coin_type").notNull(), // BIP-44 coin type
181
+ accountIndex: integer("account_index").notNull().default(0),
182
+ createdAt: text("created_at").notNull().default(sql`(now())`),
183
+ },
184
+ (table) => [uniqueIndex("key_rings_path_unique").on(table.coinType, table.accountIndex)],
185
+ );
186
+
187
+ /**
188
+ * Pre-derived address pool — for Ed25519 chains that need offline derivation.
189
+ * Addresses are derived in batches and assigned on demand.
190
+ */
191
+ export const addressPool = pgTable(
192
+ "address_pool",
193
+ {
194
+ id: serial("id").primaryKey(),
195
+ keyRingId: text("key_ring_id")
196
+ .notNull()
197
+ .references(() => keyRings.id),
198
+ derivationIndex: integer("derivation_index").notNull(),
199
+ publicKey: text("public_key").notNull(),
200
+ address: text("address").notNull(),
201
+ assignedTo: text("assigned_to"), // charge reference or tenant ID
202
+ createdAt: text("created_at").notNull().default(sql`(now())`),
203
+ },
204
+ (table) => [uniqueIndex("address_pool_ring_index").on(table.keyRingId, table.derivationIndex)],
205
+ );