@t2000/engine 0.47.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1990,8 +1990,10 @@ declare const ratesInfoTool: Tool<{
1990
1990
  declare const transactionHistoryTool: Tool<{
1991
1991
  action?: "send" | "swap" | "transaction" | "lending" | undefined;
1992
1992
  date?: string | undefined;
1993
+ address?: string | undefined;
1993
1994
  direction?: "out" | "in" | undefined;
1994
1995
  limit?: number | undefined;
1996
+ counterparty?: string | undefined;
1995
1997
  minUsd?: number | undefined;
1996
1998
  assetSymbol?: string | undefined;
1997
1999
  }, Record<string, unknown>>;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { ALL_NAVI_ASSETS, getDecimalsForCoinType, resolveSymbol, assertAllowedAsset, SUPPORTED_ASSETS, getSwapQuote, extractTransferDetails, classifyTransaction } from '@t2000/sdk';
2
+ import { ALL_NAVI_ASSETS, getDecimalsForCoinType, resolveSymbol, normalizeCoinType, assertAllowedAsset, SUPPORTED_ASSETS, getSwapQuote, extractTransferDetails, classifyTransaction } from '@t2000/sdk';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { readdirSync, readFileSync } from 'fs';
5
5
  import { join } from 'path';
@@ -632,17 +632,18 @@ async function fetchTokenPrices(coinTypes, apiKey) {
632
632
  const cached = cacheValid ? priceMapCache.prices : {};
633
633
  const result = {};
634
634
  const stillMissing = [];
635
- for (const coinType of coinTypes) {
636
- if (cached[coinType]) {
637
- result[coinType] = cached[coinType];
635
+ for (const original of coinTypes) {
636
+ const norm = normalizeCoinType(original);
637
+ if (cached[norm]) {
638
+ result[original] = cached[norm];
638
639
  continue;
639
640
  }
640
- const stable = STABLE_USD_PRICES[coinType];
641
+ const stable = STABLE_USD_PRICES[norm];
641
642
  if (typeof stable === "number") {
642
- result[coinType] = { price: stable };
643
+ result[original] = { price: stable };
643
644
  continue;
644
645
  }
645
- stillMissing.push(coinType);
646
+ stillMissing.push(original);
646
647
  }
647
648
  if (stillMissing.length === 0) return result;
648
649
  if (!apiKey || apiKey.trim().length === 0) {
@@ -650,14 +651,24 @@ async function fetchTokenPrices(coinTypes, apiKey) {
650
651
  }
651
652
  const fetched = await fetchPricesFromBlockVision(stillMissing, apiKey);
652
653
  Object.assign(result, fetched);
653
- const merged = { ...cached, ...fetched };
654
+ const cacheUpdates = {};
655
+ for (const [original, value] of Object.entries(fetched)) {
656
+ cacheUpdates[normalizeCoinType(original)] = value;
657
+ }
658
+ const merged = { ...cached, ...cacheUpdates };
654
659
  priceMapCache = { prices: merged, ts: cacheValid ? priceMapCache.ts : now };
655
660
  return result;
656
661
  }
657
662
  async function fetchPricesFromBlockVision(coinTypes, apiKey) {
658
663
  const out = {};
659
- for (let i = 0; i < coinTypes.length; i += PRICE_LIST_CHUNK) {
660
- const chunk = coinTypes.slice(i, i + PRICE_LIST_CHUNK);
664
+ const longToOriginal = /* @__PURE__ */ new Map();
665
+ for (const original of coinTypes) {
666
+ const long = normalizeCoinType(original);
667
+ if (!longToOriginal.has(long)) longToOriginal.set(long, original);
668
+ }
669
+ const longForms = Array.from(longToOriginal.keys());
670
+ for (let i = 0; i < longForms.length; i += PRICE_LIST_CHUNK) {
671
+ const chunk = longForms.slice(i, i + PRICE_LIST_CHUNK);
661
672
  const tokenIds = encodeURIComponent(chunk.join(","));
662
673
  const url = `${BLOCKVISION_BASE}/coin/price/list?tokenIds=${tokenIds}&show24hChange=true`;
663
674
  let res;
@@ -684,11 +695,12 @@ async function fetchPricesFromBlockVision(coinTypes, apiKey) {
684
695
  if (json.code !== 200 || !json.result) continue;
685
696
  const prices = json.result.prices ?? {};
686
697
  const changes = json.result.coin24HChange ?? {};
687
- for (const [coinType, priceStr] of Object.entries(prices)) {
698
+ for (const [returnedType, priceStr] of Object.entries(prices)) {
688
699
  const price = parseNumberOrNull(priceStr);
689
700
  if (price == null) continue;
690
- const change24h = parseNumberOrNull(changes[coinType]);
691
- out[coinType] = change24h == null ? { price } : { price, change24h };
701
+ const original = longToOriginal.get(returnedType) ?? returnedType;
702
+ const change24h = parseNumberOrNull(changes[returnedType]);
703
+ out[original] = change24h == null ? { price } : { price, change24h };
692
704
  }
693
705
  }
694
706
  return out;
@@ -1305,7 +1317,8 @@ function parseRpcTx(tx, address) {
1305
1317
  gasCost
1306
1318
  };
1307
1319
  }
1308
- async function queryHistoryPage(rpcUrl, address, limit, cursor) {
1320
+ async function queryHistoryPage(rpcUrl, address, direction, limit, cursor) {
1321
+ const filter = direction === "from" ? { FromAddress: address } : { ToAddress: address };
1309
1322
  const res = await fetch(rpcUrl, {
1310
1323
  method: "POST",
1311
1324
  headers: { "Content-Type": "application/json" },
@@ -1314,7 +1327,7 @@ async function queryHistoryPage(rpcUrl, address, limit, cursor) {
1314
1327
  id: 1,
1315
1328
  method: "suix_queryTransactionBlocks",
1316
1329
  params: [
1317
- { filter: { FromAddress: address }, options: { showEffects: true, showInput: true, showBalanceChanges: true } },
1330
+ { filter, options: { showEffects: true, showInput: true, showBalanceChanges: true } },
1318
1331
  cursor,
1319
1332
  limit,
1320
1333
  true
@@ -1331,9 +1344,20 @@ async function queryHistoryPage(rpcUrl, address, limit, cursor) {
1331
1344
  hasNextPage: json.result?.hasNextPage ?? false
1332
1345
  };
1333
1346
  }
1347
+ function mergeAndDedupe(a, b) {
1348
+ const byDigest = /* @__PURE__ */ new Map();
1349
+ for (const tx of [...a, ...b]) {
1350
+ if (!byDigest.has(tx.digest)) byDigest.set(tx.digest, tx);
1351
+ }
1352
+ return [...byDigest.values()].sort((x, y) => Number(y.timestampMs ?? 0) - Number(x.timestampMs ?? 0));
1353
+ }
1334
1354
  async function queryHistoryRpc(rpcUrl, address, limit) {
1335
- const page = await queryHistoryPage(rpcUrl, address, limit, null);
1336
- return page.data.map((tx) => parseRpcTx(tx, address));
1355
+ const [fromPage, toPage] = await Promise.all([
1356
+ queryHistoryPage(rpcUrl, address, "from", limit, null).catch(() => ({ data: [], nextCursor: null, hasNextPage: false })),
1357
+ queryHistoryPage(rpcUrl, address, "to", limit, null).catch(() => ({ data: [], nextCursor: null, hasNextPage: false }))
1358
+ ]);
1359
+ const merged = mergeAndDedupe(fromPage.data, toPage.data);
1360
+ return merged.slice(0, limit).map((tx) => parseRpcTx(tx, address));
1337
1361
  }
1338
1362
  async function queryHistoryByDate(rpcUrl, address, targetDate, limit) {
1339
1363
  const target = new Date(targetDate);
@@ -1341,33 +1365,49 @@ async function queryHistoryByDate(rpcUrl, address, targetDate, limit) {
1341
1365
  const dayEnd = dayStart + 864e5;
1342
1366
  const MAX_PAGES = 20;
1343
1367
  const PAGE_SIZE = 50;
1344
- const results = [];
1345
- let cursor = null;
1346
- for (let page = 0; page < MAX_PAGES; page++) {
1347
- const res = await queryHistoryPage(rpcUrl, address, PAGE_SIZE, cursor);
1348
- if (res.data.length === 0) break;
1349
- for (const tx of res.data) {
1350
- const ts = Number(tx.timestampMs ?? 0);
1351
- if (ts === 0) continue;
1352
- if (ts < dayStart) {
1353
- return results.slice(0, limit);
1368
+ async function paginateDirection(direction) {
1369
+ const collected = [];
1370
+ let cursor = null;
1371
+ for (let page = 0; page < MAX_PAGES; page++) {
1372
+ let res;
1373
+ try {
1374
+ res = await queryHistoryPage(rpcUrl, address, direction, PAGE_SIZE, cursor);
1375
+ } catch {
1376
+ break;
1354
1377
  }
1355
- if (ts >= dayStart && ts < dayEnd) {
1356
- results.push(parseRpcTx(tx, address));
1378
+ if (res.data.length === 0) break;
1379
+ let reachedOld = false;
1380
+ for (const tx of res.data) {
1381
+ const ts = Number(tx.timestampMs ?? 0);
1382
+ if (ts === 0) continue;
1383
+ if (ts < dayStart) {
1384
+ reachedOld = true;
1385
+ break;
1386
+ }
1387
+ if (ts >= dayStart && ts < dayEnd) collected.push(tx);
1357
1388
  }
1389
+ if (reachedOld || !res.hasNextPage || !res.nextCursor) break;
1390
+ cursor = res.nextCursor;
1358
1391
  }
1359
- if (!res.hasNextPage || !res.nextCursor) break;
1360
- cursor = res.nextCursor;
1392
+ return collected;
1361
1393
  }
1362
- return results.slice(0, limit);
1394
+ const [fromTxs, toTxs] = await Promise.all([
1395
+ paginateDirection("from"),
1396
+ paginateDirection("to")
1397
+ ]);
1398
+ const merged = mergeAndDedupe(fromTxs, toTxs);
1399
+ return merged.slice(0, limit).map((tx) => parseRpcTx(tx, address));
1363
1400
  }
1364
1401
  var HISTORY_ACTIONS = ["send", "lending", "swap", "transaction"];
1365
1402
  var DEFAULT_LOOKBACK_DAYS = 30;
1403
+ var SUI_ADDRESS_REGEX = /^0x[0-9a-fA-F]{64}$/;
1366
1404
  var transactionHistoryTool = buildTool({
1367
1405
  name: "transaction_history",
1368
- 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.',
1406
+ 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.',
1369
1407
  inputSchema: z.object({
1370
1408
  limit: z.number().int().min(1).max(50).optional(),
1409
+ address: z.string().regex(SUI_ADDRESS_REGEX, "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."),
1410
+ counterparty: z.string().regex(SUI_ADDRESS_REGEX, "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).'),
1371
1411
  date: z.string().optional().describe("Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."),
1372
1412
  action: z.enum(HISTORY_ACTIONS).optional().describe("Filter by action: send, lending, swap, or transaction."),
1373
1413
  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.'),
@@ -1381,6 +1421,14 @@ var transactionHistoryTool = buildTool({
1381
1421
  type: "number",
1382
1422
  description: "Maximum number of transactions to return (1-50, default 10)"
1383
1423
  },
1424
+ address: {
1425
+ type: "string",
1426
+ 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."
1427
+ },
1428
+ counterparty: {
1429
+ type: "string",
1430
+ 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.'
1431
+ },
1384
1432
  date: {
1385
1433
  type: "string",
1386
1434
  description: "Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."
@@ -1453,6 +1501,9 @@ var transactionHistoryTool = buildTool({
1453
1501
  const assetSymbol = input.assetSymbol?.toLowerCase();
1454
1502
  const direction = input.direction;
1455
1503
  const minUsd = input.minUsd;
1504
+ const counterpartyLower = input.counterparty?.toLowerCase();
1505
+ const targetAddress = input.address ?? context.walletAddress;
1506
+ const isSelfQuery = !!targetAddress && !!context.walletAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
1456
1507
  const prices = context.tokenPrices;
1457
1508
  const priceFor = (sym) => {
1458
1509
  if (!sym || !prices) return void 0;
@@ -1467,6 +1518,9 @@ var transactionHistoryTool = buildTool({
1467
1518
  if (direction) {
1468
1519
  scoped = scoped.filter((r) => r.direction === direction);
1469
1520
  }
1521
+ if (counterpartyLower) {
1522
+ scoped = scoped.filter((r) => r.recipient?.toLowerCase() === counterpartyLower);
1523
+ }
1470
1524
  if (minUsd != null && minUsd > 0) {
1471
1525
  scoped = scoped.filter((r) => {
1472
1526
  if (r.amount == null) return false;
@@ -1484,9 +1538,17 @@ var transactionHistoryTool = buildTool({
1484
1538
  action: action ?? null,
1485
1539
  minUsd: minUsd ?? null,
1486
1540
  assetSymbol: input.assetSymbol ?? null,
1487
- direction: direction ?? null
1541
+ direction: direction ?? null,
1542
+ counterparty: input.counterparty ?? null,
1543
+ address: targetAddress ?? null,
1544
+ isSelfQuery
1488
1545
  };
1489
1546
  if (context.agent) {
1547
+ if (input.address && !isSelfQuery) {
1548
+ throw new Error(
1549
+ "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."
1550
+ );
1551
+ }
1490
1552
  const agent = requireAgent(context);
1491
1553
  const records2 = await agent.history({ limit: input.date ? limit : Math.max(limit * 4, 50) });
1492
1554
  const filtered2 = finalize(records2);
@@ -1495,13 +1557,13 @@ var transactionHistoryTool = buildTool({
1495
1557
  displayText: `${filtered2.length} recent transaction(s)`
1496
1558
  };
1497
1559
  }
1498
- if (!context.walletAddress || !context.suiRpcUrl) {
1560
+ if (!targetAddress || !context.suiRpcUrl) {
1499
1561
  throw new Error("Transaction history requires a wallet address");
1500
1562
  }
1501
1563
  if (input.date) {
1502
1564
  const records2 = await queryHistoryByDate(
1503
1565
  context.suiRpcUrl,
1504
- context.walletAddress,
1566
+ targetAddress,
1505
1567
  input.date,
1506
1568
  Math.max(limit * 4, 50)
1507
1569
  );
@@ -1515,7 +1577,7 @@ var transactionHistoryTool = buildTool({
1515
1577
  const cutoffMs = Date.now() - DEFAULT_LOOKBACK_DAYS * 864e5;
1516
1578
  const records = await queryHistoryRpc(
1517
1579
  context.suiRpcUrl,
1518
- context.walletAddress,
1580
+ targetAddress,
1519
1581
  Math.max(limit * 4, 50)
1520
1582
  );
1521
1583
  const recent = records.filter((r) => r.timestamp >= cutoffMs);
@@ -2964,21 +3026,25 @@ var renderCanvasTool = buildTool({
2964
3026
 
2965
3027
  Use when the user asks for a visual chart, simulator, or financial overview. Pick the most relevant template:
2966
3028
 
2967
- - activity_heatmap \u2014 on-chain transaction history as a GitHub-style heatmap (WORKS NOW \u2014 loads from wallet)
2968
- - portfolio_timeline \u2014 net worth over time, wallet/savings/debt breakdown (WORKS NOW \u2014 daily snapshots)
3029
+ - 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
+ - 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)
2969
3031
  - yield_projector \u2014 compound yield simulator with amount/APY/period sliders (WORKS NOW \u2014 client-side)
2970
3032
  - health_simulator \u2014 borrow health factor simulator with collateral/debt sliders (WORKS NOW \u2014 uses current position)
2971
3033
  - dca_planner \u2014 savings plan curve for regular monthly deposits (WORKS NOW \u2014 client-side)
2972
- - spending_breakdown \u2014 spending by service category (WORKS NOW \u2014 from AppEvent + ServicePurchase)
2973
- - watch_address \u2014 portfolio overview for any public Sui address (WORKS NOW \u2014 pass address in params)
3034
+ - spending_breakdown \u2014 spending by service category (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user)
3035
+ - watch_address \u2014 portfolio overview for any public Sui address (WORKS NOW \u2014 pass \`params.address\`)
2974
3036
  - full_portfolio \u2014 4-panel overview: savings, health, activity, spending (WORKS NOW \u2014 aggregates all data)
2975
3037
 
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\`. The four address-aware templates (activity_heatmap, portfolio_timeline, spending_breakdown, watch_address) will scope their data fetch to that address; the rest target the signed-in user regardless of params.
3039
+
2976
3040
  Always prefer the canvas for visualisation requests. After rendering, offer to explain what the user sees.`,
2977
3041
  inputSchema: z.object({
2978
3042
  template: z.enum(CANVAS_TEMPLATES).describe("Which canvas template to render"),
2979
3043
  params: z.object({
2980
3044
  period: z.enum(["1m", "3m", "6m", "1y"]).optional().describe("Time period for time-based templates"),
2981
- address: z.string().optional().describe("Sui address for watch_address template")
3045
+ address: z.string().optional().describe(
3046
+ "Sui address for the four address-aware templates (activity_heatmap, portfolio_timeline, spending_breakdown, watch_address). Defaults to the signed-in user; pass an explicit address to inspect a contact, watched wallet, or any other public address."
3047
+ )
2982
3048
  }).optional()
2983
3049
  }),
2984
3050
  jsonSchema: {
@@ -3003,6 +3069,13 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3003
3069
  async call(input, context) {
3004
3070
  const { template, params } = input;
3005
3071
  const title = CANVAS_TITLES[template];
3072
+ const resolveAddressTarget = () => {
3073
+ const fromParams = params?.address;
3074
+ const fromContext = context.walletAddress;
3075
+ const target = fromParams ?? fromContext ?? null;
3076
+ const isSelfRender = !!target && !!fromContext && target.toLowerCase() === fromContext.toLowerCase();
3077
+ return { address: target, isSelfRender };
3078
+ };
3006
3079
  if (template === "full_portfolio") {
3007
3080
  const pos = context.serverPositions;
3008
3081
  const rate = normalizeSavingsRate(pos?.savingsRate);
@@ -3049,45 +3122,87 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3049
3122
  };
3050
3123
  }
3051
3124
  if (template === "portfolio_timeline") {
3125
+ const { address, isSelfRender } = resolveAddressTarget();
3126
+ if (!address) {
3127
+ return {
3128
+ data: {
3129
+ __canvas: true,
3130
+ template,
3131
+ title,
3132
+ templateData: { available: false, message: "Portfolio Timeline needs an address." }
3133
+ },
3134
+ displayText: "Portfolio Timeline requires an address."
3135
+ };
3136
+ }
3137
+ const titleSuffix = isSelfRender ? "" : ` \u2014 ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
3052
3138
  return {
3053
3139
  data: {
3054
3140
  __canvas: true,
3055
3141
  template,
3056
- title,
3142
+ title: `${title}${titleSuffix}`,
3057
3143
  templateData: {
3058
3144
  available: true,
3059
- address: context.walletAddress ?? ""
3145
+ address,
3146
+ isSelfRender
3060
3147
  }
3061
3148
  },
3062
- displayText: `Opened Portfolio Timeline. Shows your net worth, savings, and debt over time.`
3149
+ 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)}.`
3063
3150
  };
3064
3151
  }
3065
3152
  if (template === "spending_breakdown") {
3153
+ const { address, isSelfRender } = resolveAddressTarget();
3154
+ if (!address) {
3155
+ return {
3156
+ data: {
3157
+ __canvas: true,
3158
+ template,
3159
+ title,
3160
+ templateData: { available: false, message: "Spending Breakdown needs an address." }
3161
+ },
3162
+ displayText: "Spending Breakdown requires an address."
3163
+ };
3164
+ }
3165
+ const titleSuffix = isSelfRender ? "" : ` \u2014 ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
3066
3166
  return {
3067
3167
  data: {
3068
3168
  __canvas: true,
3069
3169
  template,
3070
- title,
3170
+ title: `${title}${titleSuffix}`,
3071
3171
  templateData: {
3072
3172
  available: true,
3073
- address: context.walletAddress ?? ""
3173
+ address,
3174
+ isSelfRender
3074
3175
  }
3075
3176
  },
3076
- displayText: `Opened Spending Breakdown. Shows your service spending by category.`
3177
+ displayText: isSelfRender ? `Opened Spending Breakdown. Shows your service spending by category.` : `Opened Spending Breakdown for ${address.slice(0, 6)}\u2026${address.slice(-4)}.`
3077
3178
  };
3078
3179
  }
3079
3180
  if (template === "activity_heatmap") {
3181
+ const { address, isSelfRender } = resolveAddressTarget();
3182
+ if (!address) {
3183
+ return {
3184
+ data: {
3185
+ __canvas: true,
3186
+ template,
3187
+ title,
3188
+ templateData: { available: false, message: "Activity Heatmap needs an address." }
3189
+ },
3190
+ displayText: "Activity Heatmap requires an address."
3191
+ };
3192
+ }
3193
+ const titleSuffix = isSelfRender ? "" : ` \u2014 ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
3080
3194
  return {
3081
3195
  data: {
3082
3196
  __canvas: true,
3083
3197
  template,
3084
- title,
3198
+ title: `${title}${titleSuffix}`,
3085
3199
  templateData: {
3086
3200
  available: true,
3087
- address: context.walletAddress ?? ""
3201
+ address,
3202
+ isSelfRender
3088
3203
  }
3089
3204
  },
3090
- displayText: `Opened Activity Heatmap for your wallet. Click any day to explore transactions.`
3205
+ 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.`
3091
3206
  };
3092
3207
  }
3093
3208
  const positions = context.serverPositions;
@@ -3758,7 +3873,7 @@ function guardCostWarning(tool, _call, conversationText) {
3758
3873
  message: "This action has a monetary cost. Confirm the user is aware before proceeding."
3759
3874
  };
3760
3875
  }
3761
- var SUI_ADDRESS_REGEX = /^0x[a-fA-F0-9]{64}$/;
3876
+ var SUI_ADDRESS_REGEX2 = /^0x[a-fA-F0-9]{64}$/;
3762
3877
  function normalizeAddress(addr) {
3763
3878
  return addr.trim().toLowerCase();
3764
3879
  }
@@ -3823,7 +3938,7 @@ function guardAddressSource(tool, call, userText, contacts, walletAddress) {
3823
3938
  if (!rawTo) {
3824
3939
  return { verdict: "pass", gate: "address_source", tier: "safety" };
3825
3940
  }
3826
- if (!SUI_ADDRESS_REGEX.test(rawTo)) {
3941
+ if (!SUI_ADDRESS_REGEX2.test(rawTo)) {
3827
3942
  return { verdict: "pass", gate: "address_source", tier: "safety" };
3828
3943
  }
3829
3944
  const normalizedTo = normalizeAddress(rawTo);