@t2000/engine 0.47.1 → 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.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 user's full balance breakdown. Returns wallet holdings (tokens the user owns \u2014 NOT savings), NAVI savings deposits (USDC deposited into NAVI Protocol earning yield), outstanding debt, pending rewards, gas reserve, total net worth, and saveableUsdc (only USDC can be deposited into savings). IMPORTANT: wallet holdings like GOLD, SUI, USDT are NOT savings positions \u2014 they are just tokens sitting in the wallet.",
780
- inputSchema: z.object({}),
781
- jsonSchema: { type: "object", properties: {}, required: [] },
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(_input, context) {
789
- if (hasNaviMcp(context)) {
790
- const address = getWalletAddress(context);
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 = !!(context.positionFetcher && context.walletAddress);
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(context.walletAddress).catch((err) => {
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: `Balance: $${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.`
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(`Savings: $${fundStatus.supplied.toFixed(2)} at ${(earnings.currentApy * 100).toFixed(2)}% blended APY`);
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("No savings positions.");
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
- jsonSchema: { type: "object", properties: {}, required: [] },
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(_input, context) {
1073
- if (context.positionFetcher && context.walletAddress) {
1074
- const sp = await context.positionFetcher(context.walletAddress);
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
- return { data: result2, displayText: formatSavingsDisplay(result2) };
1120
+ const stamped2 = { ...result2, address: targetAddress, isSelfQuery };
1121
+ return { data: stamped2, displayText: formatSavingsDisplay(result2, isSelfQuery, targetAddress) };
1077
1122
  }
1078
- if (hasNaviMcp(context)) {
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
- return { data: savings, displayText: formatSavingsDisplay(savings) };
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
- return { data: result, displayText: formatSavingsDisplay(result) };
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 `Health Factor: \u221E (${status} \u2014 no debt)`;
1186
+ return `${subject}: \u221E (${status} \u2014 no debt)`;
1136
1187
  }
1137
- return `Health Factor: ${hf.toFixed(2)} (${status})`;
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 user has no debt the tool returns healthFactor=null (semantically infinity) \u2014 render that as "Healthy" / \u221E, never as 0 or "Critical".',
1142
- inputSchema: z.object({}),
1143
- jsonSchema: { type: "object", properties: {}, required: [] },
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(_input, context) {
1149
- if (context.positionFetcher && context.walletAddress) {
1150
- const sp = await context.positionFetcher(context.walletAddress);
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 (hasNaviMcp(context)) {
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
  });
@@ -1317,7 +1388,8 @@ function parseRpcTx(tx, address) {
1317
1388
  gasCost
1318
1389
  };
1319
1390
  }
1320
- async function queryHistoryPage(rpcUrl, address, limit, cursor) {
1391
+ async function queryHistoryPage(rpcUrl, address, direction, limit, cursor) {
1392
+ const filter = direction === "from" ? { FromAddress: address } : { ToAddress: address };
1321
1393
  const res = await fetch(rpcUrl, {
1322
1394
  method: "POST",
1323
1395
  headers: { "Content-Type": "application/json" },
@@ -1326,7 +1398,7 @@ async function queryHistoryPage(rpcUrl, address, limit, cursor) {
1326
1398
  id: 1,
1327
1399
  method: "suix_queryTransactionBlocks",
1328
1400
  params: [
1329
- { filter: { FromAddress: address }, options: { showEffects: true, showInput: true, showBalanceChanges: true } },
1401
+ { filter, options: { showEffects: true, showInput: true, showBalanceChanges: true } },
1330
1402
  cursor,
1331
1403
  limit,
1332
1404
  true
@@ -1343,9 +1415,20 @@ async function queryHistoryPage(rpcUrl, address, limit, cursor) {
1343
1415
  hasNextPage: json.result?.hasNextPage ?? false
1344
1416
  };
1345
1417
  }
1418
+ function mergeAndDedupe(a, b) {
1419
+ const byDigest = /* @__PURE__ */ new Map();
1420
+ for (const tx of [...a, ...b]) {
1421
+ if (!byDigest.has(tx.digest)) byDigest.set(tx.digest, tx);
1422
+ }
1423
+ return [...byDigest.values()].sort((x, y) => Number(y.timestampMs ?? 0) - Number(x.timestampMs ?? 0));
1424
+ }
1346
1425
  async function queryHistoryRpc(rpcUrl, address, limit) {
1347
- const page = await queryHistoryPage(rpcUrl, address, limit, null);
1348
- return page.data.map((tx) => parseRpcTx(tx, address));
1426
+ const [fromPage, toPage] = await Promise.all([
1427
+ queryHistoryPage(rpcUrl, address, "from", limit, null).catch(() => ({ data: [], nextCursor: null, hasNextPage: false })),
1428
+ queryHistoryPage(rpcUrl, address, "to", limit, null).catch(() => ({ data: [], nextCursor: null, hasNextPage: false }))
1429
+ ]);
1430
+ const merged = mergeAndDedupe(fromPage.data, toPage.data);
1431
+ return merged.slice(0, limit).map((tx) => parseRpcTx(tx, address));
1349
1432
  }
1350
1433
  async function queryHistoryByDate(rpcUrl, address, targetDate, limit) {
1351
1434
  const target = new Date(targetDate);
@@ -1353,33 +1436,49 @@ async function queryHistoryByDate(rpcUrl, address, targetDate, limit) {
1353
1436
  const dayEnd = dayStart + 864e5;
1354
1437
  const MAX_PAGES = 20;
1355
1438
  const PAGE_SIZE = 50;
1356
- const results = [];
1357
- let cursor = null;
1358
- for (let page = 0; page < MAX_PAGES; page++) {
1359
- const res = await queryHistoryPage(rpcUrl, address, PAGE_SIZE, cursor);
1360
- if (res.data.length === 0) break;
1361
- for (const tx of res.data) {
1362
- const ts = Number(tx.timestampMs ?? 0);
1363
- if (ts === 0) continue;
1364
- if (ts < dayStart) {
1365
- return results.slice(0, limit);
1439
+ async function paginateDirection(direction) {
1440
+ const collected = [];
1441
+ let cursor = null;
1442
+ for (let page = 0; page < MAX_PAGES; page++) {
1443
+ let res;
1444
+ try {
1445
+ res = await queryHistoryPage(rpcUrl, address, direction, PAGE_SIZE, cursor);
1446
+ } catch {
1447
+ break;
1366
1448
  }
1367
- if (ts >= dayStart && ts < dayEnd) {
1368
- results.push(parseRpcTx(tx, address));
1449
+ if (res.data.length === 0) break;
1450
+ let reachedOld = false;
1451
+ for (const tx of res.data) {
1452
+ const ts = Number(tx.timestampMs ?? 0);
1453
+ if (ts === 0) continue;
1454
+ if (ts < dayStart) {
1455
+ reachedOld = true;
1456
+ break;
1457
+ }
1458
+ if (ts >= dayStart && ts < dayEnd) collected.push(tx);
1369
1459
  }
1460
+ if (reachedOld || !res.hasNextPage || !res.nextCursor) break;
1461
+ cursor = res.nextCursor;
1370
1462
  }
1371
- if (!res.hasNextPage || !res.nextCursor) break;
1372
- cursor = res.nextCursor;
1463
+ return collected;
1373
1464
  }
1374
- return results.slice(0, limit);
1465
+ const [fromTxs, toTxs] = await Promise.all([
1466
+ paginateDirection("from"),
1467
+ paginateDirection("to")
1468
+ ]);
1469
+ const merged = mergeAndDedupe(fromTxs, toTxs);
1470
+ return merged.slice(0, limit).map((tx) => parseRpcTx(tx, address));
1375
1471
  }
1376
1472
  var HISTORY_ACTIONS = ["send", "lending", "swap", "transaction"];
1377
1473
  var DEFAULT_LOOKBACK_DAYS = 30;
1474
+ var SUI_ADDRESS_REGEX4 = /^0x[0-9a-fA-F]{64}$/;
1378
1475
  var transactionHistoryTool = buildTool({
1379
1476
  name: "transaction_history",
1380
- description: 'Retrieve recent transaction history (last 30 days by default): sends, saves, withdrawals, borrows, repayments, swaps, and rewards claims. Renders a rich transaction card. Filter 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.',
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.',
1381
1478
  inputSchema: z.object({
1382
1479
  limit: z.number().int().min(1).max(50).optional(),
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).'),
1383
1482
  date: z.string().optional().describe("Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."),
1384
1483
  action: z.enum(HISTORY_ACTIONS).optional().describe("Filter by action: send, lending, swap, or transaction."),
1385
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.'),
@@ -1393,6 +1492,14 @@ var transactionHistoryTool = buildTool({
1393
1492
  type: "number",
1394
1493
  description: "Maximum number of transactions to return (1-50, default 10)"
1395
1494
  },
1495
+ address: {
1496
+ type: "string",
1497
+ description: "Sui address to query history FOR (defaults to the signed-in user when omitted). Use for queries about a contact's, watched address's, or any other wallet's history."
1498
+ },
1499
+ counterparty: {
1500
+ type: "string",
1501
+ description: '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.'
1502
+ },
1396
1503
  date: {
1397
1504
  type: "string",
1398
1505
  description: "Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."
@@ -1465,6 +1572,9 @@ var transactionHistoryTool = buildTool({
1465
1572
  const assetSymbol = input.assetSymbol?.toLowerCase();
1466
1573
  const direction = input.direction;
1467
1574
  const minUsd = input.minUsd;
1575
+ const counterpartyLower = input.counterparty?.toLowerCase();
1576
+ const targetAddress = input.address ?? context.walletAddress;
1577
+ const isSelfQuery = !!targetAddress && !!context.walletAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
1468
1578
  const prices = context.tokenPrices;
1469
1579
  const priceFor = (sym) => {
1470
1580
  if (!sym || !prices) return void 0;
@@ -1479,6 +1589,9 @@ var transactionHistoryTool = buildTool({
1479
1589
  if (direction) {
1480
1590
  scoped = scoped.filter((r) => r.direction === direction);
1481
1591
  }
1592
+ if (counterpartyLower) {
1593
+ scoped = scoped.filter((r) => r.recipient?.toLowerCase() === counterpartyLower);
1594
+ }
1482
1595
  if (minUsd != null && minUsd > 0) {
1483
1596
  scoped = scoped.filter((r) => {
1484
1597
  if (r.amount == null) return false;
@@ -1496,9 +1609,17 @@ var transactionHistoryTool = buildTool({
1496
1609
  action: action ?? null,
1497
1610
  minUsd: minUsd ?? null,
1498
1611
  assetSymbol: input.assetSymbol ?? null,
1499
- direction: direction ?? null
1612
+ direction: direction ?? null,
1613
+ counterparty: input.counterparty ?? null,
1614
+ address: targetAddress ?? null,
1615
+ isSelfQuery
1500
1616
  };
1501
1617
  if (context.agent) {
1618
+ if (input.address && !isSelfQuery) {
1619
+ throw new Error(
1620
+ "transaction_history `address` parameter is not supported in CLI/SDK agent mode \u2014 only the signed-in user's history is available. Use the web client for third-party address queries."
1621
+ );
1622
+ }
1502
1623
  const agent = requireAgent(context);
1503
1624
  const records2 = await agent.history({ limit: input.date ? limit : Math.max(limit * 4, 50) });
1504
1625
  const filtered2 = finalize(records2);
@@ -1507,13 +1628,13 @@ var transactionHistoryTool = buildTool({
1507
1628
  displayText: `${filtered2.length} recent transaction(s)`
1508
1629
  };
1509
1630
  }
1510
- if (!context.walletAddress || !context.suiRpcUrl) {
1631
+ if (!targetAddress || !context.suiRpcUrl) {
1511
1632
  throw new Error("Transaction history requires a wallet address");
1512
1633
  }
1513
1634
  if (input.date) {
1514
1635
  const records2 = await queryHistoryByDate(
1515
1636
  context.suiRpcUrl,
1516
- context.walletAddress,
1637
+ targetAddress,
1517
1638
  input.date,
1518
1639
  Math.max(limit * 4, 50)
1519
1640
  );
@@ -1527,7 +1648,7 @@ var transactionHistoryTool = buildTool({
1527
1648
  const cutoffMs = Date.now() - DEFAULT_LOOKBACK_DAYS * 864e5;
1528
1649
  const records = await queryHistoryRpc(
1529
1650
  context.suiRpcUrl,
1530
- context.walletAddress,
1651
+ targetAddress,
1531
1652
  Math.max(limit * 4, 50)
1532
1653
  );
1533
1654
  const recent = records.filter((r) => r.timestamp >= cutoffMs);
@@ -2976,21 +3097,25 @@ var renderCanvasTool = buildTool({
2976
3097
 
2977
3098
  Use when the user asks for a visual chart, simulator, or financial overview. Pick the most relevant template:
2978
3099
 
2979
- - activity_heatmap \u2014 on-chain transaction history as a GitHub-style heatmap (WORKS NOW \u2014 loads from wallet)
2980
- - portfolio_timeline \u2014 net worth over time, wallet/savings/debt breakdown (WORKS NOW \u2014 daily snapshots)
2981
- - yield_projector \u2014 compound yield simulator with amount/APY/period sliders (WORKS NOW \u2014 client-side)
2982
- - health_simulator \u2014 borrow health factor simulator with collateral/debt sliders (WORKS NOW \u2014 uses current position)
2983
- - dca_planner \u2014 savings plan curve for regular monthly deposits (WORKS NOW \u2014 client-side)
2984
- - spending_breakdown \u2014 spending by service category (WORKS NOW \u2014 from AppEvent + ServicePurchase)
2985
- - watch_address \u2014 portfolio overview for any public Sui address (WORKS NOW \u2014 pass address in params)
2986
- - full_portfolio \u2014 4-panel overview: savings, health, activity, spending (WORKS NOW \u2014 aggregates all data)
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)
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)
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)
3105
+ - spending_breakdown \u2014 spending by service category (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user)
3106
+ - watch_address \u2014 portfolio overview for any public Sui address (WORKS NOW \u2014 pass \`params.address\`)
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)
3108
+
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.
2987
3110
 
2988
3111
  Always prefer the canvas for visualisation requests. After rendering, offer to explain what the user sees.`,
2989
3112
  inputSchema: z.object({
2990
3113
  template: z.enum(CANVAS_TEMPLATES).describe("Which canvas template to render"),
2991
3114
  params: z.object({
2992
3115
  period: z.enum(["1m", "3m", "6m", "1y"]).optional().describe("Time period for time-based templates"),
2993
- address: z.string().optional().describe("Sui address for watch_address template")
3116
+ address: z.string().optional().describe(
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."
3118
+ )
2994
3119
  }).optional()
2995
3120
  }),
2996
3121
  jsonSchema: {
@@ -3015,8 +3140,28 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3015
3140
  async call(input, context) {
3016
3141
  const { template, params } = input;
3017
3142
  const title = CANVAS_TITLES[template];
3143
+ const resolveAddressTarget = () => {
3144
+ const fromParams = params?.address;
3145
+ const fromContext = context.walletAddress;
3146
+ const target = fromParams ?? fromContext ?? null;
3147
+ const isSelfRender = !!target && !!fromContext && target.toLowerCase() === fromContext.toLowerCase();
3148
+ return { address: target, isSelfRender };
3149
+ };
3018
3150
  if (template === "full_portfolio") {
3019
- const pos = context.serverPositions;
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;
3020
3165
  const rate = normalizeSavingsRate(pos?.savingsRate);
3021
3166
  const savings = pos?.savings ?? 0;
3022
3167
  const borrows = pos?.borrows ?? 0;
@@ -3024,17 +3169,18 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3024
3169
  data: {
3025
3170
  __canvas: true,
3026
3171
  template,
3027
- title,
3172
+ title: `${title}${titleSuffix}`,
3028
3173
  templateData: {
3029
3174
  available: true,
3030
- address: context.walletAddress ?? "",
3175
+ address,
3176
+ isSelfRender,
3031
3177
  currentSavings: savings,
3032
3178
  currentDebt: borrows,
3033
3179
  healthFactor: pos?.healthFactor ?? null,
3034
3180
  savingsRate: rate
3035
3181
  }
3036
3182
  },
3037
- 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)}.`
3038
3184
  };
3039
3185
  }
3040
3186
  if (template === "watch_address") {
@@ -3061,45 +3207,87 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3061
3207
  };
3062
3208
  }
3063
3209
  if (template === "portfolio_timeline") {
3210
+ const { address, isSelfRender } = resolveAddressTarget();
3211
+ if (!address) {
3212
+ return {
3213
+ data: {
3214
+ __canvas: true,
3215
+ template,
3216
+ title,
3217
+ templateData: { available: false, message: "Portfolio Timeline needs an address." }
3218
+ },
3219
+ displayText: "Portfolio Timeline requires an address."
3220
+ };
3221
+ }
3222
+ const titleSuffix = isSelfRender ? "" : ` \u2014 ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
3064
3223
  return {
3065
3224
  data: {
3066
3225
  __canvas: true,
3067
3226
  template,
3068
- title,
3227
+ title: `${title}${titleSuffix}`,
3069
3228
  templateData: {
3070
3229
  available: true,
3071
- address: context.walletAddress ?? ""
3230
+ address,
3231
+ isSelfRender
3072
3232
  }
3073
3233
  },
3074
- displayText: `Opened Portfolio Timeline. Shows your net worth, savings, and debt over time.`
3234
+ displayText: isSelfRender ? `Opened Portfolio Timeline. Shows your net worth, savings, and debt over time.` : `Opened Portfolio Timeline for ${address.slice(0, 6)}\u2026${address.slice(-4)}.`
3075
3235
  };
3076
3236
  }
3077
3237
  if (template === "spending_breakdown") {
3238
+ const { address, isSelfRender } = resolveAddressTarget();
3239
+ if (!address) {
3240
+ return {
3241
+ data: {
3242
+ __canvas: true,
3243
+ template,
3244
+ title,
3245
+ templateData: { available: false, message: "Spending Breakdown needs an address." }
3246
+ },
3247
+ displayText: "Spending Breakdown requires an address."
3248
+ };
3249
+ }
3250
+ const titleSuffix = isSelfRender ? "" : ` \u2014 ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
3078
3251
  return {
3079
3252
  data: {
3080
3253
  __canvas: true,
3081
3254
  template,
3082
- title,
3255
+ title: `${title}${titleSuffix}`,
3083
3256
  templateData: {
3084
3257
  available: true,
3085
- address: context.walletAddress ?? ""
3258
+ address,
3259
+ isSelfRender
3086
3260
  }
3087
3261
  },
3088
- displayText: `Opened Spending Breakdown. Shows your service spending by category.`
3262
+ displayText: isSelfRender ? `Opened Spending Breakdown. Shows your service spending by category.` : `Opened Spending Breakdown for ${address.slice(0, 6)}\u2026${address.slice(-4)}.`
3089
3263
  };
3090
3264
  }
3091
3265
  if (template === "activity_heatmap") {
3266
+ const { address, isSelfRender } = resolveAddressTarget();
3267
+ if (!address) {
3268
+ return {
3269
+ data: {
3270
+ __canvas: true,
3271
+ template,
3272
+ title,
3273
+ templateData: { available: false, message: "Activity Heatmap needs an address." }
3274
+ },
3275
+ displayText: "Activity Heatmap requires an address."
3276
+ };
3277
+ }
3278
+ const titleSuffix = isSelfRender ? "" : ` \u2014 ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
3092
3279
  return {
3093
3280
  data: {
3094
3281
  __canvas: true,
3095
3282
  template,
3096
- title,
3283
+ title: `${title}${titleSuffix}`,
3097
3284
  templateData: {
3098
3285
  available: true,
3099
- address: context.walletAddress ?? ""
3286
+ address,
3287
+ isSelfRender
3100
3288
  }
3101
3289
  },
3102
- displayText: `Opened Activity Heatmap for your wallet. Click any day to explore transactions.`
3290
+ displayText: isSelfRender ? `Opened Activity Heatmap for your wallet. Click any day to explore transactions.` : `Opened Activity Heatmap for ${address.slice(0, 6)}\u2026${address.slice(-4)}. Click any day to explore that address's transactions.`
3103
3291
  };
3104
3292
  }
3105
3293
  const positions = context.serverPositions;
@@ -3123,20 +3311,28 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3123
3311
  };
3124
3312
  }
3125
3313
  if (template === "health_simulator") {
3126
- const roundedDebt = totalBorrows >= 1 ? Math.round(totalBorrows) : totalBorrows > 0 ? parseFloat(totalBorrows.toFixed(4)) : 0;
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)}`;
3127
3321
  return {
3128
3322
  data: {
3129
3323
  __canvas: true,
3130
3324
  template,
3131
- title,
3325
+ title: `${title}${titleSuffix}`,
3132
3326
  templateData: {
3133
3327
  available: true,
3134
- initialCollateral: totalSavings > 0 ? Math.round(totalSavings) : 1500,
3135
- initialDebt: roundedDebt > 0 ? roundedDebt : totalSavings > 0 ? 0 : 500,
3136
- currentHf: healthFactor
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
3137
3333
  }
3138
3334
  },
3139
- 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.`
3140
3336
  };
3141
3337
  }
3142
3338
  if (template === "dca_planner") {
@@ -3251,49 +3447,62 @@ var yieldSummaryTool = buildTool({
3251
3447
  }
3252
3448
  }
3253
3449
  });
3450
+ var SUI_ADDRESS_REGEX5 = /^0x[a-fA-F0-9]{1,64}$/;
3254
3451
  var activitySummaryTool = buildTool({
3255
3452
  name: "activity_summary",
3256
- description: "Returns a categorised DeFi activity summary for a period: 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 their activity, transaction history summary, or what they have done recently.",
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.",
3257
3454
  inputSchema: z.object({
3258
- 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)")
3259
3457
  }),
3260
3458
  jsonSchema: {
3261
3459
  type: "object",
3262
3460
  properties: {
3263
- 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
+ }
3264
3467
  }
3265
3468
  },
3266
3469
  isReadOnly: true,
3267
3470
  async call(input, context) {
3268
3471
  const period = input.period ?? "month";
3269
3472
  const apiUrl = context.env?.AUDRIC_INTERNAL_API_URL;
3270
- const address = context.walletAddress;
3473
+ const targetAddress = input.address ?? context.walletAddress;
3474
+ const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
3271
3475
  const empty = {
3272
3476
  period,
3273
3477
  totalTransactions: 0,
3274
3478
  byAction: [],
3275
3479
  totalMovedUsd: 0,
3276
3480
  netSavingsUsd: 0,
3277
- yieldEarnedUsd: 0
3481
+ yieldEarnedUsd: 0,
3482
+ address: targetAddress,
3483
+ isSelfQuery
3278
3484
  };
3279
- if (!apiUrl || !address) {
3485
+ if (!apiUrl || !targetAddress) {
3280
3486
  return { data: empty, displayText: "Activity summary not available." };
3281
3487
  }
3282
3488
  try {
3489
+ const callerHeader = context.walletAddress ?? targetAddress;
3283
3490
  const res = await fetch(
3284
- `${apiUrl}/api/analytics/activity-summary?address=${address}&period=${period}`,
3285
- { headers: { "x-sui-address": address }, signal: context.signal }
3491
+ `${apiUrl}/api/analytics/activity-summary?address=${targetAddress}&period=${period}`,
3492
+ { headers: { "x-sui-address": callerHeader }, signal: context.signal }
3286
3493
  );
3287
3494
  if (!res.ok) {
3288
3495
  return { data: empty, displayText: `Could not fetch activity data (HTTP ${res.status}).` };
3289
3496
  }
3290
- const data = await res.json();
3497
+ const raw = await res.json();
3498
+ const data = { ...raw, address: targetAddress, isSelfQuery };
3291
3499
  const sorted = [...data.byAction ?? []].sort((a, b) => b.count - a.count);
3292
3500
  const top = sorted.slice(0, 3).map((a) => `${a.action} (${a.count})`).join(", ");
3293
3501
  const periodLabel = data.period === "all" ? "all time" : `this ${data.period}`;
3502
+ const subjectPrefix = isSelfQuery ? "" : `${targetAddress.slice(0, 6)}\u2026${targetAddress.slice(-4)} \u2014 `;
3294
3503
  return {
3295
3504
  data,
3296
- displayText: data.totalTransactions > 0 ? `${data.totalTransactions} transactions ${periodLabel}. Top: ${top}. Total moved: $${data.totalMovedUsd.toFixed(2)}. Net savings: $${data.netSavingsUsd.toFixed(2)}.` : `No activity recorded for ${periodLabel}.`
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}.`
3297
3506
  };
3298
3507
  } catch {
3299
3508
  return { data: empty, displayText: "Error fetching activity summary." };
@@ -3770,7 +3979,7 @@ function guardCostWarning(tool, _call, conversationText) {
3770
3979
  message: "This action has a monetary cost. Confirm the user is aware before proceeding."
3771
3980
  };
3772
3981
  }
3773
- var SUI_ADDRESS_REGEX = /^0x[a-fA-F0-9]{64}$/;
3982
+ var SUI_ADDRESS_REGEX6 = /^0x[a-fA-F0-9]{64}$/;
3774
3983
  function normalizeAddress(addr) {
3775
3984
  return addr.trim().toLowerCase();
3776
3985
  }
@@ -3835,7 +4044,7 @@ function guardAddressSource(tool, call, userText, contacts, walletAddress) {
3835
4044
  if (!rawTo) {
3836
4045
  return { verdict: "pass", gate: "address_source", tier: "safety" };
3837
4046
  }
3838
- if (!SUI_ADDRESS_REGEX.test(rawTo)) {
4047
+ if (!SUI_ADDRESS_REGEX6.test(rawTo)) {
3839
4048
  return { verdict: "pass", gate: "address_source", tier: "safety" };
3840
4049
  }
3841
4050
  const normalizedTo = normalizeAddress(rawTo);