@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.
- package/dist/billing/crypto/__tests__/key-server.test.js +1 -1
- package/dist/billing/crypto/__tests__/unified-checkout.test.d.ts +1 -0
- package/dist/billing/crypto/__tests__/unified-checkout.test.js +63 -0
- package/dist/billing/crypto/__tests__/watcher-service.test.d.ts +1 -0
- package/dist/billing/crypto/__tests__/watcher-service.test.js +174 -0
- package/dist/billing/crypto/__tests__/webhook-confirmations.test.d.ts +1 -0
- package/dist/billing/crypto/__tests__/webhook-confirmations.test.js +304 -0
- package/dist/billing/crypto/btc/__tests__/settler.test.js +1 -0
- package/dist/billing/crypto/btc/__tests__/watcher.test.d.ts +1 -0
- package/dist/billing/crypto/btc/__tests__/watcher.test.js +170 -0
- package/dist/billing/crypto/btc/types.d.ts +3 -1
- package/dist/billing/crypto/btc/watcher.d.ts +6 -1
- package/dist/billing/crypto/btc/watcher.js +24 -8
- package/dist/billing/crypto/charge-store.d.ts +27 -2
- package/dist/billing/crypto/charge-store.js +67 -1
- package/dist/billing/crypto/charge-store.test.js +180 -1
- package/dist/billing/crypto/client.d.ts +2 -0
- package/dist/billing/crypto/cursor-store.d.ts +10 -3
- package/dist/billing/crypto/cursor-store.js +21 -1
- package/dist/billing/crypto/evm/__tests__/eth-checkout.test.js +3 -3
- package/dist/billing/crypto/evm/__tests__/eth-settler.test.js +2 -0
- package/dist/billing/crypto/evm/__tests__/eth-watcher.test.js +33 -6
- package/dist/billing/crypto/evm/__tests__/settler.test.js +2 -0
- package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.d.ts +1 -0
- package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.js +144 -0
- package/dist/billing/crypto/evm/__tests__/watcher.test.js +6 -2
- package/dist/billing/crypto/evm/eth-checkout.d.ts +2 -2
- package/dist/billing/crypto/evm/eth-checkout.js +3 -3
- package/dist/billing/crypto/evm/eth-watcher.d.ts +11 -8
- package/dist/billing/crypto/evm/eth-watcher.js +29 -15
- package/dist/billing/crypto/evm/types.d.ts +5 -1
- package/dist/billing/crypto/evm/watcher.d.ts +9 -1
- package/dist/billing/crypto/evm/watcher.js +36 -13
- package/dist/billing/crypto/index.d.ts +3 -3
- package/dist/billing/crypto/index.js +1 -1
- package/dist/billing/crypto/key-server-entry.js +7 -2
- package/dist/billing/crypto/key-server-webhook.d.ts +17 -4
- package/dist/billing/crypto/key-server-webhook.js +76 -15
- package/dist/billing/crypto/key-server.js +18 -7
- package/dist/billing/crypto/oracle/__tests__/chainlink.test.js +4 -4
- package/dist/billing/crypto/oracle/__tests__/coingecko.test.d.ts +1 -0
- package/dist/billing/crypto/oracle/__tests__/coingecko.test.js +65 -0
- package/dist/billing/crypto/oracle/__tests__/composite.test.d.ts +1 -0
- package/dist/billing/crypto/oracle/__tests__/composite.test.js +48 -0
- package/dist/billing/crypto/oracle/__tests__/convert.test.js +27 -17
- package/dist/billing/crypto/oracle/__tests__/fixed.test.js +5 -5
- package/dist/billing/crypto/oracle/chainlink.d.ts +2 -2
- package/dist/billing/crypto/oracle/chainlink.js +11 -10
- package/dist/billing/crypto/oracle/coingecko.d.ts +22 -0
- package/dist/billing/crypto/oracle/coingecko.js +67 -0
- package/dist/billing/crypto/oracle/composite.d.ts +14 -0
- package/dist/billing/crypto/oracle/composite.js +34 -0
- package/dist/billing/crypto/oracle/convert.d.ts +17 -7
- package/dist/billing/crypto/oracle/convert.js +26 -13
- package/dist/billing/crypto/oracle/fixed.d.ts +2 -2
- package/dist/billing/crypto/oracle/fixed.js +9 -7
- package/dist/billing/crypto/oracle/index.d.ts +4 -0
- package/dist/billing/crypto/oracle/index.js +3 -0
- package/dist/billing/crypto/oracle/types.d.ts +12 -3
- package/dist/billing/crypto/oracle/types.js +7 -1
- package/dist/billing/crypto/types.d.ts +16 -0
- package/dist/billing/crypto/unified-checkout.d.ts +10 -19
- package/dist/billing/crypto/unified-checkout.js +17 -131
- package/dist/billing/crypto/watcher-service.d.ts +22 -2
- package/dist/billing/crypto/watcher-service.js +71 -30
- package/dist/db/schema/crypto.d.ts +68 -0
- package/dist/db/schema/crypto.js +8 -0
- package/dist/monetization/crypto/__tests__/webhook.test.js +2 -1
- package/drizzle/migrations/0016_charge_progress_columns.sql +4 -0
- package/drizzle/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/billing/crypto/__tests__/key-server.test.ts +1 -1
- package/src/billing/crypto/__tests__/unified-checkout.test.ts +83 -0
- package/src/billing/crypto/__tests__/watcher-service.test.ts +242 -0
- package/src/billing/crypto/__tests__/webhook-confirmations.test.ts +367 -0
- package/src/billing/crypto/btc/__tests__/settler.test.ts +1 -0
- package/src/billing/crypto/btc/__tests__/watcher.test.ts +201 -0
- package/src/billing/crypto/btc/types.ts +3 -1
- package/src/billing/crypto/btc/watcher.ts +26 -8
- package/src/billing/crypto/charge-store.test.ts +204 -1
- package/src/billing/crypto/charge-store.ts +86 -2
- package/src/billing/crypto/client.ts +2 -0
- package/src/billing/crypto/cursor-store.ts +31 -3
- package/src/billing/crypto/evm/__tests__/eth-checkout.test.ts +3 -3
- package/src/billing/crypto/evm/__tests__/eth-settler.test.ts +2 -0
- package/src/billing/crypto/evm/__tests__/eth-watcher.test.ts +33 -6
- package/src/billing/crypto/evm/__tests__/settler.test.ts +2 -0
- package/src/billing/crypto/evm/__tests__/watcher-confirmations.test.ts +176 -0
- package/src/billing/crypto/evm/__tests__/watcher.test.ts +6 -2
- package/src/billing/crypto/evm/eth-checkout.ts +5 -5
- package/src/billing/crypto/evm/eth-watcher.ts +36 -16
- package/src/billing/crypto/evm/types.ts +5 -1
- package/src/billing/crypto/evm/watcher.ts +39 -13
- package/src/billing/crypto/index.ts +12 -3
- package/src/billing/crypto/key-server-entry.ts +7 -2
- package/src/billing/crypto/key-server-webhook.ts +92 -21
- package/src/billing/crypto/key-server.ts +17 -7
- package/src/billing/crypto/oracle/__tests__/chainlink.test.ts +4 -4
- package/src/billing/crypto/oracle/__tests__/coingecko.test.ts +75 -0
- package/src/billing/crypto/oracle/__tests__/composite.test.ts +61 -0
- package/src/billing/crypto/oracle/__tests__/convert.test.ts +29 -17
- package/src/billing/crypto/oracle/__tests__/fixed.test.ts +5 -5
- package/src/billing/crypto/oracle/chainlink.ts +11 -10
- package/src/billing/crypto/oracle/coingecko.ts +92 -0
- package/src/billing/crypto/oracle/composite.ts +35 -0
- package/src/billing/crypto/oracle/convert.ts +28 -13
- package/src/billing/crypto/oracle/fixed.ts +9 -7
- package/src/billing/crypto/oracle/index.ts +4 -0
- package/src/billing/crypto/oracle/types.ts +16 -3
- package/src/billing/crypto/types.ts +18 -0
- package/src/billing/crypto/unified-checkout.ts +22 -181
- package/src/billing/crypto/watcher-service.ts +85 -32
- package/src/db/schema/crypto.ts +8 -0
- 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
|
-
*
|
|
2
|
+
* Price units: **microdollars** (1 microdollar = $0.000001 = 10^-6 USD).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
8
|
-
* $50 in
|
|
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,
|
|
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
|
|
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,
|
|
30
|
+
export declare function nativeToCents(rawAmount: bigint, priceMicros: number, decimals: number): number;
|
|
@@ -1,38 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Price units: **microdollars** (1 microdollar = $0.000001 = 10^-6 USD).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
8
|
-
* $50 in
|
|
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,
|
|
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(
|
|
17
|
-
throw new Error(`
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
35
|
-
throw new Error(`
|
|
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(
|
|
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:
|
|
10
|
-
BTC:
|
|
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
|
|
16
|
-
if (
|
|
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 {
|
|
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
|
-
/**
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 (
|
|
22
|
-
|
|
14
|
+
/** For volatile assets: price at checkout time (microdollars per unit, 10^-6 USD). */
|
|
15
|
+
priceMicros?: number;
|
|
23
16
|
}
|
|
24
17
|
/**
|
|
25
|
-
* Unified checkout —
|
|
18
|
+
* Unified checkout — delegates to CryptoServiceClient.createCharge().
|
|
26
19
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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,
|
|
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 —
|
|
3
|
+
* Unified checkout — delegates to CryptoServiceClient.createCharge().
|
|
8
4
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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,
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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: `${
|
|
53
|
-
amountUsd,
|
|
54
|
-
token:
|
|
55
|
-
chain:
|
|
56
|
-
referenceId:
|
|
57
|
-
|
|
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 →
|
|
8
|
-
* 4. Every payment
|
|
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>;
|