@wopr-network/platform-core 1.15.0 → 1.16.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 (51) hide show
  1. package/dist/billing/crypto/charge-store.d.ts +23 -0
  2. package/dist/billing/crypto/charge-store.js +34 -0
  3. package/dist/billing/crypto/charge-store.test.js +56 -0
  4. package/dist/billing/crypto/evm/__tests__/address-gen.test.d.ts +1 -0
  5. package/dist/billing/crypto/evm/__tests__/address-gen.test.js +54 -0
  6. package/dist/billing/crypto/evm/__tests__/checkout.test.d.ts +1 -0
  7. package/dist/billing/crypto/evm/__tests__/checkout.test.js +54 -0
  8. package/dist/billing/crypto/evm/__tests__/config.test.d.ts +1 -0
  9. package/dist/billing/crypto/evm/__tests__/config.test.js +52 -0
  10. package/dist/billing/crypto/evm/__tests__/settler.test.d.ts +1 -0
  11. package/dist/billing/crypto/evm/__tests__/settler.test.js +196 -0
  12. package/dist/billing/crypto/evm/__tests__/watcher.test.d.ts +1 -0
  13. package/dist/billing/crypto/evm/__tests__/watcher.test.js +109 -0
  14. package/dist/billing/crypto/evm/address-gen.d.ts +8 -0
  15. package/dist/billing/crypto/evm/address-gen.js +29 -0
  16. package/dist/billing/crypto/evm/checkout.d.ts +26 -0
  17. package/dist/billing/crypto/evm/checkout.js +57 -0
  18. package/dist/billing/crypto/evm/config.d.ts +13 -0
  19. package/dist/billing/crypto/evm/config.js +46 -0
  20. package/dist/billing/crypto/evm/index.d.ts +9 -0
  21. package/dist/billing/crypto/evm/index.js +5 -0
  22. package/dist/billing/crypto/evm/settler.d.ts +23 -0
  23. package/dist/billing/crypto/evm/settler.js +60 -0
  24. package/dist/billing/crypto/evm/types.d.ts +40 -0
  25. package/dist/billing/crypto/evm/types.js +1 -0
  26. package/dist/billing/crypto/evm/watcher.d.ts +31 -0
  27. package/dist/billing/crypto/evm/watcher.js +91 -0
  28. package/dist/billing/crypto/index.d.ts +2 -1
  29. package/dist/billing/crypto/index.js +1 -0
  30. package/dist/db/schema/crypto.d.ts +68 -0
  31. package/dist/db/schema/crypto.js +7 -0
  32. package/docs/superpowers/plans/2026-03-14-stablecoin-phase1.md +1413 -0
  33. package/drizzle/migrations/0005_stablecoin_columns.sql +7 -0
  34. package/drizzle/migrations/meta/_journal.json +7 -0
  35. package/package.json +4 -1
  36. package/src/billing/crypto/charge-store.test.ts +61 -0
  37. package/src/billing/crypto/charge-store.ts +54 -0
  38. package/src/billing/crypto/evm/__tests__/address-gen.test.ts +63 -0
  39. package/src/billing/crypto/evm/__tests__/checkout.test.ts +83 -0
  40. package/src/billing/crypto/evm/__tests__/config.test.ts +63 -0
  41. package/src/billing/crypto/evm/__tests__/settler.test.ts +218 -0
  42. package/src/billing/crypto/evm/__tests__/watcher.test.ts +128 -0
  43. package/src/billing/crypto/evm/address-gen.ts +29 -0
  44. package/src/billing/crypto/evm/checkout.ts +82 -0
  45. package/src/billing/crypto/evm/config.ts +50 -0
  46. package/src/billing/crypto/evm/index.ts +16 -0
  47. package/src/billing/crypto/evm/settler.ts +79 -0
  48. package/src/billing/crypto/evm/types.ts +45 -0
  49. package/src/billing/crypto/evm/watcher.ts +126 -0
  50. package/src/billing/crypto/index.ts +2 -1
  51. package/src/db/schema/crypto.ts +7 -0
@@ -0,0 +1,29 @@
1
+ import { HDKey } from "@scure/bip32";
2
+ import { publicKeyToAddress } from "viem/accounts";
3
+ /**
4
+ * Derive a deposit address from an xpub at a given BIP-44 index.
5
+ * Path: xpub / 0 / index (external chain / address index).
6
+ * Returns a checksummed Ethereum address. No private keys involved.
7
+ */
8
+ export function deriveDepositAddress(xpub, index) {
9
+ if (!Number.isInteger(index) || index < 0)
10
+ throw new Error(`Invalid derivation index: ${index}`);
11
+ const master = HDKey.fromExtendedKey(xpub);
12
+ const child = master.deriveChild(0).deriveChild(index);
13
+ if (!child.publicKey)
14
+ throw new Error("Failed to derive public key");
15
+ const hexPubKey = `0x${Array.from(child.publicKey, (b) => b.toString(16).padStart(2, "0")).join("")}`;
16
+ return publicKeyToAddress(hexPubKey);
17
+ }
18
+ /** Validate that a string is an xpub (not xprv). */
19
+ export function isValidXpub(key) {
20
+ if (!key.startsWith("xpub"))
21
+ return false;
22
+ try {
23
+ HDKey.fromExtendedKey(key);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
@@ -0,0 +1,26 @@
1
+ import type { ICryptoChargeRepository } from "../charge-store.js";
2
+ import type { StablecoinCheckoutOpts } from "./types.js";
3
+ export declare const MIN_STABLECOIN_USD = 10;
4
+ export interface StablecoinCheckoutDeps {
5
+ chargeStore: Pick<ICryptoChargeRepository, "getNextDerivationIndex" | "createStablecoinCharge">;
6
+ xpub: string;
7
+ }
8
+ export interface StablecoinCheckoutResult {
9
+ depositAddress: string;
10
+ amountRaw: string;
11
+ amountUsd: number;
12
+ chain: string;
13
+ token: string;
14
+ referenceId: string;
15
+ }
16
+ /**
17
+ * Create a stablecoin checkout — derive a unique deposit address, store the charge.
18
+ *
19
+ * Race safety: the unique constraint on derivation_index prevents two concurrent
20
+ * checkouts from claiming the same index. On conflict, we retry with the next index.
21
+ *
22
+ * CRITICAL: amountUsd is converted to integer cents via Credit.fromDollars().toCentsRounded().
23
+ * The charge store holds USD cents (integer). Credit.fromCents() handles the
24
+ * cents → nanodollars conversion when crediting the ledger in the settler.
25
+ */
26
+ export declare function createStablecoinCheckout(deps: StablecoinCheckoutDeps, opts: StablecoinCheckoutOpts): Promise<StablecoinCheckoutResult>;
@@ -0,0 +1,57 @@
1
+ import { Credit } from "../../../credits/credit.js";
2
+ import { deriveDepositAddress } from "./address-gen.js";
3
+ import { getTokenConfig, tokenAmountFromCents } from "./config.js";
4
+ export const MIN_STABLECOIN_USD = 10;
5
+ /**
6
+ * Create a stablecoin checkout — derive a unique deposit address, store the charge.
7
+ *
8
+ * Race safety: the unique constraint on derivation_index prevents two concurrent
9
+ * checkouts from claiming the same index. On conflict, we retry with the next index.
10
+ *
11
+ * CRITICAL: amountUsd is converted to integer cents via Credit.fromDollars().toCentsRounded().
12
+ * The charge store holds USD cents (integer). Credit.fromCents() handles the
13
+ * cents → nanodollars conversion when crediting the ledger in the settler.
14
+ */
15
+ export async function createStablecoinCheckout(deps, opts) {
16
+ if (!Number.isFinite(opts.amountUsd) || opts.amountUsd < MIN_STABLECOIN_USD) {
17
+ throw new Error(`Minimum payment amount is $${MIN_STABLECOIN_USD}`);
18
+ }
19
+ const tokenCfg = getTokenConfig(opts.token, opts.chain);
20
+ // Convert dollars to integer cents via Credit (no floating point in billing path).
21
+ const amountUsdCents = Credit.fromDollars(opts.amountUsd).toCentsRounded();
22
+ const rawAmount = tokenAmountFromCents(amountUsdCents, tokenCfg.decimals);
23
+ const maxRetries = 3;
24
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
25
+ const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
26
+ const depositAddress = deriveDepositAddress(deps.xpub, derivationIndex);
27
+ const referenceId = `sc:${opts.chain}:${opts.token.toLowerCase()}:${depositAddress.toLowerCase()}`;
28
+ try {
29
+ await deps.chargeStore.createStablecoinCharge({
30
+ referenceId,
31
+ tenantId: opts.tenant,
32
+ amountUsdCents,
33
+ chain: opts.chain,
34
+ token: opts.token,
35
+ depositAddress: depositAddress.toLowerCase(),
36
+ derivationIndex,
37
+ });
38
+ return {
39
+ depositAddress,
40
+ amountRaw: rawAmount.toString(),
41
+ amountUsd: opts.amountUsd,
42
+ chain: opts.chain,
43
+ token: opts.token,
44
+ referenceId,
45
+ };
46
+ }
47
+ catch (err) {
48
+ // Unique constraint violation = another checkout claimed this index concurrently.
49
+ // Retry with the next available index.
50
+ const msg = err instanceof Error ? err.message : "";
51
+ const isConflict = msg.includes("unique") || msg.includes("duplicate") || msg.includes("23505");
52
+ if (!isConflict || attempt === maxRetries)
53
+ throw err;
54
+ }
55
+ }
56
+ throw new Error("Failed to claim derivation index after retries");
57
+ }
@@ -0,0 +1,13 @@
1
+ import type { ChainConfig, EvmChain, StablecoinToken, TokenConfig } from "./types.js";
2
+ export declare function getChainConfig(chain: EvmChain): ChainConfig;
3
+ export declare function getTokenConfig(token: StablecoinToken, chain: EvmChain): TokenConfig;
4
+ /**
5
+ * Convert USD cents (integer) to token raw amount (BigInt).
6
+ * Stablecoins are 1:1 USD. Integer math only.
7
+ */
8
+ export declare function tokenAmountFromCents(cents: number, decimals: number): bigint;
9
+ /**
10
+ * Convert token raw amount (BigInt) to USD cents (integer).
11
+ * Truncates fractional cents.
12
+ */
13
+ export declare function centsFromTokenAmount(rawAmount: bigint, decimals: number): number;
@@ -0,0 +1,46 @@
1
+ const CHAINS = {
2
+ base: {
3
+ chain: "base",
4
+ rpcUrl: process.env.EVM_RPC_BASE ?? "http://op-geth:8545",
5
+ confirmations: 1,
6
+ blockTimeMs: 2000,
7
+ chainId: 8453,
8
+ },
9
+ };
10
+ const TOKENS = {
11
+ "USDC:base": {
12
+ token: "USDC",
13
+ chain: "base",
14
+ contractAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
15
+ decimals: 6,
16
+ },
17
+ };
18
+ export function getChainConfig(chain) {
19
+ const cfg = CHAINS[chain];
20
+ if (!cfg)
21
+ throw new Error(`Unsupported chain: ${chain}`);
22
+ return cfg;
23
+ }
24
+ export function getTokenConfig(token, chain) {
25
+ const key = `${token}:${chain}`;
26
+ const cfg = TOKENS[key];
27
+ if (!cfg)
28
+ throw new Error(`Unsupported token ${token} on ${chain}`);
29
+ return cfg;
30
+ }
31
+ /**
32
+ * Convert USD cents (integer) to token raw amount (BigInt).
33
+ * Stablecoins are 1:1 USD. Integer math only.
34
+ */
35
+ export function tokenAmountFromCents(cents, decimals) {
36
+ if (!Number.isInteger(cents))
37
+ throw new Error("cents must be an integer");
38
+ return (BigInt(cents) * 10n ** BigInt(decimals)) / 100n;
39
+ }
40
+ /**
41
+ * Convert token raw amount (BigInt) to USD cents (integer).
42
+ * Truncates fractional cents.
43
+ */
44
+ export function centsFromTokenAmount(rawAmount, decimals) {
45
+ return Number((rawAmount * 100n) / 10n ** BigInt(decimals));
46
+ }
@@ -0,0 +1,9 @@
1
+ export { deriveDepositAddress, isValidXpub } from "./address-gen.js";
2
+ export type { StablecoinCheckoutDeps, StablecoinCheckoutResult } from "./checkout.js";
3
+ export { createStablecoinCheckout, MIN_STABLECOIN_USD } from "./checkout.js";
4
+ export { centsFromTokenAmount, getChainConfig, getTokenConfig, tokenAmountFromCents } from "./config.js";
5
+ export type { EvmSettlerDeps } from "./settler.js";
6
+ export { settleEvmPayment } from "./settler.js";
7
+ export type { ChainConfig, EvmChain, EvmPaymentEvent, StablecoinCheckoutOpts, StablecoinToken, TokenConfig, } from "./types.js";
8
+ export type { EvmWatcherOpts } from "./watcher.js";
9
+ export { createRpcCaller, EvmWatcher } from "./watcher.js";
@@ -0,0 +1,5 @@
1
+ export { deriveDepositAddress, isValidXpub } from "./address-gen.js";
2
+ export { createStablecoinCheckout, MIN_STABLECOIN_USD } from "./checkout.js";
3
+ export { centsFromTokenAmount, getChainConfig, getTokenConfig, tokenAmountFromCents } from "./config.js";
4
+ export { settleEvmPayment } from "./settler.js";
5
+ export { createRpcCaller, EvmWatcher } from "./watcher.js";
@@ -0,0 +1,23 @@
1
+ import type { ILedger } from "../../../credits/ledger.js";
2
+ import type { ICryptoChargeRepository } from "../charge-store.js";
3
+ import type { CryptoWebhookResult } from "../types.js";
4
+ import type { EvmPaymentEvent } from "./types.js";
5
+ export interface EvmSettlerDeps {
6
+ chargeStore: Pick<ICryptoChargeRepository, "getByDepositAddress" | "updateStatus" | "markCredited">;
7
+ creditLedger: Pick<ILedger, "credit" | "hasReferenceId">;
8
+ onCreditsPurchased?: (tenantId: string, ledger: ILedger) => Promise<string[]>;
9
+ }
10
+ /**
11
+ * Settle an EVM payment event — look up charge by deposit address, credit ledger.
12
+ *
13
+ * Same idempotency pattern as handleCryptoWebhook():
14
+ * Primary: creditLedger.hasReferenceId() — atomic in ledger transaction
15
+ * Secondary: chargeStore.markCredited() — advisory
16
+ *
17
+ * Credits the CHARGE amount (not the transfer amount) for overpayment safety.
18
+ *
19
+ * CRITICAL: charge.amountUsdCents is in USD cents (integer).
20
+ * Credit.fromCents() converts cents → nanodollars for the ledger.
21
+ * Never pass raw cents to the ledger — always go through Credit.fromCents().
22
+ */
23
+ export declare function settleEvmPayment(deps: EvmSettlerDeps, event: EvmPaymentEvent): Promise<CryptoWebhookResult>;
@@ -0,0 +1,60 @@
1
+ import { Credit } from "../../../credits/credit.js";
2
+ /**
3
+ * Settle an EVM payment event — look up charge by deposit address, credit ledger.
4
+ *
5
+ * Same idempotency pattern as handleCryptoWebhook():
6
+ * Primary: creditLedger.hasReferenceId() — atomic in ledger transaction
7
+ * Secondary: chargeStore.markCredited() — advisory
8
+ *
9
+ * Credits the CHARGE amount (not the transfer amount) for overpayment safety.
10
+ *
11
+ * CRITICAL: charge.amountUsdCents is in USD cents (integer).
12
+ * Credit.fromCents() converts cents → nanodollars for the ledger.
13
+ * Never pass raw cents to the ledger — always go through Credit.fromCents().
14
+ */
15
+ export async function settleEvmPayment(deps, event) {
16
+ const { chargeStore, creditLedger } = deps;
17
+ const charge = await chargeStore.getByDepositAddress(event.to.toLowerCase());
18
+ if (!charge) {
19
+ return { handled: false, status: "Settled" };
20
+ }
21
+ // Update charge status to Settled.
22
+ await chargeStore.updateStatus(charge.referenceId, "Settled");
23
+ // Charge-level idempotency: if this charge was already credited (by any transfer),
24
+ // reject. Prevents double-credit when a user sends two transactions to the same address.
25
+ if (charge.creditedAt != null) {
26
+ return { handled: true, status: "Settled", tenant: charge.tenantId, creditedCents: 0 };
27
+ }
28
+ // Transfer-level idempotency: if this specific tx was already processed, skip.
29
+ const creditRef = `evm:${event.chain}:${event.txHash}:${event.logIndex}`;
30
+ if (await creditLedger.hasReferenceId(creditRef)) {
31
+ return { handled: true, status: "Settled", tenant: charge.tenantId, creditedCents: 0 };
32
+ }
33
+ // Reject underpayment — transfer must cover the charge amount.
34
+ if (event.amountUsdCents < charge.amountUsdCents) {
35
+ return { handled: true, status: "Settled", tenant: charge.tenantId, creditedCents: 0 };
36
+ }
37
+ // Credit the CHARGE amount (NOT the transfer amount — overpayment stays in wallet).
38
+ // charge.amountUsdCents is in USD cents (integer).
39
+ // Credit.fromCents() converts to nanodollars for the ledger.
40
+ const creditCents = charge.amountUsdCents;
41
+ await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase", {
42
+ description: `Stablecoin credit purchase (${event.token} on ${event.chain}, tx: ${event.txHash})`,
43
+ referenceId: creditRef,
44
+ fundingSource: "crypto",
45
+ });
46
+ await chargeStore.markCredited(charge.referenceId);
47
+ let reactivatedBots;
48
+ if (deps.onCreditsPurchased) {
49
+ reactivatedBots = await deps.onCreditsPurchased(charge.tenantId, creditLedger);
50
+ if (reactivatedBots.length === 0)
51
+ reactivatedBots = undefined;
52
+ }
53
+ return {
54
+ handled: true,
55
+ status: "Settled",
56
+ tenant: charge.tenantId,
57
+ creditedCents: creditCents,
58
+ reactivatedBots,
59
+ };
60
+ }
@@ -0,0 +1,40 @@
1
+ /** Supported EVM chains. */
2
+ export type EvmChain = "base";
3
+ /** Supported stablecoin tokens. */
4
+ export type StablecoinToken = "USDC";
5
+ /** Chain configuration. */
6
+ export interface ChainConfig {
7
+ readonly chain: EvmChain;
8
+ readonly rpcUrl: string;
9
+ readonly confirmations: number;
10
+ readonly blockTimeMs: number;
11
+ readonly chainId: number;
12
+ }
13
+ /** Token configuration on a specific chain. */
14
+ export interface TokenConfig {
15
+ readonly token: StablecoinToken;
16
+ readonly chain: EvmChain;
17
+ readonly contractAddress: `0x${string}`;
18
+ readonly decimals: number;
19
+ }
20
+ /** Event emitted when a Transfer is detected and confirmed. */
21
+ export interface EvmPaymentEvent {
22
+ readonly chain: EvmChain;
23
+ readonly token: StablecoinToken;
24
+ readonly from: string;
25
+ readonly to: string;
26
+ /** Raw token amount (BigInt as string for serialization). */
27
+ readonly rawAmount: string;
28
+ /** USD cents equivalent (integer). */
29
+ readonly amountUsdCents: number;
30
+ readonly txHash: string;
31
+ readonly blockNumber: number;
32
+ readonly logIndex: number;
33
+ }
34
+ /** Options for creating a stablecoin checkout. */
35
+ export interface StablecoinCheckoutOpts {
36
+ tenant: string;
37
+ amountUsd: number;
38
+ chain: EvmChain;
39
+ token: StablecoinToken;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import type { EvmChain, EvmPaymentEvent, StablecoinToken } from "./types.js";
2
+ type RpcCall = (method: string, params: unknown[]) => Promise<unknown>;
3
+ export interface EvmWatcherOpts {
4
+ chain: EvmChain;
5
+ token: StablecoinToken;
6
+ rpcCall: RpcCall;
7
+ fromBlock: number;
8
+ onPayment: (event: EvmPaymentEvent) => void | Promise<void>;
9
+ /** Active deposit addresses to watch. Filters eth_getLogs by topic[2] (to address). */
10
+ watchedAddresses?: string[];
11
+ }
12
+ export declare class EvmWatcher {
13
+ private _cursor;
14
+ private readonly chain;
15
+ private readonly token;
16
+ private readonly rpc;
17
+ private readonly onPayment;
18
+ private readonly confirmations;
19
+ private readonly contractAddress;
20
+ private readonly decimals;
21
+ private _watchedAddresses;
22
+ constructor(opts: EvmWatcherOpts);
23
+ /** Update the set of watched deposit addresses (e.g. after a new checkout). */
24
+ setWatchedAddresses(addresses: string[]): void;
25
+ get cursor(): number;
26
+ /** Poll for new Transfer events. Call on an interval. */
27
+ poll(): Promise<void>;
28
+ }
29
+ /** Create an RPC caller for a given URL (plain JSON-RPC over fetch). */
30
+ export declare function createRpcCaller(rpcUrl: string): RpcCall;
31
+ export {};
@@ -0,0 +1,91 @@
1
+ import { centsFromTokenAmount, getChainConfig, getTokenConfig } from "./config.js";
2
+ const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
3
+ export class EvmWatcher {
4
+ _cursor;
5
+ chain;
6
+ token;
7
+ rpc;
8
+ onPayment;
9
+ confirmations;
10
+ contractAddress;
11
+ decimals;
12
+ _watchedAddresses;
13
+ constructor(opts) {
14
+ this.chain = opts.chain;
15
+ this.token = opts.token;
16
+ this.rpc = opts.rpcCall;
17
+ this._cursor = opts.fromBlock;
18
+ this.onPayment = opts.onPayment;
19
+ this._watchedAddresses = (opts.watchedAddresses ?? []).map((a) => a.toLowerCase());
20
+ const chainCfg = getChainConfig(opts.chain);
21
+ const tokenCfg = getTokenConfig(opts.token, opts.chain);
22
+ this.confirmations = chainCfg.confirmations;
23
+ this.contractAddress = tokenCfg.contractAddress.toLowerCase();
24
+ this.decimals = tokenCfg.decimals;
25
+ }
26
+ /** Update the set of watched deposit addresses (e.g. after a new checkout). */
27
+ setWatchedAddresses(addresses) {
28
+ this._watchedAddresses = addresses.map((a) => a.toLowerCase());
29
+ }
30
+ get cursor() {
31
+ return this._cursor;
32
+ }
33
+ /** Poll for new Transfer events. Call on an interval. */
34
+ async poll() {
35
+ const latestHex = (await this.rpc("eth_blockNumber", []));
36
+ const latest = Number.parseInt(latestHex, 16);
37
+ const confirmed = latest - this.confirmations;
38
+ if (confirmed < this._cursor)
39
+ return;
40
+ // Filter by topic[2] (to address) when watched addresses are set.
41
+ // This avoids fetching ALL USDC transfers on the chain (millions/day on Base).
42
+ // topic[2] values are 32-byte zero-padded: 0x000000000000000000000000<address>
43
+ const toFilter = this._watchedAddresses.length > 0
44
+ ? this._watchedAddresses.map((a) => `0x000000000000000000000000${a.slice(2)}`)
45
+ : null;
46
+ const logs = (await this.rpc("eth_getLogs", [
47
+ {
48
+ address: this.contractAddress,
49
+ topics: [TRANSFER_TOPIC, null, toFilter],
50
+ fromBlock: `0x${this._cursor.toString(16)}`,
51
+ toBlock: `0x${confirmed.toString(16)}`,
52
+ },
53
+ ]));
54
+ for (const log of logs) {
55
+ const to = `0x${log.topics[2].slice(26)}`.toLowerCase();
56
+ const from = `0x${log.topics[1].slice(26)}`.toLowerCase();
57
+ const rawAmount = BigInt(log.data);
58
+ const amountUsdCents = centsFromTokenAmount(rawAmount, this.decimals);
59
+ const event = {
60
+ chain: this.chain,
61
+ token: this.token,
62
+ from,
63
+ to,
64
+ rawAmount: rawAmount.toString(),
65
+ amountUsdCents,
66
+ txHash: log.transactionHash,
67
+ blockNumber: Number.parseInt(log.blockNumber, 16),
68
+ logIndex: Number.parseInt(log.logIndex, 16),
69
+ };
70
+ await this.onPayment(event);
71
+ }
72
+ this._cursor = confirmed + 1;
73
+ }
74
+ }
75
+ /** Create an RPC caller for a given URL (plain JSON-RPC over fetch). */
76
+ export function createRpcCaller(rpcUrl) {
77
+ let id = 0;
78
+ return async (method, params) => {
79
+ const res = await fetch(rpcUrl, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }),
83
+ });
84
+ if (!res.ok)
85
+ throw new Error(`RPC ${method} failed: ${res.status}`);
86
+ const data = (await res.json());
87
+ if (data.error)
88
+ throw new Error(`RPC ${method} error: ${data.error.message}`);
89
+ return data.result;
90
+ };
91
+ }
@@ -1,8 +1,9 @@
1
- export type { CryptoChargeRecord, ICryptoChargeRepository } from "./charge-store.js";
1
+ export type { CryptoChargeRecord, ICryptoChargeRepository, StablecoinChargeInput } from "./charge-store.js";
2
2
  export { CryptoChargeRepository, DrizzleCryptoChargeRepository } from "./charge-store.js";
3
3
  export { createCryptoCheckout, MIN_PAYMENT_USD } from "./checkout.js";
4
4
  export type { CryptoConfig } from "./client.js";
5
5
  export { BTCPayClient, loadCryptoConfig } from "./client.js";
6
+ export * from "./evm/index.js";
6
7
  export type { CryptoBillingConfig, CryptoCheckoutOpts, CryptoPaymentState, CryptoWebhookPayload, CryptoWebhookResult, } from "./types.js";
7
8
  export { mapBtcPayEventToStatus } from "./types.js";
8
9
  export type { CryptoWebhookDeps } from "./webhook.js";
@@ -1,5 +1,6 @@
1
1
  export { CryptoChargeRepository, DrizzleCryptoChargeRepository } from "./charge-store.js";
2
2
  export { createCryptoCheckout, MIN_PAYMENT_USD } from "./checkout.js";
3
3
  export { BTCPayClient, loadCryptoConfig } from "./client.js";
4
+ export * from "./evm/index.js";
4
5
  export { mapBtcPayEventToStatus } from "./types.js";
5
6
  export { handleCryptoWebhook, verifyCryptoWebhookSignature } from "./webhook.js";
@@ -163,6 +163,74 @@ export declare const cryptoCharges: import("drizzle-orm/pg-core").PgTableWithCol
163
163
  identity: undefined;
164
164
  generated: undefined;
165
165
  }, {}, {}>;
166
+ chain: import("drizzle-orm/pg-core").PgColumn<{
167
+ name: "chain";
168
+ tableName: "crypto_charges";
169
+ dataType: "string";
170
+ columnType: "PgText";
171
+ data: string;
172
+ driverParam: string;
173
+ notNull: false;
174
+ hasDefault: false;
175
+ isPrimaryKey: false;
176
+ isAutoincrement: false;
177
+ hasRuntimeDefault: false;
178
+ enumValues: [string, ...string[]];
179
+ baseColumn: never;
180
+ identity: undefined;
181
+ generated: undefined;
182
+ }, {}, {}>;
183
+ token: import("drizzle-orm/pg-core").PgColumn<{
184
+ name: "token";
185
+ tableName: "crypto_charges";
186
+ dataType: "string";
187
+ columnType: "PgText";
188
+ data: string;
189
+ driverParam: string;
190
+ notNull: false;
191
+ hasDefault: false;
192
+ isPrimaryKey: false;
193
+ isAutoincrement: false;
194
+ hasRuntimeDefault: false;
195
+ enumValues: [string, ...string[]];
196
+ baseColumn: never;
197
+ identity: undefined;
198
+ generated: undefined;
199
+ }, {}, {}>;
200
+ depositAddress: import("drizzle-orm/pg-core").PgColumn<{
201
+ name: "deposit_address";
202
+ tableName: "crypto_charges";
203
+ dataType: "string";
204
+ columnType: "PgText";
205
+ data: string;
206
+ driverParam: string;
207
+ notNull: false;
208
+ hasDefault: false;
209
+ isPrimaryKey: false;
210
+ isAutoincrement: false;
211
+ hasRuntimeDefault: false;
212
+ enumValues: [string, ...string[]];
213
+ baseColumn: never;
214
+ identity: undefined;
215
+ generated: undefined;
216
+ }, {}, {}>;
217
+ derivationIndex: import("drizzle-orm/pg-core").PgColumn<{
218
+ name: "derivation_index";
219
+ tableName: "crypto_charges";
220
+ dataType: "number";
221
+ columnType: "PgInteger";
222
+ data: number;
223
+ driverParam: string | number;
224
+ notNull: false;
225
+ hasDefault: false;
226
+ isPrimaryKey: false;
227
+ isAutoincrement: false;
228
+ hasRuntimeDefault: false;
229
+ enumValues: undefined;
230
+ baseColumn: never;
231
+ identity: undefined;
232
+ generated: undefined;
233
+ }, {}, {}>;
166
234
  };
167
235
  dialect: "pg";
168
236
  }>;
@@ -18,8 +18,15 @@ export const cryptoCharges = pgTable("crypto_charges", {
18
18
  createdAt: text("created_at").notNull().default(sql `(now())`),
19
19
  updatedAt: text("updated_at").notNull().default(sql `(now())`),
20
20
  creditedAt: text("credited_at"),
21
+ chain: text("chain"),
22
+ token: text("token"),
23
+ depositAddress: text("deposit_address"),
24
+ derivationIndex: integer("derivation_index"),
21
25
  }, (table) => [
22
26
  index("idx_crypto_charges_tenant").on(table.tenantId),
23
27
  index("idx_crypto_charges_status").on(table.status),
24
28
  index("idx_crypto_charges_created").on(table.createdAt),
29
+ index("idx_crypto_charges_deposit_address").on(table.depositAddress),
30
+ // uniqueIndex would be ideal but drizzle pgTable helper doesn't support it inline.
31
+ // Enforced via migration: CREATE UNIQUE INDEX.
25
32
  ]);