@t2000/engine 0.46.16 → 0.47.0

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.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { ALL_NAVI_ASSETS, resolveSymbol, getDecimalsForCoinType, assertAllowedAsset, SUPPORTED_ASSETS, getSwapQuote, extractTransferDetails, classifyTransaction } from '@t2000/sdk';
2
+ import { ALL_NAVI_ASSETS, getDecimalsForCoinType, resolveSymbol, assertAllowedAsset, SUPPORTED_ASSETS, getSwapQuote, extractTransferDetails, classifyTransaction } from '@t2000/sdk';
3
+ import { randomUUID } from 'crypto';
3
4
  import { readdirSync, readFileSync } from 'fs';
4
5
  import { join } from 'path';
5
6
  import yaml from 'js-yaml';
@@ -225,44 +226,6 @@ function applyToolFlags(tools) {
225
226
  function getToolFlags(name) {
226
227
  return TOOL_FLAGS[name] ?? {};
227
228
  }
228
- var SUI_MAINNET_URL = "https://fullnode.mainnet.sui.io:443";
229
- var EXTRA_COINS = {
230
- "0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN": { symbol: "USDT", decimals: 6 }
231
- };
232
- async function fetchWalletCoins(address, rpcUrl) {
233
- const url = rpcUrl || SUI_MAINNET_URL;
234
- const res = await fetch(url, {
235
- method: "POST",
236
- headers: { "Content-Type": "application/json" },
237
- body: JSON.stringify({
238
- jsonrpc: "2.0",
239
- id: 1,
240
- method: "suix_getAllBalances",
241
- params: [address]
242
- }),
243
- signal: AbortSignal.timeout(8e3)
244
- });
245
- if (!res.ok) {
246
- throw new Error(`Sui RPC error: ${res.status} ${res.statusText}`);
247
- }
248
- const json = await res.json();
249
- if (json.error) {
250
- throw new Error(`Sui RPC error: ${json.error.message}`);
251
- }
252
- const balances = json.result ?? [];
253
- return balances.map((b) => {
254
- const extra = EXTRA_COINS[b.coinType];
255
- const symbol = extra?.symbol ?? resolveSymbol(b.coinType);
256
- const decimals = extra?.decimals ?? getDecimalsForCoinType(b.coinType);
257
- return {
258
- coinType: b.coinType,
259
- symbol,
260
- decimals,
261
- totalBalance: b.totalBalance,
262
- coinObjectCount: b.coinObjectCount
263
- };
264
- });
265
- }
266
229
 
267
230
  // src/navi-config.ts
268
231
  var NAVI_SERVER_NAME = "navi";
@@ -494,59 +457,265 @@ function parseMcpJson(content) {
494
457
  return text;
495
458
  }
496
459
  }
460
+ var SUI_MAINNET_URL = "https://fullnode.mainnet.sui.io:443";
461
+ var EXTRA_COINS = {
462
+ "0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN": { symbol: "USDT", decimals: 6 }
463
+ };
464
+ async function fetchWalletCoins(address, rpcUrl) {
465
+ const url = rpcUrl || SUI_MAINNET_URL;
466
+ const res = await fetch(url, {
467
+ method: "POST",
468
+ headers: { "Content-Type": "application/json" },
469
+ body: JSON.stringify({
470
+ jsonrpc: "2.0",
471
+ id: 1,
472
+ method: "suix_getAllBalances",
473
+ params: [address]
474
+ }),
475
+ signal: AbortSignal.timeout(8e3)
476
+ });
477
+ if (!res.ok) {
478
+ throw new Error(`Sui RPC error: ${res.status} ${res.statusText}`);
479
+ }
480
+ const json = await res.json();
481
+ if (json.error) {
482
+ throw new Error(`Sui RPC error: ${json.error.message}`);
483
+ }
484
+ const balances = json.result ?? [];
485
+ return balances.map((b) => {
486
+ const extra = EXTRA_COINS[b.coinType];
487
+ const symbol = extra?.symbol ?? resolveSymbol(b.coinType);
488
+ const decimals = extra?.decimals ?? getDecimalsForCoinType(b.coinType);
489
+ return {
490
+ coinType: b.coinType,
491
+ symbol,
492
+ decimals,
493
+ totalBalance: b.totalBalance,
494
+ coinObjectCount: b.coinObjectCount
495
+ };
496
+ });
497
+ }
497
498
 
498
- // src/defillama-prices.ts
499
- var DEFILLAMA_PRICES_URL = "https://coins.llama.fi/prices/current";
500
- var CACHE_TTL = 6e4;
501
- var cache = null;
502
- var pendingFetches = /* @__PURE__ */ new Map();
503
- async function fetchTokenPrices(coinTypes) {
504
- if (coinTypes.length === 0) return {};
499
+ // src/blockvision-prices.ts
500
+ var BLOCKVISION_BASE = "https://api.blockvision.org/v2/sui";
501
+ var PORTFOLIO_TIMEOUT_MS = 4e3;
502
+ var PRICES_TIMEOUT_MS = 3e3;
503
+ var CACHE_TTL_MS = 6e4;
504
+ var PRICE_LIST_CHUNK = 10;
505
+ var STABLE_USD_PRICES = {
506
+ "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC": 1,
507
+ "0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT": 1,
508
+ "0x41d587e5336f1c86cad50d38a7136db99333bb9bda91cea4ba69115defeb1402::sui_usde::SUI_USDE": 1,
509
+ "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI": 1,
510
+ "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN": 1,
511
+ "0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN": 1
512
+ };
513
+ var portfolioCache = /* @__PURE__ */ new Map();
514
+ var portfolioInflight = /* @__PURE__ */ new Map();
515
+ var priceMapCache = null;
516
+ async function fetchAddressPortfolio(address, apiKey, fallbackRpcUrl) {
505
517
  const now = Date.now();
506
- const cacheValid = cache !== null && now - cache.ts < CACHE_TTL;
507
- const cachedPrices = cacheValid ? cache.prices : {};
508
- const missing = coinTypes.filter((ct) => !(ct in cachedPrices));
509
- if (missing.length === 0) return cachedPrices;
510
- const sig = missing.slice().sort().join("|");
511
- let inflight = pendingFetches.get(sig);
512
- if (!inflight) {
513
- inflight = doFetch(missing).finally(() => {
514
- pendingFetches.delete(sig);
518
+ const cached = portfolioCache.get(address);
519
+ if (cached && now - cached.ts < CACHE_TTL_MS) {
520
+ return cached.data;
521
+ }
522
+ let inflight = portfolioInflight.get(address);
523
+ if (inflight) return inflight;
524
+ inflight = (async () => {
525
+ try {
526
+ if (apiKey && apiKey.trim().length > 0) {
527
+ const blockvision = await fetchPortfolioFromBlockVision(address, apiKey);
528
+ if (blockvision) {
529
+ portfolioCache.set(address, { data: blockvision, ts: Date.now() });
530
+ return blockvision;
531
+ }
532
+ }
533
+ const degraded = await fetchPortfolioFromSuiRpc(address, fallbackRpcUrl);
534
+ portfolioCache.set(address, { data: degraded, ts: Date.now() });
535
+ return degraded;
536
+ } finally {
537
+ portfolioInflight.delete(address);
538
+ }
539
+ })();
540
+ portfolioInflight.set(address, inflight);
541
+ return inflight;
542
+ }
543
+ async function fetchPortfolioFromBlockVision(address, apiKey) {
544
+ const url = `${BLOCKVISION_BASE}/account/coins?account=${encodeURIComponent(address)}`;
545
+ let res;
546
+ try {
547
+ res = await fetch(url, {
548
+ headers: { "x-api-key": apiKey, accept: "application/json" },
549
+ signal: AbortSignal.timeout(PORTFOLIO_TIMEOUT_MS)
515
550
  });
516
- pendingFetches.set(sig, inflight);
551
+ } catch (err) {
552
+ console.warn("[blockvision-prices] portfolio fetch threw, degrading:", err);
553
+ return null;
517
554
  }
518
- const fresh = await inflight;
519
- const merged = { ...cachedPrices, ...fresh };
520
- cache = { prices: merged, ts: cacheValid ? cache.ts : now };
521
- return merged;
555
+ if (!res.ok) {
556
+ console.warn(`[blockvision-prices] portfolio HTTP ${res.status}, degrading`);
557
+ return null;
558
+ }
559
+ let json;
560
+ try {
561
+ json = await res.json();
562
+ } catch (err) {
563
+ console.warn("[blockvision-prices] portfolio JSON parse failed, degrading:", err);
564
+ return null;
565
+ }
566
+ if (json.code !== 200 || !json.result) {
567
+ console.warn(`[blockvision-prices] portfolio code=${json.code} msg=${json.message}, degrading`);
568
+ return null;
569
+ }
570
+ const rawCoins = json.result.coins ?? [];
571
+ const coins = rawCoins.map((c) => {
572
+ const coinType = c.coinType;
573
+ const symbol = c.symbol || resolveSymbol(coinType);
574
+ const decimals = typeof c.decimals === "number" ? c.decimals : getDecimalsForCoinType(coinType);
575
+ const stablePrice = STABLE_USD_PRICES[coinType];
576
+ const apiPrice = parseNumberOrNull(c.price);
577
+ const apiUsd = parseNumberOrNull(c.usdValue);
578
+ const price = apiPrice ?? stablePrice ?? null;
579
+ let usdValue = apiUsd;
580
+ if (usdValue == null && price != null) {
581
+ const amount = Number(c.balance) / 10 ** decimals;
582
+ usdValue = Number.isFinite(amount) ? amount * price : null;
583
+ }
584
+ return {
585
+ coinType,
586
+ symbol,
587
+ decimals,
588
+ balance: c.balance,
589
+ price,
590
+ usdValue
591
+ };
592
+ });
593
+ const apiTotal = parseNumberOrNull(json.result.usdValue);
594
+ const totalUsd = apiTotal ?? coins.reduce((sum, c) => sum + (c.usdValue ?? 0), 0);
595
+ return {
596
+ coins,
597
+ totalUsd,
598
+ pricedAt: Date.now(),
599
+ source: "blockvision"
600
+ };
522
601
  }
523
- async function doFetch(coinTypes) {
524
- const coins = coinTypes.map((ct) => `sui:${ct}`).join(",");
525
- const url = `${DEFILLAMA_PRICES_URL}/${encodeURIComponent(coins)}`;
526
- const res = await fetch(url, {
527
- signal: AbortSignal.timeout(1e4)
602
+ async function fetchPortfolioFromSuiRpc(address, fallbackRpcUrl) {
603
+ const walletCoins = await fetchWalletCoins(address, fallbackRpcUrl).catch((err) => {
604
+ console.warn("[blockvision-prices] sui rpc coin fetch failed:", err);
605
+ return [];
528
606
  });
529
- if (!res.ok) {
530
- console.warn(`[defillama-prices] HTTP ${res.status} from ${DEFILLAMA_PRICES_URL}`);
531
- return cache?.prices ?? {};
607
+ const coins = walletCoins.map((c) => {
608
+ const stablePrice = STABLE_USD_PRICES[c.coinType] ?? null;
609
+ const amount = Number(c.totalBalance) / 10 ** c.decimals;
610
+ const usdValue = stablePrice != null && Number.isFinite(amount) ? amount * stablePrice : null;
611
+ return {
612
+ coinType: c.coinType,
613
+ symbol: c.symbol,
614
+ decimals: c.decimals,
615
+ balance: c.totalBalance,
616
+ price: stablePrice,
617
+ usdValue
618
+ };
619
+ });
620
+ const totalUsd = coins.reduce((sum, c) => sum + (c.usdValue ?? 0), 0);
621
+ return {
622
+ coins,
623
+ totalUsd,
624
+ pricedAt: Date.now(),
625
+ source: "sui-rpc-degraded"
626
+ };
627
+ }
628
+ async function fetchTokenPrices(coinTypes, apiKey) {
629
+ if (coinTypes.length === 0) return {};
630
+ const now = Date.now();
631
+ const cacheValid = priceMapCache !== null && now - priceMapCache.ts < CACHE_TTL_MS;
632
+ const cached = cacheValid ? priceMapCache.prices : {};
633
+ const result = {};
634
+ const stillMissing = [];
635
+ for (const coinType of coinTypes) {
636
+ if (cached[coinType]) {
637
+ result[coinType] = cached[coinType];
638
+ continue;
639
+ }
640
+ const stable = STABLE_USD_PRICES[coinType];
641
+ if (typeof stable === "number") {
642
+ result[coinType] = { price: stable };
643
+ continue;
644
+ }
645
+ stillMissing.push(coinType);
532
646
  }
533
- const json = await res.json();
534
- const prices = {};
535
- if (json.coins) {
536
- for (const [key, val] of Object.entries(json.coins)) {
537
- const coinType = key.replace(/^sui:/, "");
538
- prices[coinType] = val.price;
647
+ if (stillMissing.length === 0) return result;
648
+ if (!apiKey || apiKey.trim().length === 0) {
649
+ return result;
650
+ }
651
+ const fetched = await fetchPricesFromBlockVision(stillMissing, apiKey);
652
+ Object.assign(result, fetched);
653
+ const merged = { ...cached, ...fetched };
654
+ priceMapCache = { prices: merged, ts: cacheValid ? priceMapCache.ts : now };
655
+ return result;
656
+ }
657
+ async function fetchPricesFromBlockVision(coinTypes, apiKey) {
658
+ const out = {};
659
+ for (let i = 0; i < coinTypes.length; i += PRICE_LIST_CHUNK) {
660
+ const chunk = coinTypes.slice(i, i + PRICE_LIST_CHUNK);
661
+ const tokenIds = encodeURIComponent(chunk.join(","));
662
+ const url = `${BLOCKVISION_BASE}/coin/price/list?tokenIds=${tokenIds}&show24hChange=true`;
663
+ let res;
664
+ try {
665
+ res = await fetch(url, {
666
+ headers: { "x-api-key": apiKey, accept: "application/json" },
667
+ signal: AbortSignal.timeout(PRICES_TIMEOUT_MS)
668
+ });
669
+ } catch (err) {
670
+ console.warn("[blockvision-prices] price chunk threw, skipping:", err);
671
+ continue;
672
+ }
673
+ if (!res.ok) {
674
+ console.warn(`[blockvision-prices] price chunk HTTP ${res.status}`);
675
+ continue;
676
+ }
677
+ let json;
678
+ try {
679
+ json = await res.json();
680
+ } catch (err) {
681
+ console.warn("[blockvision-prices] price chunk JSON parse failed:", err);
682
+ continue;
683
+ }
684
+ if (json.code !== 200 || !json.result) continue;
685
+ const prices = json.result.prices ?? {};
686
+ const changes = json.result.coin24HChange ?? {};
687
+ for (const [coinType, priceStr] of Object.entries(prices)) {
688
+ const price = parseNumberOrNull(priceStr);
689
+ if (price == null) continue;
690
+ const change24h = parseNumberOrNull(changes[coinType]);
691
+ out[coinType] = change24h == null ? { price } : { price, change24h };
539
692
  }
540
693
  }
541
- cache = { prices, ts: Date.now() };
542
- return prices;
694
+ return out;
695
+ }
696
+ function parseNumberOrNull(input) {
697
+ if (typeof input === "number") return Number.isFinite(input) ? input : null;
698
+ if (typeof input !== "string" || input.trim().length === 0) return null;
699
+ const n = Number(input);
700
+ return Number.isFinite(n) ? n : null;
701
+ }
702
+ function clearPortfolioCache() {
703
+ portfolioCache.clear();
704
+ portfolioInflight.clear();
543
705
  }
544
- function clearPriceCache() {
545
- cache = null;
706
+ function clearPortfolioCacheFor(address) {
707
+ portfolioCache.delete(address);
708
+ portfolioInflight.delete(address);
709
+ }
710
+ function clearPriceMapCache() {
711
+ priceMapCache = null;
546
712
  }
547
713
 
548
714
  // src/tools/balance.ts
549
715
  var GAS_RESERVE_SUI2 = 0.05;
716
+ var VSUI_COIN_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
717
+ var SUI_COIN_TYPE = "0x2::sui::SUI";
718
+ var VSUI_FALLBACK_RATE = 1.05;
550
719
  async function callNavi(manager, tool, args = {}) {
551
720
  const result = await manager.callTool(NAVI_SERVER_NAME, tool, args);
552
721
  if (result.isError) {
@@ -555,83 +724,103 @@ async function callNavi(manager, tool, args = {}) {
555
724
  }
556
725
  return parseMcpJson(result.content);
557
726
  }
727
+ async function applyVsuiPriceFallback(portfolio) {
728
+ const vsuiIdx = portfolio.coins.findIndex((c) => c.coinType === VSUI_COIN_TYPE);
729
+ if (vsuiIdx === -1) return;
730
+ const vsui = portfolio.coins[vsuiIdx];
731
+ if (vsui.price != null) return;
732
+ const suiCoin = portfolio.coins.find((c) => c.coinType === SUI_COIN_TYPE);
733
+ const suiPrice = suiCoin?.price ?? null;
734
+ if (suiPrice == null) return;
735
+ let rate = VSUI_FALLBACK_RATE;
736
+ try {
737
+ const statsRes = await fetch("https://open-api.naviprotocol.io/api/volo/stats", {
738
+ signal: AbortSignal.timeout(5e3)
739
+ });
740
+ if (statsRes.ok) {
741
+ const json = await statsRes.json();
742
+ const data = json.data ?? json;
743
+ rate = data.exchange_rate ?? data.exchangeRate ?? VSUI_FALLBACK_RATE;
744
+ }
745
+ } catch {
746
+ }
747
+ const price = rate * suiPrice;
748
+ const amount = Number(vsui.balance) / 10 ** vsui.decimals;
749
+ const usdValue = Number.isFinite(amount) ? amount * price : null;
750
+ const previousUsd = vsui.usdValue ?? 0;
751
+ portfolio.coins[vsuiIdx] = { ...vsui, price, usdValue };
752
+ if (usdValue != null) {
753
+ portfolio.totalUsd = portfolio.totalUsd - previousUsd + usdValue;
754
+ }
755
+ }
756
+ async function loadPortfolio(address, blockvisionApiKey, fallbackRpcUrl, cache) {
757
+ if (cache) {
758
+ const hit = cache.get(address);
759
+ if (hit) return hit;
760
+ }
761
+ const portfolio = await fetchAddressPortfolio(address, blockvisionApiKey, fallbackRpcUrl);
762
+ if (cache) cache.set(address, portfolio);
763
+ return portfolio;
764
+ }
558
765
  var balanceCheckTool = buildTool({
559
766
  name: "balance_check",
560
767
  description: "Get the user's full balance breakdown. Returns wallet holdings (tokens the user owns \u2014 NOT savings), NAVI savings deposits (USDC deposited into NAVI Protocol earning yield), outstanding debt, pending rewards, gas reserve, total net worth, and saveableUsdc (only USDC can be deposited into savings). IMPORTANT: wallet holdings like GOLD, SUI, USDT are NOT savings positions \u2014 they are just tokens sitting in the wallet.",
561
768
  inputSchema: z.object({}),
562
769
  jsonSchema: { type: "object", properties: {}, required: [] },
563
770
  isReadOnly: true,
564
- // [v1.5.1] Wallet contents change after every send/swap/save/etc.
565
- // Microcompact must NEVER dedupe these calls each one reflects a
566
- // different on-chain state.
771
+ // [v1.4 BlockVision] Wallet contents change after every send / swap /
772
+ // save / etc. and the price half of this result is sourced from
773
+ // BlockVision's Indexer REST API. Microcompact must NEVER dedupe these
774
+ // calls — each one reflects a different on-chain + market snapshot.
567
775
  cacheable: false,
568
776
  async call(_input, context) {
569
777
  if (hasNaviMcp(context)) {
570
778
  const address = getWalletAddress(context);
571
779
  const mgr = getMcpManager(context);
572
780
  const hasPositionFetcher = !!(context.positionFetcher && context.walletAddress);
573
- const [walletCoins, positions, rewards] = await Promise.all([
574
- fetchWalletCoins(address, context.suiRpcUrl).catch((err) => {
575
- console.warn("[balance_check] Sui RPC coin fetch failed, falling back to MCP:", err);
576
- return null;
781
+ const [portfolio, positions, rewards, serverPositions] = await Promise.all([
782
+ loadPortfolio(
783
+ address,
784
+ context.blockvisionApiKey,
785
+ context.suiRpcUrl,
786
+ context.portfolioCache
787
+ ).catch((err) => {
788
+ console.warn("[balance_check] portfolio fetch failed, returning empty:", err);
789
+ const fallback = {
790
+ coins: [],
791
+ totalUsd: 0,
792
+ pricedAt: Date.now(),
793
+ source: "sui-rpc-degraded"
794
+ };
795
+ return fallback;
577
796
  }),
578
797
  hasPositionFetcher ? Promise.resolve(null) : callNavi(mgr, NaviTools.GET_POSITIONS, {
579
798
  address,
580
799
  protocols: "navi",
581
800
  format: "json"
801
+ }).catch((err) => {
802
+ console.warn("[balance_check] NAVI GET_POSITIONS failed:", err);
803
+ return null;
582
804
  }),
583
- hasPositionFetcher ? Promise.resolve(null) : callNavi(mgr, NaviTools.GET_AVAILABLE_REWARDS, { address })
584
- ]);
585
- let coins = walletCoins;
586
- if (!coins || coins.length === 0) {
587
- const mcpCoins = await callNavi(mgr, NaviTools.GET_COINS, { address }).catch(() => []);
588
- const coinArr = Array.isArray(mcpCoins) ? mcpCoins : [];
589
- coins = coinArr.map((c) => ({
590
- coinType: c.coinType ?? "",
591
- symbol: c.symbol ?? "",
592
- decimals: c.decimals ?? getDecimalsForCoinType(c.coinType ?? ""),
593
- totalBalance: c.totalBalance ?? "0",
594
- coinObjectCount: 0
595
- }));
596
- }
597
- const VSUI_COIN_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
598
- const coinTypes = coins.map((c) => c.coinType).filter(Boolean);
599
- const [prices, serverPositions] = await Promise.all([
600
- fetchTokenPrices(coinTypes).catch((err) => {
601
- console.warn("[balance_check] DefiLlama price fetch failed:", err);
602
- return {};
805
+ hasPositionFetcher ? Promise.resolve(null) : callNavi(mgr, NaviTools.GET_AVAILABLE_REWARDS, { address }).catch((err) => {
806
+ console.warn("[balance_check] NAVI GET_AVAILABLE_REWARDS failed:", err);
807
+ return null;
603
808
  }),
604
809
  hasPositionFetcher ? context.positionFetcher(context.walletAddress).catch((err) => {
605
810
  console.warn("[balance_check] positionFetcher failed:", err);
606
811
  return null;
607
812
  }) : Promise.resolve(null)
608
813
  ]);
609
- if (coins.some((c) => c.coinType === VSUI_COIN_TYPE) && !prices[VSUI_COIN_TYPE]) {
610
- try {
611
- const statsRes = await fetch("https://open-api.naviprotocol.io/api/volo/stats", {
612
- signal: AbortSignal.timeout(5e3)
613
- });
614
- if (statsRes.ok) {
615
- const statsJson = await statsRes.json();
616
- const d = statsJson.data ?? statsJson;
617
- const rate = d.exchange_rate ?? d.exchangeRate ?? 1.05;
618
- const suiPrice = prices["0x2::sui::SUI"] ?? 0;
619
- prices[VSUI_COIN_TYPE] = rate * suiPrice;
620
- }
621
- } catch {
622
- const suiPrice = prices["0x2::sui::SUI"] ?? 0;
623
- prices[VSUI_COIN_TYPE] = suiPrice * 1.05;
624
- }
625
- }
814
+ await applyVsuiPriceFallback(portfolio);
626
815
  let availableUsd = 0;
627
816
  let stablesUsd = 0;
628
817
  let gasReserveUsd2 = 0;
629
818
  const STABLE_SYMBOLS = /* @__PURE__ */ new Set(["USDC", "USDT", "USDe", "USDsui", "wUSDC", "wUSDT"]);
630
819
  const holdings = [];
631
- for (const coin of coins) {
632
- const balance2 = Number(coin.totalBalance) / 10 ** coin.decimals;
633
- const price = prices[coin.coinType] ?? 0;
634
- if (coin.symbol === "SUI" || coin.coinType === "0x2::sui::SUI") {
820
+ for (const coin of portfolio.coins) {
821
+ const balance2 = Number(coin.balance) / 10 ** coin.decimals;
822
+ const price = coin.price ?? 0;
823
+ if (coin.symbol === "SUI" || coin.coinType === SUI_COIN_TYPE) {
635
824
  const reserveAmount = Math.min(balance2, GAS_RESERVE_SUI2);
636
825
  gasReserveUsd2 = reserveAmount * price;
637
826
  availableUsd += (balance2 - reserveAmount) * price;
@@ -676,7 +865,8 @@ var balanceCheckTool = buildTool({
676
865
  total: availableUsd + savings + gasReserveUsd2 + pendingRewardsUsd - debt,
677
866
  stables: stablesUsd,
678
867
  holdings: visibleHoldings,
679
- saveableUsdc
868
+ saveableUsdc,
869
+ priceSource: portfolio.source
680
870
  };
681
871
  const holdingsList = visibleHoldings.map((h) => `${h.symbol}: ${h.balance < 1 ? h.balance.toFixed(6) : h.balance.toFixed(2)} ($${h.usdValue.toFixed(2)})`).join(", ");
682
872
  return {
@@ -993,7 +1183,6 @@ var healthCheckTool = buildTool({
993
1183
  };
994
1184
  }
995
1185
  });
996
- var YIELDS_API = "https://yields.llama.fi";
997
1186
  var STABLECOIN_SYMBOLS2 = /* @__PURE__ */ new Set([
998
1187
  "usdc",
999
1188
  "wusdc",
@@ -1027,24 +1216,9 @@ function applyFilters(rates, opts) {
1027
1216
  function formatRatesSummary(rates) {
1028
1217
  return Object.entries(rates).map(([asset, r]) => `${asset}: Save ${(r.saveApy * 100).toFixed(2)}% / Borrow ${(r.borrowApy * 100).toFixed(2)}%`).join(", ");
1029
1218
  }
1030
- async function fetchRatesFromDefiLlama() {
1031
- const res = await fetch(`${YIELDS_API}/pools`, { signal: AbortSignal.timeout(15e3) });
1032
- if (!res.ok) throw new Error(`DefiLlama API error: HTTP ${res.status}`);
1033
- const data = await res.json();
1034
- const naviPools = (data.data ?? []).filter(
1035
- (p) => p.chain === "Sui" && p.project === "navi-lending" && p.tvlUsd > 1e4
1036
- );
1037
- const result = {};
1038
- for (const pool of naviPools) {
1039
- const saveApy = (pool.apy ?? 0) / 100;
1040
- const borrowApy = pool.apyBorrow != null ? Math.abs(pool.apyBorrow) / 100 : 0;
1041
- result[pool.symbol] = { saveApy, borrowApy };
1042
- }
1043
- return result;
1044
- }
1045
1219
  var ratesInfoTool = buildTool({
1046
1220
  name: "rates_info",
1047
- description: 'NAVI Protocol lending markets ONLY (single-sided save/borrow, no impermanent-loss risk). Use this for stablecoin and bluechip lending yields. Renders a rich rates card. Filter args: `assets` (specific symbols like ["USDC"]), `stableOnly` (true to show only USD-pegged assets), `topN` (max rows in card, default 8, max 50). Do NOT call defillama_yield_pools in the same turn \u2014 that tool is for LP/farming pools with IL risk, not lending.',
1221
+ description: 'NAVI Protocol lending markets ONLY (single-sided save/borrow, no impermanent-loss risk). Use this for stablecoin and bluechip lending yields. Renders a rich rates card. Filter args: `assets` (specific symbols like ["USDC"]), `stableOnly` (true to show only USD-pegged assets), `topN` (max rows in card, default 8, max 50).',
1048
1222
  inputSchema: z.object({
1049
1223
  assets: z.array(z.string()).optional().describe('Filter to specific asset symbols (e.g. ["USDC"], ["USDC","USDT","USDSUI"]). Case-insensitive.'),
1050
1224
  stableOnly: z.boolean().optional().describe("When true, return only stablecoin markets (USDC, USDT, USDSUI, USDY, suiUSDT, etc.). Ignored when `assets` is supplied."),
@@ -1077,19 +1251,19 @@ var ratesInfoTool = buildTool({
1077
1251
  topN: input.topN ?? 8
1078
1252
  };
1079
1253
  if (hasNaviMcpGlobal(context)) {
1080
- const all2 = await fetchRates(getMcpManager(context));
1081
- const filtered2 = applyFilters(all2, opts);
1082
- return { data: filtered2, displayText: formatRatesSummary(filtered2) };
1254
+ const all = await fetchRates(getMcpManager(context));
1255
+ const filtered = applyFilters(all, opts);
1256
+ return { data: filtered, displayText: formatRatesSummary(filtered) };
1083
1257
  }
1084
1258
  if (hasAgent(context)) {
1085
1259
  const agent = requireAgent(context);
1086
- const all2 = await agent.rates();
1087
- const filtered2 = applyFilters(all2, opts);
1088
- return { data: filtered2, displayText: formatRatesSummary(filtered2) };
1260
+ const all = await agent.rates();
1261
+ const filtered = applyFilters(all, opts);
1262
+ return { data: filtered, displayText: formatRatesSummary(filtered) };
1089
1263
  }
1090
- const all = await fetchRatesFromDefiLlama();
1091
- const filtered = applyFilters(all, opts);
1092
- return { data: filtered, displayText: formatRatesSummary(filtered) };
1264
+ throw new Error(
1265
+ "rates_info: NAVI lending data is currently unavailable. Try again shortly."
1266
+ );
1093
1267
  }
1094
1268
  });
1095
1269
  function parseRpcTx(tx, address) {
@@ -1759,10 +1933,10 @@ Always use ISO-3166 country codes (GB not UK, US not USA). A return address ("fr
1759
1933
  });
1760
1934
  var MPP_GATEWAY2 = "https://mpp.t2000.ai";
1761
1935
  var CATALOG_URL = `${MPP_GATEWAY2}/api/services`;
1762
- var CACHE_TTL2 = 12e4;
1936
+ var CACHE_TTL = 12e4;
1763
1937
  var catalogCache = null;
1764
1938
  async function fetchCatalog() {
1765
- if (catalogCache && Date.now() - catalogCache.ts < CACHE_TTL2) {
1939
+ if (catalogCache && Date.now() - catalogCache.ts < CACHE_TTL) {
1766
1940
  return catalogCache.data;
1767
1941
  }
1768
1942
  const res = await fetch(CATALOG_URL, { signal: AbortSignal.timeout(1e4) });
@@ -2256,11 +2430,31 @@ var portfolioAnalysisTool = buildTool({
2256
2430
  if (!address) {
2257
2431
  throw new Error("No wallet address provided. Sign in first.");
2258
2432
  }
2259
- const rpcUrl = context.suiRpcUrl ?? "https://fullnode.mainnet.sui.io:443";
2260
2433
  const DUST_USD = 0.01;
2261
2434
  const apiUrl = context.env?.AUDRIC_INTERNAL_API_URL;
2262
- const [coins, positions, weekHistResult] = await Promise.all([
2263
- fetchWalletCoins(address, rpcUrl),
2435
+ const [portfolio, positions, weekHistResult] = await Promise.all([
2436
+ (async () => {
2437
+ if (context.portfolioCache) {
2438
+ const hit = context.portfolioCache.get(address);
2439
+ if (hit) return hit;
2440
+ }
2441
+ const fresh = await fetchAddressPortfolio(
2442
+ address,
2443
+ context.blockvisionApiKey,
2444
+ context.suiRpcUrl
2445
+ );
2446
+ context.portfolioCache?.set(address, fresh);
2447
+ return fresh;
2448
+ })().catch((err) => {
2449
+ console.warn("[portfolio_analysis] portfolio fetch failed:", err);
2450
+ const empty = {
2451
+ coins: [],
2452
+ totalUsd: 0,
2453
+ pricedAt: Date.now(),
2454
+ source: "sui-rpc-degraded"
2455
+ };
2456
+ return empty;
2457
+ }),
2264
2458
  context.positionFetcher ? context.positionFetcher(address).catch((err) => {
2265
2459
  console.warn("[portfolio_analysis] positionFetcher failed:", err);
2266
2460
  return null;
@@ -2270,14 +2464,12 @@ var portfolioAnalysisTool = buildTool({
2270
2464
  { headers: { "x-sui-address": address }, signal: context.signal }
2271
2465
  ).then((res) => res.ok ? res.json() : null).catch(() => null) : Promise.resolve(null)
2272
2466
  ]);
2273
- const nonZero = coins.filter((c) => Number(c.totalBalance) > 0);
2274
- const prices = await fetchTokenPrices(nonZero.map((c) => c.coinType)).catch(() => ({}));
2275
2467
  let walletValue = 0;
2276
2468
  const allAllocations = [];
2277
- for (const coin of nonZero) {
2278
- const amount = Number(coin.totalBalance) / 10 ** coin.decimals;
2279
- const price = prices[coin.coinType] ?? 0;
2280
- const usdValue = amount * price;
2469
+ for (const coin of portfolio.coins) {
2470
+ const amount = Number(coin.balance) / 10 ** coin.decimals;
2471
+ if (!Number.isFinite(amount) || amount <= 0) continue;
2472
+ const usdValue = coin.usdValue ?? (coin.price != null ? amount * coin.price : 0);
2281
2473
  walletValue += usdValue;
2282
2474
  allAllocations.push({ symbol: coin.symbol, amount, usdValue, percentage: 0 });
2283
2475
  }
@@ -2349,7 +2541,8 @@ var portfolioAnalysisTool = buildTool({
2349
2541
  insights,
2350
2542
  savingsApy,
2351
2543
  dailyEarning,
2352
- weekChange
2544
+ weekChange,
2545
+ priceSource: portfolio.source
2353
2546
  };
2354
2547
  const topLine = `Total: $${totalValue.toFixed(2)} | Wallet: $${walletValue.toFixed(2)} | Savings: $${savingsValue.toFixed(2)}`;
2355
2548
  const insightLines = insights.map((i) => `${i.type === "warning" ? "\u26A0" : "\u2192"} ${i.message}`).join("\n");
@@ -3095,335 +3288,59 @@ var activitySummaryTool = buildTool({
3095
3288
  }
3096
3289
  }
3097
3290
  });
3098
- var LLAMA_API2 = "https://api.llama.fi";
3099
- var YIELDS_API2 = "https://yields.llama.fi";
3100
- var COINS_API = "https://coins.llama.fi";
3101
- var CACHE_TTL3 = 6e4;
3102
- var apiCache = /* @__PURE__ */ new Map();
3103
- async function cachedFetch(url) {
3104
- const hit = apiCache.get(url);
3105
- if (hit && Date.now() - hit.ts < CACHE_TTL3) return hit.data;
3106
- const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
3107
- if (!res.ok) throw new Error(`DefiLlama API error: HTTP ${res.status}`);
3108
- const data = await res.json();
3109
- apiCache.set(url, { data, ts: Date.now() });
3110
- return data;
3111
- }
3112
- async function fetchDefillamaYieldPools() {
3113
- const data = await cachedFetch(`${YIELDS_API2}/pools`);
3114
- return data.data ?? [];
3115
- }
3116
- function fmtToolTvl(tvl) {
3117
- if (tvl >= 1e9) return `$${(tvl / 1e9).toFixed(1)}B`;
3118
- if (tvl >= 1e6) return `$${(tvl / 1e6).toFixed(1)}M`;
3119
- if (tvl >= 1e3) return `$${(tvl / 1e3).toFixed(0)}K`;
3120
- return `$${tvl}`;
3121
- }
3122
- var POOL_STABLE_LEGS = /* @__PURE__ */ new Set([
3123
- "USDC",
3124
- "WUSDC",
3125
- "USDT",
3126
- "WUSDT",
3127
- "SUIUSDT",
3128
- "USDY",
3129
- "USDSUI",
3130
- "USDE",
3131
- "AUSD",
3132
- "FDUSD",
3133
- "BUCK",
3134
- "DAI",
3135
- "LUSD",
3136
- "FRAX",
3137
- "GUSD",
3138
- "PYUSD",
3139
- "USDS",
3140
- "CRVUSD"
3141
- ]);
3142
- var defillamaYieldPoolsTool = buildTool({
3143
- name: "defillama_yield_pools",
3144
- description: 'Cross-protocol LP / vault yields with IMPERMANENT-LOSS RISK (Cetus, Bluefin, Full Sail, etc.). ONLY call when the user explicitly asks about LP pools, DeFi farming, or "higher yield with more risk". For safe single-sided lending yields (USDC save, NAVI, etc.) use rates_info instead \u2014 NEVER both in the same turn. Filter by chain (e.g. "Sui"), project, and minimum TVL.',
3291
+ var tokenPricesTool = buildTool({
3292
+ name: "token_prices",
3293
+ description: 'Get current USD prices for Sui tokens, with optional 24h change. Accepts full coin type strings (e.g. "0x2::sui::SUI"). Returns price per token and (when requested) 24h change percentage. Use for "what is X worth?" or "did Y move today?". For balance + portfolio rendering, prefer balance_check / portfolio_analysis instead \u2014 they bundle the same prices into the standard cards.',
3145
3294
  inputSchema: z.object({
3146
- chain: z.string().optional().describe('Filter by chain name (e.g. "Sui", "Ethereum")'),
3147
- project: z.string().optional().describe('Filter by protocol project name (e.g. "cetus-clmm", "bluefin-spot")'),
3148
- limit: z.number().min(1).max(20).optional().describe("Max results (default 5)"),
3149
- minTvl: z.number().optional().describe("Minimum TVL in USD to filter out small/risky pools (default 100000)"),
3150
- stableOnly: z.boolean().optional().describe('When true, only return pools where every leg is a stablecoin (USDC, USDT, USDSUI, etc.). Use this for "show stablecoin yield options" \u2014 keeps volatile-pair LPs (WAL-SUI, DEEP-SUI) out.')
3295
+ coinTypes: z.array(z.string()).min(1).max(10).describe("Array of Sui coin type strings (max 10 per call)."),
3296
+ include24hChange: z.boolean().optional().describe("When true, include 24h change percentage per token in the output.")
3151
3297
  }),
3152
3298
  jsonSchema: {
3153
3299
  type: "object",
3154
3300
  properties: {
3155
- chain: { type: "string", description: "Filter by chain name" },
3156
- project: { type: "string", description: 'Filter by protocol project name (e.g. "cetus-clmm")' },
3157
- limit: { type: "number", description: "Max results (default 5)" },
3158
- minTvl: { type: "number", description: "Minimum TVL in USD (default 100000)" },
3159
- stableOnly: { type: "boolean", description: "When true, only return all-stablecoin pools." }
3160
- },
3161
- required: []
3162
- },
3163
- isReadOnly: true,
3164
- maxResultSizeChars: 6e3,
3165
- async call(input) {
3166
- if (!input.chain && !input.project) {
3167
- const all = await fetchDefillamaYieldPools();
3168
- const chainCounts = /* @__PURE__ */ new Map();
3169
- for (const p of all) {
3170
- chainCounts.set(p.chain, (chainCounts.get(p.chain) ?? 0) + 1);
3301
+ coinTypes: {
3302
+ type: "array",
3303
+ items: { type: "string" },
3304
+ description: "Sui coin type strings (max 10)."
3305
+ },
3306
+ include24hChange: {
3307
+ type: "boolean",
3308
+ description: "Include 24h change percentage per token."
3171
3309
  }
3172
- const topChains = [...chainCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([chain, count]) => ({ chain, pools: count }));
3173
- return {
3174
- data: {
3175
- _refine: {
3176
- reason: "Cross-chain yield search is too broad; pick a chain.",
3177
- suggestedParams: { chain: "Sui" },
3178
- availableChains: topChains
3179
- }
3180
- },
3181
- displayText: "Yield query needs a chain filter. Common chains: " + topChains.map((c) => c.chain).join(", ")
3182
- };
3183
- }
3184
- let pools = await fetchDefillamaYieldPools();
3185
- if (input.chain) {
3186
- const chain = input.chain.toLowerCase();
3187
- pools = pools.filter((p) => p.chain.toLowerCase() === chain);
3188
- }
3189
- if (input.project) {
3190
- const project = input.project.toLowerCase();
3191
- pools = pools.filter((p) => p.project.toLowerCase() === project);
3192
- }
3193
- const minTvl = input.minTvl ?? 1e5;
3194
- pools = pools.filter((p) => p.tvlUsd >= minTvl);
3195
- if (input.stableOnly) {
3196
- pools = pools.filter((p) => {
3197
- const legs = p.symbol.split("-");
3198
- return legs.every((leg) => POOL_STABLE_LEGS.has(leg.trim().toUpperCase()));
3199
- });
3200
- }
3201
- pools.sort((a, b) => b.apy - a.apy);
3202
- const limit = input.limit ?? 5;
3203
- const top = pools.slice(0, limit);
3204
- const results = top.map((p) => ({
3205
- pool: p.symbol,
3206
- protocol: p.project,
3207
- chain: p.chain,
3208
- apy: Math.round(p.apy * 100) / 100,
3209
- apyBase: p.apyBase != null ? Math.round(p.apyBase * 100) / 100 : void 0,
3210
- apyReward: p.apyReward != null ? Math.round(p.apyReward * 100) / 100 : void 0,
3211
- tvl: Math.round(p.tvlUsd)
3212
- }));
3213
- return {
3214
- data: results,
3215
- displayText: results.map((r) => `${r.pool} (${r.protocol}): ${r.apy}% APY, ${fmtToolTvl(r.tvl)} TVL`).join("\n")
3216
- };
3217
- }
3218
- });
3219
- var defillamaProtocolInfoTool = buildTool({
3220
- name: "defillama_protocol_info",
3221
- description: 'Get detailed info about a DeFi protocol: TVL, category, chains it operates on, and TVL changes. Use for "Is this protocol safe?" or "Tell me about NAVI."',
3222
- inputSchema: z.object({
3223
- name: z.string().describe('Protocol name (e.g. "navi-lending", "cetus")')
3224
- }),
3225
- jsonSchema: {
3226
- type: "object",
3227
- properties: {
3228
- name: { type: "string", description: 'Protocol slug (e.g. "navi-lending")' }
3229
- },
3230
- required: ["name"]
3231
- },
3232
- isReadOnly: true,
3233
- maxResultSizeChars: 4e3,
3234
- async call(input) {
3235
- const data = await cachedFetch(`${LLAMA_API2}/protocol/${encodeURIComponent(input.name)}`);
3236
- const result = {
3237
- name: data.name,
3238
- category: data.category,
3239
- chains: data.chains,
3240
- tvl: Math.round(data.tvl),
3241
- change1d: data.change_1d,
3242
- change7d: data.change_7d,
3243
- url: data.url,
3244
- description: data.description
3245
- };
3246
- return {
3247
- data: result,
3248
- displayText: `${result.name}: ${fmtToolTvl(result.tvl)} TVL (${result.category}) on ${result.chains.join(", ")}`
3249
- };
3250
- }
3251
- });
3252
- var defillamaTokenPricesTool = buildTool({
3253
- name: "defillama_token_prices",
3254
- description: 'Get current USD prices for Sui tokens. Accepts full coin type strings (e.g. "0x2::sui::SUI"). Returns price per token.',
3255
- inputSchema: z.object({
3256
- coinTypes: z.array(z.string()).min(1).max(10).describe("Array of Sui coin type strings")
3257
- }),
3258
- jsonSchema: {
3259
- type: "object",
3260
- properties: {
3261
- coinTypes: { type: "array", items: { type: "string" }, description: "Sui coin type strings" }
3262
3310
  },
3263
3311
  required: ["coinTypes"]
3264
3312
  },
3265
3313
  isReadOnly: true,
3266
- async call(input) {
3267
- const prices = await fetchTokenPrices(input.coinTypes);
3268
- const results = input.coinTypes.map((ct) => ({
3269
- coinType: ct,
3270
- symbol: ct.split("::").pop() ?? ct,
3271
- price: prices[ct] ?? null
3272
- }));
3273
- return {
3274
- data: results,
3275
- displayText: results.map((r) => `${r.symbol}: ${r.price != null ? `$${r.price.toFixed(4)}` : "price unavailable"}`).join(", ")
3276
- };
3277
- }
3278
- });
3279
- var defillamaPriceChangeTool = buildTool({
3280
- name: "defillama_price_change",
3281
- description: "Get price change for a Sui token over a period. Shows current price and historical price to calculate % change.",
3282
- inputSchema: z.object({
3283
- coinType: z.string().describe('Sui coin type (e.g. "0x2::sui::SUI")'),
3284
- period: z.enum(["1h", "24h", "7d", "30d"]).optional().describe('Period (default "24h")')
3285
- }),
3286
- jsonSchema: {
3287
- type: "object",
3288
- properties: {
3289
- coinType: { type: "string", description: "Sui coin type string" },
3290
- period: { type: "string", description: "Period: 1h, 24h, 7d, 30d" }
3291
- },
3292
- required: ["coinType"]
3293
- },
3294
- isReadOnly: true,
3295
- async call(input) {
3296
- const period = input.period ?? "24h";
3297
- const hoursMap = { "1h": 1, "24h": 24, "7d": 168, "30d": 720 };
3298
- const hours = hoursMap[period] ?? 24;
3299
- const historicalTs = Math.floor(Date.now() / 1e3) - hours * 3600;
3300
- const coinKey = `sui:${input.coinType}`;
3301
- const [current, historical] = await Promise.all([
3302
- cachedFetch(
3303
- `${COINS_API}/prices/current/${encodeURIComponent(coinKey)}`
3304
- ),
3305
- cachedFetch(
3306
- `${COINS_API}/prices/historical/${historicalTs}/${encodeURIComponent(coinKey)}`
3307
- )
3308
- ]);
3309
- const currentPrice = current.coins[coinKey]?.price;
3310
- const historicalPrice = historical.coins[coinKey]?.price;
3311
- const symbol = input.coinType.split("::").pop() ?? input.coinType;
3312
- if (currentPrice == null) {
3313
- return {
3314
- data: { symbol, currentPrice: 0, historicalPrice: null, change: null, period },
3315
- displayText: "Token price not available on DefiLlama."
3316
- };
3317
- }
3318
- const change = historicalPrice ? Math.round((currentPrice - historicalPrice) / historicalPrice * 1e4) / 100 : null;
3319
- return {
3320
- data: {
3314
+ async call(input, context) {
3315
+ const prices = await fetchTokenPrices(input.coinTypes, context.blockvisionApiKey);
3316
+ const results = input.coinTypes.map((coinType) => {
3317
+ const entry = prices[coinType];
3318
+ const symbol = coinType.split("::").pop() ?? coinType;
3319
+ if (!entry) {
3320
+ return {
3321
+ coinType,
3322
+ symbol,
3323
+ price: null,
3324
+ priceUnavailable: true
3325
+ };
3326
+ }
3327
+ const out = {
3328
+ coinType,
3321
3329
  symbol,
3322
- currentPrice,
3323
- historicalPrice: historicalPrice ?? null,
3324
- change,
3325
- period
3326
- },
3327
- displayText: change != null ? `${symbol}: $${currentPrice.toFixed(4)} (${change >= 0 ? "+" : ""}${change.toFixed(2)}% over ${period})` : `${symbol}: $${currentPrice.toFixed(4)}`
3328
- };
3329
- }
3330
- });
3331
- var defillamaChainTvlTool = buildTool({
3332
- name: "defillama_chain_tvl",
3333
- description: 'Get chain TVL rankings. Shows top chains by total value locked. Use for "How big is Sui?" or "Compare chains."',
3334
- inputSchema: z.object({
3335
- limit: z.number().min(1).max(20).optional().describe("Max results (default 10)")
3336
- }),
3337
- jsonSchema: {
3338
- type: "object",
3339
- properties: {
3340
- limit: { type: "number", description: "Max results (default 10)" }
3341
- },
3342
- required: []
3343
- },
3344
- isReadOnly: true,
3345
- async call(input) {
3346
- const data = await cachedFetch(`${LLAMA_API2}/v2/chains`);
3347
- const sorted = [...data].sort((a, b) => b.tvl - a.tvl);
3348
- const limit = input.limit ?? 10;
3349
- const top = sorted.slice(0, limit);
3350
- const results = top.map((c, i) => ({
3351
- rank: i + 1,
3352
- chain: c.name,
3353
- tvl: Math.round(c.tvl)
3354
- }));
3355
- return {
3356
- data: results,
3357
- displayText: results.map((r) => `#${r.rank} ${r.chain}: $${(r.tvl / 1e9).toFixed(2)}B`).join("\n")
3358
- };
3359
- }
3360
- });
3361
- var defillamaProtocolFeesTool = buildTool({
3362
- name: "defillama_protocol_fees",
3363
- description: 'Get protocol fee/revenue rankings. Shows which protocols earn the most in fees. Use for "Which protocols are most profitable?"',
3364
- inputSchema: z.object({
3365
- chain: z.string().optional().describe("Filter by chain"),
3366
- limit: z.number().min(1).max(20).optional().describe("Max results (default 5)")
3367
- }),
3368
- jsonSchema: {
3369
- type: "object",
3370
- properties: {
3371
- chain: { type: "string", description: "Filter by chain" },
3372
- limit: { type: "number", description: "Max results (default 5)" }
3373
- },
3374
- required: []
3375
- },
3376
- isReadOnly: true,
3377
- async call(input) {
3378
- const data = await cachedFetch(`${LLAMA_API2}/overview/fees`);
3379
- let protocols = data.protocols ?? [];
3380
- if (input.chain) {
3381
- const chain = input.chain.toLowerCase();
3382
- protocols = protocols.filter(
3383
- (p) => p.chains?.some((c) => c.toLowerCase() === chain)
3384
- );
3385
- }
3386
- protocols.sort((a, b) => (b.total24h ?? 0) - (a.total24h ?? 0));
3387
- const limit = input.limit ?? 5;
3388
- const top = protocols.slice(0, limit);
3389
- const results = top.map((p) => ({
3390
- name: p.name,
3391
- fees24h: p.total24h != null ? Math.round(p.total24h) : null,
3392
- fees7d: p.total7d != null ? Math.round(p.total7d) : null,
3393
- category: p.category
3394
- }));
3395
- return {
3396
- data: results,
3397
- displayText: results.map((r) => `${r.name}: $${r.fees24h != null ? (r.fees24h / 1e3).toFixed(1) + "K" : "?"}/day`).join("\n")
3398
- };
3399
- }
3400
- });
3401
- var defillamaSuiProtocolsTool = buildTool({
3402
- name: "defillama_sui_protocols",
3403
- description: "List top DeFi protocols on Sui by TVL. Shows name, TVL, category, and slug for each protocol. Use to discover protocols before calling defillama_protocol_info.",
3404
- inputSchema: z.object({
3405
- limit: z.number().int().min(1).max(50).optional().describe("Max protocols to return (default 10)")
3406
- }),
3407
- jsonSchema: {
3408
- type: "object",
3409
- properties: {
3410
- limit: { type: "number", description: "Max protocols to return (default 10)" }
3411
- }
3412
- },
3413
- isReadOnly: true,
3414
- async call(input) {
3415
- const limit = input.limit ?? 10;
3416
- const data = await cachedFetch(`${LLAMA_API2}/protocols`);
3417
- const suiProtocols = data.filter((p) => p.chains?.includes("Sui") && p.tvl > 0).sort((a, b) => b.tvl - a.tvl).slice(0, limit);
3418
- const results = suiProtocols.map((p) => ({
3419
- name: p.name,
3420
- slug: p.slug,
3421
- tvl: Math.round(p.tvl),
3422
- category: p.category
3423
- }));
3330
+ price: entry.price
3331
+ };
3332
+ if (input.include24hChange && entry.change24h !== void 0) {
3333
+ out.change24h = entry.change24h;
3334
+ }
3335
+ return out;
3336
+ });
3424
3337
  return {
3425
3338
  data: results,
3426
- displayText: results.map((r, i) => `${i + 1}. ${r.name} (${fmtToolTvl(r.tvl)} TVL, ${r.category})`).join("\n")
3339
+ displayText: results.map((r) => {
3340
+ if (r.price === null) return `${r.symbol}: price unavailable`;
3341
+ const change = r.change24h;
3342
+ return change !== void 0 ? `${r.symbol}: $${r.price.toFixed(4)} (${change >= 0 ? "+" : ""}${change.toFixed(2)}% 24h)` : `${r.symbol}: $${r.price.toFixed(4)}`;
3343
+ }).join(", ")
3427
3344
  };
3428
3345
  }
3429
3346
  });
@@ -3443,13 +3360,7 @@ var READ_TOOLS = [
3443
3360
  explainTxTool,
3444
3361
  portfolioAnalysisTool,
3445
3362
  protocolDeepDiveTool,
3446
- defillamaYieldPoolsTool,
3447
- defillamaProtocolInfoTool,
3448
- defillamaTokenPricesTool,
3449
- defillamaPriceChangeTool,
3450
- defillamaChainTvlTool,
3451
- defillamaProtocolFeesTool,
3452
- defillamaSuiProtocolsTool,
3363
+ tokenPricesTool,
3453
3364
  listPaymentLinksTool,
3454
3365
  cancelPaymentLinkTool,
3455
3366
  listInvoicesTool,
@@ -3512,7 +3423,7 @@ function getModifiableFields(toolName) {
3512
3423
  }
3513
3424
 
3514
3425
  // src/prompt.ts
3515
- var DEFAULT_SYSTEM_PROMPT = `You are Audric \u2014 a financial agent on Sui. Audric is exactly five products: Audric Passport (the trust layer \u2014 Google sign-in, non-custodial wallet, tap-to-confirm consent, sponsored gas \u2014 wraps every other product), Audric Intelligence (you \u2014 the 5-system brain: Agent Harness with 40 tools, Reasoning Engine with 9 guards and 7 skill recipes, Silent Profile, Chain Memory, AdviceLog), Audric Finance (manage money on Sui \u2014 Save via NAVI lending at 3-8% APY USDC, Credit via NAVI borrowing with health factor, Swap via Cetus aggregator across 20+ DEXs at 0.1% fee, Charts for yield/health/portfolio viz), Audric Pay (move money \u2014 send USDC, receive via payment links / invoices / QR; free, global, instant on Sui), and Audric Store (creator marketplace, ships Phase 5 \u2014 say "coming soon" if asked). Save, swap, borrow, repay, withdraw, charts \u2192 Audric Finance. Send, receive, payment-link, invoice, QR \u2192 Audric Pay. Your silent context (profile, memory, chain facts, advice log) shapes your replies but never surfaces as a notification \u2014 you act only when the user asks, and every write waits on their tap-to-confirm via Passport. You can also call 41 paid APIs (music, image, research, translation, weather, fulfilment) via MPP micropayments using the pay_api tool \u2014 this is an internal capability, not a promoted product, so only mention it when the user asks for something that needs it.
3426
+ var DEFAULT_SYSTEM_PROMPT = `You are Audric \u2014 a financial agent on Sui. Audric is exactly five products: Audric Passport (the trust layer \u2014 Google sign-in, non-custodial wallet, tap-to-confirm consent, sponsored gas \u2014 wraps every other product), Audric Intelligence (you \u2014 the 5-system brain: Agent Harness with 34 tools, Reasoning Engine with 9 guards and 7 skill recipes, Silent Profile, Chain Memory, AdviceLog), Audric Finance (manage money on Sui \u2014 Save via NAVI lending at 3-8% APY USDC, Credit via NAVI borrowing with health factor, Swap via Cetus aggregator across 20+ DEXs at 0.1% fee, Charts for yield/health/portfolio viz), Audric Pay (move money \u2014 send USDC, receive via payment links / invoices / QR; free, global, instant on Sui), and Audric Store (creator marketplace, ships Phase 5 \u2014 say "coming soon" if asked). Save, swap, borrow, repay, withdraw, charts \u2192 Audric Finance. Send, receive, payment-link, invoice, QR \u2192 Audric Pay. Your silent context (profile, memory, chain facts, advice log) shapes your replies but never surfaces as a notification \u2014 you act only when the user asks, and every write waits on their tap-to-confirm via Passport. You can also call 41 paid APIs (music, image, research, translation, weather, fulfilment) via MPP micropayments using the pay_api tool \u2014 this is an internal capability, not a promoted product, so only mention it when the user asks for something that needs it.
3516
3427
 
3517
3428
  ## Response rules
3518
3429
  - 1-2 sentences max. No bullet lists unless asked. No preambles.
@@ -3532,8 +3443,8 @@ Only offer to execute actions you have tools for. If you retrieved a quote, data
3532
3443
  ## Tool usage
3533
3444
  - Use tools proactively \u2014 don't refuse requests you can handle.
3534
3445
  - For real-world questions (weather, search, news, prices), use pay_api. Tell the user the cost first.
3535
- - For broad market data (yields across protocols, token prices, TVL, protocol comparisons), use defillama_* tools.
3536
- - To discover Sui protocols, use defillama_sui_protocols first, then defillama_protocol_info with the slug.
3446
+ - For NAVI lending APYs, use rates_info; for VOLO liquid staking stats, use volo_stats; for spot token prices, use token_prices.
3447
+ - For protocol-level due diligence (TVL, fees, audits, safety) on Sui DeFi protocols, use protocol_deep_dive with the slug.
3537
3448
  - Run multiple read-only tools in parallel when you need several data points.
3538
3449
  - If a tool errors, say what went wrong and what to try instead. One sentence.
3539
3450
 
@@ -3546,11 +3457,11 @@ Only offer to execute actions you have tools for. If you retrieved a quote, data
3546
3457
  ## Multi-step flows
3547
3458
  - "How much X for Y?": swap_quote first, then swap_execute if user confirms.
3548
3459
  - "Swap then save": swap_execute \u2192 balance_check \u2192 save_deposit. Confirm each step.
3549
- - "Buy $X of token": defillama_token_prices \u2192 calculate amount \u2192 swap_execute.
3550
- - "Best yield on SUI": compare rates_info (NAVI lending) + defillama_yield_pools (broader) + volo_stats.
3460
+ - "Buy $X of token": token_prices \u2192 calculate amount \u2192 swap_execute.
3461
+ - "Best yield on SUI": compare rates_info (NAVI lending) + volo_stats (vSUI liquid staking).
3551
3462
  - withdraw supports legacy positions: USDC, USDe, USDsui, SUI. Pass asset param to withdraw a specific token.
3552
3463
  - "Deposit SUI to earn yield": volo_stake for SUI liquid staking. save_deposit is USDC only.
3553
- - "What protocols are on Sui?": defillama_sui_protocols \u2192 defillama_protocol_info for details.
3464
+ - "Is protocol X safe?" / "Tell me about NAVI": protocol_deep_dive with the slug.
3554
3465
  - "Full account report" / "account summary" / "give me everything" / "complete overview": triggers the \`account_report\` recipe \u2014 when the recipe block appears, follow EVERY step including all six tool calls. Each step renders a distinct rich card; skipping a step means a missing card.
3555
3466
 
3556
3467
  ## Safety
@@ -4701,6 +4612,12 @@ var QueryEngine = class {
4701
4612
  sessionSpendUsd;
4702
4613
  onAutoExecuted;
4703
4614
  onGuardFired;
4615
+ // [v1.4 BlockVision] BlockVision Indexer API key + per-request portfolio
4616
+ // cache. Forwarded into every `ToolContext` build site so read tools
4617
+ // (`balance_check`, `portfolio_analysis`, future `token_prices`) hit the
4618
+ // shared host-paid endpoint and dedupe across each other within a turn.
4619
+ blockvisionApiKey;
4620
+ portfolioCache;
4704
4621
  // [v1.5] See `EngineConfig.postWriteRefresh` — drives the post-write
4705
4622
  // synthetic read injection in `resumeWithToolResult`.
4706
4623
  postWriteRefresh;
@@ -4750,6 +4667,8 @@ var QueryEngine = class {
4750
4667
  this.onAutoExecuted = config.onAutoExecuted;
4751
4668
  this.onGuardFired = config.onGuardFired;
4752
4669
  this.postWriteRefresh = config.postWriteRefresh;
4670
+ this.blockvisionApiKey = config.blockvisionApiKey;
4671
+ this.portfolioCache = config.portfolioCache;
4753
4672
  this.tools = config.tools ?? (config.agent ? getDefaultTools() : []);
4754
4673
  }
4755
4674
  /**
@@ -4867,8 +4786,14 @@ var QueryEngine = class {
4867
4786
  signal,
4868
4787
  priceCache: this.priceCache,
4869
4788
  permissionConfig: this.permissionConfig,
4870
- sessionSpendUsd: this.sessionSpendUsd
4789
+ sessionSpendUsd: this.sessionSpendUsd,
4790
+ blockvisionApiKey: this.blockvisionApiKey,
4791
+ portfolioCache: this.portfolioCache
4871
4792
  };
4793
+ if (this.walletAddress) {
4794
+ this.portfolioCache?.delete(this.walletAddress);
4795
+ clearPortfolioCacheFor(this.walletAddress);
4796
+ }
4872
4797
  if (!signal.aborted) {
4873
4798
  await new Promise((resolve) => {
4874
4799
  const t = setTimeout(resolve, 1500);
@@ -5014,7 +4939,9 @@ var QueryEngine = class {
5014
4939
  signal,
5015
4940
  priceCache: this.priceCache,
5016
4941
  permissionConfig: this.permissionConfig,
5017
- sessionSpendUsd: this.sessionSpendUsd
4942
+ sessionSpendUsd: this.sessionSpendUsd,
4943
+ blockvisionApiKey: this.blockvisionApiKey,
4944
+ portfolioCache: this.portfolioCache
5018
4945
  };
5019
4946
  try {
5020
4947
  const result = await tool.call(parsed.data, context);
@@ -5057,7 +4984,9 @@ var QueryEngine = class {
5057
4984
  signal,
5058
4985
  priceCache: this.priceCache,
5059
4986
  permissionConfig: this.permissionConfig,
5060
- sessionSpendUsd: this.sessionSpendUsd
4987
+ sessionSpendUsd: this.sessionSpendUsd,
4988
+ blockvisionApiKey: this.blockvisionApiKey,
4989
+ portfolioCache: this.portfolioCache
5061
4990
  };
5062
4991
  let turns = 0;
5063
4992
  let hasRetriedWithCleanHistory = false;
@@ -5421,7 +5350,8 @@ ${recipeCtx}`;
5421
5350
  );
5422
5351
  Promise.resolve().then(() => this.onAutoExecuted({
5423
5352
  toolName: toolEvent.toolName,
5424
- usdValue
5353
+ usdValue,
5354
+ walletAddress: this.walletAddress
5425
5355
  })).catch((err) => {
5426
5356
  console.warn("[engine] onAutoExecuted callback failed:", err);
5427
5357
  });
@@ -5476,6 +5406,7 @@ ${recipeCtx}`;
5476
5406
  const writeGuardInjections = pendingWrite.call._guardInjections;
5477
5407
  const modifiableFields = getModifiableFields(pendingWrite.call.name);
5478
5408
  const turnIndex = this.messages.filter((m) => m.role === "assistant").length;
5409
+ const attemptId = randomUUID();
5479
5410
  this.turnPaused = true;
5480
5411
  yield {
5481
5412
  type: "pending_action",
@@ -5492,7 +5423,8 @@ ${recipeCtx}`;
5492
5423
  })),
5493
5424
  ...writeGuardInjections?.length ? { guardInjections: writeGuardInjections } : {},
5494
5425
  ...modifiableFields?.length ? { modifiableFields } : {},
5495
- turnIndex
5426
+ turnIndex,
5427
+ attemptId
5496
5428
  }
5497
5429
  };
5498
5430
  return;
@@ -6748,6 +6680,6 @@ function sanitizeAnthropicMessages(messages) {
6748
6680
  return merged;
6749
6681
  }
6750
6682
 
6751
- export { AnthropicProvider, BalanceTracker, CANVAS_TEMPLATES, ContextBudget, CostTracker, DEFAULT_GUARD_CONFIG, DEFAULT_PERMISSION_CONFIG, DEFAULT_SYSTEM_PROMPT, EarlyToolDispatcher, McpClientManager, McpResponseCache, MemorySessionStore, NAVI_MCP_CONFIG, NAVI_MCP_URL, NAVI_SERVER_NAME, NaviTools, PERMISSION_PRESETS, QueryEngine, READ_TOOLS, RecipeRegistry, RetryTracker, TOOL_FLAGS, TOOL_MODIFIABLE_FIELDS, TxMutex, WRITE_TOOLS, activitySummaryTool, adaptAllMcpTools, adaptAllServerTools, adaptMcpTool, applyToolFlags, balanceCheckTool, borrowTool, budgetToolResult, buildCachedSystemPrompt, buildMcpTools, buildProactivenessInstructions, buildProfileContext, buildSelfEvaluationInstruction, buildStateContext, buildTool, claimRewardsTool, classifyEffort, clearPriceCache, compactMessages, createGuardRunnerState, defillamaChainTvlTool, defillamaPriceChangeTool, defillamaProtocolFeesTool, defillamaProtocolInfoTool, defillamaSuiProtocolsTool, defillamaTokenPricesTool, defillamaYieldPoolsTool, engineToSSE, estimateTokens, explainTxTool, extractConversationText, extractMcpText, fetchAvailableRewards, fetchBalance, fetchHealthFactor, fetchPositions, fetchProtocolStats, fetchRates, fetchSavings, fetchTokenPrices, fetchWalletCoins, findTool, getDefaultTools, getMcpManager, getModifiableFields, getToolFlags, getWalletAddress, guardArtifactPreview, guardStaleData, hasNaviMcp, healthCheckTool, loadRecipes, microcompact, mppServicesTool, parseMcpJson, parseRecipe, parseSSE, payApiTool, portfolioAnalysisTool, protocolDeepDiveTool, ratesInfoTool, registerEngineTools, renderCanvasTool, repayDebtTool, requireAgent, resolvePermissionTier, resolveUsdValue, runGuards, runTools, saveContactTool, saveDepositTool, savingsInfoTool, sendTransferTool, serializeSSE, spendingAnalyticsTool, swapExecuteTool, swapQuoteTool, toolNameToOperation, toolsToDefinitions, transactionHistoryTool, transformBalance, transformHealthFactor, transformPositions, transformRates, transformRewards, transformSavings, updateGuardStateAfterToolResult, validateHistory, voloStakeTool, voloStatsTool, voloUnstakeTool, webSearchTool, withdrawTool, yieldSummaryTool };
6683
+ export { AnthropicProvider, BalanceTracker, CANVAS_TEMPLATES, ContextBudget, CostTracker, DEFAULT_GUARD_CONFIG, DEFAULT_PERMISSION_CONFIG, DEFAULT_SYSTEM_PROMPT, EarlyToolDispatcher, McpClientManager, McpResponseCache, MemorySessionStore, NAVI_MCP_CONFIG, NAVI_MCP_URL, NAVI_SERVER_NAME, NaviTools, PERMISSION_PRESETS, QueryEngine, READ_TOOLS, RecipeRegistry, RetryTracker, TOOL_FLAGS, TOOL_MODIFIABLE_FIELDS, TxMutex, WRITE_TOOLS, activitySummaryTool, adaptAllMcpTools, adaptAllServerTools, adaptMcpTool, applyToolFlags, balanceCheckTool, borrowTool, budgetToolResult, buildCachedSystemPrompt, buildMcpTools, buildProactivenessInstructions, buildProfileContext, buildSelfEvaluationInstruction, buildStateContext, buildTool, claimRewardsTool, classifyEffort, clearPortfolioCache, clearPortfolioCacheFor, clearPriceMapCache, compactMessages, createGuardRunnerState, engineToSSE, estimateTokens, explainTxTool, extractConversationText, extractMcpText, fetchAddressPortfolio, fetchAvailableRewards, fetchBalance, fetchHealthFactor, fetchPositions, fetchProtocolStats, fetchRates, fetchSavings, fetchTokenPrices, fetchWalletCoins, findTool, getDefaultTools, getMcpManager, getModifiableFields, getToolFlags, getWalletAddress, guardArtifactPreview, guardStaleData, hasNaviMcp, healthCheckTool, loadRecipes, microcompact, mppServicesTool, parseMcpJson, parseRecipe, parseSSE, payApiTool, portfolioAnalysisTool, protocolDeepDiveTool, ratesInfoTool, registerEngineTools, renderCanvasTool, repayDebtTool, requireAgent, resolvePermissionTier, resolveUsdValue, runGuards, runTools, saveContactTool, saveDepositTool, savingsInfoTool, sendTransferTool, serializeSSE, spendingAnalyticsTool, swapExecuteTool, swapQuoteTool, tokenPricesTool, toolNameToOperation, toolsToDefinitions, transactionHistoryTool, transformBalance, transformHealthFactor, transformPositions, transformRates, transformRewards, transformSavings, updateGuardStateAfterToolResult, validateHistory, voloStakeTool, voloStatsTool, voloUnstakeTool, webSearchTool, withdrawTool, yieldSummaryTool };
6752
6684
  //# sourceMappingURL=index.js.map
6753
6685
  //# sourceMappingURL=index.js.map