shll-skills 5.2.0 → 5.3.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/mcp.js +138 -24
- package/dist/mcp.mjs +138 -24
- package/package.json +1 -1
- package/src/mcp.ts +150 -33
package/dist/mcp.js
CHANGED
|
@@ -613,7 +613,12 @@ var WBNB_ABI = [
|
|
|
613
613
|
];
|
|
614
614
|
var SPENDING_LIMIT_ABI = [
|
|
615
615
|
{ type: "function", name: "setLimits", inputs: [{ name: "instanceId", type: "uint256" }, { name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
|
|
616
|
-
{ type: "function", name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" }
|
|
616
|
+
{ type: "function", name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" },
|
|
617
|
+
{ type: "function", name: "tokenRestrictionEnabled", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "bool" }], stateMutability: "view" },
|
|
618
|
+
{ type: "function", name: "getTokenList", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "address[]" }], stateMutability: "view" },
|
|
619
|
+
{ type: "function", name: "addToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" },
|
|
620
|
+
{ type: "function", name: "removeToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" },
|
|
621
|
+
{ type: "function", name: "setTokenRestriction", inputs: [{ name: "instanceId", type: "uint256" }, { name: "enabled", type: "bool" }], outputs: [], stateMutability: "nonpayable" }
|
|
617
622
|
];
|
|
618
623
|
var COOLDOWN_ABI = [
|
|
619
624
|
{ type: "function", name: "setCooldown", inputs: [{ name: "instanceId", type: "uint256" }, { name: "seconds_", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
|
|
@@ -683,6 +688,25 @@ async function checkAgentExpiry(tokenId) {
|
|
|
683
688
|
}
|
|
684
689
|
return { expired: false };
|
|
685
690
|
}
|
|
691
|
+
function policyRejectionHelp(reason, tokenId) {
|
|
692
|
+
const consoleUrl = `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety`;
|
|
693
|
+
const r = reason ?? "";
|
|
694
|
+
if (r.includes("Approve spender not allowed"))
|
|
695
|
+
return { explanation: "The DEX router address is not in the approved spender whitelist. This is a platform-level security setting.", action: "Contact the Agent owner to approve this router, or use a different DEX.", consoleUrl };
|
|
696
|
+
if (r.includes("Target not allowed"))
|
|
697
|
+
return { explanation: "The contract address is not in the DeFi target whitelist.", action: "Enable the corresponding DeFi Pack in Console > Safety, or contact the Agent owner.", consoleUrl };
|
|
698
|
+
if (r.includes("Token not in whitelist"))
|
|
699
|
+
return { explanation: "Token restriction is ON and this token is not whitelisted.", action: `Add the token to the whitelist or disable token restriction at: ${consoleUrl}`, consoleUrl };
|
|
700
|
+
if (r.includes("Exceeds per-tx limit"))
|
|
701
|
+
return { explanation: "Transaction value exceeds the per-transaction spending limit.", action: `Reduce the amount, or increase the limit at: ${consoleUrl}`, consoleUrl };
|
|
702
|
+
if (r.includes("Daily limit"))
|
|
703
|
+
return { explanation: "Daily spending limit would be exceeded.", action: `Wait until tomorrow, or increase the daily limit at: ${consoleUrl}`, consoleUrl };
|
|
704
|
+
if (r.includes("Approve exceeds limit"))
|
|
705
|
+
return { explanation: "The approve amount exceeds the configured approve limit.", action: `Reduce the amount, or increase the approve limit at: ${consoleUrl}`, consoleUrl };
|
|
706
|
+
if (r.includes("Cooldown"))
|
|
707
|
+
return { explanation: "Cooldown period has not elapsed since the last transaction.", action: "Wait for the cooldown to expire before retrying.", consoleUrl };
|
|
708
|
+
return { explanation: "Transaction was rejected by an on-chain security policy.", action: `Review your security settings at: ${consoleUrl}`, consoleUrl };
|
|
709
|
+
}
|
|
686
710
|
var server = new import_mcp.McpServer({
|
|
687
711
|
name: "shll-defi",
|
|
688
712
|
version: "5.2.0"
|
|
@@ -850,7 +874,44 @@ server.tool(
|
|
|
850
874
|
}
|
|
851
875
|
for (const action of actions) {
|
|
852
876
|
const sim = await policyClient.validate(tokenId, action);
|
|
853
|
-
if (!sim.ok)
|
|
877
|
+
if (!sim.ok) {
|
|
878
|
+
if (useV3 && sim.reason?.includes("Approve spender not allowed") && v2Available) {
|
|
879
|
+
const v2Actions = [];
|
|
880
|
+
const v2Router = PANCAKE_V2_ROUTER;
|
|
881
|
+
const v2MinOut = v2Quote * BigInt(100 - slippage) / 100n;
|
|
882
|
+
if (!isNativeIn) {
|
|
883
|
+
const allow = await publicClient.readContract({ address: fromToken.address, abi: ERC20_ABI, functionName: "allowance", args: [vault, v2Router] }).catch(() => 0n);
|
|
884
|
+
if (allow < amountIn) {
|
|
885
|
+
v2Actions.push({ target: fromToken.address, value: 0n, data: (0, import_viem2.encodeFunctionData)({ abi: ERC20_ABI, functionName: "approve", args: [v2Router, amountIn] }) });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const v2Path = tokenInAddr.toLowerCase() !== WBNB.toLowerCase() && tokenOutAddr.toLowerCase() !== WBNB.toLowerCase() ? [tokenInAddr, WBNB, tokenOutAddr] : [tokenInAddr, tokenOutAddr];
|
|
889
|
+
if (isNativeIn) {
|
|
890
|
+
v2Actions.push({ target: v2Router, value: amountIn, data: (0, import_viem2.encodeFunctionData)({ abi: SWAP_EXACT_ETH_ABI, functionName: "swapExactETHForTokens", args: [v2MinOut, v2Path, vault, deadline] }) });
|
|
891
|
+
} else {
|
|
892
|
+
v2Actions.push({ target: v2Router, value: 0n, data: (0, import_viem2.encodeFunctionData)({ abi: SWAP_EXACT_TOKENS_ABI, functionName: "swapExactTokensForTokens", args: [amountIn, v2MinOut, v2Path, vault, deadline] }) });
|
|
893
|
+
}
|
|
894
|
+
for (const v2a of v2Actions) {
|
|
895
|
+
const v2Sim = await policyClient.validate(tokenId, v2a);
|
|
896
|
+
if (!v2Sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: v2Sim.reason, ...policyRejectionHelp(v2Sim.reason, token_id) }) }] };
|
|
897
|
+
}
|
|
898
|
+
const v2Result = v2Actions.length === 1 ? await policyClient.execute(tokenId, v2Actions[0], true) : await policyClient.executeBatch(tokenId, v2Actions, true);
|
|
899
|
+
return {
|
|
900
|
+
content: [{
|
|
901
|
+
type: "text",
|
|
902
|
+
text: JSON.stringify({
|
|
903
|
+
status: "success",
|
|
904
|
+
hash: v2Result.hash,
|
|
905
|
+
dex: "v2",
|
|
906
|
+
quote: v2Quote.toString(),
|
|
907
|
+
minOut: v2MinOut.toString(),
|
|
908
|
+
note: "V3 was rejected by policy (Approve spender not allowed). Auto-switched to V2."
|
|
909
|
+
})
|
|
910
|
+
}]
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
|
|
914
|
+
}
|
|
854
915
|
}
|
|
855
916
|
const result = actions.length === 1 ? await policyClient.execute(tokenId, actions[0], true) : await policyClient.executeBatch(tokenId, actions, true);
|
|
856
917
|
return {
|
|
@@ -899,7 +960,7 @@ server.tool(
|
|
|
899
960
|
}
|
|
900
961
|
for (const action of actions) {
|
|
901
962
|
const sim = await policyClient.validate(tokenId, action);
|
|
902
|
-
if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
|
|
963
|
+
if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
|
|
903
964
|
}
|
|
904
965
|
const result = actions.length === 1 ? await policyClient.execute(tokenId, actions[0], true) : await policyClient.executeBatch(tokenId, actions, true);
|
|
905
966
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, protocol: "venus", action: "supply", token: symbol, amount }) }] };
|
|
@@ -996,17 +1057,14 @@ server.tool(
|
|
|
996
1057
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, token, amount, to: recipient }) }] };
|
|
997
1058
|
}
|
|
998
1059
|
);
|
|
999
|
-
var
|
|
1000
|
-
type: "function",
|
|
1001
|
-
name: "
|
|
1002
|
-
|
|
1003
|
-
outputs: [{ name: "", type: "address" }],
|
|
1004
|
-
stateMutability: "view"
|
|
1005
|
-
}];
|
|
1060
|
+
var MY_AGENTS_ABI = [
|
|
1061
|
+
{ type: "function", name: "operatorOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], stateMutability: "view" },
|
|
1062
|
+
{ type: "function", name: "operatorExpiresOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" }
|
|
1063
|
+
];
|
|
1006
1064
|
var DEFAULT_INDEXER = "https://indexer-mainnet.shll.run";
|
|
1007
1065
|
server.tool(
|
|
1008
1066
|
"my_agents",
|
|
1009
|
-
"List all agents where the current operator key is authorized. Returns
|
|
1067
|
+
"List all agents where the current operator key is or was authorized. Returns active agents and expired agents that need renewal.",
|
|
1010
1068
|
{},
|
|
1011
1069
|
async () => {
|
|
1012
1070
|
const { account, publicClient, config } = createClients();
|
|
@@ -1023,18 +1081,34 @@ server.tool(
|
|
|
1023
1081
|
agents.map(async (a) => {
|
|
1024
1082
|
const tokenId = BigInt(a.tokenId);
|
|
1025
1083
|
try {
|
|
1026
|
-
const op = await
|
|
1027
|
-
address: nfaAddr,
|
|
1028
|
-
abi:
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1084
|
+
const [op, opExpires] = await Promise.all([
|
|
1085
|
+
publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorOf", args: [tokenId] }),
|
|
1086
|
+
publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorExpiresOf", args: [tokenId] })
|
|
1087
|
+
]);
|
|
1088
|
+
const isActive = op.toLowerCase() === operator;
|
|
1089
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
1090
|
+
const isExpired = !isActive && Number(opExpires) > 0 && now > opExpires;
|
|
1091
|
+
if (isActive) {
|
|
1092
|
+
return {
|
|
1093
|
+
tokenId: tokenId.toString(),
|
|
1094
|
+
vault: a.account || "",
|
|
1095
|
+
owner: a.owner || "",
|
|
1096
|
+
agentType: a.agentType || "unknown",
|
|
1097
|
+
status: "active",
|
|
1098
|
+
operatorExpires: new Date(Number(opExpires) * 1e3).toISOString()
|
|
1099
|
+
};
|
|
1100
|
+
} else if (isExpired) {
|
|
1101
|
+
return {
|
|
1102
|
+
tokenId: tokenId.toString(),
|
|
1103
|
+
vault: a.account || "",
|
|
1104
|
+
owner: a.owner || "",
|
|
1105
|
+
agentType: a.agentType || "unknown",
|
|
1106
|
+
status: "expired",
|
|
1107
|
+
operatorExpires: new Date(Number(opExpires) * 1e3).toISOString(),
|
|
1108
|
+
note: "Operator authorization expired. Renew at https://shll.run/me"
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
return null;
|
|
1038
1112
|
} catch {
|
|
1039
1113
|
return null;
|
|
1040
1114
|
}
|
|
@@ -1142,7 +1216,15 @@ server.tool(
|
|
|
1142
1216
|
const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
|
|
1143
1217
|
const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
|
|
1144
1218
|
const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
|
|
1145
|
-
|
|
1219
|
+
let tokenRestriction = {};
|
|
1220
|
+
try {
|
|
1221
|
+
const enabled = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] });
|
|
1222
|
+
const tokenList = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] });
|
|
1223
|
+
tokenRestriction = { tokenRestrictionEnabled: enabled, whitelistedTokens: tokenList, whitelistedTokenCount: tokenList.length };
|
|
1224
|
+
summaryParts.push(enabled ? `Token whitelist ON (${tokenList.length} tokens)` : "Token whitelist OFF (any token allowed)");
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString(), ...tokenRestriction };
|
|
1146
1228
|
summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
|
|
1147
1229
|
} catch {
|
|
1148
1230
|
}
|
|
@@ -1166,6 +1248,38 @@ server.tool(
|
|
|
1166
1248
|
return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, humanSummary, securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT.", policies: enriched }) }] };
|
|
1167
1249
|
}
|
|
1168
1250
|
);
|
|
1251
|
+
server.tool(
|
|
1252
|
+
"token_restriction",
|
|
1253
|
+
"Check token whitelist restriction status. Shows whether token trading is restricted and which tokens are whitelisted.",
|
|
1254
|
+
{ token_id: import_zod.z.string().describe("Agent NFA Token ID") },
|
|
1255
|
+
async ({ token_id }) => {
|
|
1256
|
+
const { publicClient, policyClient } = createClients();
|
|
1257
|
+
const tokenId = BigInt(token_id);
|
|
1258
|
+
const policies = await policyClient.getPolicies(tokenId);
|
|
1259
|
+
const spendingPolicy = policies.find((p) => p.policyTypeName === "spending_limit");
|
|
1260
|
+
if (!spendingPolicy) {
|
|
1261
|
+
return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, error: "No spending_limit policy found \u2014 token restriction is not available for this agent." }) }] };
|
|
1262
|
+
}
|
|
1263
|
+
try {
|
|
1264
|
+
const [enabled, tokenList] = await Promise.all([
|
|
1265
|
+
publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] }),
|
|
1266
|
+
publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] })
|
|
1267
|
+
]);
|
|
1268
|
+
const result = {
|
|
1269
|
+
tokenId: token_id,
|
|
1270
|
+
tokenRestrictionEnabled: enabled,
|
|
1271
|
+
status: enabled ? "ON \u2014 only whitelisted tokens can be traded" : "OFF \u2014 any token can be traded",
|
|
1272
|
+
whitelistedTokens: tokenList,
|
|
1273
|
+
whitelistedTokenCount: tokenList.length,
|
|
1274
|
+
manageUrl: `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${token_id}/console/safety`,
|
|
1275
|
+
note: enabled ? "To add/remove tokens or disable restriction, visit the management URL above (requires connected wallet as renter/owner)." : "Token restriction is disabled. The agent can trade any token. To enable, visit the management URL."
|
|
1276
|
+
};
|
|
1277
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1278
|
+
} catch {
|
|
1279
|
+
return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, error: "Failed to read token restriction \u2014 the SpendingLimitPolicy may not support this feature." }) }] };
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
);
|
|
1169
1283
|
server.tool(
|
|
1170
1284
|
"status",
|
|
1171
1285
|
"One-shot security overview: vault balance, operator status, policies, and recent activity",
|
package/dist/mcp.mjs
CHANGED
|
@@ -124,7 +124,12 @@ var WBNB_ABI = [
|
|
|
124
124
|
];
|
|
125
125
|
var SPENDING_LIMIT_ABI = [
|
|
126
126
|
{ type: "function", name: "setLimits", inputs: [{ name: "instanceId", type: "uint256" }, { name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
|
|
127
|
-
{ type: "function", name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" }
|
|
127
|
+
{ type: "function", name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" },
|
|
128
|
+
{ type: "function", name: "tokenRestrictionEnabled", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "bool" }], stateMutability: "view" },
|
|
129
|
+
{ type: "function", name: "getTokenList", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "address[]" }], stateMutability: "view" },
|
|
130
|
+
{ type: "function", name: "addToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" },
|
|
131
|
+
{ type: "function", name: "removeToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" },
|
|
132
|
+
{ type: "function", name: "setTokenRestriction", inputs: [{ name: "instanceId", type: "uint256" }, { name: "enabled", type: "bool" }], outputs: [], stateMutability: "nonpayable" }
|
|
128
133
|
];
|
|
129
134
|
var COOLDOWN_ABI = [
|
|
130
135
|
{ type: "function", name: "setCooldown", inputs: [{ name: "instanceId", type: "uint256" }, { name: "seconds_", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
|
|
@@ -194,6 +199,25 @@ async function checkAgentExpiry(tokenId) {
|
|
|
194
199
|
}
|
|
195
200
|
return { expired: false };
|
|
196
201
|
}
|
|
202
|
+
function policyRejectionHelp(reason, tokenId) {
|
|
203
|
+
const consoleUrl = `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety`;
|
|
204
|
+
const r = reason ?? "";
|
|
205
|
+
if (r.includes("Approve spender not allowed"))
|
|
206
|
+
return { explanation: "The DEX router address is not in the approved spender whitelist. This is a platform-level security setting.", action: "Contact the Agent owner to approve this router, or use a different DEX.", consoleUrl };
|
|
207
|
+
if (r.includes("Target not allowed"))
|
|
208
|
+
return { explanation: "The contract address is not in the DeFi target whitelist.", action: "Enable the corresponding DeFi Pack in Console > Safety, or contact the Agent owner.", consoleUrl };
|
|
209
|
+
if (r.includes("Token not in whitelist"))
|
|
210
|
+
return { explanation: "Token restriction is ON and this token is not whitelisted.", action: `Add the token to the whitelist or disable token restriction at: ${consoleUrl}`, consoleUrl };
|
|
211
|
+
if (r.includes("Exceeds per-tx limit"))
|
|
212
|
+
return { explanation: "Transaction value exceeds the per-transaction spending limit.", action: `Reduce the amount, or increase the limit at: ${consoleUrl}`, consoleUrl };
|
|
213
|
+
if (r.includes("Daily limit"))
|
|
214
|
+
return { explanation: "Daily spending limit would be exceeded.", action: `Wait until tomorrow, or increase the daily limit at: ${consoleUrl}`, consoleUrl };
|
|
215
|
+
if (r.includes("Approve exceeds limit"))
|
|
216
|
+
return { explanation: "The approve amount exceeds the configured approve limit.", action: `Reduce the amount, or increase the approve limit at: ${consoleUrl}`, consoleUrl };
|
|
217
|
+
if (r.includes("Cooldown"))
|
|
218
|
+
return { explanation: "Cooldown period has not elapsed since the last transaction.", action: "Wait for the cooldown to expire before retrying.", consoleUrl };
|
|
219
|
+
return { explanation: "Transaction was rejected by an on-chain security policy.", action: `Review your security settings at: ${consoleUrl}`, consoleUrl };
|
|
220
|
+
}
|
|
197
221
|
var server = new McpServer({
|
|
198
222
|
name: "shll-defi",
|
|
199
223
|
version: "5.2.0"
|
|
@@ -361,7 +385,44 @@ server.tool(
|
|
|
361
385
|
}
|
|
362
386
|
for (const action of actions) {
|
|
363
387
|
const sim = await policyClient.validate(tokenId, action);
|
|
364
|
-
if (!sim.ok)
|
|
388
|
+
if (!sim.ok) {
|
|
389
|
+
if (useV3 && sim.reason?.includes("Approve spender not allowed") && v2Available) {
|
|
390
|
+
const v2Actions = [];
|
|
391
|
+
const v2Router = PANCAKE_V2_ROUTER;
|
|
392
|
+
const v2MinOut = v2Quote * BigInt(100 - slippage) / 100n;
|
|
393
|
+
if (!isNativeIn) {
|
|
394
|
+
const allow = await publicClient.readContract({ address: fromToken.address, abi: ERC20_ABI, functionName: "allowance", args: [vault, v2Router] }).catch(() => 0n);
|
|
395
|
+
if (allow < amountIn) {
|
|
396
|
+
v2Actions.push({ target: fromToken.address, value: 0n, data: encodeFunctionData({ abi: ERC20_ABI, functionName: "approve", args: [v2Router, amountIn] }) });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const v2Path = tokenInAddr.toLowerCase() !== WBNB.toLowerCase() && tokenOutAddr.toLowerCase() !== WBNB.toLowerCase() ? [tokenInAddr, WBNB, tokenOutAddr] : [tokenInAddr, tokenOutAddr];
|
|
400
|
+
if (isNativeIn) {
|
|
401
|
+
v2Actions.push({ target: v2Router, value: amountIn, data: encodeFunctionData({ abi: SWAP_EXACT_ETH_ABI, functionName: "swapExactETHForTokens", args: [v2MinOut, v2Path, vault, deadline] }) });
|
|
402
|
+
} else {
|
|
403
|
+
v2Actions.push({ target: v2Router, value: 0n, data: encodeFunctionData({ abi: SWAP_EXACT_TOKENS_ABI, functionName: "swapExactTokensForTokens", args: [amountIn, v2MinOut, v2Path, vault, deadline] }) });
|
|
404
|
+
}
|
|
405
|
+
for (const v2a of v2Actions) {
|
|
406
|
+
const v2Sim = await policyClient.validate(tokenId, v2a);
|
|
407
|
+
if (!v2Sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: v2Sim.reason, ...policyRejectionHelp(v2Sim.reason, token_id) }) }] };
|
|
408
|
+
}
|
|
409
|
+
const v2Result = v2Actions.length === 1 ? await policyClient.execute(tokenId, v2Actions[0], true) : await policyClient.executeBatch(tokenId, v2Actions, true);
|
|
410
|
+
return {
|
|
411
|
+
content: [{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: JSON.stringify({
|
|
414
|
+
status: "success",
|
|
415
|
+
hash: v2Result.hash,
|
|
416
|
+
dex: "v2",
|
|
417
|
+
quote: v2Quote.toString(),
|
|
418
|
+
minOut: v2MinOut.toString(),
|
|
419
|
+
note: "V3 was rejected by policy (Approve spender not allowed). Auto-switched to V2."
|
|
420
|
+
})
|
|
421
|
+
}]
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
|
|
425
|
+
}
|
|
365
426
|
}
|
|
366
427
|
const result = actions.length === 1 ? await policyClient.execute(tokenId, actions[0], true) : await policyClient.executeBatch(tokenId, actions, true);
|
|
367
428
|
return {
|
|
@@ -410,7 +471,7 @@ server.tool(
|
|
|
410
471
|
}
|
|
411
472
|
for (const action of actions) {
|
|
412
473
|
const sim = await policyClient.validate(tokenId, action);
|
|
413
|
-
if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
|
|
474
|
+
if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
|
|
414
475
|
}
|
|
415
476
|
const result = actions.length === 1 ? await policyClient.execute(tokenId, actions[0], true) : await policyClient.executeBatch(tokenId, actions, true);
|
|
416
477
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, protocol: "venus", action: "supply", token: symbol, amount }) }] };
|
|
@@ -507,17 +568,14 @@ server.tool(
|
|
|
507
568
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, token, amount, to: recipient }) }] };
|
|
508
569
|
}
|
|
509
570
|
);
|
|
510
|
-
var
|
|
511
|
-
type: "function",
|
|
512
|
-
name: "
|
|
513
|
-
|
|
514
|
-
outputs: [{ name: "", type: "address" }],
|
|
515
|
-
stateMutability: "view"
|
|
516
|
-
}];
|
|
571
|
+
var MY_AGENTS_ABI = [
|
|
572
|
+
{ type: "function", name: "operatorOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], stateMutability: "view" },
|
|
573
|
+
{ type: "function", name: "operatorExpiresOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" }
|
|
574
|
+
];
|
|
517
575
|
var DEFAULT_INDEXER = "https://indexer-mainnet.shll.run";
|
|
518
576
|
server.tool(
|
|
519
577
|
"my_agents",
|
|
520
|
-
"List all agents where the current operator key is authorized. Returns
|
|
578
|
+
"List all agents where the current operator key is or was authorized. Returns active agents and expired agents that need renewal.",
|
|
521
579
|
{},
|
|
522
580
|
async () => {
|
|
523
581
|
const { account, publicClient, config } = createClients();
|
|
@@ -534,18 +592,34 @@ server.tool(
|
|
|
534
592
|
agents.map(async (a) => {
|
|
535
593
|
const tokenId = BigInt(a.tokenId);
|
|
536
594
|
try {
|
|
537
|
-
const op = await
|
|
538
|
-
address: nfaAddr,
|
|
539
|
-
abi:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
595
|
+
const [op, opExpires] = await Promise.all([
|
|
596
|
+
publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorOf", args: [tokenId] }),
|
|
597
|
+
publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorExpiresOf", args: [tokenId] })
|
|
598
|
+
]);
|
|
599
|
+
const isActive = op.toLowerCase() === operator;
|
|
600
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
601
|
+
const isExpired = !isActive && Number(opExpires) > 0 && now > opExpires;
|
|
602
|
+
if (isActive) {
|
|
603
|
+
return {
|
|
604
|
+
tokenId: tokenId.toString(),
|
|
605
|
+
vault: a.account || "",
|
|
606
|
+
owner: a.owner || "",
|
|
607
|
+
agentType: a.agentType || "unknown",
|
|
608
|
+
status: "active",
|
|
609
|
+
operatorExpires: new Date(Number(opExpires) * 1e3).toISOString()
|
|
610
|
+
};
|
|
611
|
+
} else if (isExpired) {
|
|
612
|
+
return {
|
|
613
|
+
tokenId: tokenId.toString(),
|
|
614
|
+
vault: a.account || "",
|
|
615
|
+
owner: a.owner || "",
|
|
616
|
+
agentType: a.agentType || "unknown",
|
|
617
|
+
status: "expired",
|
|
618
|
+
operatorExpires: new Date(Number(opExpires) * 1e3).toISOString(),
|
|
619
|
+
note: "Operator authorization expired. Renew at https://shll.run/me"
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
return null;
|
|
549
623
|
} catch {
|
|
550
624
|
return null;
|
|
551
625
|
}
|
|
@@ -653,7 +727,15 @@ server.tool(
|
|
|
653
727
|
const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
|
|
654
728
|
const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
|
|
655
729
|
const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
|
|
656
|
-
|
|
730
|
+
let tokenRestriction = {};
|
|
731
|
+
try {
|
|
732
|
+
const enabled = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] });
|
|
733
|
+
const tokenList = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] });
|
|
734
|
+
tokenRestriction = { tokenRestrictionEnabled: enabled, whitelistedTokens: tokenList, whitelistedTokenCount: tokenList.length };
|
|
735
|
+
summaryParts.push(enabled ? `Token whitelist ON (${tokenList.length} tokens)` : "Token whitelist OFF (any token allowed)");
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString(), ...tokenRestriction };
|
|
657
739
|
summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
|
|
658
740
|
} catch {
|
|
659
741
|
}
|
|
@@ -677,6 +759,38 @@ server.tool(
|
|
|
677
759
|
return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, humanSummary, securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT.", policies: enriched }) }] };
|
|
678
760
|
}
|
|
679
761
|
);
|
|
762
|
+
server.tool(
|
|
763
|
+
"token_restriction",
|
|
764
|
+
"Check token whitelist restriction status. Shows whether token trading is restricted and which tokens are whitelisted.",
|
|
765
|
+
{ token_id: z.string().describe("Agent NFA Token ID") },
|
|
766
|
+
async ({ token_id }) => {
|
|
767
|
+
const { publicClient, policyClient } = createClients();
|
|
768
|
+
const tokenId = BigInt(token_id);
|
|
769
|
+
const policies = await policyClient.getPolicies(tokenId);
|
|
770
|
+
const spendingPolicy = policies.find((p) => p.policyTypeName === "spending_limit");
|
|
771
|
+
if (!spendingPolicy) {
|
|
772
|
+
return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, error: "No spending_limit policy found \u2014 token restriction is not available for this agent." }) }] };
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const [enabled, tokenList] = await Promise.all([
|
|
776
|
+
publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] }),
|
|
777
|
+
publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] })
|
|
778
|
+
]);
|
|
779
|
+
const result = {
|
|
780
|
+
tokenId: token_id,
|
|
781
|
+
tokenRestrictionEnabled: enabled,
|
|
782
|
+
status: enabled ? "ON \u2014 only whitelisted tokens can be traded" : "OFF \u2014 any token can be traded",
|
|
783
|
+
whitelistedTokens: tokenList,
|
|
784
|
+
whitelistedTokenCount: tokenList.length,
|
|
785
|
+
manageUrl: `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${token_id}/console/safety`,
|
|
786
|
+
note: enabled ? "To add/remove tokens or disable restriction, visit the management URL above (requires connected wallet as renter/owner)." : "Token restriction is disabled. The agent can trade any token. To enable, visit the management URL."
|
|
787
|
+
};
|
|
788
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
789
|
+
} catch {
|
|
790
|
+
return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, error: "Failed to read token restriction \u2014 the SpendingLimitPolicy may not support this feature." }) }] };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
);
|
|
680
794
|
server.tool(
|
|
681
795
|
"status",
|
|
682
796
|
"One-shot security overview: vault balance, operator status, policies, and recent activity",
|
package/package.json
CHANGED
package/src/mcp.ts
CHANGED
|
@@ -153,6 +153,11 @@ const WBNB_ABI = [
|
|
|
153
153
|
const SPENDING_LIMIT_ABI = [
|
|
154
154
|
{ type: "function" as const, name: "setLimits", inputs: [{ name: "instanceId", type: "uint256" }, { name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], outputs: [], stateMutability: "nonpayable" as const },
|
|
155
155
|
{ type: "function" as const, name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" as const },
|
|
156
|
+
{ type: "function" as const, name: "tokenRestrictionEnabled", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "bool" }], stateMutability: "view" as const },
|
|
157
|
+
{ type: "function" as const, name: "getTokenList", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "address[]" }], stateMutability: "view" as const },
|
|
158
|
+
{ type: "function" as const, name: "addToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" as const },
|
|
159
|
+
{ type: "function" as const, name: "removeToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" as const },
|
|
160
|
+
{ type: "function" as const, name: "setTokenRestriction", inputs: [{ name: "instanceId", type: "uint256" }, { name: "enabled", type: "bool" }], outputs: [], stateMutability: "nonpayable" as const },
|
|
156
161
|
] as const;
|
|
157
162
|
|
|
158
163
|
const COOLDOWN_ABI = [
|
|
@@ -234,6 +239,27 @@ async function checkAgentExpiry(tokenId: bigint) {
|
|
|
234
239
|
return { expired: false };
|
|
235
240
|
}
|
|
236
241
|
|
|
242
|
+
// Policy rejection → actionable user guidance
|
|
243
|
+
function policyRejectionHelp(reason: string | undefined, tokenId: string): Record<string, string> {
|
|
244
|
+
const consoleUrl = `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety`;
|
|
245
|
+
const r = reason ?? "";
|
|
246
|
+
if (r.includes("Approve spender not allowed"))
|
|
247
|
+
return { explanation: "The DEX router address is not in the approved spender whitelist. This is a platform-level security setting.", action: "Contact the Agent owner to approve this router, or use a different DEX.", consoleUrl };
|
|
248
|
+
if (r.includes("Target not allowed"))
|
|
249
|
+
return { explanation: "The contract address is not in the DeFi target whitelist.", action: "Enable the corresponding DeFi Pack in Console > Safety, or contact the Agent owner.", consoleUrl };
|
|
250
|
+
if (r.includes("Token not in whitelist"))
|
|
251
|
+
return { explanation: "Token restriction is ON and this token is not whitelisted.", action: `Add the token to the whitelist or disable token restriction at: ${consoleUrl}`, consoleUrl };
|
|
252
|
+
if (r.includes("Exceeds per-tx limit"))
|
|
253
|
+
return { explanation: "Transaction value exceeds the per-transaction spending limit.", action: `Reduce the amount, or increase the limit at: ${consoleUrl}`, consoleUrl };
|
|
254
|
+
if (r.includes("Daily limit"))
|
|
255
|
+
return { explanation: "Daily spending limit would be exceeded.", action: `Wait until tomorrow, or increase the daily limit at: ${consoleUrl}`, consoleUrl };
|
|
256
|
+
if (r.includes("Approve exceeds limit"))
|
|
257
|
+
return { explanation: "The approve amount exceeds the configured approve limit.", action: `Reduce the amount, or increase the approve limit at: ${consoleUrl}`, consoleUrl };
|
|
258
|
+
if (r.includes("Cooldown"))
|
|
259
|
+
return { explanation: "Cooldown period has not elapsed since the last transaction.", action: "Wait for the cooldown to expire before retrying.", consoleUrl };
|
|
260
|
+
return { explanation: "Transaction was rejected by an on-chain security policy.", action: `Review your security settings at: ${consoleUrl}`, consoleUrl };
|
|
261
|
+
}
|
|
262
|
+
|
|
237
263
|
// ═══════════════════════════════════════════════════════
|
|
238
264
|
// MCP Server
|
|
239
265
|
// ═══════════════════════════════════════════════════════
|
|
@@ -349,7 +375,7 @@ server.tool(
|
|
|
349
375
|
async ({ token_id, from, to, amount, dex, slippage }) => {
|
|
350
376
|
const { publicClient, policyClient } = createClients();
|
|
351
377
|
const tokenId = BigInt(token_id);
|
|
352
|
-
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
378
|
+
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
353
379
|
const vault = await policyClient.getVault(tokenId);
|
|
354
380
|
|
|
355
381
|
const fromToken = resolveToken(from);
|
|
@@ -421,10 +447,48 @@ server.tool(
|
|
|
421
447
|
}
|
|
422
448
|
}
|
|
423
449
|
|
|
424
|
-
// Validate + execute
|
|
450
|
+
// Validate + execute (with V3→V2 fallback on approve rejection)
|
|
425
451
|
for (const action of actions) {
|
|
426
452
|
const sim = await policyClient.validate(tokenId, action);
|
|
427
|
-
if (!sim.ok)
|
|
453
|
+
if (!sim.ok) {
|
|
454
|
+
// If V3 approve was rejected, try V2 fallback
|
|
455
|
+
if (useV3 && sim.reason?.includes("Approve spender not allowed") && v2Available) {
|
|
456
|
+
const v2Actions: Action[] = [];
|
|
457
|
+
const v2Router = PANCAKE_V2_ROUTER as Address;
|
|
458
|
+
const v2MinOut = (v2Quote * BigInt(100 - slippage)) / 100n;
|
|
459
|
+
if (!isNativeIn) {
|
|
460
|
+
const allow = await publicClient.readContract({ address: fromToken.address, abi: ERC20_ABI, functionName: "allowance", args: [vault, v2Router] }).catch(() => 0n);
|
|
461
|
+
if (allow < amountIn) {
|
|
462
|
+
v2Actions.push({ target: fromToken.address, value: 0n, data: encodeFunctionData({ abi: ERC20_ABI, functionName: "approve", args: [v2Router, amountIn] }) });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const v2Path: Address[] = tokenInAddr.toLowerCase() !== WBNB.toLowerCase() && tokenOutAddr.toLowerCase() !== WBNB.toLowerCase()
|
|
466
|
+
? [tokenInAddr, WBNB as Address, tokenOutAddr] : [tokenInAddr, tokenOutAddr];
|
|
467
|
+
if (isNativeIn) {
|
|
468
|
+
v2Actions.push({ target: v2Router, value: amountIn, data: encodeFunctionData({ abi: SWAP_EXACT_ETH_ABI, functionName: "swapExactETHForTokens", args: [v2MinOut, v2Path, vault, deadline] }) });
|
|
469
|
+
} else {
|
|
470
|
+
v2Actions.push({ target: v2Router, value: 0n, data: encodeFunctionData({ abi: SWAP_EXACT_TOKENS_ABI, functionName: "swapExactTokensForTokens", args: [amountIn, v2MinOut, v2Path, vault, deadline] }) });
|
|
471
|
+
}
|
|
472
|
+
// Validate V2 fallback
|
|
473
|
+
for (const v2a of v2Actions) {
|
|
474
|
+
const v2Sim = await policyClient.validate(tokenId, v2a);
|
|
475
|
+
if (!v2Sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: v2Sim.reason, ...policyRejectionHelp(v2Sim.reason, token_id) }) }] };
|
|
476
|
+
}
|
|
477
|
+
const v2Result = v2Actions.length === 1
|
|
478
|
+
? await policyClient.execute(tokenId, v2Actions[0], true)
|
|
479
|
+
: await policyClient.executeBatch(tokenId, v2Actions, true);
|
|
480
|
+
return {
|
|
481
|
+
content: [{
|
|
482
|
+
type: "text" as const, text: JSON.stringify({
|
|
483
|
+
status: "success", hash: v2Result.hash, dex: "v2",
|
|
484
|
+
quote: v2Quote.toString(), minOut: v2MinOut.toString(),
|
|
485
|
+
note: "V3 was rejected by policy (Approve spender not allowed). Auto-switched to V2.",
|
|
486
|
+
})
|
|
487
|
+
}]
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
|
|
491
|
+
}
|
|
428
492
|
}
|
|
429
493
|
|
|
430
494
|
const result = actions.length === 1
|
|
@@ -454,7 +518,7 @@ server.tool(
|
|
|
454
518
|
async ({ token_id, token, amount }) => {
|
|
455
519
|
const { publicClient, policyClient } = createClients();
|
|
456
520
|
const tokenId = BigInt(token_id);
|
|
457
|
-
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
521
|
+
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
458
522
|
const symbol = token.toUpperCase();
|
|
459
523
|
const vTokenAddr = VENUS_VTOKENS[symbol];
|
|
460
524
|
if (!vTokenAddr) return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Unsupported: ${symbol}. Use: ${Object.keys(VENUS_VTOKENS).join(", ")}` }) }] };
|
|
@@ -477,7 +541,7 @@ server.tool(
|
|
|
477
541
|
|
|
478
542
|
for (const action of actions) {
|
|
479
543
|
const sim = await policyClient.validate(tokenId, action);
|
|
480
|
-
if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
|
|
544
|
+
if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
|
|
481
545
|
}
|
|
482
546
|
|
|
483
547
|
const result = actions.length === 1
|
|
@@ -500,7 +564,7 @@ server.tool(
|
|
|
500
564
|
async ({ token_id, token, amount }) => {
|
|
501
565
|
const { policyClient } = createClients();
|
|
502
566
|
const tokenId = BigInt(token_id);
|
|
503
|
-
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
567
|
+
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
504
568
|
const symbol = token.toUpperCase();
|
|
505
569
|
const vTokenAddr = VENUS_VTOKENS[symbol];
|
|
506
570
|
if (!vTokenAddr) return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Unsupported: ${symbol}` }) }] };
|
|
@@ -565,7 +629,7 @@ server.tool(
|
|
|
565
629
|
async ({ token_id, token, amount, to }) => {
|
|
566
630
|
const { policyClient } = createClients();
|
|
567
631
|
const tokenId = BigInt(token_id);
|
|
568
|
-
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
632
|
+
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
569
633
|
const tokenInfo = resolveToken(token);
|
|
570
634
|
const amt = parseAmount(amount, tokenInfo.decimals);
|
|
571
635
|
const recipient = to as Address;
|
|
@@ -593,18 +657,16 @@ server.tool(
|
|
|
593
657
|
);
|
|
594
658
|
|
|
595
659
|
// ── Tool: my_agents ─────────────────────────────────────
|
|
596
|
-
const
|
|
597
|
-
type: "function" as const, name: "operatorOf",
|
|
598
|
-
inputs: [{ name: "tokenId", type: "uint256" }],
|
|
599
|
-
|
|
600
|
-
stateMutability: "view" as const,
|
|
601
|
-
}] as const;
|
|
660
|
+
const MY_AGENTS_ABI = [
|
|
661
|
+
{ type: "function" as const, name: "operatorOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], stateMutability: "view" as const },
|
|
662
|
+
{ type: "function" as const, name: "operatorExpiresOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" as const },
|
|
663
|
+
] as const;
|
|
602
664
|
|
|
603
665
|
const DEFAULT_INDEXER = "https://indexer-mainnet.shll.run";
|
|
604
666
|
|
|
605
667
|
server.tool(
|
|
606
668
|
"my_agents",
|
|
607
|
-
"List all agents where the current operator key is authorized. Returns
|
|
669
|
+
"List all agents where the current operator key is or was authorized. Returns active agents and expired agents that need renewal.",
|
|
608
670
|
{},
|
|
609
671
|
async () => {
|
|
610
672
|
const { account, publicClient, config } = createClients();
|
|
@@ -621,23 +683,34 @@ server.tool(
|
|
|
621
683
|
return { content: [{ type: "text" as const, text: JSON.stringify({ operator, agents: [], count: 0 }) }] };
|
|
622
684
|
}
|
|
623
685
|
|
|
624
|
-
// 2.
|
|
686
|
+
// 2. Check operatorOf AND operatorExpiresOf for all agents
|
|
625
687
|
const checks = await Promise.all(
|
|
626
688
|
agents.map(async (a) => {
|
|
627
689
|
const tokenId = BigInt(a.tokenId!);
|
|
628
690
|
try {
|
|
629
|
-
const op = await
|
|
630
|
-
address: nfaAddr,
|
|
631
|
-
abi:
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
691
|
+
const [op, opExpires] = await Promise.all([
|
|
692
|
+
publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorOf", args: [tokenId] }) as Promise<string>,
|
|
693
|
+
publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorExpiresOf", args: [tokenId] }) as Promise<bigint>,
|
|
694
|
+
]);
|
|
695
|
+
const isActive = op.toLowerCase() === operator;
|
|
696
|
+
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
697
|
+
const isExpired = !isActive && Number(opExpires) > 0 && now > opExpires;
|
|
698
|
+
|
|
699
|
+
if (isActive) {
|
|
700
|
+
return {
|
|
701
|
+
tokenId: tokenId.toString(), vault: a.account || "", owner: a.owner || "",
|
|
702
|
+
agentType: a.agentType || "unknown", status: "active" as const,
|
|
703
|
+
operatorExpires: new Date(Number(opExpires) * 1000).toISOString(),
|
|
704
|
+
};
|
|
705
|
+
} else if (isExpired) {
|
|
706
|
+
return {
|
|
707
|
+
tokenId: tokenId.toString(), vault: a.account || "", owner: a.owner || "",
|
|
708
|
+
agentType: a.agentType || "unknown", status: "expired" as const,
|
|
709
|
+
operatorExpires: new Date(Number(opExpires) * 1000).toISOString(),
|
|
710
|
+
note: "Operator authorization expired. Renew at https://shll.run/me",
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
641
714
|
} catch { return null; }
|
|
642
715
|
})
|
|
643
716
|
);
|
|
@@ -663,7 +736,7 @@ server.tool(
|
|
|
663
736
|
async ({ token_id, amount }) => {
|
|
664
737
|
const { policyClient } = createClients();
|
|
665
738
|
const tokenId = BigInt(token_id);
|
|
666
|
-
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
739
|
+
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
667
740
|
const amt = parseEther(amount);
|
|
668
741
|
const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "deposit" });
|
|
669
742
|
const action: Action = { target: WBNB as Address, value: amt, data };
|
|
@@ -687,7 +760,7 @@ server.tool(
|
|
|
687
760
|
async ({ token_id, amount }) => {
|
|
688
761
|
const { policyClient } = createClients();
|
|
689
762
|
const tokenId = BigInt(token_id);
|
|
690
|
-
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
763
|
+
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
691
764
|
const amt = parseEther(amount);
|
|
692
765
|
const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "withdraw", args: [amt] });
|
|
693
766
|
const action: Action = { target: WBNB as Address, value: 0n, data };
|
|
@@ -759,7 +832,15 @@ server.tool(
|
|
|
759
832
|
const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
|
|
760
833
|
const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
|
|
761
834
|
const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
|
|
762
|
-
|
|
835
|
+
// Also read token restriction status
|
|
836
|
+
let tokenRestriction: Record<string, unknown> = {};
|
|
837
|
+
try {
|
|
838
|
+
const enabled = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] }) as boolean;
|
|
839
|
+
const tokenList = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] }) as string[];
|
|
840
|
+
tokenRestriction = { tokenRestrictionEnabled: enabled, whitelistedTokens: tokenList, whitelistedTokenCount: tokenList.length };
|
|
841
|
+
summaryParts.push(enabled ? `Token whitelist ON (${tokenList.length} tokens)` : "Token whitelist OFF (any token allowed)");
|
|
842
|
+
} catch { /* token restriction not available on this version */ }
|
|
843
|
+
entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString(), ...tokenRestriction };
|
|
763
844
|
summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
|
|
764
845
|
} catch { /* policy read failed */ }
|
|
765
846
|
}
|
|
@@ -784,6 +865,42 @@ server.tool(
|
|
|
784
865
|
}
|
|
785
866
|
);
|
|
786
867
|
|
|
868
|
+
// ── Tool: token_restriction ─────────────────────────────
|
|
869
|
+
server.tool(
|
|
870
|
+
"token_restriction",
|
|
871
|
+
"Check token whitelist restriction status. Shows whether token trading is restricted and which tokens are whitelisted.",
|
|
872
|
+
{ token_id: z.string().describe("Agent NFA Token ID") },
|
|
873
|
+
async ({ token_id }) => {
|
|
874
|
+
const { publicClient, policyClient } = createClients();
|
|
875
|
+
const tokenId = BigInt(token_id);
|
|
876
|
+
const policies = await policyClient.getPolicies(tokenId);
|
|
877
|
+
const spendingPolicy = policies.find(p => p.policyTypeName === "spending_limit");
|
|
878
|
+
if (!spendingPolicy) {
|
|
879
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ tokenId: token_id, error: "No spending_limit policy found — token restriction is not available for this agent." }) }] };
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
const [enabled, tokenList] = await Promise.all([
|
|
883
|
+
publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] }) as Promise<boolean>,
|
|
884
|
+
publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] }) as Promise<string[]>,
|
|
885
|
+
]);
|
|
886
|
+
const result = {
|
|
887
|
+
tokenId: token_id,
|
|
888
|
+
tokenRestrictionEnabled: enabled,
|
|
889
|
+
status: enabled ? "ON — only whitelisted tokens can be traded" : "OFF — any token can be traded",
|
|
890
|
+
whitelistedTokens: tokenList,
|
|
891
|
+
whitelistedTokenCount: tokenList.length,
|
|
892
|
+
manageUrl: `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${token_id}/console/safety`,
|
|
893
|
+
note: enabled
|
|
894
|
+
? "To add/remove tokens or disable restriction, visit the management URL above (requires connected wallet as renter/owner)."
|
|
895
|
+
: "Token restriction is disabled. The agent can trade any token. To enable, visit the management URL.",
|
|
896
|
+
};
|
|
897
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }] };
|
|
898
|
+
} catch {
|
|
899
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ tokenId: token_id, error: "Failed to read token restriction — the SpendingLimitPolicy may not support this feature." }) }] };
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
);
|
|
903
|
+
|
|
787
904
|
// ── Tool: status ────────────────────────────────────────
|
|
788
905
|
server.tool(
|
|
789
906
|
"status",
|
|
@@ -911,7 +1028,7 @@ server.tool(
|
|
|
911
1028
|
|
|
912
1029
|
const { account, publicClient, policyClient, config } = createClients();
|
|
913
1030
|
const tokenId = BigInt(token_id);
|
|
914
|
-
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
1031
|
+
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
915
1032
|
const walletClient = createWalletClient({ account, chain: bsc, transport: http(config.rpc) });
|
|
916
1033
|
const policies = await policyClient.getPolicies(tokenId);
|
|
917
1034
|
const results: string[] = [];
|
|
@@ -1033,7 +1150,7 @@ server.tool(
|
|
|
1033
1150
|
async ({ token_id, target, data, value }) => {
|
|
1034
1151
|
const { policyClient } = createClients();
|
|
1035
1152
|
const tokenId = BigInt(token_id);
|
|
1036
|
-
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
1153
|
+
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
1037
1154
|
const action: Action = {
|
|
1038
1155
|
target: target as Address,
|
|
1039
1156
|
value: BigInt(value),
|
|
@@ -1082,7 +1199,7 @@ server.tool(
|
|
|
1082
1199
|
async ({ token_id, actions: rawActions }) => {
|
|
1083
1200
|
const { policyClient } = createClients();
|
|
1084
1201
|
const tokenId = BigInt(token_id);
|
|
1085
|
-
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
1202
|
+
const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
|
|
1086
1203
|
const actions: Action[] = rawActions.map(a => ({
|
|
1087
1204
|
target: a.target as Address,
|
|
1088
1205
|
value: BigInt(a.value || "0"),
|