@t2000/engine 0.47.1 → 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
@@ -1317,7 +1317,8 @@ function parseRpcTx(tx, address) {
1317
1317
  gasCost
1318
1318
  };
1319
1319
  }
1320
- 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 };
1321
1322
  const res = await fetch(rpcUrl, {
1322
1323
  method: "POST",
1323
1324
  headers: { "Content-Type": "application/json" },
@@ -1326,7 +1327,7 @@ async function queryHistoryPage(rpcUrl, address, limit, cursor) {
1326
1327
  id: 1,
1327
1328
  method: "suix_queryTransactionBlocks",
1328
1329
  params: [
1329
- { filter: { FromAddress: address }, options: { showEffects: true, showInput: true, showBalanceChanges: true } },
1330
+ { filter, options: { showEffects: true, showInput: true, showBalanceChanges: true } },
1330
1331
  cursor,
1331
1332
  limit,
1332
1333
  true
@@ -1343,9 +1344,20 @@ async function queryHistoryPage(rpcUrl, address, limit, cursor) {
1343
1344
  hasNextPage: json.result?.hasNextPage ?? false
1344
1345
  };
1345
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
+ }
1346
1354
  async function queryHistoryRpc(rpcUrl, address, limit) {
1347
- const page = await queryHistoryPage(rpcUrl, address, limit, null);
1348
- 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));
1349
1361
  }
1350
1362
  async function queryHistoryByDate(rpcUrl, address, targetDate, limit) {
1351
1363
  const target = new Date(targetDate);
@@ -1353,33 +1365,49 @@ async function queryHistoryByDate(rpcUrl, address, targetDate, limit) {
1353
1365
  const dayEnd = dayStart + 864e5;
1354
1366
  const MAX_PAGES = 20;
1355
1367
  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);
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;
1366
1377
  }
1367
- if (ts >= dayStart && ts < dayEnd) {
1368
- 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);
1369
1388
  }
1389
+ if (reachedOld || !res.hasNextPage || !res.nextCursor) break;
1390
+ cursor = res.nextCursor;
1370
1391
  }
1371
- if (!res.hasNextPage || !res.nextCursor) break;
1372
- cursor = res.nextCursor;
1392
+ return collected;
1373
1393
  }
1374
- 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));
1375
1400
  }
1376
1401
  var HISTORY_ACTIONS = ["send", "lending", "swap", "transaction"];
1377
1402
  var DEFAULT_LOOKBACK_DAYS = 30;
1403
+ var SUI_ADDRESS_REGEX = /^0x[0-9a-fA-F]{64}$/;
1378
1404
  var transactionHistoryTool = buildTool({
1379
1405
  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.',
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.',
1381
1407
  inputSchema: z.object({
1382
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).'),
1383
1411
  date: z.string().optional().describe("Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."),
1384
1412
  action: z.enum(HISTORY_ACTIONS).optional().describe("Filter by action: send, lending, swap, or transaction."),
1385
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.'),
@@ -1393,6 +1421,14 @@ var transactionHistoryTool = buildTool({
1393
1421
  type: "number",
1394
1422
  description: "Maximum number of transactions to return (1-50, default 10)"
1395
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
+ },
1396
1432
  date: {
1397
1433
  type: "string",
1398
1434
  description: "Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."
@@ -1465,6 +1501,9 @@ var transactionHistoryTool = buildTool({
1465
1501
  const assetSymbol = input.assetSymbol?.toLowerCase();
1466
1502
  const direction = input.direction;
1467
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();
1468
1507
  const prices = context.tokenPrices;
1469
1508
  const priceFor = (sym) => {
1470
1509
  if (!sym || !prices) return void 0;
@@ -1479,6 +1518,9 @@ var transactionHistoryTool = buildTool({
1479
1518
  if (direction) {
1480
1519
  scoped = scoped.filter((r) => r.direction === direction);
1481
1520
  }
1521
+ if (counterpartyLower) {
1522
+ scoped = scoped.filter((r) => r.recipient?.toLowerCase() === counterpartyLower);
1523
+ }
1482
1524
  if (minUsd != null && minUsd > 0) {
1483
1525
  scoped = scoped.filter((r) => {
1484
1526
  if (r.amount == null) return false;
@@ -1496,9 +1538,17 @@ var transactionHistoryTool = buildTool({
1496
1538
  action: action ?? null,
1497
1539
  minUsd: minUsd ?? null,
1498
1540
  assetSymbol: input.assetSymbol ?? null,
1499
- direction: direction ?? null
1541
+ direction: direction ?? null,
1542
+ counterparty: input.counterparty ?? null,
1543
+ address: targetAddress ?? null,
1544
+ isSelfQuery
1500
1545
  };
1501
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
+ }
1502
1552
  const agent = requireAgent(context);
1503
1553
  const records2 = await agent.history({ limit: input.date ? limit : Math.max(limit * 4, 50) });
1504
1554
  const filtered2 = finalize(records2);
@@ -1507,13 +1557,13 @@ var transactionHistoryTool = buildTool({
1507
1557
  displayText: `${filtered2.length} recent transaction(s)`
1508
1558
  };
1509
1559
  }
1510
- if (!context.walletAddress || !context.suiRpcUrl) {
1560
+ if (!targetAddress || !context.suiRpcUrl) {
1511
1561
  throw new Error("Transaction history requires a wallet address");
1512
1562
  }
1513
1563
  if (input.date) {
1514
1564
  const records2 = await queryHistoryByDate(
1515
1565
  context.suiRpcUrl,
1516
- context.walletAddress,
1566
+ targetAddress,
1517
1567
  input.date,
1518
1568
  Math.max(limit * 4, 50)
1519
1569
  );
@@ -1527,7 +1577,7 @@ var transactionHistoryTool = buildTool({
1527
1577
  const cutoffMs = Date.now() - DEFAULT_LOOKBACK_DAYS * 864e5;
1528
1578
  const records = await queryHistoryRpc(
1529
1579
  context.suiRpcUrl,
1530
- context.walletAddress,
1580
+ targetAddress,
1531
1581
  Math.max(limit * 4, 50)
1532
1582
  );
1533
1583
  const recent = records.filter((r) => r.timestamp >= cutoffMs);
@@ -2976,21 +3026,25 @@ var renderCanvasTool = buildTool({
2976
3026
 
2977
3027
  Use when the user asks for a visual chart, simulator, or financial overview. Pick the most relevant template:
2978
3028
 
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)
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)
2981
3031
  - yield_projector \u2014 compound yield simulator with amount/APY/period sliders (WORKS NOW \u2014 client-side)
2982
3032
  - health_simulator \u2014 borrow health factor simulator with collateral/debt sliders (WORKS NOW \u2014 uses current position)
2983
3033
  - 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)
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\`)
2986
3036
  - full_portfolio \u2014 4-panel overview: savings, health, activity, spending (WORKS NOW \u2014 aggregates all data)
2987
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
+
2988
3040
  Always prefer the canvas for visualisation requests. After rendering, offer to explain what the user sees.`,
2989
3041
  inputSchema: z.object({
2990
3042
  template: z.enum(CANVAS_TEMPLATES).describe("Which canvas template to render"),
2991
3043
  params: z.object({
2992
3044
  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")
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
+ )
2994
3048
  }).optional()
2995
3049
  }),
2996
3050
  jsonSchema: {
@@ -3015,6 +3069,13 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3015
3069
  async call(input, context) {
3016
3070
  const { template, params } = input;
3017
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
+ };
3018
3079
  if (template === "full_portfolio") {
3019
3080
  const pos = context.serverPositions;
3020
3081
  const rate = normalizeSavingsRate(pos?.savingsRate);
@@ -3061,45 +3122,87 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3061
3122
  };
3062
3123
  }
3063
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)}`;
3064
3138
  return {
3065
3139
  data: {
3066
3140
  __canvas: true,
3067
3141
  template,
3068
- title,
3142
+ title: `${title}${titleSuffix}`,
3069
3143
  templateData: {
3070
3144
  available: true,
3071
- address: context.walletAddress ?? ""
3145
+ address,
3146
+ isSelfRender
3072
3147
  }
3073
3148
  },
3074
- 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)}.`
3075
3150
  };
3076
3151
  }
3077
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)}`;
3078
3166
  return {
3079
3167
  data: {
3080
3168
  __canvas: true,
3081
3169
  template,
3082
- title,
3170
+ title: `${title}${titleSuffix}`,
3083
3171
  templateData: {
3084
3172
  available: true,
3085
- address: context.walletAddress ?? ""
3173
+ address,
3174
+ isSelfRender
3086
3175
  }
3087
3176
  },
3088
- 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)}.`
3089
3178
  };
3090
3179
  }
3091
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)}`;
3092
3194
  return {
3093
3195
  data: {
3094
3196
  __canvas: true,
3095
3197
  template,
3096
- title,
3198
+ title: `${title}${titleSuffix}`,
3097
3199
  templateData: {
3098
3200
  available: true,
3099
- address: context.walletAddress ?? ""
3201
+ address,
3202
+ isSelfRender
3100
3203
  }
3101
3204
  },
3102
- 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.`
3103
3206
  };
3104
3207
  }
3105
3208
  const positions = context.serverPositions;
@@ -3770,7 +3873,7 @@ function guardCostWarning(tool, _call, conversationText) {
3770
3873
  message: "This action has a monetary cost. Confirm the user is aware before proceeding."
3771
3874
  };
3772
3875
  }
3773
- var SUI_ADDRESS_REGEX = /^0x[a-fA-F0-9]{64}$/;
3876
+ var SUI_ADDRESS_REGEX2 = /^0x[a-fA-F0-9]{64}$/;
3774
3877
  function normalizeAddress(addr) {
3775
3878
  return addr.trim().toLowerCase();
3776
3879
  }
@@ -3835,7 +3938,7 @@ function guardAddressSource(tool, call, userText, contacts, walletAddress) {
3835
3938
  if (!rawTo) {
3836
3939
  return { verdict: "pass", gate: "address_source", tier: "safety" };
3837
3940
  }
3838
- if (!SUI_ADDRESS_REGEX.test(rawTo)) {
3941
+ if (!SUI_ADDRESS_REGEX2.test(rawTo)) {
3839
3942
  return { verdict: "pass", gate: "address_source", tier: "safety" };
3840
3943
  }
3841
3944
  const normalizedTo = normalizeAddress(rawTo);