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 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) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
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
- entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString() };
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) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
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
- entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString() };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shll-skills",
3
- "version": "5.2.1",
3
+ "version": "5.3.0",
4
4
  "description": "SHLL DeFi Agent — CLI + MCP Server for BSC",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
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) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
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
- entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString() };
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",