@t2000/engine 0.54.0 → 0.54.2

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/index.d.ts CHANGED
@@ -2274,6 +2274,20 @@ interface PortfolioResult {
2274
2274
  totalValue: number;
2275
2275
  walletValue: number;
2276
2276
  savingsValue: number;
2277
+ /**
2278
+ * [Bug — 2026-04-28] Aggregated DeFi value across non-NAVI protocols
2279
+ * (Cetus LPs, Bluefin, Suilend, etc.) — same field that's been on
2280
+ * `balance_check` since v0.50. Pre-fix this tool ignored DeFi entirely:
2281
+ * a wallet with $1,569 in Cetus LPs reported a $228 totalValue
2282
+ * (wallet only), under-counting net worth by 87% and prompting the LLM
2283
+ * to misclassify the wallet as "concentrated in FAITH" when actually
2284
+ * the bulk was in liquidity pools. Same SSOT-divergence class the v0.54
2285
+ * cache work fixed for FullPortfolioCanvas, manifesting in a different
2286
+ * tool that was written before DeFi support was bolted on.
2287
+ */
2288
+ defiValue: number;
2289
+ /** Provenance of the DeFi read — used by the UI card to caveat partial/degraded. */
2290
+ defiSource: DefiSummary['source'];
2277
2291
  debtValue: number;
2278
2292
  healthFactor: number | null;
2279
2293
  allocations: AssetAllocation[];
@@ -2508,12 +2522,21 @@ interface AudricPortfolioResult {
2508
2522
  portfolio: AddressPortfolio;
2509
2523
  /** NAVI lending positions, normalized to the engine's `ServerPositionData`. */
2510
2524
  positions: ServerPositionData;
2511
- /** Net worth derived audric-side (`wallet + savings - borrows`). */
2525
+ /** Net worth derived audric-side (`wallet + savings + defi - borrows`). */
2512
2526
  netWorthUsd: number;
2513
2527
  /** `savings * savingsRate / 365`, capped at 0. */
2514
2528
  estimatedDailyYield: number;
2515
2529
  /** Per-symbol balance map — convenient for adapters that already used `WalletBalances`. */
2516
2530
  walletAllocations: Record<string, number>;
2531
+ /**
2532
+ * [Bug — 2026-04-28] Aggregated DeFi value (Cetus LPs, Bluefin, Suilend,
2533
+ * etc.) when available. `defiSource === 'degraded'` when audric's wire
2534
+ * didn't include the field — callers should treat that as "fall back to
2535
+ * a direct fetchAddressDefiPortfolio call" (same convention used
2536
+ * inside the audric web app's UI components).
2537
+ */
2538
+ defiValueUsd: number;
2539
+ defiSource: DefiSummary['source'];
2517
2540
  }
2518
2541
  /**
2519
2542
  * Fetch the canonical portfolio snapshot from audric. Returns `null` if
package/dist/index.js CHANGED
@@ -537,6 +537,86 @@ var BLOCKVISION_BASE = "https://api.blockvision.org/v2/sui";
537
537
  var PORTFOLIO_TIMEOUT_MS = 4e3;
538
538
  var PRICES_TIMEOUT_MS = 3e3;
539
539
  var CACHE_TTL_MS = 6e4;
540
+ var BV_RETRY_MAX_ATTEMPTS = 3;
541
+ var BV_RETRY_BASE_DELAY_MS = 250;
542
+ var BV_RETRY_BACKOFF_FACTOR = 3;
543
+ var BV_RETRY_JITTER = 0.25;
544
+ var BV_RETRY_AFTER_CAP_MS = 5e3;
545
+ var CB_WINDOW_MS = 5e3;
546
+ var CB_THRESHOLD = 10;
547
+ var CB_COOLDOWN_MS = 3e4;
548
+ var cb429Timestamps = [];
549
+ var cbOpenUntil = 0;
550
+ function cbIsOpen(now) {
551
+ return now < cbOpenUntil;
552
+ }
553
+ function cbRecord429(now) {
554
+ cb429Timestamps.push(now);
555
+ cb429Timestamps = cb429Timestamps.filter((t) => now - t < CB_WINDOW_MS);
556
+ if (cb429Timestamps.length >= CB_THRESHOLD && !cbIsOpen(now)) {
557
+ cbOpenUntil = now + CB_COOLDOWN_MS;
558
+ console.warn(
559
+ `[blockvision] circuit breaker OPEN \u2014 ${CB_THRESHOLD} 429s in ${CB_WINDOW_MS}ms, retries disabled for ${CB_COOLDOWN_MS / 1e3}s`
560
+ );
561
+ cb429Timestamps = [];
562
+ }
563
+ }
564
+ async function fetchBlockVisionWithRetry(url, init, opts = {}) {
565
+ const rng = opts.rng ?? Math.random;
566
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve, reject) => {
567
+ const timer = setTimeout(resolve, ms);
568
+ if (opts.signal) {
569
+ const onAbort = () => {
570
+ clearTimeout(timer);
571
+ reject(new DOMException("Aborted", "AbortError"));
572
+ };
573
+ if (opts.signal.aborted) onAbort();
574
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
575
+ }
576
+ }));
577
+ let lastError = null;
578
+ let lastResponse = null;
579
+ for (let attempt = 0; attempt < BV_RETRY_MAX_ATTEMPTS; attempt++) {
580
+ if (attempt > 0) {
581
+ let waitMs = BV_RETRY_BASE_DELAY_MS * Math.pow(BV_RETRY_BACKOFF_FACTOR, attempt - 1);
582
+ const retryAfter = lastResponse?.headers.get("retry-after");
583
+ if (retryAfter) {
584
+ const secs = Number(retryAfter);
585
+ if (Number.isFinite(secs) && secs > 0) {
586
+ waitMs = Math.min(secs * 1e3, BV_RETRY_AFTER_CAP_MS);
587
+ }
588
+ }
589
+ const jitterPx = (rng() * 2 - 1) * BV_RETRY_JITTER * waitMs;
590
+ const delay = Math.max(0, waitMs + jitterPx);
591
+ try {
592
+ await sleep2(delay);
593
+ } catch (err) {
594
+ if (lastResponse) return lastResponse;
595
+ throw err;
596
+ }
597
+ }
598
+ try {
599
+ lastResponse = await fetch(url, init);
600
+ } catch (err) {
601
+ lastError = err;
602
+ if (err?.name === "AbortError") throw err;
603
+ continue;
604
+ }
605
+ if (lastResponse.ok) return lastResponse;
606
+ if (lastResponse.status !== 429 && lastResponse.status < 500) {
607
+ return lastResponse;
608
+ }
609
+ if (lastResponse.status === 429) {
610
+ const now = (opts.now ?? Date.now)();
611
+ cbRecord429(now);
612
+ if (cbIsOpen(now)) {
613
+ return lastResponse;
614
+ }
615
+ }
616
+ }
617
+ if (lastResponse) return lastResponse;
618
+ throw lastError ?? new Error("fetch failed after retries");
619
+ }
540
620
  var DEGRADED_CACHE_TTL_MS = 15e3;
541
621
  var PRICE_LIST_CHUNK = 10;
542
622
  var STABLE_USD_PRICES = {
@@ -582,12 +662,17 @@ async function fetchAddressPortfolio(address, apiKey, fallbackRpcUrl) {
582
662
  }
583
663
  async function fetchPortfolioFromBlockVision(address, apiKey) {
584
664
  const url = `${BLOCKVISION_BASE}/account/coins?account=${encodeURIComponent(address)}`;
665
+ const signal = AbortSignal.timeout(PORTFOLIO_TIMEOUT_MS);
585
666
  let res;
586
667
  try {
587
- res = await fetch(url, {
588
- headers: { "x-api-key": apiKey, accept: "application/json" },
589
- signal: AbortSignal.timeout(PORTFOLIO_TIMEOUT_MS)
590
- });
668
+ res = await fetchBlockVisionWithRetry(
669
+ url,
670
+ {
671
+ headers: { "x-api-key": apiKey, accept: "application/json" },
672
+ signal
673
+ },
674
+ { signal }
675
+ );
591
676
  } catch (err) {
592
677
  console.warn("[blockvision-prices] portfolio fetch threw, degrading:", err);
593
678
  return null;
@@ -718,12 +803,17 @@ async function fetchPricesFromBlockVision(coinTypes, apiKey) {
718
803
  const chunk = longForms.slice(i, i + PRICE_LIST_CHUNK);
719
804
  const tokenIds = encodeURIComponent(chunk.join(","));
720
805
  const url = `${BLOCKVISION_BASE}/coin/price/list?tokenIds=${tokenIds}&show24hChange=true`;
806
+ const signal = AbortSignal.timeout(PRICES_TIMEOUT_MS);
721
807
  let res;
722
808
  try {
723
- res = await fetch(url, {
724
- headers: { "x-api-key": apiKey, accept: "application/json" },
725
- signal: AbortSignal.timeout(PRICES_TIMEOUT_MS)
726
- });
809
+ res = await fetchBlockVisionWithRetry(
810
+ url,
811
+ {
812
+ headers: { "x-api-key": apiKey, accept: "application/json" },
813
+ signal
814
+ },
815
+ { signal }
816
+ );
727
817
  } catch (err) {
728
818
  console.warn("[blockvision-prices] price chunk threw, skipping:", err);
729
819
  continue;
@@ -914,12 +1004,17 @@ async function fetchAddressDefiPortfolio(address, apiKey, priceHints = {}) {
914
1004
  }
915
1005
  async function fetchOneDefiProtocol(address, protocol, apiKey) {
916
1006
  const url = `${BLOCKVISION_BASE}/account/defiPortfolio?address=${encodeURIComponent(address)}&protocol=${protocol}`;
1007
+ const signal = AbortSignal.timeout(DEFI_PORTFOLIO_TIMEOUT_MS);
917
1008
  let res;
918
1009
  try {
919
- res = await fetch(url, {
920
- headers: { "x-api-key": apiKey, accept: "application/json" },
921
- signal: AbortSignal.timeout(DEFI_PORTFOLIO_TIMEOUT_MS)
922
- });
1010
+ res = await fetchBlockVisionWithRetry(
1011
+ url,
1012
+ {
1013
+ headers: { "x-api-key": apiKey, accept: "application/json" },
1014
+ signal
1015
+ },
1016
+ { signal }
1017
+ );
923
1018
  } catch (err) {
924
1019
  console.warn(`[defi] ${protocol} fetch threw:`, err);
925
1020
  return null;
@@ -1243,7 +1338,14 @@ async function fetchAudricPortfolio(address, env, signal) {
1243
1338
  positions,
1244
1339
  netWorthUsd: json.netWorthUsd ?? portfolio.totalUsd + positions.savings - positions.borrows,
1245
1340
  estimatedDailyYield: json.estimatedDailyYield ?? 0,
1246
- walletAllocations: json.walletAllocations ?? {}
1341
+ walletAllocations: json.walletAllocations ?? {},
1342
+ // Default to 'degraded' (not 'partial') when the wire shape lacks
1343
+ // DeFi: 'partial' implies "we tried and got partial data" which is
1344
+ // misleading for a route that simply doesn't return the field.
1345
+ // Callers that need DeFi must fall back to a direct fetch on
1346
+ // 'degraded' — exactly the convention BalanceCard already uses.
1347
+ defiValueUsd: typeof json.defiValueUsd === "number" ? json.defiValueUsd : 0,
1348
+ defiSource: json.defiSource ?? "degraded"
1247
1349
  };
1248
1350
  } catch (err) {
1249
1351
  console.warn(`[audric-api] portfolio ${address.slice(0, 10)} fetch failed:`, err);
@@ -1932,10 +2034,25 @@ var STABLECOIN_SYMBOLS2 = /* @__PURE__ */ new Set([
1932
2034
  function isStable(symbol) {
1933
2035
  return STABLECOIN_SYMBOLS2.has(symbol.toLowerCase());
1934
2036
  }
2037
+ var TOKEN_ALIASES = {
2038
+ usdt: ["usdt", "wusdt", "suiusdt"],
2039
+ usdc: ["usdc", "wusdc"],
2040
+ usde: ["usde", "suiusde", "sui_usde"],
2041
+ usdsui: ["usdsui"]
2042
+ };
2043
+ function expandAliases(symbols) {
2044
+ const out = /* @__PURE__ */ new Set();
2045
+ for (const s of symbols) {
2046
+ const norm = s.toLowerCase();
2047
+ const aliases = TOKEN_ALIASES[norm] ?? [norm];
2048
+ for (const a of aliases) out.add(a);
2049
+ }
2050
+ return out;
2051
+ }
1935
2052
  function applyFilters(rates, opts) {
1936
2053
  let entries = Object.entries(rates);
1937
2054
  if (opts.assets && opts.assets.length) {
1938
- const wanted = new Set(opts.assets.map((a) => a.toLowerCase()));
2055
+ const wanted = expandAliases(opts.assets);
1939
2056
  entries = entries.filter(([sym]) => wanted.has(sym.toLowerCase()));
1940
2057
  } else if (opts.stableOnly) {
1941
2058
  entries = entries.filter(([sym]) => isStable(sym));
@@ -3277,7 +3394,7 @@ var portfolioAnalysisTool = buildTool({
3277
3394
  context.signal
3278
3395
  );
3279
3396
  const apiUrl = context.env?.AUDRIC_INTERNAL_API_URL;
3280
- const [portfolio, positions, weekHistResult] = await Promise.all([
3397
+ const [portfolio, positions, weekHistResult, defiSummary] = await Promise.all([
3281
3398
  audricSnapshot ? Promise.resolve(audricSnapshot.portfolio) : (async () => {
3282
3399
  if (context.portfolioCache) {
3283
3400
  const hit = context.portfolioCache.get(address);
@@ -3306,7 +3423,42 @@ var portfolioAnalysisTool = buildTool({
3306
3423
  apiUrl ? fetch(
3307
3424
  `${apiUrl}/api/analytics/portfolio-history?days=7`,
3308
3425
  { headers: { "x-sui-address": address }, signal: context.signal }
3309
- ).then((res) => res.ok ? res.json() : null).catch(() => null) : Promise.resolve(null)
3426
+ ).then((res) => res.ok ? res.json() : null).catch(() => null) : Promise.resolve(null),
3427
+ // DeFi fetch — prefer the audric snapshot's already-computed
3428
+ // value, but only when we can trust it. Two trust signals:
3429
+ // 1. `source === 'blockvision'` — fully successful fresh read
3430
+ // (even if value is 0, that's a confirmed empty position).
3431
+ // 2. `defiValueUsd > 0` — any positive value, regardless
3432
+ // of source. `partial-stale` with a positive total is fine,
3433
+ // `partial` with a positive total is the live equivalent.
3434
+ //
3435
+ // [Bug — 2026-04-28 round 2] Pre-fix the trust gate was
3436
+ // `defiSource !== 'degraded'`, which let `partial + 0` through
3437
+ // as authoritative. During a BlockVision 429 burst the audric
3438
+ // host's `/api/portfolio` returns `partial + 0` (some protocols
3439
+ // failed, the rest reported $0, no sticky-positive available
3440
+ // *in that process*) — but the engine's direct fetcher in the
3441
+ // chat route may have a sticky-positive in *this* Vercel
3442
+ // instance's cache. Trusting audric's $0 silently dropped the
3443
+ // DeFi line that `balance_check` (which always calls direct)
3444
+ // showed correctly on the same turn — same SSOT-divergence bug
3445
+ // class, manifested in a different layer.
3446
+ //
3447
+ // The new condition routes around audric's $0 in exactly that
3448
+ // case. When the direct fetch ALSO returns $0 the answer is
3449
+ // consistent across tools (both report degraded), which is the
3450
+ // honest UX during a real outage.
3451
+ audricSnapshot && (audricSnapshot.defiSource === "blockvision" || audricSnapshot.defiValueUsd > 0) ? Promise.resolve({
3452
+ totalUsd: audricSnapshot.defiValueUsd,
3453
+ perProtocol: {},
3454
+ pricedAt: Date.now(),
3455
+ source: audricSnapshot.defiSource
3456
+ }) : fetchAddressDefiPortfolio(address, context.blockvisionApiKey).catch(
3457
+ (err) => {
3458
+ console.warn("[portfolio_analysis] defi fetch failed:", err);
3459
+ return { totalUsd: 0, perProtocol: {}, pricedAt: Date.now(), source: "degraded" };
3460
+ }
3461
+ )
3310
3462
  ]);
3311
3463
  let walletValue = 0;
3312
3464
  const allAllocations = [];
@@ -3336,7 +3488,18 @@ var portfolioAnalysisTool = buildTool({
3336
3488
  if (weekHistResult?.change && weekHistResult.change.absoluteUsd !== 0) {
3337
3489
  weekChange = weekHistResult.change;
3338
3490
  }
3339
- const totalValue = walletValue + savingsValue;
3491
+ const defiValue = defiSummary.totalUsd;
3492
+ if (defiSummary.source !== "degraded") {
3493
+ for (const [protocol, usdValue] of Object.entries(defiSummary.perProtocol)) {
3494
+ if (typeof usdValue === "number" && usdValue >= DUST_USD) {
3495
+ const label = protocol.charAt(0).toUpperCase() + protocol.slice(1) + " DeFi";
3496
+ allocations.push({ symbol: label, amount: 0, usdValue, percentage: 0 });
3497
+ }
3498
+ }
3499
+ } else if (defiValue > 0) {
3500
+ allocations.push({ symbol: "DeFi (aggregate)", amount: 0, usdValue: defiValue, percentage: 0 });
3501
+ }
3502
+ const totalValue = walletValue + savingsValue + defiValue;
3340
3503
  for (const a of allocations) {
3341
3504
  a.percentage = totalValue > 0 ? a.usdValue / totalValue * 100 : 0;
3342
3505
  }
@@ -3374,10 +3537,23 @@ var portfolioAnalysisTool = buildTool({
3374
3537
  message: "Portfolio is concentrated in a single asset."
3375
3538
  });
3376
3539
  }
3540
+ if (defiSummary.source === "degraded") {
3541
+ insights.push({
3542
+ type: "warning",
3543
+ message: "DeFi positions could not be loaded \u2014 total may under-count any Cetus/Bluefin/Suilend value."
3544
+ });
3545
+ } else if (defiSummary.source === "partial") {
3546
+ insights.push({
3547
+ type: "warning",
3548
+ message: "DeFi data is partial \u2014 at least one protocol failed; total may under-count."
3549
+ });
3550
+ }
3377
3551
  const result = {
3378
3552
  totalValue,
3379
3553
  walletValue,
3380
3554
  savingsValue,
3555
+ defiValue,
3556
+ defiSource: defiSummary.source,
3381
3557
  debtValue,
3382
3558
  healthFactor,
3383
3559
  allocations: allocations.slice(0, 10),
@@ -3388,7 +3564,8 @@ var portfolioAnalysisTool = buildTool({
3388
3564
  weekChange,
3389
3565
  priceSource: portfolio.source
3390
3566
  };
3391
- const topLine = `Total: $${totalValue.toFixed(2)} | Wallet: $${walletValue.toFixed(2)} | Savings: $${savingsValue.toFixed(2)}`;
3567
+ const defiSegment = defiValue > 0 ? ` | DeFi: $${defiValue.toFixed(2)}${defiSummary.source === "partial" ? " (partial)" : ""}` : "";
3568
+ const topLine = `Total: $${totalValue.toFixed(2)} | Wallet: $${walletValue.toFixed(2)} | Savings: $${savingsValue.toFixed(2)}${defiSegment}`;
3392
3569
  const insightLines = insights.map((i) => `${i.type === "warning" ? "\u26A0" : "\u2192"} ${i.message}`).join("\n");
3393
3570
  return {
3394
3571
  data: result,