@wopr-network/platform-core 1.66.1 → 1.67.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/btc/checkout.d.ts +4 -0
- package/dist/billing/crypto/btc/checkout.js +1 -2
- package/dist/billing/crypto/btc/index.d.ts +0 -4
- package/dist/billing/crypto/btc/index.js +0 -2
- package/dist/billing/crypto/evm/__tests__/checkout.test.js +8 -11
- package/dist/billing/crypto/evm/__tests__/eth-checkout.test.js +15 -1
- package/dist/billing/crypto/evm/checkout.d.ts +2 -0
- package/dist/billing/crypto/evm/checkout.js +1 -2
- package/dist/billing/crypto/evm/eth-checkout.d.ts +13 -2
- package/dist/billing/crypto/evm/eth-checkout.js +2 -4
- package/dist/billing/crypto/evm/eth-settler.d.ts +1 -1
- package/dist/billing/crypto/evm/index.d.ts +2 -8
- package/dist/billing/crypto/evm/index.js +0 -3
- package/dist/billing/crypto/evm/types.d.ts +16 -0
- package/dist/billing/crypto/index.d.ts +1 -6
- package/dist/billing/crypto/index.js +2 -3
- package/dist/billing/crypto/types.d.ts +0 -43
- package/dist/billing/crypto/types.js +1 -24
- package/package.json +1 -5
- package/src/billing/crypto/btc/checkout.ts +3 -2
- package/src/billing/crypto/btc/index.ts +0 -4
- package/src/billing/crypto/evm/__tests__/checkout.test.ts +10 -12
- package/src/billing/crypto/evm/__tests__/eth-checkout.test.ts +17 -1
- package/src/billing/crypto/evm/__tests__/eth-settler.test.ts +1 -1
- package/src/billing/crypto/evm/checkout.ts +3 -2
- package/src/billing/crypto/evm/eth-checkout.ts +15 -6
- package/src/billing/crypto/evm/eth-settler.ts +1 -1
- package/src/billing/crypto/evm/index.ts +8 -7
- package/src/billing/crypto/evm/types.ts +17 -0
- package/src/billing/crypto/index.ts +14 -12
- package/src/billing/crypto/types.ts +0 -63
- package/dist/billing/crypto/__tests__/address-gen.test.d.ts +0 -1
- package/dist/billing/crypto/__tests__/address-gen.test.js +0 -219
- package/dist/billing/crypto/__tests__/key-server.test.d.ts +0 -1
- package/dist/billing/crypto/__tests__/key-server.test.js +0 -363
- package/dist/billing/crypto/__tests__/watcher-service.test.d.ts +0 -1
- package/dist/billing/crypto/__tests__/watcher-service.test.js +0 -174
- package/dist/billing/crypto/address-gen.d.ts +0 -24
- package/dist/billing/crypto/address-gen.js +0 -176
- package/dist/billing/crypto/btc/__tests__/watcher.test.d.ts +0 -1
- package/dist/billing/crypto/btc/__tests__/watcher.test.js +0 -170
- package/dist/billing/crypto/btc/watcher.d.ts +0 -44
- package/dist/billing/crypto/btc/watcher.js +0 -118
- package/dist/billing/crypto/evm/__tests__/eth-watcher.test.d.ts +0 -1
- package/dist/billing/crypto/evm/__tests__/eth-watcher.test.js +0 -167
- package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.d.ts +0 -1
- package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.js +0 -159
- package/dist/billing/crypto/evm/__tests__/watcher.test.d.ts +0 -1
- package/dist/billing/crypto/evm/__tests__/watcher.test.js +0 -145
- package/dist/billing/crypto/evm/eth-watcher.d.ts +0 -66
- package/dist/billing/crypto/evm/eth-watcher.js +0 -121
- package/dist/billing/crypto/evm/watcher.d.ts +0 -51
- package/dist/billing/crypto/evm/watcher.js +0 -156
- package/dist/billing/crypto/key-server-entry.d.ts +0 -1
- package/dist/billing/crypto/key-server-entry.js +0 -122
- package/dist/billing/crypto/key-server.d.ts +0 -32
- package/dist/billing/crypto/key-server.js +0 -348
- package/dist/billing/crypto/oracle/__tests__/chainlink.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/chainlink.test.js +0 -83
- package/dist/billing/crypto/oracle/__tests__/coingecko.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/coingecko.test.js +0 -65
- package/dist/billing/crypto/oracle/__tests__/composite.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/composite.test.js +0 -48
- package/dist/billing/crypto/oracle/__tests__/convert.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/convert.test.js +0 -61
- package/dist/billing/crypto/oracle/__tests__/fixed.test.d.ts +0 -1
- package/dist/billing/crypto/oracle/__tests__/fixed.test.js +0 -20
- package/dist/billing/crypto/oracle/chainlink.d.ts +0 -26
- package/dist/billing/crypto/oracle/chainlink.js +0 -62
- package/dist/billing/crypto/oracle/coingecko.d.ts +0 -22
- package/dist/billing/crypto/oracle/coingecko.js +0 -71
- package/dist/billing/crypto/oracle/composite.d.ts +0 -14
- package/dist/billing/crypto/oracle/composite.js +0 -34
- package/dist/billing/crypto/oracle/convert.d.ts +0 -30
- package/dist/billing/crypto/oracle/convert.js +0 -51
- package/dist/billing/crypto/oracle/fixed.d.ts +0 -10
- package/dist/billing/crypto/oracle/fixed.js +0 -22
- package/dist/billing/crypto/oracle/index.d.ts +0 -9
- package/dist/billing/crypto/oracle/index.js +0 -6
- package/dist/billing/crypto/oracle/types.d.ts +0 -22
- package/dist/billing/crypto/oracle/types.js +0 -7
- package/dist/billing/crypto/plugin/__tests__/integration.test.d.ts +0 -1
- package/dist/billing/crypto/plugin/__tests__/integration.test.js +0 -58
- package/dist/billing/crypto/plugin/__tests__/interfaces.test.d.ts +0 -1
- package/dist/billing/crypto/plugin/__tests__/interfaces.test.js +0 -46
- package/dist/billing/crypto/plugin/__tests__/registry.test.d.ts +0 -1
- package/dist/billing/crypto/plugin/__tests__/registry.test.js +0 -49
- package/dist/billing/crypto/plugin/index.d.ts +0 -2
- package/dist/billing/crypto/plugin/index.js +0 -1
- package/dist/billing/crypto/plugin/interfaces.d.ts +0 -97
- package/dist/billing/crypto/plugin/interfaces.js +0 -2
- package/dist/billing/crypto/plugin/registry.d.ts +0 -8
- package/dist/billing/crypto/plugin/registry.js +0 -21
- package/dist/billing/crypto/plugin-watcher-service.d.ts +0 -32
- package/dist/billing/crypto/plugin-watcher-service.js +0 -113
- package/dist/billing/crypto/tron/__tests__/address-convert.test.d.ts +0 -1
- package/dist/billing/crypto/tron/__tests__/address-convert.test.js +0 -55
- package/dist/billing/crypto/tron/address-convert.d.ts +0 -14
- package/dist/billing/crypto/tron/address-convert.js +0 -93
- package/dist/billing/crypto/watcher-service.d.ts +0 -55
- package/dist/billing/crypto/watcher-service.js +0 -438
- package/src/billing/crypto/__tests__/address-gen.test.ts +0 -264
- package/src/billing/crypto/__tests__/key-server.test.ts +0 -395
- package/src/billing/crypto/__tests__/watcher-service.test.ts +0 -242
- package/src/billing/crypto/address-gen.ts +0 -185
- package/src/billing/crypto/btc/__tests__/watcher.test.ts +0 -201
- package/src/billing/crypto/btc/watcher.ts +0 -161
- package/src/billing/crypto/evm/__tests__/eth-watcher.test.ts +0 -190
- package/src/billing/crypto/evm/__tests__/watcher-confirmations.test.ts +0 -191
- package/src/billing/crypto/evm/__tests__/watcher.test.ts +0 -167
- package/src/billing/crypto/evm/eth-watcher.ts +0 -182
- package/src/billing/crypto/evm/watcher.ts +0 -204
- package/src/billing/crypto/key-server-entry.ts +0 -144
- package/src/billing/crypto/key-server.ts +0 -444
- package/src/billing/crypto/oracle/__tests__/chainlink.test.ts +0 -107
- package/src/billing/crypto/oracle/__tests__/coingecko.test.ts +0 -75
- package/src/billing/crypto/oracle/__tests__/composite.test.ts +0 -61
- package/src/billing/crypto/oracle/__tests__/convert.test.ts +0 -74
- package/src/billing/crypto/oracle/__tests__/fixed.test.ts +0 -23
- package/src/billing/crypto/oracle/chainlink.ts +0 -86
- package/src/billing/crypto/oracle/coingecko.ts +0 -96
- package/src/billing/crypto/oracle/composite.ts +0 -35
- package/src/billing/crypto/oracle/convert.ts +0 -53
- package/src/billing/crypto/oracle/fixed.ts +0 -25
- package/src/billing/crypto/oracle/index.ts +0 -9
- package/src/billing/crypto/oracle/types.ts +0 -28
- package/src/billing/crypto/plugin/__tests__/integration.test.ts +0 -64
- package/src/billing/crypto/plugin/__tests__/interfaces.test.ts +0 -51
- package/src/billing/crypto/plugin/__tests__/registry.test.ts +0 -58
- package/src/billing/crypto/plugin/index.ts +0 -17
- package/src/billing/crypto/plugin/interfaces.ts +0 -106
- package/src/billing/crypto/plugin/registry.ts +0 -26
- package/src/billing/crypto/plugin-watcher-service.ts +0 -148
- package/src/billing/crypto/tron/__tests__/address-convert.test.ts +0 -67
- package/src/billing/crypto/tron/address-convert.ts +0 -89
- package/src/billing/crypto/watcher-service.ts +0 -549
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { centsToNative, nativeToCents } from "../convert.js";
|
|
3
|
-
|
|
4
|
-
describe("centsToNative", () => {
|
|
5
|
-
it("converts $50 to ETH wei at $3,500", () => {
|
|
6
|
-
// 5000 cents × 10000 × 10^18 / 3_500_000_000 micros = 14285714285714285n wei
|
|
7
|
-
const wei = centsToNative(5000, 3_500_000_000, 18);
|
|
8
|
-
expect(wei).toBe(14_285_714_285_714_285n);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("converts $50 to BTC sats at $65,000", () => {
|
|
12
|
-
// 5000 cents × 10000 × 10^8 / 65_000_000_000 micros = 76923n sats
|
|
13
|
-
const sats = centsToNative(5000, 65_000_000_000, 8);
|
|
14
|
-
expect(sats).toBe(76_923n);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("converts $50 to DOGE at $0.094147", () => {
|
|
18
|
-
// 5000 cents × 10000 × 10^8 / 94_147 micros = 53_107_898_982n base units (531.08 DOGE)
|
|
19
|
-
const dogeUnits = centsToNative(5000, 94_147, 8);
|
|
20
|
-
expect(Number(dogeUnits) / 1e8).toBeCloseTo(531.08, 0);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("converts $50 to LTC at $55.79", () => {
|
|
24
|
-
// 5000 cents × 10000 × 10^8 / 55_790_000 micros = 89_622_512n base units (0.896 LTC)
|
|
25
|
-
const ltcUnits = centsToNative(5000, 55_790_000, 8);
|
|
26
|
-
expect(Number(ltcUnits) / 1e8).toBeCloseTo(0.896, 2);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("converts $100 to ETH wei at $2,000", () => {
|
|
30
|
-
// 10000 cents × 10000 × 10^18 / 2_000_000_000 micros = 50_000_000_000_000_000n (0.05 ETH)
|
|
31
|
-
const wei = centsToNative(10_000, 2_000_000_000, 18);
|
|
32
|
-
expect(wei).toBe(50_000_000_000_000_000n);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("rejects non-integer amountCents", () => {
|
|
36
|
-
expect(() => centsToNative(50.5, 3_500_000_000, 18)).toThrow("positive integer");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("rejects zero amountCents", () => {
|
|
40
|
-
expect(() => centsToNative(0, 3_500_000_000, 18)).toThrow("positive integer");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("rejects zero priceMicros", () => {
|
|
44
|
-
expect(() => centsToNative(5000, 0, 18)).toThrow("positive integer");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("rejects negative decimals", () => {
|
|
48
|
-
expect(() => centsToNative(5000, 3_500_000_000, -1)).toThrow("non-negative integer");
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("nativeToCents", () => {
|
|
53
|
-
it("converts ETH wei back to cents at $3,500", () => {
|
|
54
|
-
// 14285714285714285n × 3_500_000_000 / (10000 × 10^18) = 4999 cents (truncated)
|
|
55
|
-
const cents = nativeToCents(14_285_714_285_714_285n, 3_500_000_000, 18);
|
|
56
|
-
expect(cents).toBe(4999);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("converts BTC sats back to cents at $65,000", () => {
|
|
60
|
-
// 76923n × 65_000_000_000 / (10000 × 10^8) = 4999 cents
|
|
61
|
-
const cents = nativeToCents(76_923n, 65_000_000_000, 8);
|
|
62
|
-
expect(cents).toBe(4999);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("exact round-trip for clean division", () => {
|
|
66
|
-
// 0.05 ETH at $2,000 = $100
|
|
67
|
-
const cents = nativeToCents(50_000_000_000_000_000n, 2_000_000_000, 18);
|
|
68
|
-
expect(cents).toBe(10_000); // $100.00
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("rejects negative rawAmount", () => {
|
|
72
|
-
expect(() => nativeToCents(-1n, 3_500_000_000, 18)).toThrow("non-negative");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { FixedPriceOracle } from "../fixed.js";
|
|
3
|
-
|
|
4
|
-
describe("FixedPriceOracle", () => {
|
|
5
|
-
it("returns default ETH price", async () => {
|
|
6
|
-
const oracle = new FixedPriceOracle();
|
|
7
|
-
const result = await oracle.getPrice("ETH");
|
|
8
|
-
expect(result.priceMicros).toBe(3_500_000_000); // $3,500
|
|
9
|
-
expect(result.updatedAt).toBeInstanceOf(Date);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("returns default BTC price", async () => {
|
|
13
|
-
const oracle = new FixedPriceOracle();
|
|
14
|
-
const result = await oracle.getPrice("BTC");
|
|
15
|
-
expect(result.priceMicros).toBe(65_000_000_000); // $65,000
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("accepts custom prices", async () => {
|
|
19
|
-
const oracle = new FixedPriceOracle({ ETH: 2_000_000_000, BTC: 50_000_000_000 });
|
|
20
|
-
expect((await oracle.getPrice("ETH")).priceMicros).toBe(2_000_000_000);
|
|
21
|
-
expect((await oracle.getPrice("BTC")).priceMicros).toBe(50_000_000_000);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import type { IPriceOracle, PriceAsset, PriceResult } from "./types.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Chainlink price feed addresses on Base mainnet.
|
|
5
|
-
* These are ERC-1967 proxy contracts — addresses are stable.
|
|
6
|
-
*/
|
|
7
|
-
const FEED_ADDRESSES: Record<PriceAsset, `0x${string}`> = {
|
|
8
|
-
ETH: "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70",
|
|
9
|
-
BTC: "0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/** Function selector for latestRoundData(). */
|
|
13
|
-
const LATEST_ROUND_DATA = "0xfeaf968c";
|
|
14
|
-
|
|
15
|
-
/** Default max staleness: 1 hour. */
|
|
16
|
-
const DEFAULT_MAX_STALENESS_MS = 60 * 60 * 1000;
|
|
17
|
-
|
|
18
|
-
type RpcCall = (method: string, params: unknown[]) => Promise<unknown>;
|
|
19
|
-
|
|
20
|
-
export interface ChainlinkOracleOpts {
|
|
21
|
-
rpcCall: RpcCall;
|
|
22
|
-
/** Override feed addresses (e.g. for testnet or Anvil forks). */
|
|
23
|
-
feedAddresses?: Partial<Record<PriceAsset, `0x${string}`>>;
|
|
24
|
-
/** Maximum age of price data before rejecting (ms). Default: 1 hour. */
|
|
25
|
-
maxStalenessMs?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* On-chain Chainlink price oracle.
|
|
30
|
-
*
|
|
31
|
-
* Reads latestRoundData() from Chainlink aggregator contracts via eth_call.
|
|
32
|
-
* No API key, no rate limits — just an RPC call to our own node.
|
|
33
|
-
*
|
|
34
|
-
* Chainlink USD feeds use 8 decimals. We convert to integer USD cents:
|
|
35
|
-
* priceMicros = answer / 100 (i.e. answer / 10^8 * 10^6)
|
|
36
|
-
*/
|
|
37
|
-
export class ChainlinkOracle implements IPriceOracle {
|
|
38
|
-
private readonly rpc: RpcCall;
|
|
39
|
-
private readonly feeds: Map<string, `0x${string}`>;
|
|
40
|
-
private readonly maxStalenessMs: number;
|
|
41
|
-
|
|
42
|
-
constructor(opts: ChainlinkOracleOpts) {
|
|
43
|
-
this.rpc = opts.rpcCall;
|
|
44
|
-
this.feeds = new Map(Object.entries({ ...FEED_ADDRESSES, ...opts.feedAddresses })) as Map<string, `0x${string}`>;
|
|
45
|
-
this.maxStalenessMs = opts.maxStalenessMs ?? DEFAULT_MAX_STALENESS_MS;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async getPrice(asset: PriceAsset, feedAddress?: `0x${string}`): Promise<PriceResult> {
|
|
49
|
-
const resolvedFeed = feedAddress ?? this.feeds.get(asset);
|
|
50
|
-
if (!resolvedFeed) throw new Error(`No price feed for asset: ${asset}`);
|
|
51
|
-
|
|
52
|
-
const result = (await this.rpc("eth_call", [{ to: resolvedFeed, data: LATEST_ROUND_DATA }, "latest"])) as string;
|
|
53
|
-
|
|
54
|
-
// ABI decode latestRoundData() return:
|
|
55
|
-
// [0] roundId (uint80) — skip
|
|
56
|
-
// [1] answer (int256) — price × 10^8
|
|
57
|
-
// [2] startedAt (uint256) — skip
|
|
58
|
-
// [3] updatedAt (uint256) — unix seconds
|
|
59
|
-
// [4] answeredInRound (uint80) — skip
|
|
60
|
-
const hex = result.slice(2);
|
|
61
|
-
if (hex.length < 320) {
|
|
62
|
-
throw new Error(`Malformed Chainlink response for ${asset}: expected 320 hex chars, got ${hex.length}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const answer = BigInt(`0x${hex.slice(64, 128)}`);
|
|
66
|
-
const updatedAtSec = Number(BigInt(`0x${hex.slice(192, 256)}`));
|
|
67
|
-
const updatedAt = new Date(updatedAtSec * 1000);
|
|
68
|
-
|
|
69
|
-
// Staleness guard.
|
|
70
|
-
const ageMs = Date.now() - updatedAt.getTime();
|
|
71
|
-
if (ageMs > this.maxStalenessMs) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
`Price feed for ${asset} is stale (${Math.round(ageMs / 1000)}s old, max ${Math.round(this.maxStalenessMs / 1000)}s)`,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Chainlink USD feeds: 8 decimals. answer / 100 = microdollars (10^-6 USD).
|
|
78
|
-
// e.g. BTC at $70,315 → answer = 7_031_500_000_000 → 70_315_000_000 microdollars
|
|
79
|
-
const priceMicros = Number(answer / 100n);
|
|
80
|
-
if (priceMicros <= 0) {
|
|
81
|
-
throw new Error(`Invalid price for ${asset}: ${priceMicros} microdollars`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { priceMicros, updatedAt };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import type { IPriceOracle, PriceAsset, PriceResult } from "./types.js";
|
|
2
|
-
import { AssetNotSupportedError } from "./types.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Token symbol → CoinGecko API ID mapping.
|
|
6
|
-
* CoinGecko uses lowercase slugs, not ticker symbols.
|
|
7
|
-
*/
|
|
8
|
-
const COINGECKO_IDS: Record<string, string> = {
|
|
9
|
-
BTC: "bitcoin",
|
|
10
|
-
ETH: "ethereum",
|
|
11
|
-
DOGE: "dogecoin",
|
|
12
|
-
LTC: "litecoin",
|
|
13
|
-
SOL: "solana",
|
|
14
|
-
LINK: "chainlink",
|
|
15
|
-
UNI: "uniswap",
|
|
16
|
-
AERO: "aerodrome-finance",
|
|
17
|
-
TRX: "tron",
|
|
18
|
-
BNB: "binancecoin",
|
|
19
|
-
POL: "matic-network",
|
|
20
|
-
AVAX: "avalanche-2",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/** Default cache TTL: 60 seconds. CoinGecko free tier allows 10-30 req/min. */
|
|
24
|
-
const DEFAULT_CACHE_TTL_MS = 60_000;
|
|
25
|
-
|
|
26
|
-
interface CachedPrice {
|
|
27
|
-
priceMicros: number;
|
|
28
|
-
updatedAt: Date;
|
|
29
|
-
fetchedAt: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface CoinGeckoOracleOpts {
|
|
33
|
-
/** Override token→id mapping. */
|
|
34
|
-
tokenIds?: Record<string, string>;
|
|
35
|
-
/** Cache TTL in ms. Default: 60s. */
|
|
36
|
-
cacheTtlMs?: number;
|
|
37
|
-
/** Custom fetch function (for testing). */
|
|
38
|
-
fetchFn?: typeof fetch;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* CoinGecko price oracle — free API, no key required.
|
|
43
|
-
* Used for assets without Chainlink on-chain feeds (DOGE, LTC).
|
|
44
|
-
* Caches prices to stay within rate limits.
|
|
45
|
-
*/
|
|
46
|
-
export class CoinGeckoOracle implements IPriceOracle {
|
|
47
|
-
private readonly ids: Record<string, string>;
|
|
48
|
-
private readonly cacheTtlMs: number;
|
|
49
|
-
private readonly fetchFn: typeof fetch;
|
|
50
|
-
private readonly cache = new Map<string, CachedPrice>();
|
|
51
|
-
|
|
52
|
-
constructor(opts: CoinGeckoOracleOpts = {}) {
|
|
53
|
-
this.ids = { ...COINGECKO_IDS, ...opts.tokenIds };
|
|
54
|
-
this.cacheTtlMs = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
55
|
-
this.fetchFn = opts.fetchFn ?? fetch;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async getPrice(asset: PriceAsset, _feedAddress?: `0x${string}`): Promise<PriceResult> {
|
|
59
|
-
const cached = this.cache.get(asset);
|
|
60
|
-
if (cached && Date.now() - cached.fetchedAt < this.cacheTtlMs) {
|
|
61
|
-
return { priceMicros: cached.priceMicros, updatedAt: cached.updatedAt };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const coinId = this.ids[asset];
|
|
65
|
-
if (!coinId) throw new AssetNotSupportedError(asset);
|
|
66
|
-
|
|
67
|
-
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd`;
|
|
68
|
-
try {
|
|
69
|
-
const res = await this.fetchFn(url);
|
|
70
|
-
if (!res.ok) {
|
|
71
|
-
throw new Error(`CoinGecko API error for ${asset}: ${res.status} ${res.statusText}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const data = (await res.json()) as Record<string, { usd?: number }>;
|
|
75
|
-
const usdPrice = data[coinId]?.usd;
|
|
76
|
-
if (usdPrice === undefined || usdPrice <= 0) {
|
|
77
|
-
throw new Error(`Invalid CoinGecko price for ${asset}: ${usdPrice}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const priceMicros = Math.round(usdPrice * 1_000_000);
|
|
81
|
-
const updatedAt = new Date();
|
|
82
|
-
|
|
83
|
-
this.cache.set(asset, { priceMicros, updatedAt, fetchedAt: Date.now() });
|
|
84
|
-
|
|
85
|
-
return { priceMicros, updatedAt };
|
|
86
|
-
} catch (err) {
|
|
87
|
-
// Serve stale cache on transient failure (rate limit, network error).
|
|
88
|
-
// A slightly old price is better than rejecting the charge entirely.
|
|
89
|
-
const stale = this.cache.get(asset);
|
|
90
|
-
if (stale) {
|
|
91
|
-
return { priceMicros: stale.priceMicros, updatedAt: stale.updatedAt };
|
|
92
|
-
}
|
|
93
|
-
throw err;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { IPriceOracle, PriceAsset, PriceResult } from "./types.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Composite oracle — tries primary (Chainlink on-chain), falls back to secondary (CoinGecko).
|
|
5
|
-
*
|
|
6
|
-
* When a feedAddress is provided (from payment_methods.oracle_address), the primary
|
|
7
|
-
* oracle is used with that address. When no feed exists or the primary fails,
|
|
8
|
-
* the fallback oracle is consulted.
|
|
9
|
-
*/
|
|
10
|
-
export class CompositeOracle implements IPriceOracle {
|
|
11
|
-
constructor(
|
|
12
|
-
private readonly primary: IPriceOracle,
|
|
13
|
-
private readonly fallback: IPriceOracle,
|
|
14
|
-
) {}
|
|
15
|
-
|
|
16
|
-
async getPrice(asset: PriceAsset, feedAddress?: `0x${string}`): Promise<PriceResult> {
|
|
17
|
-
// If a specific feed address is provided, try the primary (Chainlink) first
|
|
18
|
-
if (feedAddress) {
|
|
19
|
-
try {
|
|
20
|
-
return await this.primary.getPrice(asset, feedAddress);
|
|
21
|
-
} catch {
|
|
22
|
-
// Primary failed (stale, network error) — fall through to fallback
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Try primary without explicit feed (uses built-in feed map for BTC/ETH)
|
|
27
|
-
try {
|
|
28
|
-
return await this.primary.getPrice(asset);
|
|
29
|
-
} catch {
|
|
30
|
-
// No feed configured or call failed — use fallback
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return this.fallback.getPrice(asset);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Price units: **microdollars** (1 microdollar = $0.000001 = 10^-6 USD).
|
|
3
|
-
*
|
|
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
|
-
/** Microdollars per cent. Multiply amountCents by this to get microdollars. */
|
|
13
|
-
const MICROS_PER_CENT = 10_000n;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Convert USD cents to native token amount using a price in microdollars.
|
|
17
|
-
*
|
|
18
|
-
* Formula: rawAmount = (amountCents × 10_000) × 10^decimals / priceMicros
|
|
19
|
-
*
|
|
20
|
-
* Examples:
|
|
21
|
-
* $50 in BTC at $70,315: centsToNative(5000, 70_315_000_000, 8) = 71,119 sats
|
|
22
|
-
* $50 in DOGE at $0.094: centsToNative(5000, 94_147, 8) = 53,107,898,982 base units (531.08 DOGE)
|
|
23
|
-
*
|
|
24
|
-
* Integer math only. No floating point.
|
|
25
|
-
*/
|
|
26
|
-
export function centsToNative(amountCents: number, priceMicros: number, decimals: number): bigint {
|
|
27
|
-
if (!Number.isInteger(amountCents) || amountCents <= 0) {
|
|
28
|
-
throw new Error(`amountCents must be a positive integer, got ${amountCents}`);
|
|
29
|
-
}
|
|
30
|
-
if (!Number.isInteger(priceMicros) || priceMicros <= 0) {
|
|
31
|
-
throw new Error(`priceMicros must be a positive integer, got ${priceMicros}`);
|
|
32
|
-
}
|
|
33
|
-
if (!Number.isInteger(decimals) || decimals < 0) {
|
|
34
|
-
throw new Error(`decimals must be a non-negative integer, got ${decimals}`);
|
|
35
|
-
}
|
|
36
|
-
// Convert amountCents to microdollars to match priceMicros units
|
|
37
|
-
return (BigInt(amountCents) * MICROS_PER_CENT * 10n ** BigInt(decimals)) / BigInt(priceMicros);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Convert native token amount back to USD cents using a price in microdollars.
|
|
42
|
-
*
|
|
43
|
-
* Inverse of centsToNative. Truncates fractional cents.
|
|
44
|
-
*
|
|
45
|
-
* Integer math only.
|
|
46
|
-
*/
|
|
47
|
-
export function nativeToCents(rawAmount: bigint, priceMicros: number, decimals: number): number {
|
|
48
|
-
if (rawAmount < 0n) throw new Error("rawAmount must be non-negative");
|
|
49
|
-
if (!Number.isInteger(priceMicros) || priceMicros <= 0) {
|
|
50
|
-
throw new Error(`priceMicros must be a positive integer, got ${priceMicros}`);
|
|
51
|
-
}
|
|
52
|
-
return Number((rawAmount * BigInt(priceMicros)) / (MICROS_PER_CENT * 10n ** BigInt(decimals)));
|
|
53
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { IPriceOracle, PriceAsset, PriceResult } from "./types.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Fixed-price oracle for testing and local dev (Anvil, regtest).
|
|
5
|
-
* Returns hardcoded prices in microdollars — no RPC calls.
|
|
6
|
-
*/
|
|
7
|
-
export class FixedPriceOracle implements IPriceOracle {
|
|
8
|
-
private readonly prices: Record<string, number>;
|
|
9
|
-
|
|
10
|
-
constructor(prices: Partial<Record<PriceAsset, number>> = {}) {
|
|
11
|
-
this.prices = {
|
|
12
|
-
ETH: 3_500_000_000, // $3,500 in microdollars
|
|
13
|
-
BTC: 65_000_000_000, // $65,000 in microdollars
|
|
14
|
-
DOGE: 94_000, // $0.094 in microdollars
|
|
15
|
-
LTC: 55_000_000, // $55 in microdollars
|
|
16
|
-
...prices,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async getPrice(asset: PriceAsset, _feedAddress?: `0x${string}`): Promise<PriceResult> {
|
|
21
|
-
const priceMicros = this.prices[asset];
|
|
22
|
-
if (priceMicros === undefined) throw new Error(`No fixed price for ${asset}`);
|
|
23
|
-
return { priceMicros, updatedAt: new Date() };
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export type { ChainlinkOracleOpts } from "./chainlink.js";
|
|
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";
|
|
6
|
-
export { centsToNative, nativeToCents } from "./convert.js";
|
|
7
|
-
export { FixedPriceOracle } from "./fixed.js";
|
|
8
|
-
export type { IPriceOracle, PriceAsset, PriceResult } from "./types.js";
|
|
9
|
-
export { AssetNotSupportedError } from "./types.js";
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/** Assets with Chainlink price feeds. */
|
|
2
|
-
export type PriceAsset = string;
|
|
3
|
-
|
|
4
|
-
/** Thrown when no oracle supports a given asset (not a transient failure). */
|
|
5
|
-
export class AssetNotSupportedError extends Error {
|
|
6
|
-
constructor(asset: string) {
|
|
7
|
-
super(`No price oracle supports asset: ${asset}`);
|
|
8
|
-
this.name = "AssetNotSupportedError";
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Result from a price oracle query. */
|
|
13
|
-
export interface PriceResult {
|
|
14
|
-
/** Microdollars per 1 unit of asset (integer, 10^-6 USD). */
|
|
15
|
-
priceMicros: number;
|
|
16
|
-
/** When the price was last updated on-chain. */
|
|
17
|
-
updatedAt: Date;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Read-only price oracle. */
|
|
21
|
-
export interface IPriceOracle {
|
|
22
|
-
/**
|
|
23
|
-
* Get the current USD price for an asset.
|
|
24
|
-
* @param asset — token symbol (e.g. "BTC", "DOGE")
|
|
25
|
-
* @param feedAddress — optional Chainlink feed address override (from payment_methods.oracle_address)
|
|
26
|
-
*/
|
|
27
|
-
getPrice(asset: PriceAsset, feedAddress?: `0x${string}`): Promise<PriceResult>;
|
|
28
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { IChainPlugin, PaymentEvent, WatcherOpts } from "../interfaces.js";
|
|
3
|
-
import { PluginRegistry } from "../registry.js";
|
|
4
|
-
|
|
5
|
-
describe("plugin integration — registry → watcher → events", () => {
|
|
6
|
-
it("full lifecycle: register → create watcher → poll → events", async () => {
|
|
7
|
-
const mockEvent: PaymentEvent = {
|
|
8
|
-
chain: "test",
|
|
9
|
-
token: "TEST",
|
|
10
|
-
from: "0xsender",
|
|
11
|
-
to: "0xreceiver",
|
|
12
|
-
rawAmount: "1000",
|
|
13
|
-
amountUsdCents: 100,
|
|
14
|
-
txHash: "0xhash",
|
|
15
|
-
blockNumber: 42,
|
|
16
|
-
confirmations: 6,
|
|
17
|
-
confirmationsRequired: 6,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const plugin: IChainPlugin = {
|
|
21
|
-
pluginId: "test",
|
|
22
|
-
supportedCurve: "secp256k1",
|
|
23
|
-
encoders: {},
|
|
24
|
-
createWatcher: (_opts: WatcherOpts) => ({
|
|
25
|
-
init: async () => {},
|
|
26
|
-
poll: async () => [mockEvent],
|
|
27
|
-
setWatchedAddresses: () => {},
|
|
28
|
-
getCursor: () => 42,
|
|
29
|
-
stop: () => {},
|
|
30
|
-
}),
|
|
31
|
-
createSweeper: () => ({ scan: async () => [], sweep: async () => [] }),
|
|
32
|
-
version: 1,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const registry = new PluginRegistry();
|
|
36
|
-
registry.register(plugin);
|
|
37
|
-
|
|
38
|
-
const resolved = registry.getOrThrow("test");
|
|
39
|
-
const watcher = resolved.createWatcher({
|
|
40
|
-
rpcUrl: "http://localhost:8545",
|
|
41
|
-
rpcHeaders: {},
|
|
42
|
-
oracle: {
|
|
43
|
-
getPrice: async () => ({ priceMicros: 3500_000000 }),
|
|
44
|
-
},
|
|
45
|
-
cursorStore: {
|
|
46
|
-
get: async () => null,
|
|
47
|
-
save: async () => {},
|
|
48
|
-
getConfirmationCount: async () => null,
|
|
49
|
-
saveConfirmationCount: async () => {},
|
|
50
|
-
},
|
|
51
|
-
token: "TEST",
|
|
52
|
-
chain: "test",
|
|
53
|
-
decimals: 18,
|
|
54
|
-
confirmations: 6,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
await watcher.init();
|
|
58
|
-
const events = await watcher.poll();
|
|
59
|
-
expect(events).toHaveLength(1);
|
|
60
|
-
expect(events[0].txHash).toBe("0xhash");
|
|
61
|
-
expect(watcher.getCursor()).toBe(42);
|
|
62
|
-
watcher.stop();
|
|
63
|
-
});
|
|
64
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// src/billing/crypto/plugin/__tests__/interfaces.test.ts
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import type { EncodingParams, IAddressEncoder, IChainWatcher, ICurveDeriver, PaymentEvent } from "../interfaces.js";
|
|
4
|
-
|
|
5
|
-
describe("plugin interfaces — type contracts", () => {
|
|
6
|
-
it("PaymentEvent has required fields", () => {
|
|
7
|
-
const event: PaymentEvent = {
|
|
8
|
-
chain: "ethereum",
|
|
9
|
-
token: "ETH",
|
|
10
|
-
from: "0xabc",
|
|
11
|
-
to: "0xdef",
|
|
12
|
-
rawAmount: "1000000000000000000",
|
|
13
|
-
amountUsdCents: 350000,
|
|
14
|
-
txHash: "0x123",
|
|
15
|
-
blockNumber: 100,
|
|
16
|
-
confirmations: 6,
|
|
17
|
-
confirmationsRequired: 6,
|
|
18
|
-
};
|
|
19
|
-
expect(event.chain).toBe("ethereum");
|
|
20
|
-
expect(event.amountUsdCents).toBe(350000);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("ICurveDeriver contract is satisfiable", () => {
|
|
24
|
-
const deriver: ICurveDeriver = {
|
|
25
|
-
derivePublicKey: (_chain: number, _index: number) => new Uint8Array(33),
|
|
26
|
-
getCurve: () => "secp256k1",
|
|
27
|
-
};
|
|
28
|
-
expect(deriver.getCurve()).toBe("secp256k1");
|
|
29
|
-
expect(deriver.derivePublicKey(0, 0)).toBeInstanceOf(Uint8Array);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("IAddressEncoder contract is satisfiable", () => {
|
|
33
|
-
const encoder: IAddressEncoder = {
|
|
34
|
-
encode: (_pk: Uint8Array, _params: EncodingParams) => "bc1qtest",
|
|
35
|
-
encodingType: () => "bech32",
|
|
36
|
-
};
|
|
37
|
-
expect(encoder.encodingType()).toBe("bech32");
|
|
38
|
-
expect(encoder.encode(new Uint8Array(33), { hrp: "bc" })).toBe("bc1qtest");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("IChainWatcher contract is satisfiable", () => {
|
|
42
|
-
const watcher: IChainWatcher = {
|
|
43
|
-
init: async () => {},
|
|
44
|
-
poll: async () => [],
|
|
45
|
-
setWatchedAddresses: () => {},
|
|
46
|
-
getCursor: () => 0,
|
|
47
|
-
stop: () => {},
|
|
48
|
-
};
|
|
49
|
-
expect(watcher.getCursor()).toBe(0);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { IChainPlugin } from "../interfaces.js";
|
|
3
|
-
import { PluginRegistry } from "../registry.js";
|
|
4
|
-
|
|
5
|
-
function mockPlugin(id: string, curve: "secp256k1" | "ed25519" = "secp256k1"): IChainPlugin {
|
|
6
|
-
return {
|
|
7
|
-
pluginId: id,
|
|
8
|
-
supportedCurve: curve,
|
|
9
|
-
encoders: {},
|
|
10
|
-
createWatcher: () => ({
|
|
11
|
-
init: async () => {},
|
|
12
|
-
poll: async () => [],
|
|
13
|
-
setWatchedAddresses: () => {},
|
|
14
|
-
getCursor: () => 0,
|
|
15
|
-
stop: () => {},
|
|
16
|
-
}),
|
|
17
|
-
createSweeper: () => ({ scan: async () => [], sweep: async () => [] }),
|
|
18
|
-
version: 1,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("PluginRegistry", () => {
|
|
23
|
-
it("registers and retrieves a plugin", () => {
|
|
24
|
-
const reg = new PluginRegistry();
|
|
25
|
-
reg.register(mockPlugin("evm"));
|
|
26
|
-
expect(reg.get("evm")).toBeDefined();
|
|
27
|
-
expect(reg.get("evm")?.pluginId).toBe("evm");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("throws on duplicate registration", () => {
|
|
31
|
-
const reg = new PluginRegistry();
|
|
32
|
-
reg.register(mockPlugin("evm"));
|
|
33
|
-
expect(() => reg.register(mockPlugin("evm"))).toThrow("already registered");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns undefined for unknown plugin", () => {
|
|
37
|
-
const reg = new PluginRegistry();
|
|
38
|
-
expect(reg.get("unknown")).toBeUndefined();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("lists all registered plugins", () => {
|
|
42
|
-
const reg = new PluginRegistry();
|
|
43
|
-
reg.register(mockPlugin("evm"));
|
|
44
|
-
reg.register(mockPlugin("solana", "ed25519"));
|
|
45
|
-
expect(reg.list()).toHaveLength(2);
|
|
46
|
-
expect(
|
|
47
|
-
reg
|
|
48
|
-
.list()
|
|
49
|
-
.map((p) => p.pluginId)
|
|
50
|
-
.sort(),
|
|
51
|
-
).toEqual(["evm", "solana"]);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("getOrThrow throws for unknown plugin", () => {
|
|
55
|
-
const reg = new PluginRegistry();
|
|
56
|
-
expect(() => reg.getOrThrow("nope")).toThrow("not registered");
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
DepositInfo,
|
|
3
|
-
EncodingParams,
|
|
4
|
-
IAddressEncoder,
|
|
5
|
-
IChainPlugin,
|
|
6
|
-
IChainWatcher,
|
|
7
|
-
ICurveDeriver,
|
|
8
|
-
IPriceOracle,
|
|
9
|
-
ISweepStrategy,
|
|
10
|
-
IWatcherCursorStore,
|
|
11
|
-
KeyPair,
|
|
12
|
-
PaymentEvent,
|
|
13
|
-
SweeperOpts,
|
|
14
|
-
SweepResult,
|
|
15
|
-
WatcherOpts,
|
|
16
|
-
} from "./interfaces.js";
|
|
17
|
-
export { PluginRegistry } from "./registry.js";
|