@t2000/engine 0.46.16 → 0.47.1

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, normalizeCoinType, 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,277 @@ 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 original of coinTypes) {
636
+ const norm = normalizeCoinType(original);
637
+ if (cached[norm]) {
638
+ result[original] = cached[norm];
639
+ continue;
640
+ }
641
+ const stable = STABLE_USD_PRICES[norm];
642
+ if (typeof stable === "number") {
643
+ result[original] = { price: stable };
644
+ continue;
645
+ }
646
+ stillMissing.push(original);
532
647
  }
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;
648
+ if (stillMissing.length === 0) return result;
649
+ if (!apiKey || apiKey.trim().length === 0) {
650
+ return result;
651
+ }
652
+ const fetched = await fetchPricesFromBlockVision(stillMissing, apiKey);
653
+ Object.assign(result, fetched);
654
+ const cacheUpdates = {};
655
+ for (const [original, value] of Object.entries(fetched)) {
656
+ cacheUpdates[normalizeCoinType(original)] = value;
657
+ }
658
+ const merged = { ...cached, ...cacheUpdates };
659
+ priceMapCache = { prices: merged, ts: cacheValid ? priceMapCache.ts : now };
660
+ return result;
661
+ }
662
+ async function fetchPricesFromBlockVision(coinTypes, apiKey) {
663
+ const out = {};
664
+ const longToOriginal = /* @__PURE__ */ new Map();
665
+ for (const original of coinTypes) {
666
+ const long = normalizeCoinType(original);
667
+ if (!longToOriginal.has(long)) longToOriginal.set(long, original);
668
+ }
669
+ const longForms = Array.from(longToOriginal.keys());
670
+ for (let i = 0; i < longForms.length; i += PRICE_LIST_CHUNK) {
671
+ const chunk = longForms.slice(i, i + PRICE_LIST_CHUNK);
672
+ const tokenIds = encodeURIComponent(chunk.join(","));
673
+ const url = `${BLOCKVISION_BASE}/coin/price/list?tokenIds=${tokenIds}&show24hChange=true`;
674
+ let res;
675
+ try {
676
+ res = await fetch(url, {
677
+ headers: { "x-api-key": apiKey, accept: "application/json" },
678
+ signal: AbortSignal.timeout(PRICES_TIMEOUT_MS)
679
+ });
680
+ } catch (err) {
681
+ console.warn("[blockvision-prices] price chunk threw, skipping:", err);
682
+ continue;
683
+ }
684
+ if (!res.ok) {
685
+ console.warn(`[blockvision-prices] price chunk HTTP ${res.status}`);
686
+ continue;
687
+ }
688
+ let json;
689
+ try {
690
+ json = await res.json();
691
+ } catch (err) {
692
+ console.warn("[blockvision-prices] price chunk JSON parse failed:", err);
693
+ continue;
694
+ }
695
+ if (json.code !== 200 || !json.result) continue;
696
+ const prices = json.result.prices ?? {};
697
+ const changes = json.result.coin24HChange ?? {};
698
+ for (const [returnedType, priceStr] of Object.entries(prices)) {
699
+ const price = parseNumberOrNull(priceStr);
700
+ if (price == null) continue;
701
+ const original = longToOriginal.get(returnedType) ?? returnedType;
702
+ const change24h = parseNumberOrNull(changes[returnedType]);
703
+ out[original] = change24h == null ? { price } : { price, change24h };
539
704
  }
540
705
  }
541
- cache = { prices, ts: Date.now() };
542
- return prices;
706
+ return out;
707
+ }
708
+ function parseNumberOrNull(input) {
709
+ if (typeof input === "number") return Number.isFinite(input) ? input : null;
710
+ if (typeof input !== "string" || input.trim().length === 0) return null;
711
+ const n = Number(input);
712
+ return Number.isFinite(n) ? n : null;
713
+ }
714
+ function clearPortfolioCache() {
715
+ portfolioCache.clear();
716
+ portfolioInflight.clear();
543
717
  }
544
- function clearPriceCache() {
545
- cache = null;
718
+ function clearPortfolioCacheFor(address) {
719
+ portfolioCache.delete(address);
720
+ portfolioInflight.delete(address);
721
+ }
722
+ function clearPriceMapCache() {
723
+ priceMapCache = null;
546
724
  }
547
725
 
548
726
  // src/tools/balance.ts
549
727
  var GAS_RESERVE_SUI2 = 0.05;
728
+ var VSUI_COIN_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
729
+ var SUI_COIN_TYPE = "0x2::sui::SUI";
730
+ var VSUI_FALLBACK_RATE = 1.05;
550
731
  async function callNavi(manager, tool, args = {}) {
551
732
  const result = await manager.callTool(NAVI_SERVER_NAME, tool, args);
552
733
  if (result.isError) {
@@ -555,83 +736,103 @@ async function callNavi(manager, tool, args = {}) {
555
736
  }
556
737
  return parseMcpJson(result.content);
557
738
  }
739
+ async function applyVsuiPriceFallback(portfolio) {
740
+ const vsuiIdx = portfolio.coins.findIndex((c) => c.coinType === VSUI_COIN_TYPE);
741
+ if (vsuiIdx === -1) return;
742
+ const vsui = portfolio.coins[vsuiIdx];
743
+ if (vsui.price != null) return;
744
+ const suiCoin = portfolio.coins.find((c) => c.coinType === SUI_COIN_TYPE);
745
+ const suiPrice = suiCoin?.price ?? null;
746
+ if (suiPrice == null) return;
747
+ let rate = VSUI_FALLBACK_RATE;
748
+ try {
749
+ const statsRes = await fetch("https://open-api.naviprotocol.io/api/volo/stats", {
750
+ signal: AbortSignal.timeout(5e3)
751
+ });
752
+ if (statsRes.ok) {
753
+ const json = await statsRes.json();
754
+ const data = json.data ?? json;
755
+ rate = data.exchange_rate ?? data.exchangeRate ?? VSUI_FALLBACK_RATE;
756
+ }
757
+ } catch {
758
+ }
759
+ const price = rate * suiPrice;
760
+ const amount = Number(vsui.balance) / 10 ** vsui.decimals;
761
+ const usdValue = Number.isFinite(amount) ? amount * price : null;
762
+ const previousUsd = vsui.usdValue ?? 0;
763
+ portfolio.coins[vsuiIdx] = { ...vsui, price, usdValue };
764
+ if (usdValue != null) {
765
+ portfolio.totalUsd = portfolio.totalUsd - previousUsd + usdValue;
766
+ }
767
+ }
768
+ async function loadPortfolio(address, blockvisionApiKey, fallbackRpcUrl, cache) {
769
+ if (cache) {
770
+ const hit = cache.get(address);
771
+ if (hit) return hit;
772
+ }
773
+ const portfolio = await fetchAddressPortfolio(address, blockvisionApiKey, fallbackRpcUrl);
774
+ if (cache) cache.set(address, portfolio);
775
+ return portfolio;
776
+ }
558
777
  var balanceCheckTool = buildTool({
559
778
  name: "balance_check",
560
779
  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
780
  inputSchema: z.object({}),
562
781
  jsonSchema: { type: "object", properties: {}, required: [] },
563
782
  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.
783
+ // [v1.4 BlockVision] Wallet contents change after every send / swap /
784
+ // save / etc. and the price half of this result is sourced from
785
+ // BlockVision's Indexer REST API. Microcompact must NEVER dedupe these
786
+ // calls — each one reflects a different on-chain + market snapshot.
567
787
  cacheable: false,
568
788
  async call(_input, context) {
569
789
  if (hasNaviMcp(context)) {
570
790
  const address = getWalletAddress(context);
571
791
  const mgr = getMcpManager(context);
572
792
  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;
793
+ const [portfolio, positions, rewards, serverPositions] = await Promise.all([
794
+ loadPortfolio(
795
+ address,
796
+ context.blockvisionApiKey,
797
+ context.suiRpcUrl,
798
+ context.portfolioCache
799
+ ).catch((err) => {
800
+ console.warn("[balance_check] portfolio fetch failed, returning empty:", err);
801
+ const fallback = {
802
+ coins: [],
803
+ totalUsd: 0,
804
+ pricedAt: Date.now(),
805
+ source: "sui-rpc-degraded"
806
+ };
807
+ return fallback;
577
808
  }),
578
809
  hasPositionFetcher ? Promise.resolve(null) : callNavi(mgr, NaviTools.GET_POSITIONS, {
579
810
  address,
580
811
  protocols: "navi",
581
812
  format: "json"
813
+ }).catch((err) => {
814
+ console.warn("[balance_check] NAVI GET_POSITIONS failed:", err);
815
+ return null;
582
816
  }),
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 {};
817
+ hasPositionFetcher ? Promise.resolve(null) : callNavi(mgr, NaviTools.GET_AVAILABLE_REWARDS, { address }).catch((err) => {
818
+ console.warn("[balance_check] NAVI GET_AVAILABLE_REWARDS failed:", err);
819
+ return null;
603
820
  }),
604
821
  hasPositionFetcher ? context.positionFetcher(context.walletAddress).catch((err) => {
605
822
  console.warn("[balance_check] positionFetcher failed:", err);
606
823
  return null;
607
824
  }) : Promise.resolve(null)
608
825
  ]);
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
- }
826
+ await applyVsuiPriceFallback(portfolio);
626
827
  let availableUsd = 0;
627
828
  let stablesUsd = 0;
628
829
  let gasReserveUsd2 = 0;
629
830
  const STABLE_SYMBOLS = /* @__PURE__ */ new Set(["USDC", "USDT", "USDe", "USDsui", "wUSDC", "wUSDT"]);
630
831
  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") {
832
+ for (const coin of portfolio.coins) {
833
+ const balance2 = Number(coin.balance) / 10 ** coin.decimals;
834
+ const price = coin.price ?? 0;
835
+ if (coin.symbol === "SUI" || coin.coinType === SUI_COIN_TYPE) {
635
836
  const reserveAmount = Math.min(balance2, GAS_RESERVE_SUI2);
636
837
  gasReserveUsd2 = reserveAmount * price;
637
838
  availableUsd += (balance2 - reserveAmount) * price;
@@ -676,7 +877,8 @@ var balanceCheckTool = buildTool({
676
877
  total: availableUsd + savings + gasReserveUsd2 + pendingRewardsUsd - debt,
677
878
  stables: stablesUsd,
678
879
  holdings: visibleHoldings,
679
- saveableUsdc
880
+ saveableUsdc,
881
+ priceSource: portfolio.source
680
882
  };
681
883
  const holdingsList = visibleHoldings.map((h) => `${h.symbol}: ${h.balance < 1 ? h.balance.toFixed(6) : h.balance.toFixed(2)} ($${h.usdValue.toFixed(2)})`).join(", ");
682
884
  return {
@@ -993,7 +1195,6 @@ var healthCheckTool = buildTool({
993
1195
  };
994
1196
  }
995
1197
  });
996
- var YIELDS_API = "https://yields.llama.fi";
997
1198
  var STABLECOIN_SYMBOLS2 = /* @__PURE__ */ new Set([
998
1199
  "usdc",
999
1200
  "wusdc",
@@ -1027,24 +1228,9 @@ function applyFilters(rates, opts) {
1027
1228
  function formatRatesSummary(rates) {
1028
1229
  return Object.entries(rates).map(([asset, r]) => `${asset}: Save ${(r.saveApy * 100).toFixed(2)}% / Borrow ${(r.borrowApy * 100).toFixed(2)}%`).join(", ");
1029
1230
  }
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
1231
  var ratesInfoTool = buildTool({
1046
1232
  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.',
1233
+ 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
1234
  inputSchema: z.object({
1049
1235
  assets: z.array(z.string()).optional().describe('Filter to specific asset symbols (e.g. ["USDC"], ["USDC","USDT","USDSUI"]). Case-insensitive.'),
1050
1236
  stableOnly: z.boolean().optional().describe("When true, return only stablecoin markets (USDC, USDT, USDSUI, USDY, suiUSDT, etc.). Ignored when `assets` is supplied."),
@@ -1077,19 +1263,19 @@ var ratesInfoTool = buildTool({
1077
1263
  topN: input.topN ?? 8
1078
1264
  };
1079
1265
  if (hasNaviMcpGlobal(context)) {
1080
- const all2 = await fetchRates(getMcpManager(context));
1081
- const filtered2 = applyFilters(all2, opts);
1082
- return { data: filtered2, displayText: formatRatesSummary(filtered2) };
1266
+ const all = await fetchRates(getMcpManager(context));
1267
+ const filtered = applyFilters(all, opts);
1268
+ return { data: filtered, displayText: formatRatesSummary(filtered) };
1083
1269
  }
1084
1270
  if (hasAgent(context)) {
1085
1271
  const agent = requireAgent(context);
1086
- const all2 = await agent.rates();
1087
- const filtered2 = applyFilters(all2, opts);
1088
- return { data: filtered2, displayText: formatRatesSummary(filtered2) };
1272
+ const all = await agent.rates();
1273
+ const filtered = applyFilters(all, opts);
1274
+ return { data: filtered, displayText: formatRatesSummary(filtered) };
1089
1275
  }
1090
- const all = await fetchRatesFromDefiLlama();
1091
- const filtered = applyFilters(all, opts);
1092
- return { data: filtered, displayText: formatRatesSummary(filtered) };
1276
+ throw new Error(
1277
+ "rates_info: NAVI lending data is currently unavailable. Try again shortly."
1278
+ );
1093
1279
  }
1094
1280
  });
1095
1281
  function parseRpcTx(tx, address) {
@@ -1759,10 +1945,10 @@ Always use ISO-3166 country codes (GB not UK, US not USA). A return address ("fr
1759
1945
  });
1760
1946
  var MPP_GATEWAY2 = "https://mpp.t2000.ai";
1761
1947
  var CATALOG_URL = `${MPP_GATEWAY2}/api/services`;
1762
- var CACHE_TTL2 = 12e4;
1948
+ var CACHE_TTL = 12e4;
1763
1949
  var catalogCache = null;
1764
1950
  async function fetchCatalog() {
1765
- if (catalogCache && Date.now() - catalogCache.ts < CACHE_TTL2) {
1951
+ if (catalogCache && Date.now() - catalogCache.ts < CACHE_TTL) {
1766
1952
  return catalogCache.data;
1767
1953
  }
1768
1954
  const res = await fetch(CATALOG_URL, { signal: AbortSignal.timeout(1e4) });
@@ -2256,11 +2442,31 @@ var portfolioAnalysisTool = buildTool({
2256
2442
  if (!address) {
2257
2443
  throw new Error("No wallet address provided. Sign in first.");
2258
2444
  }
2259
- const rpcUrl = context.suiRpcUrl ?? "https://fullnode.mainnet.sui.io:443";
2260
2445
  const DUST_USD = 0.01;
2261
2446
  const apiUrl = context.env?.AUDRIC_INTERNAL_API_URL;
2262
- const [coins, positions, weekHistResult] = await Promise.all([
2263
- fetchWalletCoins(address, rpcUrl),
2447
+ const [portfolio, positions, weekHistResult] = await Promise.all([
2448
+ (async () => {
2449
+ if (context.portfolioCache) {
2450
+ const hit = context.portfolioCache.get(address);
2451
+ if (hit) return hit;
2452
+ }
2453
+ const fresh = await fetchAddressPortfolio(
2454
+ address,
2455
+ context.blockvisionApiKey,
2456
+ context.suiRpcUrl
2457
+ );
2458
+ context.portfolioCache?.set(address, fresh);
2459
+ return fresh;
2460
+ })().catch((err) => {
2461
+ console.warn("[portfolio_analysis] portfolio fetch failed:", err);
2462
+ const empty = {
2463
+ coins: [],
2464
+ totalUsd: 0,
2465
+ pricedAt: Date.now(),
2466
+ source: "sui-rpc-degraded"
2467
+ };
2468
+ return empty;
2469
+ }),
2264
2470
  context.positionFetcher ? context.positionFetcher(address).catch((err) => {
2265
2471
  console.warn("[portfolio_analysis] positionFetcher failed:", err);
2266
2472
  return null;
@@ -2270,14 +2476,12 @@ var portfolioAnalysisTool = buildTool({
2270
2476
  { headers: { "x-sui-address": address }, signal: context.signal }
2271
2477
  ).then((res) => res.ok ? res.json() : null).catch(() => null) : Promise.resolve(null)
2272
2478
  ]);
2273
- const nonZero = coins.filter((c) => Number(c.totalBalance) > 0);
2274
- const prices = await fetchTokenPrices(nonZero.map((c) => c.coinType)).catch(() => ({}));
2275
2479
  let walletValue = 0;
2276
2480
  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;
2481
+ for (const coin of portfolio.coins) {
2482
+ const amount = Number(coin.balance) / 10 ** coin.decimals;
2483
+ if (!Number.isFinite(amount) || amount <= 0) continue;
2484
+ const usdValue = coin.usdValue ?? (coin.price != null ? amount * coin.price : 0);
2281
2485
  walletValue += usdValue;
2282
2486
  allAllocations.push({ symbol: coin.symbol, amount, usdValue, percentage: 0 });
2283
2487
  }
@@ -2349,7 +2553,8 @@ var portfolioAnalysisTool = buildTool({
2349
2553
  insights,
2350
2554
  savingsApy,
2351
2555
  dailyEarning,
2352
- weekChange
2556
+ weekChange,
2557
+ priceSource: portfolio.source
2353
2558
  };
2354
2559
  const topLine = `Total: $${totalValue.toFixed(2)} | Wallet: $${walletValue.toFixed(2)} | Savings: $${savingsValue.toFixed(2)}`;
2355
2560
  const insightLines = insights.map((i) => `${i.type === "warning" ? "\u26A0" : "\u2192"} ${i.message}`).join("\n");
@@ -3095,335 +3300,59 @@ var activitySummaryTool = buildTool({
3095
3300
  }
3096
3301
  }
3097
3302
  });
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.',
3303
+ var tokenPricesTool = buildTool({
3304
+ name: "token_prices",
3305
+ 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
3306
  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.')
3307
+ coinTypes: z.array(z.string()).min(1).max(10).describe("Array of Sui coin type strings (max 10 per call)."),
3308
+ include24hChange: z.boolean().optional().describe("When true, include 24h change percentage per token in the output.")
3151
3309
  }),
3152
3310
  jsonSchema: {
3153
3311
  type: "object",
3154
3312
  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);
3313
+ coinTypes: {
3314
+ type: "array",
3315
+ items: { type: "string" },
3316
+ description: "Sui coin type strings (max 10)."
3317
+ },
3318
+ include24hChange: {
3319
+ type: "boolean",
3320
+ description: "Include 24h change percentage per token."
3171
3321
  }
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
3322
  },
3263
3323
  required: ["coinTypes"]
3264
3324
  },
3265
3325
  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: {
3326
+ async call(input, context) {
3327
+ const prices = await fetchTokenPrices(input.coinTypes, context.blockvisionApiKey);
3328
+ const results = input.coinTypes.map((coinType) => {
3329
+ const entry = prices[coinType];
3330
+ const symbol = coinType.split("::").pop() ?? coinType;
3331
+ if (!entry) {
3332
+ return {
3333
+ coinType,
3334
+ symbol,
3335
+ price: null,
3336
+ priceUnavailable: true
3337
+ };
3338
+ }
3339
+ const out = {
3340
+ coinType,
3321
3341
  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
- }));
3342
+ price: entry.price
3343
+ };
3344
+ if (input.include24hChange && entry.change24h !== void 0) {
3345
+ out.change24h = entry.change24h;
3346
+ }
3347
+ return out;
3348
+ });
3424
3349
  return {
3425
3350
  data: results,
3426
- displayText: results.map((r, i) => `${i + 1}. ${r.name} (${fmtToolTvl(r.tvl)} TVL, ${r.category})`).join("\n")
3351
+ displayText: results.map((r) => {
3352
+ if (r.price === null) return `${r.symbol}: price unavailable`;
3353
+ const change = r.change24h;
3354
+ return change !== void 0 ? `${r.symbol}: $${r.price.toFixed(4)} (${change >= 0 ? "+" : ""}${change.toFixed(2)}% 24h)` : `${r.symbol}: $${r.price.toFixed(4)}`;
3355
+ }).join(", ")
3427
3356
  };
3428
3357
  }
3429
3358
  });
@@ -3443,13 +3372,7 @@ var READ_TOOLS = [
3443
3372
  explainTxTool,
3444
3373
  portfolioAnalysisTool,
3445
3374
  protocolDeepDiveTool,
3446
- defillamaYieldPoolsTool,
3447
- defillamaProtocolInfoTool,
3448
- defillamaTokenPricesTool,
3449
- defillamaPriceChangeTool,
3450
- defillamaChainTvlTool,
3451
- defillamaProtocolFeesTool,
3452
- defillamaSuiProtocolsTool,
3375
+ tokenPricesTool,
3453
3376
  listPaymentLinksTool,
3454
3377
  cancelPaymentLinkTool,
3455
3378
  listInvoicesTool,
@@ -3512,7 +3435,7 @@ function getModifiableFields(toolName) {
3512
3435
  }
3513
3436
 
3514
3437
  // 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.
3438
+ 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
3439
 
3517
3440
  ## Response rules
3518
3441
  - 1-2 sentences max. No bullet lists unless asked. No preambles.
@@ -3532,8 +3455,8 @@ Only offer to execute actions you have tools for. If you retrieved a quote, data
3532
3455
  ## Tool usage
3533
3456
  - Use tools proactively \u2014 don't refuse requests you can handle.
3534
3457
  - 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.
3458
+ - For NAVI lending APYs, use rates_info; for VOLO liquid staking stats, use volo_stats; for spot token prices, use token_prices.
3459
+ - For protocol-level due diligence (TVL, fees, audits, safety) on Sui DeFi protocols, use protocol_deep_dive with the slug.
3537
3460
  - Run multiple read-only tools in parallel when you need several data points.
3538
3461
  - If a tool errors, say what went wrong and what to try instead. One sentence.
3539
3462
 
@@ -3546,11 +3469,11 @@ Only offer to execute actions you have tools for. If you retrieved a quote, data
3546
3469
  ## Multi-step flows
3547
3470
  - "How much X for Y?": swap_quote first, then swap_execute if user confirms.
3548
3471
  - "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.
3472
+ - "Buy $X of token": token_prices \u2192 calculate amount \u2192 swap_execute.
3473
+ - "Best yield on SUI": compare rates_info (NAVI lending) + volo_stats (vSUI liquid staking).
3551
3474
  - withdraw supports legacy positions: USDC, USDe, USDsui, SUI. Pass asset param to withdraw a specific token.
3552
3475
  - "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.
3476
+ - "Is protocol X safe?" / "Tell me about NAVI": protocol_deep_dive with the slug.
3554
3477
  - "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
3478
 
3556
3479
  ## Safety
@@ -4701,6 +4624,12 @@ var QueryEngine = class {
4701
4624
  sessionSpendUsd;
4702
4625
  onAutoExecuted;
4703
4626
  onGuardFired;
4627
+ // [v1.4 BlockVision] BlockVision Indexer API key + per-request portfolio
4628
+ // cache. Forwarded into every `ToolContext` build site so read tools
4629
+ // (`balance_check`, `portfolio_analysis`, future `token_prices`) hit the
4630
+ // shared host-paid endpoint and dedupe across each other within a turn.
4631
+ blockvisionApiKey;
4632
+ portfolioCache;
4704
4633
  // [v1.5] See `EngineConfig.postWriteRefresh` — drives the post-write
4705
4634
  // synthetic read injection in `resumeWithToolResult`.
4706
4635
  postWriteRefresh;
@@ -4750,6 +4679,8 @@ var QueryEngine = class {
4750
4679
  this.onAutoExecuted = config.onAutoExecuted;
4751
4680
  this.onGuardFired = config.onGuardFired;
4752
4681
  this.postWriteRefresh = config.postWriteRefresh;
4682
+ this.blockvisionApiKey = config.blockvisionApiKey;
4683
+ this.portfolioCache = config.portfolioCache;
4753
4684
  this.tools = config.tools ?? (config.agent ? getDefaultTools() : []);
4754
4685
  }
4755
4686
  /**
@@ -4867,8 +4798,14 @@ var QueryEngine = class {
4867
4798
  signal,
4868
4799
  priceCache: this.priceCache,
4869
4800
  permissionConfig: this.permissionConfig,
4870
- sessionSpendUsd: this.sessionSpendUsd
4801
+ sessionSpendUsd: this.sessionSpendUsd,
4802
+ blockvisionApiKey: this.blockvisionApiKey,
4803
+ portfolioCache: this.portfolioCache
4871
4804
  };
4805
+ if (this.walletAddress) {
4806
+ this.portfolioCache?.delete(this.walletAddress);
4807
+ clearPortfolioCacheFor(this.walletAddress);
4808
+ }
4872
4809
  if (!signal.aborted) {
4873
4810
  await new Promise((resolve) => {
4874
4811
  const t = setTimeout(resolve, 1500);
@@ -5014,7 +4951,9 @@ var QueryEngine = class {
5014
4951
  signal,
5015
4952
  priceCache: this.priceCache,
5016
4953
  permissionConfig: this.permissionConfig,
5017
- sessionSpendUsd: this.sessionSpendUsd
4954
+ sessionSpendUsd: this.sessionSpendUsd,
4955
+ blockvisionApiKey: this.blockvisionApiKey,
4956
+ portfolioCache: this.portfolioCache
5018
4957
  };
5019
4958
  try {
5020
4959
  const result = await tool.call(parsed.data, context);
@@ -5057,7 +4996,9 @@ var QueryEngine = class {
5057
4996
  signal,
5058
4997
  priceCache: this.priceCache,
5059
4998
  permissionConfig: this.permissionConfig,
5060
- sessionSpendUsd: this.sessionSpendUsd
4999
+ sessionSpendUsd: this.sessionSpendUsd,
5000
+ blockvisionApiKey: this.blockvisionApiKey,
5001
+ portfolioCache: this.portfolioCache
5061
5002
  };
5062
5003
  let turns = 0;
5063
5004
  let hasRetriedWithCleanHistory = false;
@@ -5421,7 +5362,8 @@ ${recipeCtx}`;
5421
5362
  );
5422
5363
  Promise.resolve().then(() => this.onAutoExecuted({
5423
5364
  toolName: toolEvent.toolName,
5424
- usdValue
5365
+ usdValue,
5366
+ walletAddress: this.walletAddress
5425
5367
  })).catch((err) => {
5426
5368
  console.warn("[engine] onAutoExecuted callback failed:", err);
5427
5369
  });
@@ -5476,6 +5418,7 @@ ${recipeCtx}`;
5476
5418
  const writeGuardInjections = pendingWrite.call._guardInjections;
5477
5419
  const modifiableFields = getModifiableFields(pendingWrite.call.name);
5478
5420
  const turnIndex = this.messages.filter((m) => m.role === "assistant").length;
5421
+ const attemptId = randomUUID();
5479
5422
  this.turnPaused = true;
5480
5423
  yield {
5481
5424
  type: "pending_action",
@@ -5492,7 +5435,8 @@ ${recipeCtx}`;
5492
5435
  })),
5493
5436
  ...writeGuardInjections?.length ? { guardInjections: writeGuardInjections } : {},
5494
5437
  ...modifiableFields?.length ? { modifiableFields } : {},
5495
- turnIndex
5438
+ turnIndex,
5439
+ attemptId
5496
5440
  }
5497
5441
  };
5498
5442
  return;
@@ -6748,6 +6692,6 @@ function sanitizeAnthropicMessages(messages) {
6748
6692
  return merged;
6749
6693
  }
6750
6694
 
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 };
6695
+ 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
6696
  //# sourceMappingURL=index.js.map
6753
6697
  //# sourceMappingURL=index.js.map