@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 +2 -0
- package/dist/index.js +168 -53
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
636
|
-
|
|
637
|
-
|
|
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[
|
|
641
|
+
const stable = STABLE_USD_PRICES[norm];
|
|
641
642
|
if (typeof stable === "number") {
|
|
642
|
-
result[
|
|
643
|
+
result[original] = { price: stable };
|
|
643
644
|
continue;
|
|
644
645
|
}
|
|
645
|
-
stillMissing.push(
|
|
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
|
|
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
|
-
|
|
660
|
-
|
|
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 [
|
|
698
|
+
for (const [returnedType, priceStr] of Object.entries(prices)) {
|
|
688
699
|
const price = parseNumberOrNull(priceStr);
|
|
689
700
|
if (price == null) continue;
|
|
690
|
-
const
|
|
691
|
-
|
|
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
|
|
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
|
|
1336
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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 (
|
|
1356
|
-
|
|
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
|
-
|
|
1360
|
-
cursor = res.nextCursor;
|
|
1392
|
+
return collected;
|
|
1361
1393
|
}
|
|
1362
|
-
|
|
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.
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2968
|
-
- portfolio_timeline \u2014 net worth over time, wallet/savings/debt breakdown (WORKS NOW \u2014
|
|
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
|
|
2973
|
-
- watch_address \u2014 portfolio overview for any public Sui address (WORKS NOW \u2014 pass address
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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);
|