@t2000/engine 0.48.0 → 0.49.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 +33 -3
- package/dist/index.js +185 -79
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1954,7 +1954,9 @@ declare const renderCanvasTool: Tool<{
|
|
|
1954
1954
|
} | undefined;
|
|
1955
1955
|
}, unknown>;
|
|
1956
1956
|
|
|
1957
|
-
declare const balanceCheckTool: Tool<{
|
|
1957
|
+
declare const balanceCheckTool: Tool<{
|
|
1958
|
+
address?: string | undefined;
|
|
1959
|
+
}, {
|
|
1958
1960
|
available: number;
|
|
1959
1961
|
savings: number;
|
|
1960
1962
|
debt: number;
|
|
@@ -1964,17 +1966,42 @@ declare const balanceCheckTool: Tool<{}, {
|
|
|
1964
1966
|
stables: number;
|
|
1965
1967
|
holdings: any[];
|
|
1966
1968
|
saveableUsdc: number;
|
|
1969
|
+
address: string;
|
|
1970
|
+
isSelfQuery: boolean;
|
|
1967
1971
|
}>;
|
|
1968
1972
|
|
|
1969
|
-
declare const savingsInfoTool: Tool<{
|
|
1973
|
+
declare const savingsInfoTool: Tool<{
|
|
1974
|
+
address?: string | undefined;
|
|
1975
|
+
}, {
|
|
1976
|
+
address: string;
|
|
1977
|
+
isSelfQuery: boolean;
|
|
1978
|
+
positions: PositionEntry[];
|
|
1979
|
+
earnings: {
|
|
1980
|
+
totalYieldEarned: number;
|
|
1981
|
+
currentApy: number;
|
|
1982
|
+
dailyEarning: number;
|
|
1983
|
+
supplied: number;
|
|
1984
|
+
};
|
|
1985
|
+
fundStatus: {
|
|
1986
|
+
supplied: number;
|
|
1987
|
+
apy: number;
|
|
1988
|
+
earnedToday: number;
|
|
1989
|
+
earnedAllTime: number;
|
|
1990
|
+
projectedMonthly: number;
|
|
1991
|
+
};
|
|
1992
|
+
}>;
|
|
1970
1993
|
|
|
1971
|
-
declare const healthCheckTool: Tool<{
|
|
1994
|
+
declare const healthCheckTool: Tool<{
|
|
1995
|
+
address?: string | undefined;
|
|
1996
|
+
}, {
|
|
1972
1997
|
healthFactor: number | null;
|
|
1973
1998
|
supplied: number;
|
|
1974
1999
|
borrowed: number;
|
|
1975
2000
|
maxBorrow: number;
|
|
1976
2001
|
liquidationThreshold: number;
|
|
1977
2002
|
status: string;
|
|
2003
|
+
address: string;
|
|
2004
|
+
isSelfQuery: boolean;
|
|
1978
2005
|
}>;
|
|
1979
2006
|
|
|
1980
2007
|
type RateMap = Record<string, {
|
|
@@ -2274,8 +2301,11 @@ interface ActivitySummary {
|
|
|
2274
2301
|
totalMovedUsd: number;
|
|
2275
2302
|
netSavingsUsd: number;
|
|
2276
2303
|
yieldEarnedUsd: number;
|
|
2304
|
+
address?: string;
|
|
2305
|
+
isSelfQuery?: boolean;
|
|
2277
2306
|
}
|
|
2278
2307
|
declare const activitySummaryTool: Tool<{
|
|
2308
|
+
address?: string | undefined;
|
|
2279
2309
|
period?: "month" | "year" | "all" | "week" | undefined;
|
|
2280
2310
|
}, ActivitySummary>;
|
|
2281
2311
|
|
package/dist/index.js
CHANGED
|
@@ -725,6 +725,7 @@ function clearPriceMapCache() {
|
|
|
725
725
|
|
|
726
726
|
// src/tools/balance.ts
|
|
727
727
|
var GAS_RESERVE_SUI2 = 0.05;
|
|
728
|
+
var SUI_ADDRESS_REGEX = /^0x[a-fA-F0-9]{1,64}$/;
|
|
728
729
|
var VSUI_COIN_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
|
|
729
730
|
var SUI_COIN_TYPE = "0x2::sui::SUI";
|
|
730
731
|
var VSUI_FALLBACK_RATE = 1.05;
|
|
@@ -776,20 +777,37 @@ async function loadPortfolio(address, blockvisionApiKey, fallbackRpcUrl, cache)
|
|
|
776
777
|
}
|
|
777
778
|
var balanceCheckTool = buildTool({
|
|
778
779
|
name: "balance_check",
|
|
779
|
-
description: "Get the
|
|
780
|
-
inputSchema: z.object({
|
|
781
|
-
|
|
780
|
+
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.",
|
|
781
|
+
inputSchema: z.object({
|
|
782
|
+
address: z.string().regex(SUI_ADDRESS_REGEX).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
|
|
783
|
+
}),
|
|
784
|
+
jsonSchema: {
|
|
785
|
+
type: "object",
|
|
786
|
+
properties: {
|
|
787
|
+
address: {
|
|
788
|
+
type: "string",
|
|
789
|
+
pattern: "^0x[a-fA-F0-9]{1,64}$",
|
|
790
|
+
description: "Sui address to inspect (defaults to the signed-in wallet)"
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
required: []
|
|
794
|
+
},
|
|
782
795
|
isReadOnly: true,
|
|
783
796
|
// [v1.4 BlockVision] Wallet contents change after every send / swap /
|
|
784
797
|
// save / etc. and the price half of this result is sourced from
|
|
785
798
|
// BlockVision's Indexer REST API. Microcompact must NEVER dedupe these
|
|
786
799
|
// calls — each one reflects a different on-chain + market snapshot.
|
|
787
800
|
cacheable: false,
|
|
788
|
-
async call(
|
|
789
|
-
|
|
790
|
-
|
|
801
|
+
async call(input, context) {
|
|
802
|
+
const targetAddress = input.address ?? context.walletAddress;
|
|
803
|
+
const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
|
|
804
|
+
if (hasNaviMcpGlobal(context)) {
|
|
805
|
+
if (!targetAddress) {
|
|
806
|
+
throw new Error("No wallet address provided. Sign in or pass `address` to inspect a public wallet.");
|
|
807
|
+
}
|
|
808
|
+
const address = targetAddress;
|
|
791
809
|
const mgr = getMcpManager(context);
|
|
792
|
-
const hasPositionFetcher = !!
|
|
810
|
+
const hasPositionFetcher = !!context.positionFetcher;
|
|
793
811
|
const [portfolio, positions, rewards, serverPositions] = await Promise.all([
|
|
794
812
|
loadPortfolio(
|
|
795
813
|
address,
|
|
@@ -818,7 +836,7 @@ var balanceCheckTool = buildTool({
|
|
|
818
836
|
console.warn("[balance_check] NAVI GET_AVAILABLE_REWARDS failed:", err);
|
|
819
837
|
return null;
|
|
820
838
|
}),
|
|
821
|
-
hasPositionFetcher ? context.positionFetcher(
|
|
839
|
+
hasPositionFetcher ? context.positionFetcher(address).catch((err) => {
|
|
822
840
|
console.warn("[balance_check] positionFetcher failed:", err);
|
|
823
841
|
return null;
|
|
824
842
|
}) : Promise.resolve(null)
|
|
@@ -878,14 +896,22 @@ var balanceCheckTool = buildTool({
|
|
|
878
896
|
stables: stablesUsd,
|
|
879
897
|
holdings: visibleHoldings,
|
|
880
898
|
saveableUsdc,
|
|
881
|
-
priceSource: portfolio.source
|
|
899
|
+
priceSource: portfolio.source,
|
|
900
|
+
address,
|
|
901
|
+
isSelfQuery
|
|
882
902
|
};
|
|
883
903
|
const holdingsList = visibleHoldings.map((h) => `${h.symbol}: ${h.balance < 1 ? h.balance.toFixed(6) : h.balance.toFixed(2)} ($${h.usdValue.toFixed(2)})`).join(", ");
|
|
904
|
+
const subjectPrefix = isSelfQuery ? "Balance" : `Balance for ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
|
|
884
905
|
return {
|
|
885
906
|
data: bal,
|
|
886
|
-
displayText:
|
|
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.`
|
|
887
908
|
};
|
|
888
909
|
}
|
|
910
|
+
if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
|
|
911
|
+
throw new Error(
|
|
912
|
+
`Cannot inspect ${input.address.slice(0, 8)}\u2026 without NAVI MCP enabled. Configure NAVI MCP to enable third-party address reads.`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
889
915
|
const agent = requireAgent(context);
|
|
890
916
|
const balance = await agent.balance();
|
|
891
917
|
const gasReserveUsd = typeof balance.gasReserve === "number" ? balance.gasReserve : balance.gasReserve.usdEquiv ?? 0;
|
|
@@ -904,7 +930,9 @@ var balanceCheckTool = buildTool({
|
|
|
904
930
|
total: balance.total,
|
|
905
931
|
stables: stablesTotal,
|
|
906
932
|
holdings: holdingsArr,
|
|
907
|
-
saveableUsdc: sdkSaveableUsdc
|
|
933
|
+
saveableUsdc: sdkSaveableUsdc,
|
|
934
|
+
address: targetAddress ?? "",
|
|
935
|
+
isSelfQuery: true
|
|
908
936
|
},
|
|
909
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.`
|
|
910
938
|
};
|
|
@@ -998,6 +1026,7 @@ async function fetchProtocolStats(manager, opts) {
|
|
|
998
1026
|
|
|
999
1027
|
// src/tools/savings.ts
|
|
1000
1028
|
var DUST_THRESHOLD_USD = 0.01;
|
|
1029
|
+
var SUI_ADDRESS_REGEX2 = /^0x[a-fA-F0-9]{1,64}$/;
|
|
1001
1030
|
function buildSavingsFromPositions(sp) {
|
|
1002
1031
|
const positions = [
|
|
1003
1032
|
...sp.supplies.filter((s) => s.amountUsd >= DUST_THRESHOLD_USD).map((s) => ({
|
|
@@ -1039,18 +1068,19 @@ function buildSavingsFromPositions(sp) {
|
|
|
1039
1068
|
}
|
|
1040
1069
|
};
|
|
1041
1070
|
}
|
|
1042
|
-
function formatSavingsDisplay(result) {
|
|
1071
|
+
function formatSavingsDisplay(result, isSelfQuery = true, address) {
|
|
1043
1072
|
const { positions, earnings, fundStatus } = result;
|
|
1044
1073
|
const supplies = positions.filter((p) => p.type === "supply");
|
|
1045
1074
|
const borrows = positions.filter((p) => p.type === "borrow");
|
|
1075
|
+
const subjectPrefix = isSelfQuery || !address ? "" : `${address.slice(0, 6)}\u2026${address.slice(-4)} \u2014 `;
|
|
1046
1076
|
const lines = [];
|
|
1047
1077
|
if (supplies.length > 0) {
|
|
1048
|
-
lines.push(
|
|
1078
|
+
lines.push(`${subjectPrefix}Savings: $${fundStatus.supplied.toFixed(2)} at ${(earnings.currentApy * 100).toFixed(2)}% blended APY`);
|
|
1049
1079
|
for (const s of supplies) {
|
|
1050
1080
|
lines.push(` ${s.symbol}: ${s.amount.toFixed(s.amount < 1 ? 6 : 2)} ($${s.valueUsd.toFixed(2)}) at ${(s.apy * 100).toFixed(2)}% APY`);
|
|
1051
1081
|
}
|
|
1052
1082
|
} else {
|
|
1053
|
-
lines.push(
|
|
1083
|
+
lines.push(`${subjectPrefix}No savings positions.`);
|
|
1054
1084
|
}
|
|
1055
1085
|
if (borrows.length > 0) {
|
|
1056
1086
|
const totalDebt = borrows.reduce((s, b) => s + b.valueUsd, 0);
|
|
@@ -1062,26 +1092,44 @@ function formatSavingsDisplay(result) {
|
|
|
1062
1092
|
}
|
|
1063
1093
|
var savingsInfoTool = buildTool({
|
|
1064
1094
|
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
|
-
|
|
1095
|
+
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.",
|
|
1096
|
+
inputSchema: z.object({
|
|
1097
|
+
address: z.string().regex(SUI_ADDRESS_REGEX2).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
|
|
1098
|
+
}),
|
|
1099
|
+
jsonSchema: {
|
|
1100
|
+
type: "object",
|
|
1101
|
+
properties: {
|
|
1102
|
+
address: {
|
|
1103
|
+
type: "string",
|
|
1104
|
+
pattern: "^0x[a-fA-F0-9]{1,64}$",
|
|
1105
|
+
description: "Sui address to inspect (defaults to the signed-in wallet)"
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
required: []
|
|
1109
|
+
},
|
|
1068
1110
|
isReadOnly: true,
|
|
1069
1111
|
// [v1.5.1] NAVI deposits change on save_deposit / withdraw / claim.
|
|
1070
1112
|
// Each call reflects a fresh on-chain snapshot — never dedupe.
|
|
1071
1113
|
cacheable: false,
|
|
1072
|
-
async call(
|
|
1073
|
-
|
|
1074
|
-
|
|
1114
|
+
async call(input, context) {
|
|
1115
|
+
const targetAddress = input.address ?? context.walletAddress;
|
|
1116
|
+
const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
|
|
1117
|
+
if (context.positionFetcher && targetAddress) {
|
|
1118
|
+
const sp = await context.positionFetcher(targetAddress);
|
|
1075
1119
|
const result2 = buildSavingsFromPositions(sp);
|
|
1076
|
-
|
|
1120
|
+
const stamped2 = { ...result2, address: targetAddress, isSelfQuery };
|
|
1121
|
+
return { data: stamped2, displayText: formatSavingsDisplay(result2, isSelfQuery, targetAddress) };
|
|
1077
1122
|
}
|
|
1078
|
-
if (
|
|
1079
|
-
const savings = await fetchSavings(
|
|
1080
|
-
getMcpManager(context),
|
|
1081
|
-
getWalletAddress(context)
|
|
1082
|
-
);
|
|
1123
|
+
if (hasNaviMcpGlobal(context) && targetAddress) {
|
|
1124
|
+
const savings = await fetchSavings(getMcpManager(context), targetAddress);
|
|
1083
1125
|
savings.positions = savings.positions.filter((p) => p.valueUsd >= DUST_THRESHOLD_USD);
|
|
1084
|
-
|
|
1126
|
+
const stamped2 = { ...savings, address: targetAddress, isSelfQuery };
|
|
1127
|
+
return { data: stamped2, displayText: formatSavingsDisplay(savings, isSelfQuery, targetAddress) };
|
|
1128
|
+
}
|
|
1129
|
+
if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
`Cannot inspect ${input.address.slice(0, 8)}\u2026 without NAVI MCP or a positionFetcher. Configure NAVI MCP to enable third-party address reads.`
|
|
1132
|
+
);
|
|
1085
1133
|
}
|
|
1086
1134
|
const agent = requireAgent(context);
|
|
1087
1135
|
const [posResult, earnings, fundStatus] = await Promise.all([
|
|
@@ -1114,9 +1162,11 @@ var savingsInfoTool = buildTool({
|
|
|
1114
1162
|
projectedMonthly: fundStatus.projectedMonthly
|
|
1115
1163
|
}
|
|
1116
1164
|
};
|
|
1117
|
-
|
|
1165
|
+
const stamped = { ...result, address: targetAddress ?? "", isSelfQuery: true };
|
|
1166
|
+
return { data: stamped, displayText: formatSavingsDisplay(result, true, void 0) };
|
|
1118
1167
|
}
|
|
1119
1168
|
});
|
|
1169
|
+
var SUI_ADDRESS_REGEX3 = /^0x[a-fA-F0-9]{1,64}$/;
|
|
1120
1170
|
var DEBT_DUST_USD = 0.01;
|
|
1121
1171
|
function hfStatus(hf, borrowed) {
|
|
1122
1172
|
if (borrowed <= DEBT_DUST_USD) return "healthy";
|
|
@@ -1130,24 +1180,39 @@ function serializeHf(hf, borrowed) {
|
|
|
1130
1180
|
if (hf == null || !Number.isFinite(hf)) return null;
|
|
1131
1181
|
return hf;
|
|
1132
1182
|
}
|
|
1133
|
-
function displayHfText(hf, borrowed, status) {
|
|
1183
|
+
function displayHfText(hf, borrowed, status, isSelfQuery = true, address) {
|
|
1184
|
+
const subject = isSelfQuery || !address ? "Health Factor" : `Health Factor for ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
|
|
1134
1185
|
if (hf == null) {
|
|
1135
|
-
return
|
|
1186
|
+
return `${subject}: \u221E (${status} \u2014 no debt)`;
|
|
1136
1187
|
}
|
|
1137
|
-
return
|
|
1188
|
+
return `${subject}: ${hf.toFixed(2)} (${status})`;
|
|
1138
1189
|
}
|
|
1139
1190
|
var healthCheckTool = buildTool({
|
|
1140
1191
|
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
|
-
|
|
1192
|
+
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.',
|
|
1193
|
+
inputSchema: z.object({
|
|
1194
|
+
address: z.string().regex(SUI_ADDRESS_REGEX3).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
|
|
1195
|
+
}),
|
|
1196
|
+
jsonSchema: {
|
|
1197
|
+
type: "object",
|
|
1198
|
+
properties: {
|
|
1199
|
+
address: {
|
|
1200
|
+
type: "string",
|
|
1201
|
+
pattern: "^0x[a-fA-F0-9]{1,64}$",
|
|
1202
|
+
description: "Sui address to inspect (defaults to the signed-in wallet)"
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
required: []
|
|
1206
|
+
},
|
|
1144
1207
|
isReadOnly: true,
|
|
1145
1208
|
// [v1.5.1] Health factor changes on every borrow / repay / collateral
|
|
1146
1209
|
// movement and even passively as oracle prices update. Never dedupe.
|
|
1147
1210
|
cacheable: false,
|
|
1148
|
-
async call(
|
|
1149
|
-
|
|
1150
|
-
|
|
1211
|
+
async call(input, context) {
|
|
1212
|
+
const targetAddress = input.address ?? context.walletAddress;
|
|
1213
|
+
const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
|
|
1214
|
+
if (context.positionFetcher && targetAddress) {
|
|
1215
|
+
const sp = await context.positionFetcher(targetAddress);
|
|
1151
1216
|
const borrowed2 = sp.borrows;
|
|
1152
1217
|
const rawHf = sp.healthFactor ?? (borrowed2 > 0 ? 0 : Infinity);
|
|
1153
1218
|
const status2 = hfStatus(rawHf, borrowed2);
|
|
@@ -1159,24 +1224,28 @@ var healthCheckTool = buildTool({
|
|
|
1159
1224
|
borrowed: borrowed2,
|
|
1160
1225
|
maxBorrow: sp.maxBorrow,
|
|
1161
1226
|
liquidationThreshold: 0,
|
|
1162
|
-
status: status2
|
|
1227
|
+
status: status2,
|
|
1228
|
+
address: targetAddress,
|
|
1229
|
+
isSelfQuery
|
|
1163
1230
|
},
|
|
1164
|
-
displayText: displayHfText(transportHf2, borrowed2, status2)
|
|
1231
|
+
displayText: displayHfText(transportHf2, borrowed2, status2, isSelfQuery, targetAddress)
|
|
1165
1232
|
};
|
|
1166
1233
|
}
|
|
1167
|
-
if (
|
|
1168
|
-
const hf2 = await fetchHealthFactor(
|
|
1169
|
-
getMcpManager(context),
|
|
1170
|
-
getWalletAddress(context)
|
|
1171
|
-
);
|
|
1234
|
+
if (hasNaviMcpGlobal(context) && targetAddress) {
|
|
1235
|
+
const hf2 = await fetchHealthFactor(getMcpManager(context), targetAddress);
|
|
1172
1236
|
const borrowed2 = hf2.borrowed;
|
|
1173
1237
|
const status2 = hfStatus(hf2.healthFactor, borrowed2);
|
|
1174
1238
|
const transportHf2 = serializeHf(hf2.healthFactor, borrowed2);
|
|
1175
1239
|
return {
|
|
1176
|
-
data: { ...hf2, healthFactor: transportHf2, status: status2 },
|
|
1177
|
-
displayText: displayHfText(transportHf2, borrowed2, status2)
|
|
1240
|
+
data: { ...hf2, healthFactor: transportHf2, status: status2, address: targetAddress, isSelfQuery },
|
|
1241
|
+
displayText: displayHfText(transportHf2, borrowed2, status2, isSelfQuery, targetAddress)
|
|
1178
1242
|
};
|
|
1179
1243
|
}
|
|
1244
|
+
if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
`Cannot inspect ${input.address.slice(0, 8)}\u2026 without NAVI MCP or a positionFetcher. Configure NAVI MCP to enable third-party address reads.`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1180
1249
|
const agent = requireAgent(context);
|
|
1181
1250
|
const hf = await agent.healthFactor();
|
|
1182
1251
|
const borrowed = hf.borrowed;
|
|
@@ -1189,9 +1258,11 @@ var healthCheckTool = buildTool({
|
|
|
1189
1258
|
borrowed,
|
|
1190
1259
|
maxBorrow: hf.maxBorrow,
|
|
1191
1260
|
liquidationThreshold: hf.liquidationThreshold,
|
|
1192
|
-
status
|
|
1261
|
+
status,
|
|
1262
|
+
address: targetAddress ?? "",
|
|
1263
|
+
isSelfQuery: true
|
|
1193
1264
|
},
|
|
1194
|
-
displayText: displayHfText(transportHf, borrowed, status)
|
|
1265
|
+
displayText: displayHfText(transportHf, borrowed, status, true, void 0)
|
|
1195
1266
|
};
|
|
1196
1267
|
}
|
|
1197
1268
|
});
|
|
@@ -1400,14 +1471,14 @@ async function queryHistoryByDate(rpcUrl, address, targetDate, limit) {
|
|
|
1400
1471
|
}
|
|
1401
1472
|
var HISTORY_ACTIONS = ["send", "lending", "swap", "transaction"];
|
|
1402
1473
|
var DEFAULT_LOOKBACK_DAYS = 30;
|
|
1403
|
-
var
|
|
1474
|
+
var SUI_ADDRESS_REGEX4 = /^0x[0-9a-fA-F]{64}$/;
|
|
1404
1475
|
var transactionHistoryTool = buildTool({
|
|
1405
1476
|
name: "transaction_history",
|
|
1406
1477
|
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
1478
|
inputSchema: z.object({
|
|
1408
1479
|
limit: z.number().int().min(1).max(50).optional(),
|
|
1409
|
-
address: z.string().regex(
|
|
1410
|
-
counterparty: z.string().regex(
|
|
1480
|
+
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."),
|
|
1481
|
+
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
1482
|
date: z.string().optional().describe("Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."),
|
|
1412
1483
|
action: z.enum(HISTORY_ACTIONS).optional().describe("Filter by action: send, lending, swap, or transaction."),
|
|
1413
1484
|
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.'),
|
|
@@ -3028,14 +3099,14 @@ Use when the user asks for a visual chart, simulator, or financial overview. Pic
|
|
|
3028
3099
|
|
|
3029
3100
|
- 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
3101
|
- 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)
|
|
3102
|
+
- yield_projector \u2014 compound yield simulator with amount/APY/period sliders (WORKS NOW \u2014 client-side, no address needed)
|
|
3103
|
+
- 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)
|
|
3104
|
+
- dca_planner \u2014 savings plan curve for regular monthly deposits (WORKS NOW \u2014 client-side, no address needed)
|
|
3034
3105
|
- spending_breakdown \u2014 spending by service category (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user)
|
|
3035
3106
|
- 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
|
|
3107
|
+
- 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
3108
|
|
|
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\`.
|
|
3109
|
+
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
3110
|
|
|
3040
3111
|
Always prefer the canvas for visualisation requests. After rendering, offer to explain what the user sees.`,
|
|
3041
3112
|
inputSchema: z.object({
|
|
@@ -3043,7 +3114,7 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
|
|
|
3043
3114
|
params: z.object({
|
|
3044
3115
|
period: z.enum(["1m", "3m", "6m", "1y"]).optional().describe("Time period for time-based templates"),
|
|
3045
3116
|
address: z.string().optional().describe(
|
|
3046
|
-
"Sui address for the
|
|
3117
|
+
"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
3118
|
)
|
|
3048
3119
|
}).optional()
|
|
3049
3120
|
}),
|
|
@@ -3077,7 +3148,20 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
|
|
|
3077
3148
|
return { address: target, isSelfRender };
|
|
3078
3149
|
};
|
|
3079
3150
|
if (template === "full_portfolio") {
|
|
3080
|
-
const
|
|
3151
|
+
const { address, isSelfRender } = resolveAddressTarget();
|
|
3152
|
+
if (!address) {
|
|
3153
|
+
return {
|
|
3154
|
+
data: {
|
|
3155
|
+
__canvas: true,
|
|
3156
|
+
template,
|
|
3157
|
+
title,
|
|
3158
|
+
templateData: { available: false, message: "Full Portfolio needs an address." }
|
|
3159
|
+
},
|
|
3160
|
+
displayText: "Full Portfolio requires an address."
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
const titleSuffix = isSelfRender ? "" : ` \u2014 ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
|
|
3164
|
+
const pos = isSelfRender ? context.serverPositions : null;
|
|
3081
3165
|
const rate = normalizeSavingsRate(pos?.savingsRate);
|
|
3082
3166
|
const savings = pos?.savings ?? 0;
|
|
3083
3167
|
const borrows = pos?.borrows ?? 0;
|
|
@@ -3085,17 +3169,18 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
|
|
|
3085
3169
|
data: {
|
|
3086
3170
|
__canvas: true,
|
|
3087
3171
|
template,
|
|
3088
|
-
title
|
|
3172
|
+
title: `${title}${titleSuffix}`,
|
|
3089
3173
|
templateData: {
|
|
3090
3174
|
available: true,
|
|
3091
|
-
address
|
|
3175
|
+
address,
|
|
3176
|
+
isSelfRender,
|
|
3092
3177
|
currentSavings: savings,
|
|
3093
3178
|
currentDebt: borrows,
|
|
3094
3179
|
healthFactor: pos?.healthFactor ?? null,
|
|
3095
3180
|
savingsRate: rate
|
|
3096
3181
|
}
|
|
3097
3182
|
},
|
|
3098
|
-
displayText: `Opened Full Portfolio Overview.`
|
|
3183
|
+
displayText: isSelfRender ? `Opened Full Portfolio Overview.` : `Opened Full Portfolio Overview for ${address.slice(0, 6)}\u2026${address.slice(-4)}.`
|
|
3099
3184
|
};
|
|
3100
3185
|
}
|
|
3101
3186
|
if (template === "watch_address") {
|
|
@@ -3226,20 +3311,28 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
|
|
|
3226
3311
|
};
|
|
3227
3312
|
}
|
|
3228
3313
|
if (template === "health_simulator") {
|
|
3229
|
-
const
|
|
3314
|
+
const { address: targetAddress, isSelfRender } = resolveAddressTarget();
|
|
3315
|
+
const seedFromPos = isSelfRender;
|
|
3316
|
+
const seedSavings = seedFromPos ? totalSavings : 0;
|
|
3317
|
+
const seedBorrows = seedFromPos ? totalBorrows : 0;
|
|
3318
|
+
const seedHf = seedFromPos ? healthFactor : null;
|
|
3319
|
+
const roundedDebt = seedBorrows >= 1 ? Math.round(seedBorrows) : seedBorrows > 0 ? parseFloat(seedBorrows.toFixed(4)) : 0;
|
|
3320
|
+
const titleSuffix = !targetAddress || isSelfRender ? "" : ` \u2014 ${targetAddress.slice(0, 6)}\u2026${targetAddress.slice(-4)}`;
|
|
3230
3321
|
return {
|
|
3231
3322
|
data: {
|
|
3232
3323
|
__canvas: true,
|
|
3233
3324
|
template,
|
|
3234
|
-
title
|
|
3325
|
+
title: `${title}${titleSuffix}`,
|
|
3235
3326
|
templateData: {
|
|
3236
3327
|
available: true,
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3328
|
+
address: targetAddress ?? "",
|
|
3329
|
+
isSelfRender,
|
|
3330
|
+
initialCollateral: seedSavings > 0 ? Math.round(seedSavings) : 1500,
|
|
3331
|
+
initialDebt: roundedDebt > 0 ? roundedDebt : seedSavings > 0 ? 0 : 500,
|
|
3332
|
+
currentHf: seedHf
|
|
3240
3333
|
}
|
|
3241
3334
|
},
|
|
3242
|
-
displayText: `Opened Health Factor Simulator. Current HF: ${healthFactor !== null ? healthFactor.toFixed(2) : "no active position"}.`
|
|
3335
|
+
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
3336
|
};
|
|
3244
3337
|
}
|
|
3245
3338
|
if (template === "dca_planner") {
|
|
@@ -3354,49 +3447,62 @@ var yieldSummaryTool = buildTool({
|
|
|
3354
3447
|
}
|
|
3355
3448
|
}
|
|
3356
3449
|
});
|
|
3450
|
+
var SUI_ADDRESS_REGEX5 = /^0x[a-fA-F0-9]{1,64}$/;
|
|
3357
3451
|
var activitySummaryTool = buildTool({
|
|
3358
3452
|
name: "activity_summary",
|
|
3359
|
-
description: "Returns a categorised DeFi activity summary for
|
|
3453
|
+
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
3454
|
inputSchema: z.object({
|
|
3361
|
-
period: z.enum(["week", "month", "year", "all"]).optional().describe("Time period. Defaults to current month.")
|
|
3455
|
+
period: z.enum(["week", "month", "year", "all"]).optional().describe("Time period. Defaults to current month."),
|
|
3456
|
+
address: z.string().regex(SUI_ADDRESS_REGEX5).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
|
|
3362
3457
|
}),
|
|
3363
3458
|
jsonSchema: {
|
|
3364
3459
|
type: "object",
|
|
3365
3460
|
properties: {
|
|
3366
|
-
period: { type: "string", enum: ["week", "month", "year", "all"] }
|
|
3461
|
+
period: { type: "string", enum: ["week", "month", "year", "all"] },
|
|
3462
|
+
address: {
|
|
3463
|
+
type: "string",
|
|
3464
|
+
pattern: "^0x[a-fA-F0-9]{1,64}$",
|
|
3465
|
+
description: "Sui address to inspect (defaults to the signed-in wallet)"
|
|
3466
|
+
}
|
|
3367
3467
|
}
|
|
3368
3468
|
},
|
|
3369
3469
|
isReadOnly: true,
|
|
3370
3470
|
async call(input, context) {
|
|
3371
3471
|
const period = input.period ?? "month";
|
|
3372
3472
|
const apiUrl = context.env?.AUDRIC_INTERNAL_API_URL;
|
|
3373
|
-
const
|
|
3473
|
+
const targetAddress = input.address ?? context.walletAddress;
|
|
3474
|
+
const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
|
|
3374
3475
|
const empty = {
|
|
3375
3476
|
period,
|
|
3376
3477
|
totalTransactions: 0,
|
|
3377
3478
|
byAction: [],
|
|
3378
3479
|
totalMovedUsd: 0,
|
|
3379
3480
|
netSavingsUsd: 0,
|
|
3380
|
-
yieldEarnedUsd: 0
|
|
3481
|
+
yieldEarnedUsd: 0,
|
|
3482
|
+
address: targetAddress,
|
|
3483
|
+
isSelfQuery
|
|
3381
3484
|
};
|
|
3382
|
-
if (!apiUrl || !
|
|
3485
|
+
if (!apiUrl || !targetAddress) {
|
|
3383
3486
|
return { data: empty, displayText: "Activity summary not available." };
|
|
3384
3487
|
}
|
|
3385
3488
|
try {
|
|
3489
|
+
const callerHeader = context.walletAddress ?? targetAddress;
|
|
3386
3490
|
const res = await fetch(
|
|
3387
|
-
`${apiUrl}/api/analytics/activity-summary?address=${
|
|
3388
|
-
{ headers: { "x-sui-address":
|
|
3491
|
+
`${apiUrl}/api/analytics/activity-summary?address=${targetAddress}&period=${period}`,
|
|
3492
|
+
{ headers: { "x-sui-address": callerHeader }, signal: context.signal }
|
|
3389
3493
|
);
|
|
3390
3494
|
if (!res.ok) {
|
|
3391
3495
|
return { data: empty, displayText: `Could not fetch activity data (HTTP ${res.status}).` };
|
|
3392
3496
|
}
|
|
3393
|
-
const
|
|
3497
|
+
const raw = await res.json();
|
|
3498
|
+
const data = { ...raw, address: targetAddress, isSelfQuery };
|
|
3394
3499
|
const sorted = [...data.byAction ?? []].sort((a, b) => b.count - a.count);
|
|
3395
3500
|
const top = sorted.slice(0, 3).map((a) => `${a.action} (${a.count})`).join(", ");
|
|
3396
3501
|
const periodLabel = data.period === "all" ? "all time" : `this ${data.period}`;
|
|
3502
|
+
const subjectPrefix = isSelfQuery ? "" : `${targetAddress.slice(0, 6)}\u2026${targetAddress.slice(-4)} \u2014 `;
|
|
3397
3503
|
return {
|
|
3398
3504
|
data,
|
|
3399
|
-
displayText: data.totalTransactions > 0 ? `${data.totalTransactions} transactions ${periodLabel}. Top: ${top}. Total moved: $${data.totalMovedUsd.toFixed(2)}. Net savings: $${data.netSavingsUsd.toFixed(2)}.` :
|
|
3505
|
+
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
3506
|
};
|
|
3401
3507
|
} catch {
|
|
3402
3508
|
return { data: empty, displayText: "Error fetching activity summary." };
|
|
@@ -3873,7 +3979,7 @@ function guardCostWarning(tool, _call, conversationText) {
|
|
|
3873
3979
|
message: "This action has a monetary cost. Confirm the user is aware before proceeding."
|
|
3874
3980
|
};
|
|
3875
3981
|
}
|
|
3876
|
-
var
|
|
3982
|
+
var SUI_ADDRESS_REGEX6 = /^0x[a-fA-F0-9]{64}$/;
|
|
3877
3983
|
function normalizeAddress(addr) {
|
|
3878
3984
|
return addr.trim().toLowerCase();
|
|
3879
3985
|
}
|
|
@@ -3938,7 +4044,7 @@ function guardAddressSource(tool, call, userText, contacts, walletAddress) {
|
|
|
3938
4044
|
if (!rawTo) {
|
|
3939
4045
|
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3940
4046
|
}
|
|
3941
|
-
if (!
|
|
4047
|
+
if (!SUI_ADDRESS_REGEX6.test(rawTo)) {
|
|
3942
4048
|
return { verdict: "pass", gate: "address_source", tier: "safety" };
|
|
3943
4049
|
}
|
|
3944
4050
|
const normalizedTo = normalizeAddress(rawTo);
|