@t2000/engine 0.48.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 +36 -3
- package/dist/index.js +488 -88
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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();
|
|
@@ -725,6 +981,7 @@ function clearPriceMapCache() {
|
|
|
725
981
|
|
|
726
982
|
// src/tools/balance.ts
|
|
727
983
|
var GAS_RESERVE_SUI2 = 0.05;
|
|
984
|
+
var SUI_ADDRESS_REGEX = /^0x[a-fA-F0-9]{1,64}$/;
|
|
728
985
|
var VSUI_COIN_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
|
|
729
986
|
var SUI_COIN_TYPE = "0x2::sui::SUI";
|
|
730
987
|
var VSUI_FALLBACK_RATE = 1.05;
|
|
@@ -776,21 +1033,38 @@ async function loadPortfolio(address, blockvisionApiKey, fallbackRpcUrl, cache)
|
|
|
776
1033
|
}
|
|
777
1034
|
var balanceCheckTool = buildTool({
|
|
778
1035
|
name: "balance_check",
|
|
779
|
-
description: "Get the
|
|
780
|
-
inputSchema: z.object({
|
|
781
|
-
|
|
1036
|
+
description: "Get the full balance breakdown for the signed-in user OR any public Sui address. Returns wallet holdings (tokens the address 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. Pass `address` to inspect a contact / watched / public wallet; defaults to the signed-in user when omitted.",
|
|
1037
|
+
inputSchema: z.object({
|
|
1038
|
+
address: z.string().regex(SUI_ADDRESS_REGEX).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
|
|
1039
|
+
}),
|
|
1040
|
+
jsonSchema: {
|
|
1041
|
+
type: "object",
|
|
1042
|
+
properties: {
|
|
1043
|
+
address: {
|
|
1044
|
+
type: "string",
|
|
1045
|
+
pattern: "^0x[a-fA-F0-9]{1,64}$",
|
|
1046
|
+
description: "Sui address to inspect (defaults to the signed-in wallet)"
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
required: []
|
|
1050
|
+
},
|
|
782
1051
|
isReadOnly: true,
|
|
783
1052
|
// [v1.4 BlockVision] Wallet contents change after every send / swap /
|
|
784
1053
|
// save / etc. and the price half of this result is sourced from
|
|
785
1054
|
// BlockVision's Indexer REST API. Microcompact must NEVER dedupe these
|
|
786
1055
|
// calls — each one reflects a different on-chain + market snapshot.
|
|
787
1056
|
cacheable: false,
|
|
788
|
-
async call(
|
|
789
|
-
|
|
790
|
-
|
|
1057
|
+
async call(input, context) {
|
|
1058
|
+
const targetAddress = input.address ?? context.walletAddress;
|
|
1059
|
+
const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
|
|
1060
|
+
if (hasNaviMcpGlobal(context)) {
|
|
1061
|
+
if (!targetAddress) {
|
|
1062
|
+
throw new Error("No wallet address provided. Sign in or pass `address` to inspect a public wallet.");
|
|
1063
|
+
}
|
|
1064
|
+
const address = targetAddress;
|
|
791
1065
|
const mgr = getMcpManager(context);
|
|
792
|
-
const hasPositionFetcher = !!
|
|
793
|
-
const [portfolio, positions, rewards, serverPositions] = await Promise.all([
|
|
1066
|
+
const hasPositionFetcher = !!context.positionFetcher;
|
|
1067
|
+
const [portfolio, positions, rewards, serverPositions, defiPortfolio] = await Promise.all([
|
|
794
1068
|
loadPortfolio(
|
|
795
1069
|
address,
|
|
796
1070
|
context.blockvisionApiKey,
|
|
@@ -818,10 +1092,25 @@ var balanceCheckTool = buildTool({
|
|
|
818
1092
|
console.warn("[balance_check] NAVI GET_AVAILABLE_REWARDS failed:", err);
|
|
819
1093
|
return null;
|
|
820
1094
|
}),
|
|
821
|
-
hasPositionFetcher ? context.positionFetcher(
|
|
1095
|
+
hasPositionFetcher ? context.positionFetcher(address).catch((err) => {
|
|
822
1096
|
console.warn("[balance_check] positionFetcher failed:", err);
|
|
823
1097
|
return null;
|
|
824
|
-
}) : 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
|
+
})
|
|
825
1114
|
]);
|
|
826
1115
|
await applyVsuiPriceFallback(portfolio);
|
|
827
1116
|
let availableUsd = 0;
|
|
@@ -868,32 +1157,60 @@ var balanceCheckTool = buildTool({
|
|
|
868
1157
|
const visibleHoldings = holdings.filter((h) => h.usdValue >= 0.01).sort((a, b) => b.usdValue - a.usdValue);
|
|
869
1158
|
const usdcHolding2 = holdings.find((h) => h.symbol === "USDC");
|
|
870
1159
|
const saveableUsdc = usdcHolding2 ? usdcHolding2.balance : 0;
|
|
1160
|
+
const defi2 = defiPortfolio;
|
|
871
1161
|
const bal = {
|
|
872
1162
|
available: availableUsd,
|
|
873
1163
|
savings,
|
|
874
1164
|
debt,
|
|
875
1165
|
pendingRewards: pendingRewardsUsd,
|
|
876
1166
|
gasReserve: gasReserveUsd2,
|
|
877
|
-
|
|
1167
|
+
defi: defi2.totalUsd,
|
|
1168
|
+
defiByProtocol: defi2.perProtocol,
|
|
1169
|
+
defiSource: defi2.source,
|
|
1170
|
+
total: availableUsd + savings + gasReserveUsd2 + pendingRewardsUsd + defi2.totalUsd - debt,
|
|
878
1171
|
stables: stablesUsd,
|
|
879
1172
|
holdings: visibleHoldings,
|
|
880
1173
|
saveableUsdc,
|
|
881
|
-
priceSource: portfolio.source
|
|
1174
|
+
priceSource: portfolio.source,
|
|
1175
|
+
address,
|
|
1176
|
+
isSelfQuery
|
|
882
1177
|
};
|
|
883
1178
|
const holdingsList = visibleHoldings.map((h) => `${h.symbol}: ${h.balance < 1 ? h.balance.toFixed(6) : h.balance.toFixed(2)} ($${h.usdValue.toFixed(2)})`).join(", ");
|
|
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)}.` : "";
|
|
884
1181
|
return {
|
|
885
1182
|
data: bal,
|
|
886
|
-
displayText:
|
|
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.`
|
|
887
1184
|
};
|
|
888
1185
|
}
|
|
1186
|
+
if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`Cannot inspect ${input.address.slice(0, 8)}\u2026 without NAVI MCP enabled. Configure NAVI MCP to enable third-party address reads.`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
889
1191
|
const agent = requireAgent(context);
|
|
890
|
-
const
|
|
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
|
+
]);
|
|
891
1206
|
const gasReserveUsd = typeof balance.gasReserve === "number" ? balance.gasReserve : balance.gasReserve.usdEquiv ?? 0;
|
|
892
1207
|
const stablesTotal = typeof balance.stables === "number" ? balance.stables : Object.values(balance.stables).reduce((a, b) => a + b, 0);
|
|
893
1208
|
const sdkHoldings = balance.holdings;
|
|
894
1209
|
const holdingsArr = Array.isArray(sdkHoldings) ? sdkHoldings : [];
|
|
895
1210
|
const usdcHolding = holdingsArr.find((h) => h.symbol === "USDC");
|
|
896
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;
|
|
897
1214
|
return {
|
|
898
1215
|
data: {
|
|
899
1216
|
available: balance.available,
|
|
@@ -901,12 +1218,17 @@ var balanceCheckTool = buildTool({
|
|
|
901
1218
|
debt: balance.debt,
|
|
902
1219
|
pendingRewards: balance.pendingRewards,
|
|
903
1220
|
gasReserve: gasReserveUsd,
|
|
904
|
-
|
|
1221
|
+
defi: defi.totalUsd,
|
|
1222
|
+
defiByProtocol: defi.perProtocol,
|
|
1223
|
+
defiSource: defi.source,
|
|
1224
|
+
total: sdkTotal,
|
|
905
1225
|
stables: stablesTotal,
|
|
906
1226
|
holdings: holdingsArr,
|
|
907
|
-
saveableUsdc: sdkSaveableUsdc
|
|
1227
|
+
saveableUsdc: sdkSaveableUsdc,
|
|
1228
|
+
address: targetAddress ?? "",
|
|
1229
|
+
isSelfQuery: true
|
|
908
1230
|
},
|
|
909
|
-
displayText: `Balance: $${
|
|
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.`
|
|
910
1232
|
};
|
|
911
1233
|
}
|
|
912
1234
|
});
|
|
@@ -998,6 +1320,7 @@ async function fetchProtocolStats(manager, opts) {
|
|
|
998
1320
|
|
|
999
1321
|
// src/tools/savings.ts
|
|
1000
1322
|
var DUST_THRESHOLD_USD = 0.01;
|
|
1323
|
+
var SUI_ADDRESS_REGEX2 = /^0x[a-fA-F0-9]{1,64}$/;
|
|
1001
1324
|
function buildSavingsFromPositions(sp) {
|
|
1002
1325
|
const positions = [
|
|
1003
1326
|
...sp.supplies.filter((s) => s.amountUsd >= DUST_THRESHOLD_USD).map((s) => ({
|
|
@@ -1039,18 +1362,19 @@ function buildSavingsFromPositions(sp) {
|
|
|
1039
1362
|
}
|
|
1040
1363
|
};
|
|
1041
1364
|
}
|
|
1042
|
-
function formatSavingsDisplay(result) {
|
|
1365
|
+
function formatSavingsDisplay(result, isSelfQuery = true, address) {
|
|
1043
1366
|
const { positions, earnings, fundStatus } = result;
|
|
1044
1367
|
const supplies = positions.filter((p) => p.type === "supply");
|
|
1045
1368
|
const borrows = positions.filter((p) => p.type === "borrow");
|
|
1369
|
+
const subjectPrefix = isSelfQuery || !address ? "" : `${address.slice(0, 6)}\u2026${address.slice(-4)} \u2014 `;
|
|
1046
1370
|
const lines = [];
|
|
1047
1371
|
if (supplies.length > 0) {
|
|
1048
|
-
lines.push(
|
|
1372
|
+
lines.push(`${subjectPrefix}Savings: $${fundStatus.supplied.toFixed(2)} at ${(earnings.currentApy * 100).toFixed(2)}% blended APY`);
|
|
1049
1373
|
for (const s of supplies) {
|
|
1050
1374
|
lines.push(` ${s.symbol}: ${s.amount.toFixed(s.amount < 1 ? 6 : 2)} ($${s.valueUsd.toFixed(2)}) at ${(s.apy * 100).toFixed(2)}% APY`);
|
|
1051
1375
|
}
|
|
1052
1376
|
} else {
|
|
1053
|
-
lines.push(
|
|
1377
|
+
lines.push(`${subjectPrefix}No savings positions.`);
|
|
1054
1378
|
}
|
|
1055
1379
|
if (borrows.length > 0) {
|
|
1056
1380
|
const totalDebt = borrows.reduce((s, b) => s + b.valueUsd, 0);
|
|
@@ -1062,26 +1386,44 @@ function formatSavingsDisplay(result) {
|
|
|
1062
1386
|
}
|
|
1063
1387
|
var savingsInfoTool = buildTool({
|
|
1064
1388
|
name: "savings_info",
|
|
1065
|
-
description: "Get detailed savings positions and earnings: current deposits by protocol, APY, total yield earned, daily earning rate, and projected monthly returns.",
|
|
1066
|
-
inputSchema: z.object({
|
|
1067
|
-
|
|
1389
|
+
description: "Get detailed savings positions and earnings for the signed-in user OR any public Sui address: current deposits by protocol, APY, total yield earned, daily earning rate, and projected monthly returns. Pass `address` to inspect a contact / watched / public wallet; defaults to the signed-in user when omitted.",
|
|
1390
|
+
inputSchema: z.object({
|
|
1391
|
+
address: z.string().regex(SUI_ADDRESS_REGEX2).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
|
|
1392
|
+
}),
|
|
1393
|
+
jsonSchema: {
|
|
1394
|
+
type: "object",
|
|
1395
|
+
properties: {
|
|
1396
|
+
address: {
|
|
1397
|
+
type: "string",
|
|
1398
|
+
pattern: "^0x[a-fA-F0-9]{1,64}$",
|
|
1399
|
+
description: "Sui address to inspect (defaults to the signed-in wallet)"
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
required: []
|
|
1403
|
+
},
|
|
1068
1404
|
isReadOnly: true,
|
|
1069
1405
|
// [v1.5.1] NAVI deposits change on save_deposit / withdraw / claim.
|
|
1070
1406
|
// Each call reflects a fresh on-chain snapshot — never dedupe.
|
|
1071
1407
|
cacheable: false,
|
|
1072
|
-
async call(
|
|
1073
|
-
|
|
1074
|
-
|
|
1408
|
+
async call(input, context) {
|
|
1409
|
+
const targetAddress = input.address ?? context.walletAddress;
|
|
1410
|
+
const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
|
|
1411
|
+
if (context.positionFetcher && targetAddress) {
|
|
1412
|
+
const sp = await context.positionFetcher(targetAddress);
|
|
1075
1413
|
const result2 = buildSavingsFromPositions(sp);
|
|
1076
|
-
|
|
1414
|
+
const stamped2 = { ...result2, address: targetAddress, isSelfQuery };
|
|
1415
|
+
return { data: stamped2, displayText: formatSavingsDisplay(result2, isSelfQuery, targetAddress) };
|
|
1077
1416
|
}
|
|
1078
|
-
if (
|
|
1079
|
-
const savings = await fetchSavings(
|
|
1080
|
-
getMcpManager(context),
|
|
1081
|
-
getWalletAddress(context)
|
|
1082
|
-
);
|
|
1417
|
+
if (hasNaviMcpGlobal(context) && targetAddress) {
|
|
1418
|
+
const savings = await fetchSavings(getMcpManager(context), targetAddress);
|
|
1083
1419
|
savings.positions = savings.positions.filter((p) => p.valueUsd >= DUST_THRESHOLD_USD);
|
|
1084
|
-
|
|
1420
|
+
const stamped2 = { ...savings, address: targetAddress, isSelfQuery };
|
|
1421
|
+
return { data: stamped2, displayText: formatSavingsDisplay(savings, isSelfQuery, targetAddress) };
|
|
1422
|
+
}
|
|
1423
|
+
if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
|
|
1424
|
+
throw new Error(
|
|
1425
|
+
`Cannot inspect ${input.address.slice(0, 8)}\u2026 without NAVI MCP or a positionFetcher. Configure NAVI MCP to enable third-party address reads.`
|
|
1426
|
+
);
|
|
1085
1427
|
}
|
|
1086
1428
|
const agent = requireAgent(context);
|
|
1087
1429
|
const [posResult, earnings, fundStatus] = await Promise.all([
|
|
@@ -1114,9 +1456,11 @@ var savingsInfoTool = buildTool({
|
|
|
1114
1456
|
projectedMonthly: fundStatus.projectedMonthly
|
|
1115
1457
|
}
|
|
1116
1458
|
};
|
|
1117
|
-
|
|
1459
|
+
const stamped = { ...result, address: targetAddress ?? "", isSelfQuery: true };
|
|
1460
|
+
return { data: stamped, displayText: formatSavingsDisplay(result, true, void 0) };
|
|
1118
1461
|
}
|
|
1119
1462
|
});
|
|
1463
|
+
var SUI_ADDRESS_REGEX3 = /^0x[a-fA-F0-9]{1,64}$/;
|
|
1120
1464
|
var DEBT_DUST_USD = 0.01;
|
|
1121
1465
|
function hfStatus(hf, borrowed) {
|
|
1122
1466
|
if (borrowed <= DEBT_DUST_USD) return "healthy";
|
|
@@ -1130,24 +1474,39 @@ function serializeHf(hf, borrowed) {
|
|
|
1130
1474
|
if (hf == null || !Number.isFinite(hf)) return null;
|
|
1131
1475
|
return hf;
|
|
1132
1476
|
}
|
|
1133
|
-
function displayHfText(hf, borrowed, status) {
|
|
1477
|
+
function displayHfText(hf, borrowed, status, isSelfQuery = true, address) {
|
|
1478
|
+
const subject = isSelfQuery || !address ? "Health Factor" : `Health Factor for ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
|
|
1134
1479
|
if (hf == null) {
|
|
1135
|
-
return
|
|
1480
|
+
return `${subject}: \u221E (${status} \u2014 no debt)`;
|
|
1136
1481
|
}
|
|
1137
|
-
return
|
|
1482
|
+
return `${subject}: ${hf.toFixed(2)} (${status})`;
|
|
1138
1483
|
}
|
|
1139
1484
|
var healthCheckTool = buildTool({
|
|
1140
1485
|
name: "health_check",
|
|
1141
|
-
description: 'Check the lending health factor: current HF ratio, total supplied collateral, total borrowed, max additional borrow capacity, and liquidation threshold. HF < 1.5 is risky, < 1.2 is critical. When the
|
|
1142
|
-
inputSchema: z.object({
|
|
1143
|
-
|
|
1486
|
+
description: 'Check the lending health factor for the signed-in user OR any public Sui address: current HF ratio, total supplied collateral, total borrowed, max additional borrow capacity, and liquidation threshold. HF < 1.5 is risky, < 1.2 is critical. When the address has no debt the tool returns healthFactor=null (semantically infinity) \u2014 render that as "Healthy" / \u221E, never as 0 or "Critical". Pass `address` to inspect a contact / watched / public wallet; defaults to the signed-in user when omitted.',
|
|
1487
|
+
inputSchema: z.object({
|
|
1488
|
+
address: z.string().regex(SUI_ADDRESS_REGEX3).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
|
|
1489
|
+
}),
|
|
1490
|
+
jsonSchema: {
|
|
1491
|
+
type: "object",
|
|
1492
|
+
properties: {
|
|
1493
|
+
address: {
|
|
1494
|
+
type: "string",
|
|
1495
|
+
pattern: "^0x[a-fA-F0-9]{1,64}$",
|
|
1496
|
+
description: "Sui address to inspect (defaults to the signed-in wallet)"
|
|
1497
|
+
}
|
|
1498
|
+
},
|
|
1499
|
+
required: []
|
|
1500
|
+
},
|
|
1144
1501
|
isReadOnly: true,
|
|
1145
1502
|
// [v1.5.1] Health factor changes on every borrow / repay / collateral
|
|
1146
1503
|
// movement and even passively as oracle prices update. Never dedupe.
|
|
1147
1504
|
cacheable: false,
|
|
1148
|
-
async call(
|
|
1149
|
-
|
|
1150
|
-
|
|
1505
|
+
async call(input, context) {
|
|
1506
|
+
const targetAddress = input.address ?? context.walletAddress;
|
|
1507
|
+
const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
|
|
1508
|
+
if (context.positionFetcher && targetAddress) {
|
|
1509
|
+
const sp = await context.positionFetcher(targetAddress);
|
|
1151
1510
|
const borrowed2 = sp.borrows;
|
|
1152
1511
|
const rawHf = sp.healthFactor ?? (borrowed2 > 0 ? 0 : Infinity);
|
|
1153
1512
|
const status2 = hfStatus(rawHf, borrowed2);
|
|
@@ -1159,24 +1518,28 @@ var healthCheckTool = buildTool({
|
|
|
1159
1518
|
borrowed: borrowed2,
|
|
1160
1519
|
maxBorrow: sp.maxBorrow,
|
|
1161
1520
|
liquidationThreshold: 0,
|
|
1162
|
-
status: status2
|
|
1521
|
+
status: status2,
|
|
1522
|
+
address: targetAddress,
|
|
1523
|
+
isSelfQuery
|
|
1163
1524
|
},
|
|
1164
|
-
displayText: displayHfText(transportHf2, borrowed2, status2)
|
|
1525
|
+
displayText: displayHfText(transportHf2, borrowed2, status2, isSelfQuery, targetAddress)
|
|
1165
1526
|
};
|
|
1166
1527
|
}
|
|
1167
|
-
if (
|
|
1168
|
-
const hf2 = await fetchHealthFactor(
|
|
1169
|
-
getMcpManager(context),
|
|
1170
|
-
getWalletAddress(context)
|
|
1171
|
-
);
|
|
1528
|
+
if (hasNaviMcpGlobal(context) && targetAddress) {
|
|
1529
|
+
const hf2 = await fetchHealthFactor(getMcpManager(context), targetAddress);
|
|
1172
1530
|
const borrowed2 = hf2.borrowed;
|
|
1173
1531
|
const status2 = hfStatus(hf2.healthFactor, borrowed2);
|
|
1174
1532
|
const transportHf2 = serializeHf(hf2.healthFactor, borrowed2);
|
|
1175
1533
|
return {
|
|
1176
|
-
data: { ...hf2, healthFactor: transportHf2, status: status2 },
|
|
1177
|
-
displayText: displayHfText(transportHf2, borrowed2, status2)
|
|
1534
|
+
data: { ...hf2, healthFactor: transportHf2, status: status2, address: targetAddress, isSelfQuery },
|
|
1535
|
+
displayText: displayHfText(transportHf2, borrowed2, status2, isSelfQuery, targetAddress)
|
|
1178
1536
|
};
|
|
1179
1537
|
}
|
|
1538
|
+
if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
|
|
1539
|
+
throw new Error(
|
|
1540
|
+
`Cannot inspect ${input.address.slice(0, 8)}\u2026 without NAVI MCP or a positionFetcher. Configure NAVI MCP to enable third-party address reads.`
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1180
1543
|
const agent = requireAgent(context);
|
|
1181
1544
|
const hf = await agent.healthFactor();
|
|
1182
1545
|
const borrowed = hf.borrowed;
|
|
@@ -1189,9 +1552,11 @@ var healthCheckTool = buildTool({
|
|
|
1189
1552
|
borrowed,
|
|
1190
1553
|
maxBorrow: hf.maxBorrow,
|
|
1191
1554
|
liquidationThreshold: hf.liquidationThreshold,
|
|
1192
|
-
status
|
|
1555
|
+
status,
|
|
1556
|
+
address: targetAddress ?? "",
|
|
1557
|
+
isSelfQuery: true
|
|
1193
1558
|
},
|
|
1194
|
-
displayText: displayHfText(transportHf, borrowed, status)
|
|
1559
|
+
displayText: displayHfText(transportHf, borrowed, status, true, void 0)
|
|
1195
1560
|
};
|
|
1196
1561
|
}
|
|
1197
1562
|
});
|
|
@@ -1400,14 +1765,14 @@ async function queryHistoryByDate(rpcUrl, address, targetDate, limit) {
|
|
|
1400
1765
|
}
|
|
1401
1766
|
var HISTORY_ACTIONS = ["send", "lending", "swap", "transaction"];
|
|
1402
1767
|
var DEFAULT_LOOKBACK_DAYS = 30;
|
|
1403
|
-
var
|
|
1768
|
+
var SUI_ADDRESS_REGEX4 = /^0x[0-9a-fA-F]{64}$/;
|
|
1404
1769
|
var transactionHistoryTool = buildTool({
|
|
1405
1770
|
name: "transaction_history",
|
|
1406
1771
|
description: 'Retrieve recent transaction history (last 30 days by default): sends, saves, withdrawals, borrows, repayments, swaps, and rewards claims. Renders a rich transaction card.\n\nBy default, queries the SIGNED-IN USER\'S history. To inspect another wallet (a saved contact, a watched address, any public Sui address), pass `address` \u2014 e.g. user asks "show funkii\'s recent transactions" with funkii at 0x40cd\u20263e62, call with `address: "0x40cd\u20263e62"`. To filter the user\'s own history to a specific counterparty (user asks "show transactions WITH funkii"), pass `counterparty` \u2014 keeps the query rooted in the user\'s wallet but shows only rows where funkii is the recipient or sender.\n\nFilter args: `date` (YYYY-MM-DD), `action` (send/lending/swap), `minUsd` (minimum amount in USD \u2014 use this for "transactions over $X" instead of post-filtering), `assetSymbol` (e.g. "USDC", "SUI"), `direction` ("in" or "out"). The card itself respects all filters \u2014 never re-list the rows in narration.\n\nInternally queries both `FromAddress` and `ToAddress` filters in parallel and dedupes by digest, so pure-receive transactions (someone sends to the queried address with no balance-affecting outbound) are no longer dropped.',
|
|
1407
1772
|
inputSchema: z.object({
|
|
1408
1773
|
limit: z.number().int().min(1).max(50).optional(),
|
|
1409
|
-
address: z.string().regex(
|
|
1410
|
-
counterparty: z.string().regex(
|
|
1774
|
+
address: z.string().regex(SUI_ADDRESS_REGEX4, "Must be a 0x-prefixed 64-hex Sui address").optional().describe("Sui address to query history FOR. When omitted, defaults to the signed-in user's wallet. Pass this when the user asks about a contact's, watched address's, or any other public wallet's history."),
|
|
1775
|
+
counterparty: z.string().regex(SUI_ADDRESS_REGEX4, "Must be a 0x-prefixed 64-hex Sui address").optional().describe('Sui address to filter rows by \u2014 only transactions where the queried address sent to or received from this counterparty are returned. Use for "show transactions with <contact>" queries. Compares against `tx.recipient` (case-insensitive).'),
|
|
1411
1776
|
date: z.string().optional().describe("Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."),
|
|
1412
1777
|
action: z.enum(HISTORY_ACTIONS).optional().describe("Filter by action: send, lending, swap, or transaction."),
|
|
1413
1778
|
minUsd: z.number().min(0).optional().describe('Minimum transaction amount in USD. Use this for "transactions over $X" \u2014 the amount is converted to USD using the asset price snapshot.'),
|
|
@@ -1505,7 +1870,7 @@ var transactionHistoryTool = buildTool({
|
|
|
1505
1870
|
const targetAddress = input.address ?? context.walletAddress;
|
|
1506
1871
|
const isSelfQuery = !!targetAddress && !!context.walletAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
|
|
1507
1872
|
const prices = context.tokenPrices;
|
|
1508
|
-
const
|
|
1873
|
+
const priceFor2 = (sym) => {
|
|
1509
1874
|
if (!sym || !prices) return void 0;
|
|
1510
1875
|
return prices[sym.toUpperCase()] ?? prices[sym.toLowerCase()] ?? prices[sym];
|
|
1511
1876
|
};
|
|
@@ -1526,8 +1891,8 @@ var transactionHistoryTool = buildTool({
|
|
|
1526
1891
|
if (r.amount == null) return false;
|
|
1527
1892
|
const sym = r.asset?.toUpperCase() ?? "";
|
|
1528
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";
|
|
1529
|
-
const usd = isStableLike ? r.amount : (
|
|
1530
|
-
if (!isStableLike &&
|
|
1894
|
+
const usd = isStableLike ? r.amount : (priceFor2(sym) ?? 0) * r.amount;
|
|
1895
|
+
if (!isStableLike && priceFor2(sym) == null) return true;
|
|
1531
1896
|
return usd >= minUsd;
|
|
1532
1897
|
});
|
|
1533
1898
|
}
|
|
@@ -3028,14 +3393,14 @@ Use when the user asks for a visual chart, simulator, or financial overview. Pic
|
|
|
3028
3393
|
|
|
3029
3394
|
- activity_heatmap \u2014 on-chain transaction history as a GitHub-style heatmap (WORKS NOW \u2014 accepts \`params.address\` to inspect any public Sui wallet; defaults to the signed-in user)
|
|
3030
3395
|
- portfolio_timeline \u2014 net worth over time, wallet/savings/debt breakdown (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user)
|
|
3031
|
-
- yield_projector \u2014 compound yield simulator with amount/APY/period sliders (WORKS NOW \u2014 client-side)
|
|
3032
|
-
- health_simulator \u2014 borrow health factor simulator with collateral/debt sliders (WORKS NOW \u2014
|
|
3033
|
-
- dca_planner \u2014 savings plan curve for regular monthly deposits (WORKS NOW \u2014 client-side)
|
|
3396
|
+
- yield_projector \u2014 compound yield simulator with amount/APY/period sliders (WORKS NOW \u2014 client-side, no address needed)
|
|
3397
|
+
- health_simulator \u2014 borrow health factor simulator with collateral/debt sliders (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user's current position)
|
|
3398
|
+
- dca_planner \u2014 savings plan curve for regular monthly deposits (WORKS NOW \u2014 client-side, no address needed)
|
|
3034
3399
|
- spending_breakdown \u2014 spending by service category (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user)
|
|
3035
3400
|
- watch_address \u2014 portfolio overview for any public Sui address (WORKS NOW \u2014 pass \`params.address\`)
|
|
3036
|
-
- full_portfolio \u2014 4-panel overview: savings, health, activity, spending (WORKS NOW \u2014
|
|
3401
|
+
- full_portfolio \u2014 4-panel overview: savings, health, activity, spending (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user)
|
|
3037
3402
|
|
|
3038
|
-
When the user asks to inspect a saved contact or watched address \u2014 e.g. "show funkii's activity heatmap", "what's funkii's portfolio look like", "spending breakdown for 0x40cd\u2026" \u2014 pass that wallet's address as \`params.address\`.
|
|
3403
|
+
When the user asks to inspect a saved contact or watched address \u2014 e.g. "show funkii's activity heatmap", "what's funkii's portfolio look like", "spending breakdown for 0x40cd\u2026", "give me a full portfolio overview of 0x40cd\u2026" \u2014 pass that wallet's address as \`params.address\`. Six of the eight templates (activity_heatmap, portfolio_timeline, spending_breakdown, watch_address, health_simulator, full_portfolio) will scope their data fetch to that address; only the pure client-side simulators (yield_projector, dca_planner) ignore params.address.
|
|
3039
3404
|
|
|
3040
3405
|
Always prefer the canvas for visualisation requests. After rendering, offer to explain what the user sees.`,
|
|
3041
3406
|
inputSchema: z.object({
|
|
@@ -3043,7 +3408,7 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
|
|
|
3043
3408
|
params: z.object({
|
|
3044
3409
|
period: z.enum(["1m", "3m", "6m", "1y"]).optional().describe("Time period for time-based templates"),
|
|
3045
3410
|
address: z.string().optional().describe(
|
|
3046
|
-
"Sui address for the
|
|
3411
|
+
"Sui address for the six address-aware templates (activity_heatmap, portfolio_timeline, spending_breakdown, watch_address, health_simulator, full_portfolio). Defaults to the signed-in user; pass an explicit address to inspect a contact, watched wallet, or any other public address."
|
|
3047
3412
|
)
|
|
3048
3413
|
}).optional()
|
|
3049
3414
|
}),
|
|
@@ -3077,7 +3442,20 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
|
|
|
3077
3442
|
return { address: target, isSelfRender };
|
|
3078
3443
|
};
|
|
3079
3444
|
if (template === "full_portfolio") {
|
|
3080
|
-
const
|
|
3445
|
+
const { address, isSelfRender } = resolveAddressTarget();
|
|
3446
|
+
if (!address) {
|
|
3447
|
+
return {
|
|
3448
|
+
data: {
|
|
3449
|
+
__canvas: true,
|
|
3450
|
+
template,
|
|
3451
|
+
title,
|
|
3452
|
+
templateData: { available: false, message: "Full Portfolio needs an address." }
|
|
3453
|
+
},
|
|
3454
|
+
displayText: "Full Portfolio requires an address."
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
const titleSuffix = isSelfRender ? "" : ` \u2014 ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
|
|
3458
|
+
const pos = isSelfRender ? context.serverPositions : null;
|
|
3081
3459
|
const rate = normalizeSavingsRate(pos?.savingsRate);
|
|
3082
3460
|
const savings = pos?.savings ?? 0;
|
|
3083
3461
|
const borrows = pos?.borrows ?? 0;
|
|
@@ -3085,17 +3463,18 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
|
|
|
3085
3463
|
data: {
|
|
3086
3464
|
__canvas: true,
|
|
3087
3465
|
template,
|
|
3088
|
-
title
|
|
3466
|
+
title: `${title}${titleSuffix}`,
|
|
3089
3467
|
templateData: {
|
|
3090
3468
|
available: true,
|
|
3091
|
-
address
|
|
3469
|
+
address,
|
|
3470
|
+
isSelfRender,
|
|
3092
3471
|
currentSavings: savings,
|
|
3093
3472
|
currentDebt: borrows,
|
|
3094
3473
|
healthFactor: pos?.healthFactor ?? null,
|
|
3095
3474
|
savingsRate: rate
|
|
3096
3475
|
}
|
|
3097
3476
|
},
|
|
3098
|
-
displayText: `Opened Full Portfolio Overview.`
|
|
3477
|
+
displayText: isSelfRender ? `Opened Full Portfolio Overview.` : `Opened Full Portfolio Overview for ${address.slice(0, 6)}\u2026${address.slice(-4)}.`
|
|
3099
3478
|
};
|
|
3100
3479
|
}
|
|
3101
3480
|
if (template === "watch_address") {
|
|
@@ -3226,20 +3605,28 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
|
|
|
3226
3605
|
};
|
|
3227
3606
|
}
|
|
3228
3607
|
if (template === "health_simulator") {
|
|
3229
|
-
const
|
|
3608
|
+
const { address: targetAddress, isSelfRender } = resolveAddressTarget();
|
|
3609
|
+
const seedFromPos = isSelfRender;
|
|
3610
|
+
const seedSavings = seedFromPos ? totalSavings : 0;
|
|
3611
|
+
const seedBorrows = seedFromPos ? totalBorrows : 0;
|
|
3612
|
+
const seedHf = seedFromPos ? healthFactor : null;
|
|
3613
|
+
const roundedDebt = seedBorrows >= 1 ? Math.round(seedBorrows) : seedBorrows > 0 ? parseFloat(seedBorrows.toFixed(4)) : 0;
|
|
3614
|
+
const titleSuffix = !targetAddress || isSelfRender ? "" : ` \u2014 ${targetAddress.slice(0, 6)}\u2026${targetAddress.slice(-4)}`;
|
|
3230
3615
|
return {
|
|
3231
3616
|
data: {
|
|
3232
3617
|
__canvas: true,
|
|
3233
3618
|
template,
|
|
3234
|
-
title
|
|
3619
|
+
title: `${title}${titleSuffix}`,
|
|
3235
3620
|
templateData: {
|
|
3236
3621
|
available: true,
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3622
|
+
address: targetAddress ?? "",
|
|
3623
|
+
isSelfRender,
|
|
3624
|
+
initialCollateral: seedSavings > 0 ? Math.round(seedSavings) : 1500,
|
|
3625
|
+
initialDebt: roundedDebt > 0 ? roundedDebt : seedSavings > 0 ? 0 : 500,
|
|
3626
|
+
currentHf: seedHf
|
|
3240
3627
|
}
|
|
3241
3628
|
},
|
|
3242
|
-
displayText: `Opened Health Factor Simulator. Current HF: ${healthFactor !== null ? healthFactor.toFixed(2) : "no active position"}.`
|
|
3629
|
+
displayText: isSelfRender ? `Opened Health Factor Simulator. Current HF: ${healthFactor !== null ? healthFactor.toFixed(2) : "no active position"}.` : `Opened Health Factor Simulator${titleSuffix}. The simulator will fetch the current health factor for that wallet.`
|
|
3243
3630
|
};
|
|
3244
3631
|
}
|
|
3245
3632
|
if (template === "dca_planner") {
|
|
@@ -3354,49 +3741,62 @@ var yieldSummaryTool = buildTool({
|
|
|
3354
3741
|
}
|
|
3355
3742
|
}
|
|
3356
3743
|
});
|
|
3744
|
+
var SUI_ADDRESS_REGEX5 = /^0x[a-fA-F0-9]{1,64}$/;
|
|
3357
3745
|
var activitySummaryTool = buildTool({
|
|
3358
3746
|
name: "activity_summary",
|
|
3359
|
-
description: "Returns a categorised DeFi activity summary for
|
|
3747
|
+
description: "Returns a categorised DeFi activity summary for the signed-in user OR any public Sui address: transaction count, breakdown by action type (saves, sends, borrows, repayments, swaps, payments), total moved, net savings change, and yield earned. Use when the user asks about activity, transaction history summary, or what someone has done recently. Pass `address` to inspect a contact / watched / public wallet; defaults to the signed-in user when omitted.",
|
|
3360
3748
|
inputSchema: z.object({
|
|
3361
|
-
period: z.enum(["week", "month", "year", "all"]).optional().describe("Time period. Defaults to current month.")
|
|
3749
|
+
period: z.enum(["week", "month", "year", "all"]).optional().describe("Time period. Defaults to current month."),
|
|
3750
|
+
address: z.string().regex(SUI_ADDRESS_REGEX5).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
|
|
3362
3751
|
}),
|
|
3363
3752
|
jsonSchema: {
|
|
3364
3753
|
type: "object",
|
|
3365
3754
|
properties: {
|
|
3366
|
-
period: { type: "string", enum: ["week", "month", "year", "all"] }
|
|
3755
|
+
period: { type: "string", enum: ["week", "month", "year", "all"] },
|
|
3756
|
+
address: {
|
|
3757
|
+
type: "string",
|
|
3758
|
+
pattern: "^0x[a-fA-F0-9]{1,64}$",
|
|
3759
|
+
description: "Sui address to inspect (defaults to the signed-in wallet)"
|
|
3760
|
+
}
|
|
3367
3761
|
}
|
|
3368
3762
|
},
|
|
3369
3763
|
isReadOnly: true,
|
|
3370
3764
|
async call(input, context) {
|
|
3371
3765
|
const period = input.period ?? "month";
|
|
3372
3766
|
const apiUrl = context.env?.AUDRIC_INTERNAL_API_URL;
|
|
3373
|
-
const
|
|
3767
|
+
const targetAddress = input.address ?? context.walletAddress;
|
|
3768
|
+
const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
|
|
3374
3769
|
const empty = {
|
|
3375
3770
|
period,
|
|
3376
3771
|
totalTransactions: 0,
|
|
3377
3772
|
byAction: [],
|
|
3378
3773
|
totalMovedUsd: 0,
|
|
3379
3774
|
netSavingsUsd: 0,
|
|
3380
|
-
yieldEarnedUsd: 0
|
|
3775
|
+
yieldEarnedUsd: 0,
|
|
3776
|
+
address: targetAddress,
|
|
3777
|
+
isSelfQuery
|
|
3381
3778
|
};
|
|
3382
|
-
if (!apiUrl || !
|
|
3779
|
+
if (!apiUrl || !targetAddress) {
|
|
3383
3780
|
return { data: empty, displayText: "Activity summary not available." };
|
|
3384
3781
|
}
|
|
3385
3782
|
try {
|
|
3783
|
+
const callerHeader = context.walletAddress ?? targetAddress;
|
|
3386
3784
|
const res = await fetch(
|
|
3387
|
-
`${apiUrl}/api/analytics/activity-summary?address=${
|
|
3388
|
-
{ headers: { "x-sui-address":
|
|
3785
|
+
`${apiUrl}/api/analytics/activity-summary?address=${targetAddress}&period=${period}`,
|
|
3786
|
+
{ headers: { "x-sui-address": callerHeader }, signal: context.signal }
|
|
3389
3787
|
);
|
|
3390
3788
|
if (!res.ok) {
|
|
3391
3789
|
return { data: empty, displayText: `Could not fetch activity data (HTTP ${res.status}).` };
|
|
3392
3790
|
}
|
|
3393
|
-
const
|
|
3791
|
+
const raw = await res.json();
|
|
3792
|
+
const data = { ...raw, address: targetAddress, isSelfQuery };
|
|
3394
3793
|
const sorted = [...data.byAction ?? []].sort((a, b) => b.count - a.count);
|
|
3395
3794
|
const top = sorted.slice(0, 3).map((a) => `${a.action} (${a.count})`).join(", ");
|
|
3396
3795
|
const periodLabel = data.period === "all" ? "all time" : `this ${data.period}`;
|
|
3796
|
+
const subjectPrefix = isSelfQuery ? "" : `${targetAddress.slice(0, 6)}\u2026${targetAddress.slice(-4)} \u2014 `;
|
|
3397
3797
|
return {
|
|
3398
3798
|
data,
|
|
3399
|
-
displayText: data.totalTransactions > 0 ? `${data.totalTransactions} transactions ${periodLabel}. Top: ${top}. Total moved: $${data.totalMovedUsd.toFixed(2)}. Net savings: $${data.netSavingsUsd.toFixed(2)}.` :
|
|
3799
|
+
displayText: data.totalTransactions > 0 ? `${subjectPrefix}${data.totalTransactions} transactions ${periodLabel}. Top: ${top}. Total moved: $${data.totalMovedUsd.toFixed(2)}. Net savings: $${data.netSavingsUsd.toFixed(2)}.` : `${subjectPrefix}No activity recorded for ${periodLabel}.`
|
|
3400
3800
|
};
|
|
3401
3801
|
} catch {
|
|
3402
3802
|
return { data: empty, displayText: "Error fetching activity summary." };
|
|
@@ -3873,7 +4273,7 @@ function guardCostWarning(tool, _call, conversationText) {
|
|
|
3873
4273
|
message: "This action has a monetary cost. Confirm the user is aware before proceeding."
|
|
3874
4274
|
};
|
|
3875
4275
|
}
|
|
3876
|
-
var
|
|
4276
|
+
var SUI_ADDRESS_REGEX6 = /^0x[a-fA-F0-9]{64}$/;
|
|
3877
4277
|
function normalizeAddress(addr) {
|
|
3878
4278
|
return addr.trim().toLowerCase();
|
|
3879
4279
|
}
|
|
@@ -3938,7 +4338,7 @@ function guardAddressSource(tool, call, userText, contacts, walletAddress) {
|
|
|
3938
4338
|
if (!rawTo) {
|
|
3939
4339
|
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3940
4340
|
}
|
|
3941
|
-
if (!
|
|
4341
|
+
if (!SUI_ADDRESS_REGEX6.test(rawTo)) {
|
|
3942
4342
|
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3943
4343
|
}
|
|
3944
4344
|
const normalizedTo = normalizeAddress(rawTo);
|