shll-skills 5.2.1 → 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 +105 -4
- package/dist/mcp.mjs +105 -4
- package/package.json +1 -1
- package/src/mcp.ts +112 -4
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 }) }] };
|
|
@@ -1155,7 +1216,15 @@ server.tool(
|
|
|
1155
1216
|
const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
|
|
1156
1217
|
const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
|
|
1157
1218
|
const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
|
|
1158
|
-
|
|
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 };
|
|
1159
1228
|
summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
|
|
1160
1229
|
} catch {
|
|
1161
1230
|
}
|
|
@@ -1179,6 +1248,38 @@ server.tool(
|
|
|
1179
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 }) }] };
|
|
1180
1249
|
}
|
|
1181
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
|
+
);
|
|
1182
1283
|
server.tool(
|
|
1183
1284
|
"status",
|
|
1184
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 }) }] };
|
|
@@ -666,7 +727,15 @@ server.tool(
|
|
|
666
727
|
const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
|
|
667
728
|
const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
|
|
668
729
|
const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
|
|
669
|
-
|
|
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 };
|
|
670
739
|
summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
|
|
671
740
|
} catch {
|
|
672
741
|
}
|
|
@@ -690,6 +759,38 @@ server.tool(
|
|
|
690
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 }) }] };
|
|
691
760
|
}
|
|
692
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
|
+
);
|
|
693
794
|
server.tool(
|
|
694
795
|
"status",
|
|
695
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
|
// ═══════════════════════════════════════════════════════
|
|
@@ -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
|
|
@@ -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
|
|
@@ -768,7 +832,15 @@ server.tool(
|
|
|
768
832
|
const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
|
|
769
833
|
const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
|
|
770
834
|
const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
|
|
771
|
-
|
|
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 };
|
|
772
844
|
summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
|
|
773
845
|
} catch { /* policy read failed */ }
|
|
774
846
|
}
|
|
@@ -793,6 +865,42 @@ server.tool(
|
|
|
793
865
|
}
|
|
794
866
|
);
|
|
795
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
|
+
|
|
796
904
|
// ── Tool: status ────────────────────────────────────────
|
|
797
905
|
server.tool(
|
|
798
906
|
"status",
|