@wopr-network/platform-core 1.48.0 → 1.49.1

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 (114) hide show
  1. package/dist/billing/crypto/__tests__/key-server.test.js +1 -1
  2. package/dist/billing/crypto/__tests__/unified-checkout.test.d.ts +1 -0
  3. package/dist/billing/crypto/__tests__/unified-checkout.test.js +63 -0
  4. package/dist/billing/crypto/__tests__/watcher-service.test.d.ts +1 -0
  5. package/dist/billing/crypto/__tests__/watcher-service.test.js +174 -0
  6. package/dist/billing/crypto/__tests__/webhook-confirmations.test.d.ts +1 -0
  7. package/dist/billing/crypto/__tests__/webhook-confirmations.test.js +304 -0
  8. package/dist/billing/crypto/btc/__tests__/settler.test.js +1 -0
  9. package/dist/billing/crypto/btc/__tests__/watcher.test.d.ts +1 -0
  10. package/dist/billing/crypto/btc/__tests__/watcher.test.js +170 -0
  11. package/dist/billing/crypto/btc/types.d.ts +3 -1
  12. package/dist/billing/crypto/btc/watcher.d.ts +6 -1
  13. package/dist/billing/crypto/btc/watcher.js +24 -8
  14. package/dist/billing/crypto/charge-store.d.ts +27 -2
  15. package/dist/billing/crypto/charge-store.js +67 -1
  16. package/dist/billing/crypto/charge-store.test.js +180 -1
  17. package/dist/billing/crypto/client.d.ts +2 -0
  18. package/dist/billing/crypto/cursor-store.d.ts +10 -3
  19. package/dist/billing/crypto/cursor-store.js +21 -1
  20. package/dist/billing/crypto/evm/__tests__/eth-checkout.test.js +3 -3
  21. package/dist/billing/crypto/evm/__tests__/eth-settler.test.js +2 -0
  22. package/dist/billing/crypto/evm/__tests__/eth-watcher.test.js +33 -6
  23. package/dist/billing/crypto/evm/__tests__/settler.test.js +2 -0
  24. package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.d.ts +1 -0
  25. package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.js +144 -0
  26. package/dist/billing/crypto/evm/__tests__/watcher.test.js +6 -2
  27. package/dist/billing/crypto/evm/eth-checkout.d.ts +2 -2
  28. package/dist/billing/crypto/evm/eth-checkout.js +3 -3
  29. package/dist/billing/crypto/evm/eth-watcher.d.ts +11 -8
  30. package/dist/billing/crypto/evm/eth-watcher.js +29 -15
  31. package/dist/billing/crypto/evm/types.d.ts +5 -1
  32. package/dist/billing/crypto/evm/watcher.d.ts +9 -1
  33. package/dist/billing/crypto/evm/watcher.js +36 -13
  34. package/dist/billing/crypto/index.d.ts +3 -3
  35. package/dist/billing/crypto/index.js +1 -1
  36. package/dist/billing/crypto/key-server-entry.js +7 -2
  37. package/dist/billing/crypto/key-server-webhook.d.ts +17 -4
  38. package/dist/billing/crypto/key-server-webhook.js +76 -15
  39. package/dist/billing/crypto/key-server.js +18 -7
  40. package/dist/billing/crypto/oracle/__tests__/chainlink.test.js +4 -4
  41. package/dist/billing/crypto/oracle/__tests__/coingecko.test.d.ts +1 -0
  42. package/dist/billing/crypto/oracle/__tests__/coingecko.test.js +65 -0
  43. package/dist/billing/crypto/oracle/__tests__/composite.test.d.ts +1 -0
  44. package/dist/billing/crypto/oracle/__tests__/composite.test.js +48 -0
  45. package/dist/billing/crypto/oracle/__tests__/convert.test.js +27 -17
  46. package/dist/billing/crypto/oracle/__tests__/fixed.test.js +5 -5
  47. package/dist/billing/crypto/oracle/chainlink.d.ts +2 -2
  48. package/dist/billing/crypto/oracle/chainlink.js +11 -10
  49. package/dist/billing/crypto/oracle/coingecko.d.ts +22 -0
  50. package/dist/billing/crypto/oracle/coingecko.js +67 -0
  51. package/dist/billing/crypto/oracle/composite.d.ts +14 -0
  52. package/dist/billing/crypto/oracle/composite.js +34 -0
  53. package/dist/billing/crypto/oracle/convert.d.ts +17 -7
  54. package/dist/billing/crypto/oracle/convert.js +26 -13
  55. package/dist/billing/crypto/oracle/fixed.d.ts +2 -2
  56. package/dist/billing/crypto/oracle/fixed.js +9 -7
  57. package/dist/billing/crypto/oracle/index.d.ts +4 -0
  58. package/dist/billing/crypto/oracle/index.js +3 -0
  59. package/dist/billing/crypto/oracle/types.d.ts +12 -3
  60. package/dist/billing/crypto/oracle/types.js +7 -1
  61. package/dist/billing/crypto/types.d.ts +16 -0
  62. package/dist/billing/crypto/unified-checkout.d.ts +10 -19
  63. package/dist/billing/crypto/unified-checkout.js +17 -131
  64. package/dist/billing/crypto/watcher-service.d.ts +22 -2
  65. package/dist/billing/crypto/watcher-service.js +71 -30
  66. package/dist/db/schema/crypto.d.ts +68 -0
  67. package/dist/db/schema/crypto.js +8 -0
  68. package/dist/monetization/crypto/__tests__/webhook.test.js +2 -1
  69. package/drizzle/migrations/0016_charge_progress_columns.sql +4 -0
  70. package/drizzle/migrations/meta/_journal.json +7 -0
  71. package/package.json +1 -1
  72. package/src/billing/crypto/__tests__/key-server.test.ts +1 -1
  73. package/src/billing/crypto/__tests__/unified-checkout.test.ts +83 -0
  74. package/src/billing/crypto/__tests__/watcher-service.test.ts +242 -0
  75. package/src/billing/crypto/__tests__/webhook-confirmations.test.ts +367 -0
  76. package/src/billing/crypto/btc/__tests__/settler.test.ts +1 -0
  77. package/src/billing/crypto/btc/__tests__/watcher.test.ts +201 -0
  78. package/src/billing/crypto/btc/types.ts +3 -1
  79. package/src/billing/crypto/btc/watcher.ts +26 -8
  80. package/src/billing/crypto/charge-store.test.ts +204 -1
  81. package/src/billing/crypto/charge-store.ts +86 -2
  82. package/src/billing/crypto/client.ts +2 -0
  83. package/src/billing/crypto/cursor-store.ts +31 -3
  84. package/src/billing/crypto/evm/__tests__/eth-checkout.test.ts +3 -3
  85. package/src/billing/crypto/evm/__tests__/eth-settler.test.ts +2 -0
  86. package/src/billing/crypto/evm/__tests__/eth-watcher.test.ts +33 -6
  87. package/src/billing/crypto/evm/__tests__/settler.test.ts +2 -0
  88. package/src/billing/crypto/evm/__tests__/watcher-confirmations.test.ts +176 -0
  89. package/src/billing/crypto/evm/__tests__/watcher.test.ts +6 -2
  90. package/src/billing/crypto/evm/eth-checkout.ts +5 -5
  91. package/src/billing/crypto/evm/eth-watcher.ts +36 -16
  92. package/src/billing/crypto/evm/types.ts +5 -1
  93. package/src/billing/crypto/evm/watcher.ts +39 -13
  94. package/src/billing/crypto/index.ts +12 -3
  95. package/src/billing/crypto/key-server-entry.ts +7 -2
  96. package/src/billing/crypto/key-server-webhook.ts +92 -21
  97. package/src/billing/crypto/key-server.ts +17 -7
  98. package/src/billing/crypto/oracle/__tests__/chainlink.test.ts +4 -4
  99. package/src/billing/crypto/oracle/__tests__/coingecko.test.ts +75 -0
  100. package/src/billing/crypto/oracle/__tests__/composite.test.ts +61 -0
  101. package/src/billing/crypto/oracle/__tests__/convert.test.ts +29 -17
  102. package/src/billing/crypto/oracle/__tests__/fixed.test.ts +5 -5
  103. package/src/billing/crypto/oracle/chainlink.ts +11 -10
  104. package/src/billing/crypto/oracle/coingecko.ts +92 -0
  105. package/src/billing/crypto/oracle/composite.ts +35 -0
  106. package/src/billing/crypto/oracle/convert.ts +28 -13
  107. package/src/billing/crypto/oracle/fixed.ts +9 -7
  108. package/src/billing/crypto/oracle/index.ts +4 -0
  109. package/src/billing/crypto/oracle/types.ts +16 -3
  110. package/src/billing/crypto/types.ts +18 -0
  111. package/src/billing/crypto/unified-checkout.ts +22 -181
  112. package/src/billing/crypto/watcher-service.ts +85 -32
  113. package/src/db/schema/crypto.ts +8 -0
  114. package/src/monetization/crypto/__tests__/webhook.test.ts +2 -1
@@ -0,0 +1,67 @@
1
+ import { AssetNotSupportedError } from "./types.js";
2
+ /**
3
+ * Token symbol → CoinGecko API ID mapping.
4
+ * CoinGecko uses lowercase slugs, not ticker symbols.
5
+ */
6
+ const COINGECKO_IDS = {
7
+ BTC: "bitcoin",
8
+ ETH: "ethereum",
9
+ DOGE: "dogecoin",
10
+ LTC: "litecoin",
11
+ SOL: "solana",
12
+ LINK: "chainlink",
13
+ UNI: "uniswap",
14
+ AERO: "aerodrome-finance",
15
+ };
16
+ /** Default cache TTL: 60 seconds. CoinGecko free tier allows 10-30 req/min. */
17
+ const DEFAULT_CACHE_TTL_MS = 60_000;
18
+ /**
19
+ * CoinGecko price oracle — free API, no key required.
20
+ * Used for assets without Chainlink on-chain feeds (DOGE, LTC).
21
+ * Caches prices to stay within rate limits.
22
+ */
23
+ export class CoinGeckoOracle {
24
+ ids;
25
+ cacheTtlMs;
26
+ fetchFn;
27
+ cache = new Map();
28
+ constructor(opts = {}) {
29
+ this.ids = { ...COINGECKO_IDS, ...opts.tokenIds };
30
+ this.cacheTtlMs = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
31
+ this.fetchFn = opts.fetchFn ?? fetch;
32
+ }
33
+ async getPrice(asset, _feedAddress) {
34
+ const cached = this.cache.get(asset);
35
+ if (cached && Date.now() - cached.fetchedAt < this.cacheTtlMs) {
36
+ return { priceMicros: cached.priceMicros, updatedAt: cached.updatedAt };
37
+ }
38
+ const coinId = this.ids[asset];
39
+ if (!coinId)
40
+ throw new AssetNotSupportedError(asset);
41
+ const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd`;
42
+ try {
43
+ const res = await this.fetchFn(url);
44
+ if (!res.ok) {
45
+ throw new Error(`CoinGecko API error for ${asset}: ${res.status} ${res.statusText}`);
46
+ }
47
+ const data = (await res.json());
48
+ const usdPrice = data[coinId]?.usd;
49
+ if (usdPrice === undefined || usdPrice <= 0) {
50
+ throw new Error(`Invalid CoinGecko price for ${asset}: ${usdPrice}`);
51
+ }
52
+ const priceMicros = Math.round(usdPrice * 1_000_000);
53
+ const updatedAt = new Date();
54
+ this.cache.set(asset, { priceMicros, updatedAt, fetchedAt: Date.now() });
55
+ return { priceMicros, updatedAt };
56
+ }
57
+ catch (err) {
58
+ // Serve stale cache on transient failure (rate limit, network error).
59
+ // A slightly old price is better than rejecting the charge entirely.
60
+ const stale = this.cache.get(asset);
61
+ if (stale) {
62
+ return { priceMicros: stale.priceMicros, updatedAt: stale.updatedAt };
63
+ }
64
+ throw err;
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,14 @@
1
+ import type { IPriceOracle, PriceAsset, PriceResult } from "./types.js";
2
+ /**
3
+ * Composite oracle — tries primary (Chainlink on-chain), falls back to secondary (CoinGecko).
4
+ *
5
+ * When a feedAddress is provided (from payment_methods.oracle_address), the primary
6
+ * oracle is used with that address. When no feed exists or the primary fails,
7
+ * the fallback oracle is consulted.
8
+ */
9
+ export declare class CompositeOracle implements IPriceOracle {
10
+ private readonly primary;
11
+ private readonly fallback;
12
+ constructor(primary: IPriceOracle, fallback: IPriceOracle);
13
+ getPrice(asset: PriceAsset, feedAddress?: `0x${string}`): Promise<PriceResult>;
14
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Composite oracle — tries primary (Chainlink on-chain), falls back to secondary (CoinGecko).
3
+ *
4
+ * When a feedAddress is provided (from payment_methods.oracle_address), the primary
5
+ * oracle is used with that address. When no feed exists or the primary fails,
6
+ * the fallback oracle is consulted.
7
+ */
8
+ export class CompositeOracle {
9
+ primary;
10
+ fallback;
11
+ constructor(primary, fallback) {
12
+ this.primary = primary;
13
+ this.fallback = fallback;
14
+ }
15
+ async getPrice(asset, feedAddress) {
16
+ // If a specific feed address is provided, try the primary (Chainlink) first
17
+ if (feedAddress) {
18
+ try {
19
+ return await this.primary.getPrice(asset, feedAddress);
20
+ }
21
+ catch {
22
+ // Primary failed (stale, network error) — fall through to fallback
23
+ }
24
+ }
25
+ // Try primary without explicit feed (uses built-in feed map for BTC/ETH)
26
+ try {
27
+ return await this.primary.getPrice(asset);
28
+ }
29
+ catch {
30
+ // No feed configured or call failed — use fallback
31
+ }
32
+ return this.fallback.getPrice(asset);
33
+ }
34
+ }
@@ -1,20 +1,30 @@
1
1
  /**
2
- * Convert USD cents to native token amount using a price in cents.
2
+ * Price units: **microdollars** (1 microdollar = $0.000001 = 10^-6 USD).
3
3
  *
4
- * Formula: rawAmount = amountCents × 10^decimals / priceCents
4
+ * Why not cents? DOGE at $0.094 rounds to 9 cents — 6% error.
5
+ * Microdollars give 6 decimal places: $0.094147 = 94,147 microdollars.
6
+ *
7
+ * Chainlink feeds: 8 decimals → answer / 100 = microdollars.
8
+ * CoinGecko: Math.round(usd * 1_000_000) = microdollars.
9
+ * All math is integer bigint — no floating point.
10
+ */
11
+ /**
12
+ * Convert USD cents to native token amount using a price in microdollars.
13
+ *
14
+ * Formula: rawAmount = (amountCents × 10_000) × 10^decimals / priceMicros
5
15
  *
6
16
  * Examples:
7
- * $50 in ETH at $3,500: centsToNative(5000, 350000, 18) = 14285714285714285n (≈0.01429 ETH)
8
- * $50 in BTC at $65,000: centsToNative(5000, 6500000, 8) = 76923n (76,923 sats 0.00077 BTC)
17
+ * $50 in BTC at $70,315: centsToNative(5000, 70_315_000_000, 8) = 71,119 sats
18
+ * $50 in DOGE at $0.094: centsToNative(5000, 94_147, 8) = 53,107,898,982 base units (531.08 DOGE)
9
19
  *
10
20
  * Integer math only. No floating point.
11
21
  */
12
- export declare function centsToNative(amountCents: number, priceCents: number, decimals: number): bigint;
22
+ export declare function centsToNative(amountCents: number, priceMicros: number, decimals: number): bigint;
13
23
  /**
14
- * Convert native token amount back to USD cents using a price in cents.
24
+ * Convert native token amount back to USD cents using a price in microdollars.
15
25
  *
16
26
  * Inverse of centsToNative. Truncates fractional cents.
17
27
  *
18
28
  * Integer math only.
19
29
  */
20
- export declare function nativeToCents(rawAmount: bigint, priceCents: number, decimals: number): number;
30
+ export declare function nativeToCents(rawAmount: bigint, priceMicros: number, decimals: number): number;
@@ -1,38 +1,51 @@
1
1
  /**
2
- * Convert USD cents to native token amount using a price in cents.
2
+ * Price units: **microdollars** (1 microdollar = $0.000001 = 10^-6 USD).
3
3
  *
4
- * Formula: rawAmount = amountCents × 10^decimals / priceCents
4
+ * Why not cents? DOGE at $0.094 rounds to 9 cents — 6% error.
5
+ * Microdollars give 6 decimal places: $0.094147 = 94,147 microdollars.
6
+ *
7
+ * Chainlink feeds: 8 decimals → answer / 100 = microdollars.
8
+ * CoinGecko: Math.round(usd * 1_000_000) = microdollars.
9
+ * All math is integer bigint — no floating point.
10
+ */
11
+ /** Microdollars per cent. Multiply amountCents by this to get microdollars. */
12
+ const MICROS_PER_CENT = 10000n;
13
+ /**
14
+ * Convert USD cents to native token amount using a price in microdollars.
15
+ *
16
+ * Formula: rawAmount = (amountCents × 10_000) × 10^decimals / priceMicros
5
17
  *
6
18
  * Examples:
7
- * $50 in ETH at $3,500: centsToNative(5000, 350000, 18) = 14285714285714285n (≈0.01429 ETH)
8
- * $50 in BTC at $65,000: centsToNative(5000, 6500000, 8) = 76923n (76,923 sats 0.00077 BTC)
19
+ * $50 in BTC at $70,315: centsToNative(5000, 70_315_000_000, 8) = 71,119 sats
20
+ * $50 in DOGE at $0.094: centsToNative(5000, 94_147, 8) = 53,107,898,982 base units (531.08 DOGE)
9
21
  *
10
22
  * Integer math only. No floating point.
11
23
  */
12
- export function centsToNative(amountCents, priceCents, decimals) {
24
+ export function centsToNative(amountCents, priceMicros, decimals) {
13
25
  if (!Number.isInteger(amountCents) || amountCents <= 0) {
14
26
  throw new Error(`amountCents must be a positive integer, got ${amountCents}`);
15
27
  }
16
- if (!Number.isInteger(priceCents) || priceCents <= 0) {
17
- throw new Error(`priceCents must be a positive integer, got ${priceCents}`);
28
+ if (!Number.isInteger(priceMicros) || priceMicros <= 0) {
29
+ throw new Error(`priceMicros must be a positive integer, got ${priceMicros}`);
18
30
  }
19
31
  if (!Number.isInteger(decimals) || decimals < 0) {
20
32
  throw new Error(`decimals must be a non-negative integer, got ${decimals}`);
21
33
  }
22
- return (BigInt(amountCents) * 10n ** BigInt(decimals)) / BigInt(priceCents);
34
+ // Convert amountCents to microdollars to match priceMicros units
35
+ return (BigInt(amountCents) * MICROS_PER_CENT * 10n ** BigInt(decimals)) / BigInt(priceMicros);
23
36
  }
24
37
  /**
25
- * Convert native token amount back to USD cents using a price in cents.
38
+ * Convert native token amount back to USD cents using a price in microdollars.
26
39
  *
27
40
  * Inverse of centsToNative. Truncates fractional cents.
28
41
  *
29
42
  * Integer math only.
30
43
  */
31
- export function nativeToCents(rawAmount, priceCents, decimals) {
44
+ export function nativeToCents(rawAmount, priceMicros, decimals) {
32
45
  if (rawAmount < 0n)
33
46
  throw new Error("rawAmount must be non-negative");
34
- if (!Number.isInteger(priceCents) || priceCents <= 0) {
35
- throw new Error(`priceCents must be a positive integer, got ${priceCents}`);
47
+ if (!Number.isInteger(priceMicros) || priceMicros <= 0) {
48
+ throw new Error(`priceMicros must be a positive integer, got ${priceMicros}`);
36
49
  }
37
- return Number((rawAmount * BigInt(priceCents)) / 10n ** BigInt(decimals));
50
+ return Number((rawAmount * BigInt(priceMicros)) / (MICROS_PER_CENT * 10n ** BigInt(decimals)));
38
51
  }
@@ -1,10 +1,10 @@
1
1
  import type { IPriceOracle, PriceAsset, PriceResult } from "./types.js";
2
2
  /**
3
3
  * Fixed-price oracle for testing and local dev (Anvil, regtest).
4
- * Returns hardcoded prices — no RPC calls.
4
+ * Returns hardcoded prices in microdollars — no RPC calls.
5
5
  */
6
6
  export declare class FixedPriceOracle implements IPriceOracle {
7
7
  private readonly prices;
8
8
  constructor(prices?: Partial<Record<PriceAsset, number>>);
9
- getPrice(asset: PriceAsset): Promise<PriceResult>;
9
+ getPrice(asset: PriceAsset, _feedAddress?: `0x${string}`): Promise<PriceResult>;
10
10
  }
@@ -1,20 +1,22 @@
1
1
  /**
2
2
  * Fixed-price oracle for testing and local dev (Anvil, regtest).
3
- * Returns hardcoded prices — no RPC calls.
3
+ * Returns hardcoded prices in microdollars — no RPC calls.
4
4
  */
5
5
  export class FixedPriceOracle {
6
6
  prices;
7
7
  constructor(prices = {}) {
8
8
  this.prices = {
9
- ETH: 350_000, // $3,500
10
- BTC: 6_500_000, // $65,000
9
+ ETH: 3_500_000_000, // $3,500 in microdollars
10
+ BTC: 65_000_000_000, // $65,000 in microdollars
11
+ DOGE: 94_000, // $0.094 in microdollars
12
+ LTC: 55_000_000, // $55 in microdollars
11
13
  ...prices,
12
14
  };
13
15
  }
14
- async getPrice(asset) {
15
- const priceCents = this.prices[asset];
16
- if (priceCents === undefined)
16
+ async getPrice(asset, _feedAddress) {
17
+ const priceMicros = this.prices[asset];
18
+ if (priceMicros === undefined)
17
19
  throw new Error(`No fixed price for ${asset}`);
18
- return { priceCents, updatedAt: new Date() };
20
+ return { priceMicros, updatedAt: new Date() };
19
21
  }
20
22
  }
@@ -1,5 +1,9 @@
1
1
  export type { ChainlinkOracleOpts } from "./chainlink.js";
2
2
  export { ChainlinkOracle } from "./chainlink.js";
3
+ export type { CoinGeckoOracleOpts } from "./coingecko.js";
4
+ export { CoinGeckoOracle } from "./coingecko.js";
5
+ export { CompositeOracle } from "./composite.js";
3
6
  export { centsToNative, nativeToCents } from "./convert.js";
4
7
  export { FixedPriceOracle } from "./fixed.js";
5
8
  export type { IPriceOracle, PriceAsset, PriceResult } from "./types.js";
9
+ export { AssetNotSupportedError } from "./types.js";
@@ -1,3 +1,6 @@
1
1
  export { ChainlinkOracle } from "./chainlink.js";
2
+ export { CoinGeckoOracle } from "./coingecko.js";
3
+ export { CompositeOracle } from "./composite.js";
2
4
  export { centsToNative, nativeToCents } from "./convert.js";
3
5
  export { FixedPriceOracle } from "./fixed.js";
6
+ export { AssetNotSupportedError } from "./types.js";
@@ -1,13 +1,22 @@
1
1
  /** Assets with Chainlink price feeds. */
2
2
  export type PriceAsset = string;
3
+ /** Thrown when no oracle supports a given asset (not a transient failure). */
4
+ export declare class AssetNotSupportedError extends Error {
5
+ constructor(asset: string);
6
+ }
3
7
  /** Result from a price oracle query. */
4
8
  export interface PriceResult {
5
- /** USD cents per 1 unit of asset (integer). */
6
- priceCents: number;
9
+ /** Microdollars per 1 unit of asset (integer, 10^-6 USD). */
10
+ priceMicros: number;
7
11
  /** When the price was last updated on-chain. */
8
12
  updatedAt: Date;
9
13
  }
10
14
  /** Read-only price oracle. */
11
15
  export interface IPriceOracle {
12
- getPrice(asset: PriceAsset): Promise<PriceResult>;
16
+ /**
17
+ * Get the current USD price for an asset.
18
+ * @param asset — token symbol (e.g. "BTC", "DOGE")
19
+ * @param feedAddress — optional Chainlink feed address override (from payment_methods.oracle_address)
20
+ */
21
+ getPrice(asset: PriceAsset, feedAddress?: `0x${string}`): Promise<PriceResult>;
13
22
  }
@@ -1 +1,7 @@
1
- export {};
1
+ /** Thrown when no oracle supports a given asset (not a transient failure). */
2
+ export class AssetNotSupportedError extends Error {
3
+ constructor(asset) {
4
+ super(`No price oracle supports asset: ${asset}`);
5
+ this.name = "AssetNotSupportedError";
6
+ }
7
+ }
@@ -1,5 +1,21 @@
1
1
  /** BTCPay Server invoice states (Greenfield API v1). */
2
2
  export type CryptoPaymentState = "New" | "Processing" | "Expired" | "Invalid" | "Settled";
3
+ /** Charge status for the UI-facing payment lifecycle. */
4
+ export type CryptoChargeStatus = "pending" | "partial" | "confirmed" | "expired" | "failed";
5
+ /** Full charge record for UI display — includes partial payment progress and confirmations. */
6
+ export interface CryptoCharge {
7
+ id: string;
8
+ tenantId: string;
9
+ chain: string;
10
+ status: CryptoChargeStatus;
11
+ amountExpectedCents: number;
12
+ amountReceivedCents: number;
13
+ confirmations: number;
14
+ confirmationsRequired: number;
15
+ txHash?: string;
16
+ credited: boolean;
17
+ createdAt: Date;
18
+ }
3
19
  /** Options for creating a crypto payment session. */
4
20
  export interface CryptoCheckoutOpts {
5
21
  /** Internal tenant ID. */
@@ -1,14 +1,7 @@
1
- import type { ICryptoChargeRepository } from "./charge-store.js";
2
- import type { IPriceOracle } from "./oracle/types.js";
3
- import type { PaymentMethodRecord } from "./payment-method-store.js";
1
+ import type { CryptoServiceClient } from "./client.js";
4
2
  export declare const MIN_CHECKOUT_USD = 10;
5
3
  export interface UnifiedCheckoutDeps {
6
- chargeStore: Pick<ICryptoChargeRepository, "getNextDerivationIndex" | "createStablecoinCharge">;
7
- oracle: IPriceOracle;
8
- evmXpub: string;
9
- btcXpub?: string;
10
- /** UTXO network override (auto-detected from node in production). Default: "mainnet". */
11
- utxoNetwork?: "mainnet" | "testnet" | "regtest";
4
+ cryptoService: CryptoServiceClient;
12
5
  }
13
6
  export interface UnifiedCheckoutResult {
14
7
  depositAddress: string;
@@ -18,20 +11,18 @@ export interface UnifiedCheckoutResult {
18
11
  token: string;
19
12
  chain: string;
20
13
  referenceId: string;
21
- /** For volatile assets: price at checkout time (USD cents per unit). */
22
- priceCents?: number;
14
+ /** For volatile assets: price at checkout time (microdollars per unit, 10^-6 USD). */
15
+ priceMicros?: number;
23
16
  }
24
17
  /**
25
- * Unified checkout — one entry point for all payment methods.
18
+ * Unified checkout — delegates to CryptoServiceClient.createCharge().
26
19
  *
27
- * Looks up the method record, routes by type:
28
- * - erc20: derives EVM address, computes token amount (1:1 USD for stablecoins)
29
- * - native (ETH): derives EVM address, oracle-priced
30
- * - native (BTC): derives BTC address, oracle-priced
31
- *
32
- * CRITICAL: amountUsd → integer cents via Credit.fromDollars().toCentsRounded().
20
+ * The pay server handles xpub management, address derivation, and charge
21
+ * creation. This function is a thin wrapper that validates the amount
22
+ * and maps the response to `UnifiedCheckoutResult`.
33
23
  */
34
- export declare function createUnifiedCheckout(deps: UnifiedCheckoutDeps, method: PaymentMethodRecord, opts: {
24
+ export declare function createUnifiedCheckout(deps: UnifiedCheckoutDeps, chain: string, opts: {
35
25
  tenant: string;
36
26
  amountUsd: number;
27
+ callbackUrl?: string;
37
28
  }): Promise<UnifiedCheckoutResult>;
@@ -1,141 +1,27 @@
1
- import { Credit } from "../../credits/credit.js";
2
- import { deriveAddress, deriveP2pkhAddress } from "./btc/address-gen.js";
3
- import { deriveDepositAddress } from "./evm/address-gen.js";
4
- import { centsToNative } from "./oracle/convert.js";
5
1
  export const MIN_CHECKOUT_USD = 10;
6
2
  /**
7
- * Unified checkout — one entry point for all payment methods.
3
+ * Unified checkout — delegates to CryptoServiceClient.createCharge().
8
4
  *
9
- * Looks up the method record, routes by type:
10
- * - erc20: derives EVM address, computes token amount (1:1 USD for stablecoins)
11
- * - native (ETH): derives EVM address, oracle-priced
12
- * - native (BTC): derives BTC address, oracle-priced
13
- *
14
- * CRITICAL: amountUsd → integer cents via Credit.fromDollars().toCentsRounded().
5
+ * The pay server handles xpub management, address derivation, and charge
6
+ * creation. This function is a thin wrapper that validates the amount
7
+ * and maps the response to `UnifiedCheckoutResult`.
15
8
  */
16
- export async function createUnifiedCheckout(deps, method, opts) {
9
+ export async function createUnifiedCheckout(deps, chain, opts) {
17
10
  if (!Number.isFinite(opts.amountUsd) || opts.amountUsd < MIN_CHECKOUT_USD) {
18
11
  throw new Error(`Minimum payment amount is $${MIN_CHECKOUT_USD}`);
19
12
  }
20
- const amountUsdCents = Credit.fromDollars(opts.amountUsd).toCentsRounded();
21
- if (method.type === "erc20") {
22
- return handleErc20(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
23
- }
24
- if (method.type === "native" && method.chain === "base") {
25
- return handleNativeEvm(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
26
- }
27
- if (method.type === "native") {
28
- return handleNativeUtxo(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
29
- }
30
- throw new Error(`Unsupported payment method type: ${method.type}/${method.token}`);
31
- }
32
- async function handleErc20(deps, method, tenant, amountUsdCents, amountUsd) {
33
- const depositAddress = await deriveAndStore(deps, method, tenant, amountUsdCents);
34
- return {
35
- depositAddress,
36
- displayAmount: `${amountUsd} ${method.token}`,
37
- amountUsd,
38
- token: method.token,
39
- chain: method.chain,
40
- referenceId: `erc20:${method.chain}:${depositAddress}`,
41
- };
42
- }
43
- async function handleNativeEvm(deps, method, tenant, amountUsdCents, amountUsd) {
44
- const { priceCents } = await deps.oracle.getPrice("ETH");
45
- const expectedWei = centsToNative(amountUsdCents, priceCents, 18);
46
- const depositAddress = await deriveAndStore(deps, method, tenant, amountUsdCents);
47
- const divisor = BigInt("1000000000000000000");
48
- const whole = expectedWei / divisor;
49
- const frac = (expectedWei % divisor).toString().padStart(18, "0").slice(0, 6);
13
+ const result = await deps.cryptoService.createCharge({
14
+ chain,
15
+ amountUsd: opts.amountUsd,
16
+ callbackUrl: opts.callbackUrl,
17
+ });
50
18
  return {
51
- depositAddress,
52
- displayAmount: `${whole}.${frac} ETH`,
53
- amountUsd,
54
- token: "ETH",
55
- chain: method.chain,
56
- referenceId: `${method.type}:${method.chain}:${depositAddress}`,
57
- priceCents,
19
+ depositAddress: result.address,
20
+ displayAmount: result.displayAmount ?? `${opts.amountUsd} ${result.token}`,
21
+ amountUsd: opts.amountUsd,
22
+ token: result.token,
23
+ chain: result.chain,
24
+ referenceId: result.chargeId,
25
+ priceMicros: result.priceMicros,
58
26
  };
59
27
  }
60
- /**
61
- * Handle native UTXO coins (BTC, LTC, DOGE, BCH, etc.).
62
- * Uses the xpub from the payment method record (DB-driven).
63
- * Derives bech32 addresses for BTC/LTC, Base58 P2PKH for DOGE.
64
- */
65
- async function handleNativeUtxo(deps, method, tenant, amountUsdCents, amountUsd) {
66
- const xpub = method.xpub ?? deps.btcXpub;
67
- if (!xpub)
68
- throw new Error(`${method.token} payments not configured (no xpub)`);
69
- const { priceCents } = await deps.oracle.getPrice(method.token);
70
- const rawAmount = centsToNative(amountUsdCents, priceCents, method.decimals);
71
- const maxRetries = 3;
72
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
73
- const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
74
- // Derive address by chain type
75
- let depositAddress;
76
- if (method.chain === "dogecoin") {
77
- depositAddress = deriveP2pkhAddress(xpub, derivationIndex, "dogecoin");
78
- }
79
- else {
80
- depositAddress = deriveAddress(xpub, derivationIndex, deps.utxoNetwork ?? "mainnet", method.chain);
81
- }
82
- const referenceId = `${method.token.toLowerCase()}:${depositAddress}`;
83
- try {
84
- await deps.chargeStore.createStablecoinCharge({
85
- referenceId,
86
- tenantId: tenant,
87
- amountUsdCents,
88
- chain: method.chain,
89
- token: method.token,
90
- depositAddress,
91
- derivationIndex,
92
- });
93
- const divisor = 10 ** method.decimals;
94
- const displayAmt = (Number(rawAmount) / divisor).toFixed(method.decimals);
95
- return {
96
- depositAddress,
97
- displayAmount: `${displayAmt} ${method.token}`,
98
- amountUsd,
99
- token: method.token,
100
- chain: method.chain,
101
- referenceId,
102
- priceCents,
103
- };
104
- }
105
- catch (err) {
106
- const code = err.code;
107
- const isConflict = code === "23505" || (err instanceof Error && err.message.includes("unique_violation"));
108
- if (!isConflict || attempt === maxRetries)
109
- throw err;
110
- }
111
- }
112
- throw new Error("Failed to claim derivation index after retries");
113
- }
114
- /** Derive an EVM deposit address and store the charge. Retries on unique conflict. */
115
- async function deriveAndStore(deps, method, tenant, amountUsdCents) {
116
- const maxRetries = 3;
117
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
118
- const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
119
- const depositAddress = deriveDepositAddress(deps.evmXpub, derivationIndex);
120
- const referenceId = `${method.type}:${method.chain}:${depositAddress}`;
121
- try {
122
- await deps.chargeStore.createStablecoinCharge({
123
- referenceId,
124
- tenantId: tenant,
125
- amountUsdCents,
126
- chain: method.chain,
127
- token: method.token,
128
- depositAddress,
129
- derivationIndex,
130
- });
131
- return depositAddress;
132
- }
133
- catch (err) {
134
- const code = err.code;
135
- const isConflict = code === "23505" || (err instanceof Error && err.message.includes("unique_violation"));
136
- if (!isConflict || attempt === maxRetries)
137
- throw err;
138
- }
139
- }
140
- throw new Error("Failed to claim derivation index after retries");
141
- }
@@ -4,8 +4,8 @@
4
4
  * Payment flow:
5
5
  * 1. Watcher detects payment → handlePayment()
6
6
  * 2. Accumulate native amount (supports partial payments)
7
- * 3. When totalReceived >= expectedAmount → settle + credit
8
- * 4. Every payment (partial or full) enqueues a webhook delivery
7
+ * 3. When totalReceived >= expectedAmount AND confirmations >= required confirmed + credit
8
+ * 4. Every payment/confirmation change enqueues a webhook delivery
9
9
  * 5. Outbox processor retries failed deliveries with exponential backoff
10
10
  *
11
11
  * Amount comparison is ALWAYS in native crypto units (sats, wei, token base units).
@@ -30,4 +30,24 @@ export interface WatcherServiceOpts {
30
30
  /** Allowed callback URL prefixes. Default: ["https://"] — enforces HTTPS. */
31
31
  allowedCallbackPrefixes?: string[];
32
32
  }
33
+ export interface PaymentPayload {
34
+ txHash: string;
35
+ confirmations: number;
36
+ confirmationsRequired: number;
37
+ amountReceivedCents: number;
38
+ [key: string]: unknown;
39
+ }
40
+ /**
41
+ * Handle a payment event. Accumulates partial payments in native units.
42
+ * Fires webhook on every payment/confirmation change with canonical statuses.
43
+ *
44
+ * 3-phase webhook lifecycle:
45
+ * 1. Tx first seen -> status: "partial", confirmations: 0
46
+ * 2. Each new block -> status: "partial", confirmations: current
47
+ * 3. Threshold reached + full payment -> status: "confirmed"
48
+ *
49
+ * @param nativeAmount — received amount in native base units (sats for BTC/DOGE, raw token units for ERC20).
50
+ * Pass "0" for confirmation-only updates (no new payment, just more confirmations).
51
+ */
52
+ export declare function handlePayment(db: DrizzleDb, chargeStore: ICryptoChargeRepository, address: string, nativeAmount: string, payload: PaymentPayload, log: (msg: string, meta?: Record<string, unknown>) => void): Promise<void>;
33
53
  export declare function startWatchers(opts: WatcherServiceOpts): Promise<() => void>;