Skip to content

Commit 070148f

Browse files
Shawclaude
andcommitted
wip(cloud-shared): chainlink BNB/USD quote in direct-wallet-payments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9a5c6c6 commit 070148f

1 file changed

Lines changed: 52 additions & 0 deletions

File tree

packages/cloud-shared/src/lib/services/direct-wallet-payments.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
type Hex,
2323
http,
2424
isAddress,
25+
parseAbi,
2526
parseAbiItem,
2627
parseEventLogs,
2728
} from "viem";
@@ -118,6 +119,57 @@ const BSC_TOKEN_OPTIONS: DirectWalletTokenOption[] = [
118119
},
119120
];
120121

122+
// Chainlink BNB/USD aggregator on BSC mainnet — 8-decimal answer, ~60s
123+
// heartbeat, 0.5% deviation. Override via CRYPTO_DIRECT_BSC_BNB_USD_FEED for
124+
// tests / private deployments.
125+
const CHAINLINK_BNB_USD_FEED = "0x0567F2323251f0Aab15c8dFb1967E4e8A7D42aeE";
126+
const CHAINLINK_AGGREGATOR_ABI = parseAbi([
127+
"function latestRoundData() view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)",
128+
"function decimals() view returns (uint8)",
129+
]);
130+
// Reject the oracle if the last update is older than this. The feed normally
131+
// ticks every minute; an hour of staleness means the feed is unhealthy and
132+
// the price could be far from market. Locking a payment to a stale quote is
133+
// how exchanges get drained.
134+
const BNB_USD_MAX_AGE_SECONDS = 60 * 60;
135+
136+
async function bnbUsdQuote(
137+
env: Bindings,
138+
): Promise<{ priceUsd: Decimal; updatedAt: number; feedAddress: Hex }> {
139+
const feedOverride = envString(env, "CRYPTO_DIRECT_BSC_BNB_USD_FEED");
140+
const feed = feedOverride ? getAddress(feedOverride) : getAddress(CHAINLINK_BNB_USD_FEED);
141+
const rpcUrl =
142+
envString(env, "CRYPTO_DIRECT_BSC_RPC_URL") ??
143+
envString(env, "BSC_RPC_URL") ??
144+
"https://bsc-dataseed.binance.org";
145+
const client = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
146+
const [round, decimals] = await Promise.all([
147+
client.readContract({
148+
address: feed,
149+
abi: CHAINLINK_AGGREGATOR_ABI,
150+
functionName: "latestRoundData",
151+
}),
152+
client.readContract({
153+
address: feed,
154+
abi: CHAINLINK_AGGREGATOR_ABI,
155+
functionName: "decimals",
156+
}),
157+
]);
158+
const answer = round[1];
159+
const updatedAt = Number(round[3]);
160+
if (answer <= 0n) {
161+
throw new Error("BNB/USD oracle returned a non-positive price");
162+
}
163+
const ageSeconds = Math.floor(Date.now() / 1000) - updatedAt;
164+
if (ageSeconds < 0 || ageSeconds > BNB_USD_MAX_AGE_SECONDS) {
165+
throw new Error(
166+
`BNB/USD oracle is stale (last update ${ageSeconds}s ago); refusing to quote`,
167+
);
168+
}
169+
const priceUsd = new Decimal(answer.toString()).div(new Decimal(10).pow(Number(decimals)));
170+
return { priceUsd, updatedAt, feedAddress: feed };
171+
}
172+
121173
function resolveBscToken(symbol: string | undefined): DirectWalletTokenOption {
122174
if (!symbol) return BSC_TOKEN_OPTIONS[1]; // default USDT
123175
const match = BSC_TOKEN_OPTIONS.find(

0 commit comments

Comments
 (0)