@wopr-network/platform-core 1.64.0 → 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.
- package/dist/billing/crypto/index.d.ts +2 -0
- package/dist/billing/crypto/index.js +1 -0
- package/dist/billing/crypto/key-server.js +3 -0
- package/dist/billing/crypto/payment-method-store.d.ts +3 -0
- package/dist/billing/crypto/payment-method-store.js +9 -0
- package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +1 -0
- package/dist/billing/crypto/plugin/__tests__/integration.test.js +58 -0
- package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +1 -0
- package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +46 -0
- package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +1 -0
- package/dist/billing/crypto/plugin/__tests__/registry.test.js +49 -0
- package/dist/billing/crypto/plugin/index.d.ts +2 -0
- package/dist/billing/crypto/plugin/index.js +1 -0
- package/dist/billing/crypto/plugin/interfaces.d.ts +97 -0
- package/dist/billing/crypto/plugin/interfaces.js +2 -0
- package/dist/billing/crypto/plugin/registry.d.ts +8 -0
- package/dist/billing/crypto/plugin/registry.js +21 -0
- package/dist/db/schema/crypto.d.ts +328 -0
- package/dist/db/schema/crypto.js +33 -1
- package/dist/db/schema/snapshots.d.ts +1 -1
- package/drizzle/migrations/0023_key_rings_table.sql +35 -0
- package/drizzle/migrations/0024_backfill_key_rings.sql +75 -0
- package/package.json +5 -1
- package/src/billing/crypto/index.ts +9 -0
- package/src/billing/crypto/key-server.ts +3 -0
- package/src/billing/crypto/payment-method-store.ts +12 -0
- package/src/billing/crypto/plugin/__tests__/integration.test.ts +64 -0
- package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +51 -0
- package/src/billing/crypto/plugin/__tests__/registry.test.ts +58 -0
- package/src/billing/crypto/plugin/index.ts +17 -0
- package/src/billing/crypto/plugin/interfaces.ts +106 -0
- package/src/billing/crypto/plugin/registry.ts +26 -0
- package/src/db/schema/crypto.ts +43 -1
|
@@ -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
|
+
}
|
package/src/db/schema/crypto.ts
CHANGED
|
@@ -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.
|
|
@@ -89,6 +89,9 @@ export const paymentMethods = pgTable("payment_methods", {
|
|
|
89
89
|
oracleAssetId: text("oracle_asset_id"), // CoinGecko slug (e.g. "bitcoin", "tron"). Null = stablecoin (1:1 USD) or use token symbol fallback.
|
|
90
90
|
confirmations: integer("confirmations").notNull().default(1),
|
|
91
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")
|
|
92
95
|
createdAt: text("created_at").notNull().default(sql`(now())`),
|
|
93
96
|
});
|
|
94
97
|
|
|
@@ -161,3 +164,42 @@ export const watcherProcessed = pgTable(
|
|
|
161
164
|
},
|
|
162
165
|
(table) => [primaryKey({ columns: [table.watcherId, table.txId] })],
|
|
163
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
|
+
);
|