@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.
- package/README.md +5 -12
- package/SETUP.md +1 -35
- package/artifacts/specialist-packs/oracle-full-crypto.json +9 -31
- package/bin/oracle-data-mcp.mjs +4 -247
- package/bin/oracle-init.mjs +27 -100
- package/docs/profiles.md +7 -27
- package/package.json +1 -1
- package/profiles/_template/SOUL.md +1 -8
- package/profiles/oracle/SOUL.md +1 -8
- package/profiles/oracle/profile.json +2 -5
- package/profiles/protocol-builder/SOUL.md +6 -13
- package/profiles/protocol-builder/profile.json +1 -3
- package/src/data/catalog.mjs +3 -27
- package/src/data/desk-data.mjs +4 -34
- package/src/data/providers/magiceden-sol.mjs +2 -21
- package/src/data/providers/opensea-nft.mjs +0 -272
- package/src/data/providers/satflow.mjs +0 -1
- package/src/data/providers/uniswap-v3.mjs +17 -1
- package/src/exec-policy.mjs +0 -5
- package/src/gmx-attestation.mjs +0 -1
- package/src/router/prepare-route.mjs +23 -0
- package/src/router/route-sources.mjs +37 -0
- package/src/vault-attestation.mjs +0 -1
- package/skills/balance/SKILL.md +0 -176
- package/skills/oracle-multichain-nft-launch/SKILL.md +0 -338
- package/skills/oracle-multichain-token-launch/SKILL.md +0 -300
- package/src/data/providers/nft-gallery.mjs +0 -163
- package/src/data/providers/nft-portfolio.mjs +0 -494
- package/src/data/providers/portfolio-history.mjs +0 -394
- package/src/data/providers/portfolio.mjs +0 -594
|
@@ -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
|
-
}
|