@oracle-agent/oracle 0.3.3 → 0.3.4

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.
@@ -1,594 +0,0 @@
1
- // Read-only multichain portfolio aggregation. No keys, signing, or broadcast.
2
-
3
- import { getAddress } from "ethers";
4
- import { CHAIN_CONFIGS } from "../../scanner/chains.config.mjs";
5
- import { mapLimit, timed } from "../http.mjs";
6
- import { rpcGetBalance } from "./evm-rpc.mjs";
7
- import {
8
- SPL_TOKEN_PROGRAM_ID,
9
- solanaGetBalance,
10
- solanaTokenAccounts,
11
- solanaPubkey,
12
- } from "./solana-rpc.mjs";
13
- import { btcAddress, btcAddressInfo } from "./bitcoin-esplora.mjs";
14
- import { btcInscriptions, btcRuneBalances } from "./bitcoin-meta.mjs";
15
- import { hlAllMids, hlClearinghouse, hlUserState } from "./hl-info.mjs";
16
- import { llamaPrices } from "./defillama.mjs";
17
-
18
- export const SPL_TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
19
-
20
- const PRICE_KEY_BY_SYMBOL = Object.freeze({
21
- ETH: "coingecko:ethereum",
22
- POL: "coingecko:polygon-ecosystem-token",
23
- BNB: "coingecko:binancecoin",
24
- AVAX: "coingecko:avalanche-2",
25
- HYPE: "coingecko:hyperliquid",
26
- USDT0: "coingecko:tether",
27
- SOL: "coingecko:solana",
28
- BTC: "coingecko:bitcoin",
29
- });
30
-
31
- function sharedOpts(opts = {}, key) {
32
- return {
33
- fetchImpl: opts.fetchImpl,
34
- timeoutMs: opts.timeoutMs,
35
- ...(opts[key] || {}),
36
- };
37
- }
38
-
39
- function envAddress(env, ...names) {
40
- for (const name of names) {
41
- const value = String(env?.[name] || "").trim();
42
- if (value) return value;
43
- }
44
- return null;
45
- }
46
-
47
- function normalizeEvmAddress(value) {
48
- if (!value) return null;
49
- try {
50
- return getAddress(String(value).trim().toLowerCase());
51
- } catch {
52
- throw new Error("portfolio: EVM address must be a valid 20-byte address");
53
- }
54
- }
55
-
56
- export function resolvePortfolioAddresses(args = {}, env = process.env) {
57
- const input = args.addresses || {};
58
- const evm = normalizeEvmAddress(
59
- input.evm ||
60
- args.evmAddress ||
61
- envAddress(env, "ORACLE_EVM_ADDRESS", "ORACLE_DEFAULT_ADDRESS", "MAD_OPERATOR_ADDRESS"),
62
- );
63
- const solanaRaw =
64
- input.solana || args.solanaAddress || envAddress(env, "ORACLE_SOLANA_ADDRESS");
65
- const bitcoinRaw =
66
- input.bitcoin || args.bitcoinAddress || envAddress(env, "ORACLE_BITCOIN_ADDRESS");
67
- const hyperliquid = normalizeEvmAddress(
68
- input.hyperliquid ||
69
- args.hyperliquidAddress ||
70
- envAddress(env, "ORACLE_HYPERLIQUID_ADDRESS") ||
71
- evm,
72
- );
73
- return {
74
- evm,
75
- solana: solanaRaw ? solanaPubkey(solanaRaw, "portfolio Solana address") : null,
76
- bitcoin: bitcoinRaw ? btcAddress(bitcoinRaw, "portfolio Bitcoin address") : null,
77
- hyperliquid,
78
- };
79
- }
80
-
81
- export function formatUnits(raw, decimals) {
82
- const value = BigInt(String(raw || "0"));
83
- const places = Number(decimals);
84
- if (!Number.isInteger(places) || places < 0 || places > 36) {
85
- throw new Error("portfolio: decimals must be an integer from 0 to 36");
86
- }
87
- if (places === 0) return value.toString();
88
- const negative = value < 0n;
89
- const abs = negative ? -value : value;
90
- const base = 10n ** BigInt(places);
91
- const whole = abs / base;
92
- const fraction = String(abs % base).padStart(places, "0").replace(/0+$/, "");
93
- return `${negative ? "-" : ""}${whole}${fraction ? `.${fraction}` : ""}`;
94
- }
95
-
96
- function decimalNumber(value) {
97
- const number = Number(value);
98
- return Number.isFinite(number) ? number : null;
99
- }
100
-
101
- function roundedUsd(value) {
102
- return Number.isFinite(value) ? Number(value.toFixed(2)) : null;
103
- }
104
-
105
- function nonZero(value) {
106
- const number = Number(value);
107
- return Number.isFinite(number) && number !== 0;
108
- }
109
-
110
- function priceEntry(priceData, symbol) {
111
- const key = PRICE_KEY_BY_SYMBOL[symbol];
112
- const row = key ? priceData?.coins?.[key] : null;
113
- const priceUsd = Number(row?.price);
114
- return {
115
- key: key || null,
116
- priceUsd: Number.isFinite(priceUsd) ? priceUsd : null,
117
- timestamp: row?.timestamp ?? null,
118
- confidence: row?.confidence ?? null,
119
- };
120
- }
121
-
122
- function aggregateSolanaAccounts(results) {
123
- const byMint = new Map();
124
- for (const result of results) {
125
- if (!result?.ok) continue;
126
- for (const account of result.data?.accounts || []) {
127
- const raw = BigInt(String(account.amount || "0"));
128
- if (raw === 0n || !account.mint) continue;
129
- const key = `${account.mint}:${account.decimals ?? "unknown"}`;
130
- const current = byMint.get(key) || {
131
- mint: account.mint,
132
- amountRaw: 0n,
133
- decimals: account.decimals ?? null,
134
- programs: new Set(),
135
- accounts: 0,
136
- };
137
- current.amountRaw += raw;
138
- current.accounts += 1;
139
- if (result.program) current.programs.add(result.program);
140
- byMint.set(key, current);
141
- }
142
- }
143
- return [...byMint.values()].map((item) => {
144
- const amountRaw = item.amountRaw.toString();
145
- const amount = item.decimals == null ? null : formatUnits(amountRaw, item.decimals);
146
- const collectibleCandidate = item.decimals === 0 && item.amountRaw === 1n;
147
- return {
148
- mint: item.mint,
149
- amountRaw,
150
- amount,
151
- decimals: item.decimals,
152
- tokenPrograms: [...item.programs],
153
- accountCount: item.accounts,
154
- verification: "unverified",
155
- classification: collectibleCandidate ? "collectible-candidate" : "fungible-or-unclassified",
156
- priceUsd: null,
157
- usdValue: null,
158
- };
159
- });
160
- }
161
-
162
- function unavailableFamily(family, reason) {
163
- return { family, status: "unsupported", reason };
164
- }
165
-
166
- function decorateNative(native, priceData) {
167
- if (!native || native.status !== "ok") return native;
168
- const price = priceEntry(priceData, native.symbol);
169
- const amountNumber = decimalNumber(native.amount);
170
- const usdValue =
171
- amountNumber != null && price.priceUsd != null
172
- ? roundedUsd(amountNumber * price.priceUsd)
173
- : null;
174
- return { ...native, price, usdValue };
175
- }
176
-
177
- export async function portfolioHealth(opts = {}) {
178
- const addresses = resolvePortfolioAddresses({}, opts.env || process.env);
179
- return {
180
- ok: true,
181
- provider: "portfolio",
182
- readOnly: true,
183
- configuredFamilies: Object.entries(addresses)
184
- .filter(([, value]) => Boolean(value))
185
- .map(([family]) => family),
186
- };
187
- }
188
-
189
- export async function portfolioBalance(args = {}, opts = {}) {
190
- const queriedAt = new Date().toISOString();
191
- const addresses = resolvePortfolioAddresses(args, opts.env || process.env);
192
- const includePrices = args.includePrices !== false;
193
- const includeTokens = args.includeTokens !== false;
194
- const includeCollectibles = args.includeCollectibles !== false;
195
- const configuredChains = new Map(CHAIN_CONFIGS.map((chain) => [chain.chainId, chain]));
196
- const requestedIds = Array.isArray(args.evmChainIds) && args.evmChainIds.length
197
- ? [...new Set(args.evmChainIds.map(Number))]
198
- : [...configuredChains.keys()];
199
-
200
- const priceKeys = [...new Set(Object.values(PRICE_KEY_BY_SYMBOL))];
201
- const priceTask = includePrices
202
- ? timed(() => llamaPrices(priceKeys, sharedOpts(opts, "prices")))
203
- : Promise.resolve({ ok: false, ms: 0, error: "price lookup disabled" });
204
-
205
- const evmTask = mapLimit(requestedIds, args.concurrency || 5, async (chainId) => {
206
- const chain = configuredChains.get(chainId);
207
- if (!chain) {
208
- return {
209
- family: "evm",
210
- chainId,
211
- name: null,
212
- address: addresses.evm,
213
- status: "unsupported",
214
- error: "chainId is not in Oracle's configured EVM registry",
215
- };
216
- }
217
- if (!addresses.evm) {
218
- return {
219
- family: "evm",
220
- chainId,
221
- name: chain.name,
222
- address: null,
223
- status: "not-configured",
224
- error: "set ORACLE_EVM_ADDRESS or pass addresses.evm",
225
- };
226
- }
227
- const rpcOpts = {
228
- ...sharedOpts(opts, "evm"),
229
- rpcUrl: args.rpcUrls?.[chainId] || opts.evm?.rpcUrls?.[chainId],
230
- };
231
- const result = await timed(() => rpcGetBalance(chainId, addresses.evm, rpcOpts));
232
- if (!result.ok) {
233
- return {
234
- family: "evm",
235
- chainId,
236
- name: chain.name,
237
- address: addresses.evm,
238
- status: "unavailable",
239
- latencyMs: result.ms,
240
- error: result.error,
241
- };
242
- }
243
- const decimals = chain.nativeCurrency?.decimals ?? 18;
244
- const symbol = chain.nativeCurrency?.symbol || "NATIVE";
245
- const amountRaw = result.data.balanceWei;
246
- return {
247
- family: "evm",
248
- chainId,
249
- name: chain.name,
250
- address: addresses.evm,
251
- status: "ok",
252
- latencyMs: result.ms,
253
- native: {
254
- status: "ok",
255
- symbol,
256
- amountRaw,
257
- decimals,
258
- amount: formatUnits(amountRaw, decimals),
259
- },
260
- fungibleTokens: {
261
- status: includeTokens ? "unavailable" : "not-requested",
262
- assets: [],
263
- reason: includeTokens
264
- ? "EVM JSON-RPC cannot enumerate wallet tokens without an address indexer"
265
- : "token discovery disabled",
266
- },
267
- collectibles: {
268
- status: includeCollectibles ? "unavailable" : "not-requested",
269
- assets: [],
270
- reason: includeCollectibles
271
- ? "EVM JSON-RPC cannot enumerate wallet NFTs without an address indexer"
272
- : "collectible discovery disabled",
273
- },
274
- source: "evm-rpc",
275
- };
276
- });
277
-
278
- const solanaTask = (async () => {
279
- if (!addresses.solana) {
280
- return {
281
- family: "solana",
282
- name: "Solana Mainnet",
283
- address: null,
284
- status: "not-configured",
285
- error: "set ORACLE_SOLANA_ADDRESS or pass addresses.solana",
286
- };
287
- }
288
- const solOpts = sharedOpts(opts, "solana");
289
- const native = await timed(() => solanaGetBalance({ address: addresses.solana }, solOpts));
290
- const tokenReads = includeTokens || includeCollectibles
291
- ? await Promise.all([
292
- timed(() =>
293
- solanaTokenAccounts(
294
- { owner: addresses.solana, programId: SPL_TOKEN_PROGRAM_ID },
295
- solOpts,
296
- ),
297
- ).then((result) => ({ ...result, program: "spl-token" })),
298
- timed(() =>
299
- solanaTokenAccounts(
300
- { owner: addresses.solana, programId: SPL_TOKEN_2022_PROGRAM_ID },
301
- solOpts,
302
- ),
303
- ).then((result) => ({ ...result, program: "token-2022" })),
304
- ])
305
- : [];
306
- const assets = aggregateSolanaAccounts(tokenReads);
307
- const candidates = assets.filter((asset) => asset.classification === "collectible-candidate");
308
- const fungibles = assets.filter((asset) => asset.classification !== "collectible-candidate");
309
- const tokenErrors = tokenReads.filter((result) => !result.ok).map((result) => result.error);
310
- return {
311
- family: "solana",
312
- name: "Solana Mainnet",
313
- address: addresses.solana,
314
- status: native.ok || tokenReads.some((result) => result.ok) ? "ok" : "unavailable",
315
- native: native.ok
316
- ? {
317
- status: "ok",
318
- symbol: "SOL",
319
- amountRaw: native.data.lamports,
320
- decimals: 9,
321
- amount: formatUnits(native.data.lamports, 9),
322
- slot: native.data.slot,
323
- }
324
- : { status: "unavailable", symbol: "SOL", error: native.error },
325
- fungibleTokens: {
326
- status: !includeTokens
327
- ? "not-requested"
328
- : tokenErrors.length === tokenReads.length
329
- ? "unavailable"
330
- : tokenErrors.length
331
- ? "partial"
332
- : "ok",
333
- assets: includeTokens ? fungibles : [],
334
- errors: tokenErrors,
335
- note: "SPL accounts are unverified until a metadata/indexer source confirms symbol and spam status",
336
- },
337
- collectibles: {
338
- status: !includeCollectibles
339
- ? "not-requested"
340
- : tokenErrors.length === tokenReads.length
341
- ? "unavailable"
342
- : "partial",
343
- assets: includeCollectibles ? candidates : [],
344
- errors: tokenErrors,
345
- note: "decimals=0 and amount=1 is only a collectible candidate, not verified NFT metadata",
346
- },
347
- source: "solana-rpc",
348
- };
349
- })();
350
-
351
- const bitcoinTask = (async () => {
352
- if (!addresses.bitcoin) {
353
- return {
354
- family: "bitcoin",
355
- name: "Bitcoin Mainnet",
356
- address: null,
357
- status: "not-configured",
358
- error: "set ORACLE_BITCOIN_ADDRESS or pass addresses.bitcoin",
359
- };
360
- }
361
- const btcOpts = sharedOpts(opts, "bitcoin");
362
- const metaOpts = sharedOpts(opts, "bitcoinMeta");
363
- const [native, runes, inscriptions] = await Promise.all([
364
- timed(() => btcAddressInfo({ address: addresses.bitcoin }, btcOpts)),
365
- includeTokens
366
- ? timed(() => btcRuneBalances({ address: addresses.bitcoin }, metaOpts))
367
- : Promise.resolve({ ok: false, ms: 0, error: "token discovery disabled" }),
368
- includeCollectibles
369
- ? timed(() => btcInscriptions({ address: addresses.bitcoin }, metaOpts))
370
- : Promise.resolve({ ok: false, ms: 0, error: "collectible discovery disabled" }),
371
- ]);
372
- const runeData = runes.ok ? runes.data : null;
373
- const inscriptionData = inscriptions.ok ? inscriptions.data : null;
374
- return {
375
- family: "bitcoin",
376
- name: "Bitcoin Mainnet",
377
- address: addresses.bitcoin,
378
- status: native.ok || runeData?.ok || inscriptionData?.ok ? "ok" : "unavailable",
379
- native: native.ok
380
- ? {
381
- status: "ok",
382
- symbol: "BTC",
383
- amountRaw: native.data.balanceSats,
384
- decimals: 8,
385
- amount: formatUnits(native.data.balanceSats, 8),
386
- }
387
- : { status: "unavailable", symbol: "BTC", error: native.error },
388
- fungibleTokens: {
389
- status: !includeTokens
390
- ? "not-requested"
391
- : runeData?.ok
392
- ? "ok"
393
- : "unavailable",
394
- assets: runeData?.balances || [],
395
- error: runeData?.ok ? null : runeData?.error || runes.error,
396
- verification: "indexer-reported",
397
- },
398
- collectibles: {
399
- status: !includeCollectibles
400
- ? "not-requested"
401
- : inscriptionData?.ok
402
- ? "ok"
403
- : "unavailable",
404
- assets: inscriptionData?.inscriptions || [],
405
- error: inscriptionData?.ok ? null : inscriptionData?.error || inscriptions.error,
406
- unknownNotEmpty: Boolean(inscriptionData?.unknownNotEmpty),
407
- verification: "indexer-reported",
408
- },
409
- source: "bitcoin-esplora+bitcoin-meta",
410
- };
411
- })();
412
-
413
- const hyperliquidTask = (async () => {
414
- if (!addresses.hyperliquid) {
415
- return {
416
- family: "hyperliquid",
417
- name: "Hyperliquid HyperCore",
418
- address: null,
419
- status: "not-configured",
420
- error: "set ORACLE_HYPERLIQUID_ADDRESS or pass addresses.hyperliquid",
421
- };
422
- }
423
- const hlOpts = sharedOpts(opts, "hyperliquid");
424
- const [spot, perps, mids] = await Promise.all([
425
- timed(() => hlClearinghouse(addresses.hyperliquid, hlOpts)),
426
- timed(() => hlUserState(addresses.hyperliquid, hlOpts)),
427
- timed(() => hlAllMids(hlOpts)),
428
- ]);
429
- const spotBalances = (spot.ok && Array.isArray(spot.data?.balances) ? spot.data.balances : [])
430
- .filter((item) => nonZero(item.total) || nonZero(item.hold))
431
- .map((item) => ({
432
- coin: item.coin || null,
433
- total: String(item.total ?? "0"),
434
- hold: String(item.hold ?? "0"),
435
- priceUsd: null,
436
- usdValue: null,
437
- }));
438
- const positions = (perps.ok && Array.isArray(perps.data?.assetPositions)
439
- ? perps.data.assetPositions
440
- : [])
441
- .map((item) => item.position || item)
442
- .filter((item) => nonZero(item.szi))
443
- .map((item) => ({
444
- coin: item.coin || null,
445
- size: String(item.szi ?? "0"),
446
- entryPrice: item.entryPx ?? null,
447
- positionValueUsd: item.positionValue ?? null,
448
- unrealizedPnlUsd: item.unrealizedPnl ?? null,
449
- liquidationPrice: item.liquidationPx ?? null,
450
- }));
451
- return {
452
- family: "hyperliquid",
453
- name: "Hyperliquid HyperCore",
454
- address: addresses.hyperliquid,
455
- status: spot.ok || perps.ok ? "ok" : "unavailable",
456
- spot: {
457
- status: spot.ok ? "ok" : "unavailable",
458
- balances: spotBalances,
459
- error: spot.ok ? null : spot.error,
460
- },
461
- perps: {
462
- status: perps.ok ? "ok" : "unavailable",
463
- accountValueUsd: perps.data?.marginSummary?.accountValue ?? null,
464
- withdrawableUsd: perps.data?.withdrawable ?? null,
465
- positions,
466
- error: perps.ok ? null : perps.error,
467
- },
468
- mids: mids.ok ? mids.data : {},
469
- source: "hyperliquid-info",
470
- };
471
- })();
472
-
473
- const [evm, solana, bitcoin, hyperliquid, priceResult] = await Promise.all([
474
- evmTask,
475
- solanaTask,
476
- bitcoinTask,
477
- hyperliquidTask,
478
- priceTask,
479
- ]);
480
- const priceData = priceResult.ok ? priceResult.data : null;
481
-
482
- const decoratedEvm = evm.map((chain) =>
483
- chain.native ? { ...chain, native: decorateNative(chain.native, priceData) } : chain,
484
- );
485
- if (solana.native) solana.native = decorateNative(solana.native, priceData);
486
- if (bitcoin.native) bitcoin.native = decorateNative(bitcoin.native, priceData);
487
-
488
- let knownUsd = 0;
489
- let pricedItems = 0;
490
- let unpricedNonzeroItems = 0;
491
- for (const chain of [...decoratedEvm, solana, bitcoin]) {
492
- if (chain.native?.status !== "ok" || !nonZero(chain.native.amount)) continue;
493
- if (chain.native.usdValue != null) {
494
- knownUsd += chain.native.usdValue;
495
- pricedItems += 1;
496
- } else {
497
- unpricedNonzeroItems += 1;
498
- }
499
- }
500
-
501
- const hypePrice = priceEntry(priceData, "HYPE");
502
- for (const item of hyperliquid.spot?.balances || []) {
503
- const symbol = String(item.coin || "").toUpperCase();
504
- const priceUsd = symbol === "USDC" || symbol === "USDT" || symbol === "USDT0"
505
- ? 1
506
- : symbol === "HYPE"
507
- ? hypePrice.priceUsd || decimalNumber(hyperliquid.mids?.HYPE)
508
- : decimalNumber(hyperliquid.mids?.[item.coin]);
509
- const amount = decimalNumber(item.total);
510
- item.priceUsd = priceUsd;
511
- item.usdValue = amount != null && priceUsd != null ? roundedUsd(amount * priceUsd) : null;
512
- if (!nonZero(item.total)) continue;
513
- if (item.usdValue != null) {
514
- knownUsd += item.usdValue;
515
- pricedItems += 1;
516
- } else {
517
- unpricedNonzeroItems += 1;
518
- }
519
- }
520
- const perpsValue = decimalNumber(hyperliquid.perps?.accountValueUsd);
521
- if (perpsValue != null && perpsValue !== 0) {
522
- knownUsd += perpsValue;
523
- pricedItems += 1;
524
- }
525
-
526
- const solanaUnpriced = [
527
- ...(solana.fungibleTokens?.assets || []),
528
- ...(solana.collectibles?.assets || []),
529
- ].filter((asset) => nonZero(asset.amount)).length;
530
- const bitcoinUnpriced =
531
- (bitcoin.fungibleTokens?.assets || []).length +
532
- (bitcoin.collectibles?.assets || []).length;
533
- unpricedNonzeroItems += solanaUnpriced + bitcoinUnpriced;
534
-
535
- const allSurfaces = [...decoratedEvm, solana, bitcoin, hyperliquid];
536
- const count = (status) => allSurfaces.filter((surface) => surface.status === status).length;
537
- const warnings = [];
538
- if (decoratedEvm.some((chain) => chain.fungibleTokens?.status === "unavailable")) {
539
- warnings.push("EVM token and NFT discovery is unavailable without a configured address indexer; native balances are still live.");
540
- }
541
- if (solana.fungibleTokens?.assets?.length || solana.collectibles?.assets?.length) {
542
- warnings.push("Solana token accounts are unverified and may include spam; collectible candidates are not confirmed NFTs.");
543
- }
544
- if (bitcoin.collectibles?.unknownNotEmpty) {
545
- warnings.push("Bitcoin inscription holdings are unknown, not zero, because no address indexer is configured.");
546
- }
547
- if (!priceResult.ok) warnings.push(`Price provider unavailable: ${priceResult.error}`);
548
-
549
- const unsupportedFamilies = [
550
- unavailableFamily("cosmos", "requires a chain-specific bech32 address and LCD/RPC adapter"),
551
- unavailableFamily("sui", "Sui balance adapter is not installed"),
552
- unavailableFamily("aptos", "Aptos balance adapter is not installed"),
553
- ];
554
-
555
- const discoveryIncomplete = allSurfaces.some((surface) =>
556
- ["unavailable", "partial", "not-configured"].includes(surface.fungibleTokens?.status) ||
557
- ["unavailable", "partial", "not-configured"].includes(surface.collectibles?.status),
558
- );
559
-
560
- return {
561
- provider: "portfolio",
562
- operation: "balances",
563
- readOnly: true,
564
- queriedAt,
565
- addresses,
566
- coverage: {
567
- requestedSurfaces: allSurfaces.length,
568
- ok: count("ok"),
569
- unavailable: count("unavailable"),
570
- notConfigured: count("not-configured"),
571
- unsupported: count("unsupported") + unsupportedFamilies.length,
572
- evmChainsRequested: requestedIds.length,
573
- evmChainsOk: decoratedEvm.filter((chain) => chain.status === "ok").length,
574
- },
575
- valuation: {
576
- knownUsd: roundedUsd(knownUsd),
577
- label: "known priced value, not a complete portfolio total",
578
- pricedItems,
579
- unpricedNonzeroItems,
580
- complete:
581
- unpricedNonzeroItems === 0 &&
582
- !discoveryIncomplete &&
583
- count("unavailable") === 0 &&
584
- count("not-configured") === 0 &&
585
- unsupportedFamilies.length === 0,
586
- priceSource: includePrices ? "DefiLlama" : null,
587
- priceStatus: priceResult.ok ? "ok" : "unavailable",
588
- priceQueriedAt: queriedAt,
589
- },
590
- chains: [...decoratedEvm, solana, bitcoin, hyperliquid],
591
- unsupportedFamilies,
592
- warnings,
593
- };
594
- }