@t2000/engine 0.49.0 → 0.50.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.d.ts CHANGED
@@ -1962,6 +1962,9 @@ declare const balanceCheckTool: Tool<{
1962
1962
  debt: number;
1963
1963
  pendingRewards: number;
1964
1964
  gasReserve: number;
1965
+ defi: number;
1966
+ defiByProtocol: Partial<Record<"cetus" | "suilend" | "scallop" | "bluefin" | "aftermath" | "haedal", number>>;
1967
+ defiSource: "blockvision" | "partial" | "degraded";
1965
1968
  total: number;
1966
1969
  stables: number;
1967
1970
  holdings: any[];
package/dist/index.js CHANGED
@@ -711,6 +711,262 @@ function parseNumberOrNull(input) {
711
711
  const n = Number(input);
712
712
  return Number.isFinite(n) ? n : null;
713
713
  }
714
+ var DEFI_PORTFOLIO_TIMEOUT_MS = 4e3;
715
+ var DEFI_CACHE_TTL_MS = 6e4;
716
+ var DEFI_PROTOCOLS = [
717
+ "cetus",
718
+ "suilend",
719
+ "scallop",
720
+ "bluefin",
721
+ "aftermath",
722
+ "haedal"
723
+ ];
724
+ var defiCache = /* @__PURE__ */ new Map();
725
+ var defiInflight = /* @__PURE__ */ new Map();
726
+ async function fetchAddressDefiPortfolio(address, apiKey, priceHints = {}) {
727
+ if (!apiKey || apiKey.trim().length === 0) {
728
+ return { totalUsd: 0, perProtocol: {}, pricedAt: Date.now(), source: "degraded" };
729
+ }
730
+ const now = Date.now();
731
+ const cached = defiCache.get(address);
732
+ if (cached && now - cached.ts < DEFI_CACHE_TTL_MS) return cached.data;
733
+ let inflight = defiInflight.get(address);
734
+ if (inflight) return inflight;
735
+ inflight = (async () => {
736
+ try {
737
+ const settled = await Promise.allSettled(
738
+ DEFI_PROTOCOLS.map((p) => fetchOneDefiProtocol(address, p, apiKey))
739
+ );
740
+ const seen = /* @__PURE__ */ new Set();
741
+ for (const s of settled) {
742
+ if (s.status === "fulfilled" && s.value) collectCoinTypes(s.value, seen);
743
+ }
744
+ const normalizedHints = {};
745
+ for (const [k, v] of Object.entries(priceHints)) {
746
+ normalizedHints[normalizeCoinType(k)] = v;
747
+ }
748
+ const missing = Array.from(seen).filter((ct) => {
749
+ const norm = normalizeCoinType(ct);
750
+ return !normalizedHints[norm] && !STABLE_USD_PRICES[norm];
751
+ });
752
+ let fetchedPrices = {};
753
+ if (missing.length > 0) {
754
+ try {
755
+ fetchedPrices = await fetchTokenPrices(missing, apiKey);
756
+ } catch (err) {
757
+ console.warn("[defi] fill-missing-prices failed:", err);
758
+ }
759
+ }
760
+ const prices = { ...normalizedHints };
761
+ for (const [ct, v] of Object.entries(fetchedPrices)) {
762
+ prices[normalizeCoinType(ct)] ??= v.price;
763
+ }
764
+ for (const [ct, p] of Object.entries(STABLE_USD_PRICES)) {
765
+ prices[normalizeCoinType(ct)] ??= p;
766
+ }
767
+ let totalUsd = 0;
768
+ let failures = 0;
769
+ const perProtocol = {};
770
+ for (let i = 0; i < DEFI_PROTOCOLS.length; i++) {
771
+ const proto = DEFI_PROTOCOLS[i];
772
+ const s = settled[i];
773
+ if (s.status !== "fulfilled" || !s.value) {
774
+ failures++;
775
+ continue;
776
+ }
777
+ try {
778
+ const usd = NORMALIZERS[proto](s.value, prices);
779
+ if (Number.isFinite(usd) && usd !== 0) {
780
+ perProtocol[proto] = usd;
781
+ totalUsd += usd;
782
+ }
783
+ } catch (err) {
784
+ console.warn(`[defi] ${proto} normaliser threw:`, err);
785
+ failures++;
786
+ }
787
+ }
788
+ if (totalUsd < 0) totalUsd = 0;
789
+ const summary = {
790
+ totalUsd,
791
+ perProtocol,
792
+ pricedAt: Date.now(),
793
+ source: failures === DEFI_PROTOCOLS.length ? "degraded" : failures > 0 ? "partial" : "blockvision"
794
+ };
795
+ defiCache.set(address, { data: summary, ts: Date.now() });
796
+ return summary;
797
+ } finally {
798
+ defiInflight.delete(address);
799
+ }
800
+ })();
801
+ defiInflight.set(address, inflight);
802
+ return inflight;
803
+ }
804
+ async function fetchOneDefiProtocol(address, protocol, apiKey) {
805
+ const url = `${BLOCKVISION_BASE}/account/defiPortfolio?address=${encodeURIComponent(address)}&protocol=${protocol}`;
806
+ let res;
807
+ try {
808
+ res = await fetch(url, {
809
+ headers: { "x-api-key": apiKey, accept: "application/json" },
810
+ signal: AbortSignal.timeout(DEFI_PORTFOLIO_TIMEOUT_MS)
811
+ });
812
+ } catch (err) {
813
+ console.warn(`[defi] ${protocol} fetch threw:`, err);
814
+ return null;
815
+ }
816
+ if (!res.ok) {
817
+ console.warn(`[defi] ${protocol} HTTP ${res.status}`);
818
+ return null;
819
+ }
820
+ let json;
821
+ try {
822
+ json = await res.json();
823
+ } catch (err) {
824
+ console.warn(`[defi] ${protocol} JSON parse failed:`, err);
825
+ return null;
826
+ }
827
+ if (json.code !== 200 || !json.result) return null;
828
+ return json.result;
829
+ }
830
+ function collectCoinTypes(obj, out) {
831
+ if (!obj || typeof obj !== "object") return;
832
+ if (Array.isArray(obj)) {
833
+ for (const x of obj) collectCoinTypes(x, out);
834
+ return;
835
+ }
836
+ for (const [k, v] of Object.entries(obj)) {
837
+ if (typeof v === "string" && v.startsWith("0x") && v.includes("::")) {
838
+ const lk = k.toLowerCase();
839
+ if (lk.includes("cointype") || lk === "cointypea" || lk === "cointypeb" || lk === "tokenxtype" || lk === "tokenytype" || lk === "coinaddress" || lk === "phantomtype" || lk === "typename") {
840
+ out.add(v);
841
+ }
842
+ } else if (typeof v === "object" && v !== null) {
843
+ collectCoinTypes(v, out);
844
+ }
845
+ }
846
+ }
847
+ function priceFor(coinType, prices) {
848
+ const norm = normalizeCoinType(coinType);
849
+ return prices[norm] ?? prices[coinType] ?? STABLE_USD_PRICES[norm] ?? 0;
850
+ }
851
+ function rawToUsd(coinType, raw, decimalsHint, prices) {
852
+ if (raw == null) return 0;
853
+ const decimals = typeof decimalsHint === "number" ? decimalsHint : getDecimalsForCoinType(coinType);
854
+ const amount = Number(raw) / 10 ** decimals;
855
+ if (!Number.isFinite(amount)) return 0;
856
+ return amount * priceFor(coinType, prices);
857
+ }
858
+ var NORMALIZERS = {
859
+ cetus: normalizeCetus,
860
+ suilend: normalizeSuilend,
861
+ scallop: normalizeScallop,
862
+ bluefin: normalizeBluefin,
863
+ aftermath: normalizeAftermath,
864
+ haedal: normalizeHaedal
865
+ };
866
+ function normalizeCetus(result, prices) {
867
+ const data = result.cetus ?? {};
868
+ let total = 0;
869
+ const sumPair = (item, aField, bField) => {
870
+ if (item.coinTypeA && item[aField] != null) {
871
+ const dec = item.coinTypeADecimals ?? item.coinA?.decimals;
872
+ total += rawToUsd(item.coinTypeA, item[aField], dec, prices);
873
+ }
874
+ if (item.coinTypeB && item[bField] != null) {
875
+ const dec = item.coinTypeBDecimals ?? item.coinB?.decimals;
876
+ total += rawToUsd(item.coinTypeB, item[bField], dec, prices);
877
+ }
878
+ };
879
+ for (const lp of data.lps ?? []) sumPair(lp, "balanceA", "balanceB");
880
+ for (const farm of data.farms ?? []) sumPair(farm, "balanceA", "balanceB");
881
+ for (const vault of data.vaults ?? []) sumPair(vault, "coinAAmount", "coinBAmount");
882
+ return total;
883
+ }
884
+ function normalizeSuilend(result, prices) {
885
+ const data = result.suilend ?? {};
886
+ let total = 0;
887
+ for (const d of data.deposits ?? []) {
888
+ if (d.coinType && d.amount != null) total += rawToUsd(d.coinType, d.amount, d.decimals, prices);
889
+ }
890
+ for (const b of data.borrows ?? []) {
891
+ if (b.coinType && b.amount != null) total -= rawToUsd(b.coinType, b.amount, b.decimals, prices);
892
+ }
893
+ for (const s of data.strategies ?? []) {
894
+ if (s.coinType && s.amount != null) total += rawToUsd(s.coinType, s.amount, s.decimals, prices);
895
+ }
896
+ return total;
897
+ }
898
+ function normalizeScallop(result, _prices) {
899
+ const s = result.scallop;
900
+ if (!s) return 0;
901
+ const supply = Number(s.totalSupplyValue ?? 0);
902
+ const collateral = Number(s.totalCollateralValue ?? 0);
903
+ const locked = Number(s.totalLockedScaValue ?? 0);
904
+ const debt = Number(s.totalDebtValue ?? 0);
905
+ const net = (Number.isFinite(supply) ? supply : 0) + (Number.isFinite(collateral) ? collateral : 0) + (Number.isFinite(locked) ? locked : 0) - (Number.isFinite(debt) ? debt : 0);
906
+ return net;
907
+ }
908
+ function normalizeBluefin(result, prices) {
909
+ const data = result.bluefin ?? {};
910
+ let total = 0;
911
+ for (const lp of data.lps ?? []) {
912
+ if (lp.coinTypeA && lp.coinAmountA != null) {
913
+ total += rawToUsd(lp.coinTypeA, lp.coinAmountA, void 0, prices);
914
+ }
915
+ if (lp.coinTypeB && lp.coinAmountB != null) {
916
+ total += rawToUsd(lp.coinTypeB, lp.coinAmountB, void 0, prices);
917
+ }
918
+ }
919
+ if (data.usdcVault?.amount != null) {
920
+ total += rawToUsd(
921
+ "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
922
+ data.usdcVault.amount,
923
+ 6,
924
+ prices
925
+ );
926
+ }
927
+ if (data.blueVault?.amount != null) {
928
+ total += rawToUsd(
929
+ "0xe1b45a0e641b9955a20aa0ad1c1f4ad86aad8afb07296d4085e349a50e90bdca::blue::BLUE",
930
+ data.blueVault.amount,
931
+ 9,
932
+ prices
933
+ );
934
+ }
935
+ return total;
936
+ }
937
+ function normalizeAftermath(result, prices) {
938
+ const data = result.aftermath ?? {};
939
+ let total = 0;
940
+ const positions = [...data.lpPositions ?? [], ...data.farmPositions ?? []];
941
+ for (const pos of positions) {
942
+ for (const c of pos.coins ?? []) {
943
+ if (c.coinType && c.amount != null) {
944
+ total += rawToUsd(c.coinType, c.amount, void 0, prices);
945
+ }
946
+ }
947
+ }
948
+ return total;
949
+ }
950
+ function normalizeHaedal(result, prices) {
951
+ const SUI_TYPE_FULL = "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI";
952
+ const data = result.haedal ?? {};
953
+ let total = 0;
954
+ for (const lp of data.lps ?? []) {
955
+ const item = lp;
956
+ if (item.coinTypeA && item.balanceA != null) {
957
+ total += rawToUsd(item.coinTypeA, item.balanceA, void 0, prices);
958
+ }
959
+ if (item.coinTypeB && item.balanceB != null) {
960
+ total += rawToUsd(item.coinTypeB, item.balanceB, void 0, prices);
961
+ }
962
+ }
963
+ for (const stake of data.stakings ?? []) {
964
+ if (stake.sui_amount != null) {
965
+ total += rawToUsd(SUI_TYPE_FULL, stake.sui_amount, 9, prices);
966
+ }
967
+ }
968
+ return total;
969
+ }
714
970
  function clearPortfolioCache() {
715
971
  portfolioCache.clear();
716
972
  portfolioInflight.clear();
@@ -808,7 +1064,7 @@ var balanceCheckTool = buildTool({
808
1064
  const address = targetAddress;
809
1065
  const mgr = getMcpManager(context);
810
1066
  const hasPositionFetcher = !!context.positionFetcher;
811
- const [portfolio, positions, rewards, serverPositions] = await Promise.all([
1067
+ const [portfolio, positions, rewards, serverPositions, defiPortfolio] = await Promise.all([
812
1068
  loadPortfolio(
813
1069
  address,
814
1070
  context.blockvisionApiKey,
@@ -839,7 +1095,22 @@ var balanceCheckTool = buildTool({
839
1095
  hasPositionFetcher ? context.positionFetcher(address).catch((err) => {
840
1096
  console.warn("[balance_check] positionFetcher failed:", err);
841
1097
  return null;
842
- }) : Promise.resolve(null)
1098
+ }) : Promise.resolve(null),
1099
+ // [v0.50] DeFi leg — independent of NAVI (excluded) and the wallet
1100
+ // portfolio (which only has coin holdings). Failure here surfaces
1101
+ // as defi.totalUsd === 0 and `source: 'degraded'`, leaving the
1102
+ // rest of balance_check unaffected. The fetcher fills its own
1103
+ // prices via fetchTokenPrices for any coin types it discovers.
1104
+ fetchAddressDefiPortfolio(address, context.blockvisionApiKey).catch((err) => {
1105
+ console.warn("[balance_check] defi fetch failed:", err);
1106
+ const fallback = {
1107
+ totalUsd: 0,
1108
+ perProtocol: {},
1109
+ pricedAt: Date.now(),
1110
+ source: "degraded"
1111
+ };
1112
+ return fallback;
1113
+ })
843
1114
  ]);
844
1115
  await applyVsuiPriceFallback(portfolio);
845
1116
  let availableUsd = 0;
@@ -886,13 +1157,17 @@ var balanceCheckTool = buildTool({
886
1157
  const visibleHoldings = holdings.filter((h) => h.usdValue >= 0.01).sort((a, b) => b.usdValue - a.usdValue);
887
1158
  const usdcHolding2 = holdings.find((h) => h.symbol === "USDC");
888
1159
  const saveableUsdc = usdcHolding2 ? usdcHolding2.balance : 0;
1160
+ const defi2 = defiPortfolio;
889
1161
  const bal = {
890
1162
  available: availableUsd,
891
1163
  savings,
892
1164
  debt,
893
1165
  pendingRewards: pendingRewardsUsd,
894
1166
  gasReserve: gasReserveUsd2,
895
- total: availableUsd + savings + gasReserveUsd2 + pendingRewardsUsd - debt,
1167
+ defi: defi2.totalUsd,
1168
+ defiByProtocol: defi2.perProtocol,
1169
+ defiSource: defi2.source,
1170
+ total: availableUsd + savings + gasReserveUsd2 + pendingRewardsUsd + defi2.totalUsd - debt,
896
1171
  stables: stablesUsd,
897
1172
  holdings: visibleHoldings,
898
1173
  saveableUsdc,
@@ -902,9 +1177,10 @@ var balanceCheckTool = buildTool({
902
1177
  };
903
1178
  const holdingsList = visibleHoldings.map((h) => `${h.symbol}: ${h.balance < 1 ? h.balance.toFixed(6) : h.balance.toFixed(2)} ($${h.usdValue.toFixed(2)})`).join(", ");
904
1179
  const subjectPrefix = isSelfQuery ? "Balance" : `Balance for ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
1180
+ const defiSummaryText = defi2.totalUsd > 0 ? ` Other DeFi positions (LPs/staking/lending across ${Object.keys(defi2.perProtocol).join("/")}): $${defi2.totalUsd.toFixed(2)}.` : "";
905
1181
  return {
906
1182
  data: bal,
907
- displayText: `${subjectPrefix}: $${bal.total.toFixed(2)} total. Wallet holdings (NOT savings): ${holdingsList || "none"}. NAVI savings deposits: $${bal.savings.toFixed(2)}. Saveable USDC (only USDC can be saved): ${saveableUsdc.toFixed(2)} USDC.`
1183
+ displayText: `${subjectPrefix}: $${bal.total.toFixed(2)} total. Wallet holdings (NOT savings): ${holdingsList || "none"}. NAVI savings deposits: $${bal.savings.toFixed(2)}.${defiSummaryText} Saveable USDC (only USDC can be saved): ${saveableUsdc.toFixed(2)} USDC.`
908
1184
  };
909
1185
  }
910
1186
  if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
@@ -913,13 +1189,28 @@ var balanceCheckTool = buildTool({
913
1189
  );
914
1190
  }
915
1191
  const agent = requireAgent(context);
916
- const balance = await agent.balance();
1192
+ const fetchAddress = targetAddress ?? context.walletAddress;
1193
+ const [balance, defi] = await Promise.all([
1194
+ agent.balance(),
1195
+ fetchAddressDefiPortfolio(fetchAddress, context.blockvisionApiKey).catch((err) => {
1196
+ console.warn("[balance_check] sdk-path defi fetch failed:", err);
1197
+ const fallback = {
1198
+ totalUsd: 0,
1199
+ perProtocol: {},
1200
+ pricedAt: Date.now(),
1201
+ source: "degraded"
1202
+ };
1203
+ return fallback;
1204
+ })
1205
+ ]);
917
1206
  const gasReserveUsd = typeof balance.gasReserve === "number" ? balance.gasReserve : balance.gasReserve.usdEquiv ?? 0;
918
1207
  const stablesTotal = typeof balance.stables === "number" ? balance.stables : Object.values(balance.stables).reduce((a, b) => a + b, 0);
919
1208
  const sdkHoldings = balance.holdings;
920
1209
  const holdingsArr = Array.isArray(sdkHoldings) ? sdkHoldings : [];
921
1210
  const usdcHolding = holdingsArr.find((h) => h.symbol === "USDC");
922
1211
  const sdkSaveableUsdc = usdcHolding ? usdcHolding.balance ?? 0 : 0;
1212
+ const sdkDefiSummaryText = defi.totalUsd > 0 ? ` Other DeFi positions (LPs/staking/lending across ${Object.keys(defi.perProtocol).join("/")}): $${defi.totalUsd.toFixed(2)}.` : "";
1213
+ const sdkTotal = balance.total + defi.totalUsd;
923
1214
  return {
924
1215
  data: {
925
1216
  available: balance.available,
@@ -927,14 +1218,17 @@ var balanceCheckTool = buildTool({
927
1218
  debt: balance.debt,
928
1219
  pendingRewards: balance.pendingRewards,
929
1220
  gasReserve: gasReserveUsd,
930
- total: balance.total,
1221
+ defi: defi.totalUsd,
1222
+ defiByProtocol: defi.perProtocol,
1223
+ defiSource: defi.source,
1224
+ total: sdkTotal,
931
1225
  stables: stablesTotal,
932
1226
  holdings: holdingsArr,
933
1227
  saveableUsdc: sdkSaveableUsdc,
934
1228
  address: targetAddress ?? "",
935
1229
  isSelfQuery: true
936
1230
  },
937
- displayText: `Balance: $${balance.total.toFixed(2)} total. Wallet: $${balance.available.toFixed(2)} available. NAVI savings deposits: $${balance.savings.toFixed(2)}. Saveable USDC (only USDC can be saved): ${sdkSaveableUsdc.toFixed(2)} USDC.`
1231
+ displayText: `Balance: $${sdkTotal.toFixed(2)} total. Wallet: $${balance.available.toFixed(2)} available. NAVI savings deposits: $${balance.savings.toFixed(2)}.${sdkDefiSummaryText} Saveable USDC (only USDC can be saved): ${sdkSaveableUsdc.toFixed(2)} USDC.`
938
1232
  };
939
1233
  }
940
1234
  });
@@ -1576,7 +1870,7 @@ var transactionHistoryTool = buildTool({
1576
1870
  const targetAddress = input.address ?? context.walletAddress;
1577
1871
  const isSelfQuery = !!targetAddress && !!context.walletAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
1578
1872
  const prices = context.tokenPrices;
1579
- const priceFor = (sym) => {
1873
+ const priceFor2 = (sym) => {
1580
1874
  if (!sym || !prices) return void 0;
1581
1875
  return prices[sym.toUpperCase()] ?? prices[sym.toLowerCase()] ?? prices[sym];
1582
1876
  };
@@ -1597,8 +1891,8 @@ var transactionHistoryTool = buildTool({
1597
1891
  if (r.amount == null) return false;
1598
1892
  const sym = r.asset?.toUpperCase() ?? "";
1599
1893
  const isStableLike = sym === "USDC" || sym === "USDT" || sym === "WUSDC" || sym === "WUSDT" || sym === "SUIUSDT" || sym === "USDY" || sym === "USDSUI" || sym === "USDE" || sym === "AUSD" || sym === "FDUSD" || sym === "BUCK";
1600
- const usd = isStableLike ? r.amount : (priceFor(sym) ?? 0) * r.amount;
1601
- if (!isStableLike && priceFor(sym) == null) return true;
1894
+ const usd = isStableLike ? r.amount : (priceFor2(sym) ?? 0) * r.amount;
1895
+ if (!isStableLike && priceFor2(sym) == null) return true;
1602
1896
  return usd >= minUsd;
1603
1897
  });
1604
1898
  }