shll-skills 5.3.4 → 5.4.1
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/SKILL.md +1 -1
- package/dist/index.js +57 -0
- package/dist/index.mjs +57 -0
- package/dist/mcp.js +39 -5
- package/dist/mcp.mjs +39 -5
- package/package.json +2 -2
- package/src/index.ts +60 -1
- package/src/mcp.ts +38 -5
package/SKILL.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -797,6 +797,57 @@ function createClient(options) {
|
|
|
797
797
|
chainId: 56
|
|
798
798
|
});
|
|
799
799
|
}
|
|
800
|
+
var AGENT_NFA_ACCESS_ABI = [
|
|
801
|
+
{ name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
802
|
+
{ name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
803
|
+
{ name: "operatorOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
804
|
+
{ name: "userOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
805
|
+
{ name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] }
|
|
806
|
+
];
|
|
807
|
+
async function checkAccess(opts, tokenId) {
|
|
808
|
+
const rpcUrl = opts.rpc || DEFAULT_RPC;
|
|
809
|
+
const nfa = opts.nfaAddress || DEFAULT_NFA;
|
|
810
|
+
const pk = toHex(process.env.RUNNER_PRIVATE_KEY || "");
|
|
811
|
+
const account = (0, import_accounts2.privateKeyToAccount)(pk);
|
|
812
|
+
const pc = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(rpcUrl) });
|
|
813
|
+
const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
|
|
814
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
|
|
815
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userExpires", args: [tokenId] }),
|
|
816
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorOf", args: [tokenId] }),
|
|
817
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userOf", args: [tokenId] }),
|
|
818
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "ownerOf", args: [tokenId] })
|
|
819
|
+
]);
|
|
820
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
821
|
+
if (now > userExpires) {
|
|
822
|
+
output({ status: "error", message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me` });
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
if (now > operatorExpires) {
|
|
826
|
+
output({ status: "error", message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1e3).toISOString()}). Please re-authorize via setup_guide.` });
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
const runnerAddr = account.address.toLowerCase();
|
|
830
|
+
const isOperator = operator.toLowerCase() === runnerAddr;
|
|
831
|
+
const isRenter = renter.toLowerCase() === runnerAddr;
|
|
832
|
+
const isOwner = owner.toLowerCase() === runnerAddr;
|
|
833
|
+
if (!isOperator && !isRenter && !isOwner) {
|
|
834
|
+
output({
|
|
835
|
+
status: "error",
|
|
836
|
+
message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}.`,
|
|
837
|
+
yourWallet: account.address,
|
|
838
|
+
onChainOperator: operator,
|
|
839
|
+
onChainRenter: renter,
|
|
840
|
+
onChainOwner: owner,
|
|
841
|
+
howToFix: [
|
|
842
|
+
`1. Use 'setup_guide' command to generate an OperatorPermit for this wallet`,
|
|
843
|
+
`2. Renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry>) on AgentNFA`,
|
|
844
|
+
`3. Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety to set operator`,
|
|
845
|
+
`4. Use the correct RUNNER_PRIVATE_KEY for operator ${operator}`
|
|
846
|
+
]
|
|
847
|
+
});
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
800
851
|
var program = new import_commander.Command();
|
|
801
852
|
program.name("shll-onchain-runner").description("Execute DeFi actions securely via SHLL AgentNFA");
|
|
802
853
|
var swapCmd = new import_commander.Command("swap").description("Swap tokens on PancakeSwap (auto-routes V2/V3)").requiredOption("-f, --from <token>", "Input token (symbol or 0x address, e.g. USDC, BNB)").requiredOption("-t, --to <token>", "Output token (symbol or 0x address)").requiredOption("-a, --amount <number>", "Amount to swap (human-readable, e.g. 0.5)").option("-s, --slippage <percent>", "Slippage tolerance in percent (default: 5)", "5").option("--dex <mode>", "DEX routing: auto, v2, v3 (default: auto)", "auto").option("--fee <tier>", "V3 fee tier in bps (default: 2500 = 0.25%)", "2500").option("--router <address>", "DEX router address (override)");
|
|
@@ -805,6 +856,7 @@ swapCmd.action(async (opts) => {
|
|
|
805
856
|
try {
|
|
806
857
|
const client = createClient(opts);
|
|
807
858
|
const tokenId = BigInt(opts.tokenId);
|
|
859
|
+
await checkAccess(opts, tokenId);
|
|
808
860
|
const rpcUrl = opts.rpc || DEFAULT_RPC;
|
|
809
861
|
const fromToken = resolveToken(opts.from);
|
|
810
862
|
const toToken = resolveToken(opts.to);
|
|
@@ -1361,6 +1413,7 @@ wrapCmd.action(async (opts) => {
|
|
|
1361
1413
|
output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
|
|
1362
1414
|
process.exit(1);
|
|
1363
1415
|
}
|
|
1416
|
+
await checkAccess(opts, BigInt(opts.tokenId));
|
|
1364
1417
|
const client = new PolicyClient({
|
|
1365
1418
|
operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
|
|
1366
1419
|
rpcUrl: opts.rpc || DEFAULT_RPC,
|
|
@@ -1396,6 +1449,7 @@ unwrapCmd.action(async (opts) => {
|
|
|
1396
1449
|
output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
|
|
1397
1450
|
process.exit(1);
|
|
1398
1451
|
}
|
|
1452
|
+
await checkAccess(opts, BigInt(opts.tokenId));
|
|
1399
1453
|
const client = new PolicyClient({
|
|
1400
1454
|
operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
|
|
1401
1455
|
rpcUrl: opts.rpc || DEFAULT_RPC,
|
|
@@ -1432,6 +1486,7 @@ transferCmd.action(async (opts) => {
|
|
|
1432
1486
|
output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
|
|
1433
1487
|
process.exit(1);
|
|
1434
1488
|
}
|
|
1489
|
+
await checkAccess(opts, BigInt(opts.tokenId));
|
|
1435
1490
|
const client = new PolicyClient({
|
|
1436
1491
|
operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
|
|
1437
1492
|
rpcUrl: opts.rpc || DEFAULT_RPC,
|
|
@@ -2003,6 +2058,7 @@ lendCmd.action(async (opts) => {
|
|
|
2003
2058
|
try {
|
|
2004
2059
|
const client = createClient(opts);
|
|
2005
2060
|
const tokenId = BigInt(opts.tokenId);
|
|
2061
|
+
await checkAccess(opts, tokenId);
|
|
2006
2062
|
const rpcUrl = opts.rpc || DEFAULT_RPC;
|
|
2007
2063
|
const publicClient = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(rpcUrl) });
|
|
2008
2064
|
const symbol = opts.token.toUpperCase();
|
|
@@ -2070,6 +2126,7 @@ redeemCmd.action(async (opts) => {
|
|
|
2070
2126
|
try {
|
|
2071
2127
|
const client = createClient(opts);
|
|
2072
2128
|
const tokenId = BigInt(opts.tokenId);
|
|
2129
|
+
await checkAccess(opts, tokenId);
|
|
2073
2130
|
const symbol = opts.token.toUpperCase();
|
|
2074
2131
|
const vTokenAddr = VENUS_VTOKENS[symbol];
|
|
2075
2132
|
if (!vTokenAddr) {
|
package/dist/index.mjs
CHANGED
|
@@ -309,6 +309,57 @@ function createClient(options) {
|
|
|
309
309
|
chainId: 56
|
|
310
310
|
});
|
|
311
311
|
}
|
|
312
|
+
var AGENT_NFA_ACCESS_ABI = [
|
|
313
|
+
{ name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
314
|
+
{ name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
315
|
+
{ name: "operatorOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
316
|
+
{ name: "userOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
317
|
+
{ name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] }
|
|
318
|
+
];
|
|
319
|
+
async function checkAccess(opts, tokenId) {
|
|
320
|
+
const rpcUrl = opts.rpc || DEFAULT_RPC;
|
|
321
|
+
const nfa = opts.nfaAddress || DEFAULT_NFA;
|
|
322
|
+
const pk = toHex(process.env.RUNNER_PRIVATE_KEY || "");
|
|
323
|
+
const account = privateKeyToAccount(pk);
|
|
324
|
+
const pc = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
|
|
325
|
+
const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
|
|
326
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
|
|
327
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userExpires", args: [tokenId] }),
|
|
328
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorOf", args: [tokenId] }),
|
|
329
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userOf", args: [tokenId] }),
|
|
330
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "ownerOf", args: [tokenId] })
|
|
331
|
+
]);
|
|
332
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
333
|
+
if (now > userExpires) {
|
|
334
|
+
output({ status: "error", message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me` });
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
if (now > operatorExpires) {
|
|
338
|
+
output({ status: "error", message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1e3).toISOString()}). Please re-authorize via setup_guide.` });
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
const runnerAddr = account.address.toLowerCase();
|
|
342
|
+
const isOperator = operator.toLowerCase() === runnerAddr;
|
|
343
|
+
const isRenter = renter.toLowerCase() === runnerAddr;
|
|
344
|
+
const isOwner = owner.toLowerCase() === runnerAddr;
|
|
345
|
+
if (!isOperator && !isRenter && !isOwner) {
|
|
346
|
+
output({
|
|
347
|
+
status: "error",
|
|
348
|
+
message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}.`,
|
|
349
|
+
yourWallet: account.address,
|
|
350
|
+
onChainOperator: operator,
|
|
351
|
+
onChainRenter: renter,
|
|
352
|
+
onChainOwner: owner,
|
|
353
|
+
howToFix: [
|
|
354
|
+
`1. Use 'setup_guide' command to generate an OperatorPermit for this wallet`,
|
|
355
|
+
`2. Renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry>) on AgentNFA`,
|
|
356
|
+
`3. Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety to set operator`,
|
|
357
|
+
`4. Use the correct RUNNER_PRIVATE_KEY for operator ${operator}`
|
|
358
|
+
]
|
|
359
|
+
});
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
312
363
|
var program = new Command();
|
|
313
364
|
program.name("shll-onchain-runner").description("Execute DeFi actions securely via SHLL AgentNFA");
|
|
314
365
|
var swapCmd = new Command("swap").description("Swap tokens on PancakeSwap (auto-routes V2/V3)").requiredOption("-f, --from <token>", "Input token (symbol or 0x address, e.g. USDC, BNB)").requiredOption("-t, --to <token>", "Output token (symbol or 0x address)").requiredOption("-a, --amount <number>", "Amount to swap (human-readable, e.g. 0.5)").option("-s, --slippage <percent>", "Slippage tolerance in percent (default: 5)", "5").option("--dex <mode>", "DEX routing: auto, v2, v3 (default: auto)", "auto").option("--fee <tier>", "V3 fee tier in bps (default: 2500 = 0.25%)", "2500").option("--router <address>", "DEX router address (override)");
|
|
@@ -317,6 +368,7 @@ swapCmd.action(async (opts) => {
|
|
|
317
368
|
try {
|
|
318
369
|
const client = createClient(opts);
|
|
319
370
|
const tokenId = BigInt(opts.tokenId);
|
|
371
|
+
await checkAccess(opts, tokenId);
|
|
320
372
|
const rpcUrl = opts.rpc || DEFAULT_RPC;
|
|
321
373
|
const fromToken = resolveToken(opts.from);
|
|
322
374
|
const toToken = resolveToken(opts.to);
|
|
@@ -873,6 +925,7 @@ wrapCmd.action(async (opts) => {
|
|
|
873
925
|
output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
|
|
874
926
|
process.exit(1);
|
|
875
927
|
}
|
|
928
|
+
await checkAccess(opts, BigInt(opts.tokenId));
|
|
876
929
|
const client = new PolicyClient({
|
|
877
930
|
operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
|
|
878
931
|
rpcUrl: opts.rpc || DEFAULT_RPC,
|
|
@@ -908,6 +961,7 @@ unwrapCmd.action(async (opts) => {
|
|
|
908
961
|
output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
|
|
909
962
|
process.exit(1);
|
|
910
963
|
}
|
|
964
|
+
await checkAccess(opts, BigInt(opts.tokenId));
|
|
911
965
|
const client = new PolicyClient({
|
|
912
966
|
operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
|
|
913
967
|
rpcUrl: opts.rpc || DEFAULT_RPC,
|
|
@@ -944,6 +998,7 @@ transferCmd.action(async (opts) => {
|
|
|
944
998
|
output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
|
|
945
999
|
process.exit(1);
|
|
946
1000
|
}
|
|
1001
|
+
await checkAccess(opts, BigInt(opts.tokenId));
|
|
947
1002
|
const client = new PolicyClient({
|
|
948
1003
|
operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
|
|
949
1004
|
rpcUrl: opts.rpc || DEFAULT_RPC,
|
|
@@ -1515,6 +1570,7 @@ lendCmd.action(async (opts) => {
|
|
|
1515
1570
|
try {
|
|
1516
1571
|
const client = createClient(opts);
|
|
1517
1572
|
const tokenId = BigInt(opts.tokenId);
|
|
1573
|
+
await checkAccess(opts, tokenId);
|
|
1518
1574
|
const rpcUrl = opts.rpc || DEFAULT_RPC;
|
|
1519
1575
|
const publicClient = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
|
|
1520
1576
|
const symbol = opts.token.toUpperCase();
|
|
@@ -1582,6 +1638,7 @@ redeemCmd.action(async (opts) => {
|
|
|
1582
1638
|
try {
|
|
1583
1639
|
const client = createClient(opts);
|
|
1584
1640
|
const tokenId = BigInt(opts.tokenId);
|
|
1641
|
+
await checkAccess(opts, tokenId);
|
|
1585
1642
|
const symbol = opts.token.toUpperCase();
|
|
1586
1643
|
const vTokenAddr = VENUS_VTOKENS[symbol];
|
|
1587
1644
|
if (!vTokenAddr) {
|
package/dist/mcp.js
CHANGED
|
@@ -646,16 +646,23 @@ function createClients() {
|
|
|
646
646
|
});
|
|
647
647
|
return { account, publicClient, policyClient, config };
|
|
648
648
|
}
|
|
649
|
-
var
|
|
649
|
+
var AGENT_NFA_CHECK_ABI = [
|
|
650
650
|
{ name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
651
|
-
{ name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] }
|
|
651
|
+
{ name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
652
|
+
{ name: "operatorOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
653
|
+
{ name: "userOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
654
|
+
{ name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] }
|
|
652
655
|
];
|
|
653
656
|
async function checkAgentExpiry(tokenId) {
|
|
654
657
|
const config = getConfig();
|
|
658
|
+
const account = (0, import_accounts2.privateKeyToAccount)(config.privateKey);
|
|
655
659
|
const pc = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(config.rpc) });
|
|
656
|
-
const [operatorExpires, userExpires] = await Promise.all([
|
|
657
|
-
pc.readContract({ address: config.nfa, abi:
|
|
658
|
-
pc.readContract({ address: config.nfa, abi:
|
|
660
|
+
const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
|
|
661
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
|
|
662
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "userExpires", args: [tokenId] }),
|
|
663
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorOf", args: [tokenId] }),
|
|
664
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "userOf", args: [tokenId] }),
|
|
665
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "ownerOf", args: [tokenId] })
|
|
659
666
|
]);
|
|
660
667
|
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
661
668
|
if (now > operatorExpires) {
|
|
@@ -686,6 +693,33 @@ async function checkAgentExpiry(tokenId) {
|
|
|
686
693
|
}]
|
|
687
694
|
};
|
|
688
695
|
}
|
|
696
|
+
const runnerAddr = account.address.toLowerCase();
|
|
697
|
+
const isOperator = operator.toLowerCase() === runnerAddr;
|
|
698
|
+
const isRenter = renter.toLowerCase() === runnerAddr;
|
|
699
|
+
const isOwner = owner.toLowerCase() === runnerAddr;
|
|
700
|
+
if (!isOperator && !isRenter && !isOwner) {
|
|
701
|
+
return {
|
|
702
|
+
expired: true,
|
|
703
|
+
// reuse expired flag to block execution
|
|
704
|
+
content: [{
|
|
705
|
+
type: "text",
|
|
706
|
+
text: JSON.stringify({
|
|
707
|
+
status: "error",
|
|
708
|
+
message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}. Your wallet must be the operator, renter, or owner to execute transactions.`,
|
|
709
|
+
yourWallet: account.address,
|
|
710
|
+
onChainOperator: operator,
|
|
711
|
+
onChainRenter: renter,
|
|
712
|
+
onChainOwner: owner,
|
|
713
|
+
howToFix: [
|
|
714
|
+
`Option 1: Use the 'setup_guide' tool \u2014 it generates an EIP-712 OperatorPermit that lets the renter (${renter}) authorize your current wallet (${account.address}) as operator. The renter signs the permit in their browser wallet, then the runner submits it on-chain.`,
|
|
715
|
+
`Option 2: The renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry_timestamp>) on AgentNFA contract at ${config.nfa} to directly authorize this wallet.`,
|
|
716
|
+
`Option 3: Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety and set ${account.address} as the operator.`,
|
|
717
|
+
`Option 4: If you have access to the correct operator wallet (${operator}), set RUNNER_PRIVATE_KEY to that wallet's private key instead.`
|
|
718
|
+
]
|
|
719
|
+
})
|
|
720
|
+
}]
|
|
721
|
+
};
|
|
722
|
+
}
|
|
689
723
|
return { expired: false };
|
|
690
724
|
}
|
|
691
725
|
function policyRejectionHelp(reason, tokenId) {
|
package/dist/mcp.mjs
CHANGED
|
@@ -157,16 +157,23 @@ function createClients() {
|
|
|
157
157
|
});
|
|
158
158
|
return { account, publicClient, policyClient, config };
|
|
159
159
|
}
|
|
160
|
-
var
|
|
160
|
+
var AGENT_NFA_CHECK_ABI = [
|
|
161
161
|
{ name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
162
|
-
{ name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] }
|
|
162
|
+
{ name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
163
|
+
{ name: "operatorOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
164
|
+
{ name: "userOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
165
|
+
{ name: "ownerOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] }
|
|
163
166
|
];
|
|
164
167
|
async function checkAgentExpiry(tokenId) {
|
|
165
168
|
const config = getConfig();
|
|
169
|
+
const account = privateKeyToAccount(config.privateKey);
|
|
166
170
|
const pc = createPublicClient({ chain: bsc, transport: http(config.rpc) });
|
|
167
|
-
const [operatorExpires, userExpires] = await Promise.all([
|
|
168
|
-
pc.readContract({ address: config.nfa, abi:
|
|
169
|
-
pc.readContract({ address: config.nfa, abi:
|
|
171
|
+
const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
|
|
172
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
|
|
173
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "userExpires", args: [tokenId] }),
|
|
174
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorOf", args: [tokenId] }),
|
|
175
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "userOf", args: [tokenId] }),
|
|
176
|
+
pc.readContract({ address: config.nfa, abi: AGENT_NFA_CHECK_ABI, functionName: "ownerOf", args: [tokenId] })
|
|
170
177
|
]);
|
|
171
178
|
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
172
179
|
if (now > operatorExpires) {
|
|
@@ -197,6 +204,33 @@ async function checkAgentExpiry(tokenId) {
|
|
|
197
204
|
}]
|
|
198
205
|
};
|
|
199
206
|
}
|
|
207
|
+
const runnerAddr = account.address.toLowerCase();
|
|
208
|
+
const isOperator = operator.toLowerCase() === runnerAddr;
|
|
209
|
+
const isRenter = renter.toLowerCase() === runnerAddr;
|
|
210
|
+
const isOwner = owner.toLowerCase() === runnerAddr;
|
|
211
|
+
if (!isOperator && !isRenter && !isOwner) {
|
|
212
|
+
return {
|
|
213
|
+
expired: true,
|
|
214
|
+
// reuse expired flag to block execution
|
|
215
|
+
content: [{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: JSON.stringify({
|
|
218
|
+
status: "error",
|
|
219
|
+
message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}. Your wallet must be the operator, renter, or owner to execute transactions.`,
|
|
220
|
+
yourWallet: account.address,
|
|
221
|
+
onChainOperator: operator,
|
|
222
|
+
onChainRenter: renter,
|
|
223
|
+
onChainOwner: owner,
|
|
224
|
+
howToFix: [
|
|
225
|
+
`Option 1: Use the 'setup_guide' tool \u2014 it generates an EIP-712 OperatorPermit that lets the renter (${renter}) authorize your current wallet (${account.address}) as operator. The renter signs the permit in their browser wallet, then the runner submits it on-chain.`,
|
|
226
|
+
`Option 2: The renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry_timestamp>) on AgentNFA contract at ${config.nfa} to directly authorize this wallet.`,
|
|
227
|
+
`Option 3: Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety and set ${account.address} as the operator.`,
|
|
228
|
+
`Option 4: If you have access to the correct operator wallet (${operator}), set RUNNER_PRIVATE_KEY to that wallet's private key instead.`
|
|
229
|
+
]
|
|
230
|
+
})
|
|
231
|
+
}]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
200
234
|
return { expired: false };
|
|
201
235
|
}
|
|
202
236
|
function policyRejectionHelp(reason, tokenId) {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -325,8 +325,61 @@ function createClient(options: Record<string, string>): PolicyClient {
|
|
|
325
325
|
chainId: 56,
|
|
326
326
|
});
|
|
327
327
|
}
|
|
328
|
-
|
|
329
328
|
// ── Program ─────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
// Pre-check: prevents write operations on expired/unauthorized agents with clear error
|
|
331
|
+
const AGENT_NFA_ACCESS_ABI = [
|
|
332
|
+
{ name: "operatorExpiresOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
333
|
+
{ name: "userExpires", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
334
|
+
{ name: "operatorOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
335
|
+
{ name: "userOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
336
|
+
{ name: "ownerOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
337
|
+
] as const;
|
|
338
|
+
|
|
339
|
+
async function checkAccess(opts: Record<string, string>, tokenId: bigint) {
|
|
340
|
+
const rpcUrl = opts.rpc || DEFAULT_RPC;
|
|
341
|
+
const nfa = (opts.nfaAddress || DEFAULT_NFA) as Address;
|
|
342
|
+
const pk = toHex(process.env.RUNNER_PRIVATE_KEY || "");
|
|
343
|
+
const account = privateKeyToAccount(pk);
|
|
344
|
+
const pc = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
|
|
345
|
+
const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
|
|
346
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorExpiresOf", args: [tokenId] }) as Promise<bigint>,
|
|
347
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userExpires", args: [tokenId] }) as Promise<bigint>,
|
|
348
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "operatorOf", args: [tokenId] }) as Promise<Address>,
|
|
349
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "userOf", args: [tokenId] }) as Promise<Address>,
|
|
350
|
+
pc.readContract({ address: nfa, abi: AGENT_NFA_ACCESS_ABI, functionName: "ownerOf", args: [tokenId] }) as Promise<Address>,
|
|
351
|
+
]);
|
|
352
|
+
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
353
|
+
if (now > userExpires) {
|
|
354
|
+
output({ status: "error", message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1000).toISOString()}). Please renew at https://shll.run/me` });
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
if (now > operatorExpires) {
|
|
358
|
+
output({ status: "error", message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1000).toISOString()}). Please re-authorize via setup_guide.` });
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
const runnerAddr = account.address.toLowerCase();
|
|
362
|
+
const isOperator = operator.toLowerCase() === runnerAddr;
|
|
363
|
+
const isRenter = renter.toLowerCase() === runnerAddr;
|
|
364
|
+
const isOwner = owner.toLowerCase() === runnerAddr;
|
|
365
|
+
if (!isOperator && !isRenter && !isOwner) {
|
|
366
|
+
output({
|
|
367
|
+
status: "error",
|
|
368
|
+
message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}.`,
|
|
369
|
+
yourWallet: account.address,
|
|
370
|
+
onChainOperator: operator,
|
|
371
|
+
onChainRenter: renter,
|
|
372
|
+
onChainOwner: owner,
|
|
373
|
+
howToFix: [
|
|
374
|
+
`1. Use 'setup_guide' command to generate an OperatorPermit for this wallet`,
|
|
375
|
+
`2. Renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry>) on AgentNFA`,
|
|
376
|
+
`3. Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety to set operator`,
|
|
377
|
+
`4. Use the correct RUNNER_PRIVATE_KEY for operator ${operator}`,
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
330
383
|
const program = new Command();
|
|
331
384
|
program.name("shll-onchain-runner").description("Execute DeFi actions securely via SHLL AgentNFA");
|
|
332
385
|
|
|
@@ -346,6 +399,7 @@ swapCmd.action(async (opts) => {
|
|
|
346
399
|
try {
|
|
347
400
|
const client = createClient(opts);
|
|
348
401
|
const tokenId = BigInt(opts.tokenId);
|
|
402
|
+
await checkAccess(opts, tokenId);
|
|
349
403
|
const rpcUrl = opts.rpc || DEFAULT_RPC;
|
|
350
404
|
|
|
351
405
|
const fromToken = resolveToken(opts.from);
|
|
@@ -1067,6 +1121,7 @@ wrapCmd.action(async (opts) => {
|
|
|
1067
1121
|
output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
|
|
1068
1122
|
process.exit(1);
|
|
1069
1123
|
}
|
|
1124
|
+
await checkAccess(opts, BigInt(opts.tokenId));
|
|
1070
1125
|
const client = new PolicyClient({
|
|
1071
1126
|
operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
|
|
1072
1127
|
rpcUrl: opts.rpc || DEFAULT_RPC,
|
|
@@ -1117,6 +1172,7 @@ unwrapCmd.action(async (opts) => {
|
|
|
1117
1172
|
output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
|
|
1118
1173
|
process.exit(1);
|
|
1119
1174
|
}
|
|
1175
|
+
await checkAccess(opts, BigInt(opts.tokenId));
|
|
1120
1176
|
const client = new PolicyClient({
|
|
1121
1177
|
operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
|
|
1122
1178
|
rpcUrl: opts.rpc || DEFAULT_RPC,
|
|
@@ -1170,6 +1226,7 @@ transferCmd.action(async (opts) => {
|
|
|
1170
1226
|
output({ status: "error", message: "RUNNER_PRIVATE_KEY environment variable is missing" });
|
|
1171
1227
|
process.exit(1);
|
|
1172
1228
|
}
|
|
1229
|
+
await checkAccess(opts, BigInt(opts.tokenId));
|
|
1173
1230
|
const client = new PolicyClient({
|
|
1174
1231
|
operatorPrivateKey: toHex(process.env.RUNNER_PRIVATE_KEY),
|
|
1175
1232
|
rpcUrl: opts.rpc || DEFAULT_RPC,
|
|
@@ -1909,6 +1966,7 @@ lendCmd.action(async (opts) => {
|
|
|
1909
1966
|
try {
|
|
1910
1967
|
const client = createClient(opts);
|
|
1911
1968
|
const tokenId = BigInt(opts.tokenId);
|
|
1969
|
+
await checkAccess(opts, tokenId);
|
|
1912
1970
|
const rpcUrl = opts.rpc || DEFAULT_RPC;
|
|
1913
1971
|
const publicClient = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
|
|
1914
1972
|
|
|
@@ -1997,6 +2055,7 @@ redeemCmd.action(async (opts) => {
|
|
|
1997
2055
|
try {
|
|
1998
2056
|
const client = createClient(opts);
|
|
1999
2057
|
const tokenId = BigInt(opts.tokenId);
|
|
2058
|
+
await checkAccess(opts, tokenId);
|
|
2000
2059
|
|
|
2001
2060
|
const symbol = opts.token.toUpperCase();
|
|
2002
2061
|
const vTokenAddr = VENUS_VTOKENS[symbol];
|
package/src/mcp.ts
CHANGED
|
@@ -196,18 +196,25 @@ function createClients() {
|
|
|
196
196
|
return { account, publicClient, policyClient, config };
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
//
|
|
200
|
-
const
|
|
199
|
+
// Pre-check: prevents write operations on expired/unauthorized agents with clear errors
|
|
200
|
+
const AGENT_NFA_CHECK_ABI = [
|
|
201
201
|
{ name: "operatorExpiresOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
202
202
|
{ name: "userExpires", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
|
|
203
|
+
{ name: "operatorOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
204
|
+
{ name: "userOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
205
|
+
{ name: "ownerOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }] },
|
|
203
206
|
] as const;
|
|
204
207
|
|
|
205
208
|
async function checkAgentExpiry(tokenId: bigint) {
|
|
206
209
|
const config = getConfig();
|
|
210
|
+
const account = privateKeyToAccount(config.privateKey as Hex);
|
|
207
211
|
const pc = createPublicClient({ chain: bsc, transport: http(config.rpc) });
|
|
208
|
-
const [operatorExpires, userExpires] = await Promise.all([
|
|
209
|
-
pc.readContract({ address: config.nfa as Address, abi:
|
|
210
|
-
pc.readContract({ address: config.nfa as Address, abi:
|
|
212
|
+
const [operatorExpires, userExpires, operator, renter, owner] = await Promise.all([
|
|
213
|
+
pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorExpiresOf", args: [tokenId] }) as Promise<bigint>,
|
|
214
|
+
pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "userExpires", args: [tokenId] }) as Promise<bigint>,
|
|
215
|
+
pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "operatorOf", args: [tokenId] }) as Promise<Address>,
|
|
216
|
+
pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "userOf", args: [tokenId] }) as Promise<Address>,
|
|
217
|
+
pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_CHECK_ABI, functionName: "ownerOf", args: [tokenId] }) as Promise<Address>,
|
|
211
218
|
]);
|
|
212
219
|
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
213
220
|
if (now > operatorExpires) {
|
|
@@ -236,6 +243,32 @@ async function checkAgentExpiry(tokenId: bigint) {
|
|
|
236
243
|
}],
|
|
237
244
|
};
|
|
238
245
|
}
|
|
246
|
+
// Operator identity check: verify RUNNER_PRIVATE_KEY wallet can execute
|
|
247
|
+
const runnerAddr = account.address.toLowerCase();
|
|
248
|
+
const isOperator = operator.toLowerCase() === runnerAddr;
|
|
249
|
+
const isRenter = renter.toLowerCase() === runnerAddr;
|
|
250
|
+
const isOwner = owner.toLowerCase() === runnerAddr;
|
|
251
|
+
if (!isOperator && !isRenter && !isOwner) {
|
|
252
|
+
return {
|
|
253
|
+
expired: true, // reuse expired flag to block execution
|
|
254
|
+
content: [{
|
|
255
|
+
type: "text" as const, text: JSON.stringify({
|
|
256
|
+
status: "error",
|
|
257
|
+
message: `RUNNER_PRIVATE_KEY wallet (${account.address}) is NOT authorized for token-id ${tokenId}. On-chain operator is ${operator}. Your wallet must be the operator, renter, or owner to execute transactions.`,
|
|
258
|
+
yourWallet: account.address,
|
|
259
|
+
onChainOperator: operator,
|
|
260
|
+
onChainRenter: renter,
|
|
261
|
+
onChainOwner: owner,
|
|
262
|
+
howToFix: [
|
|
263
|
+
`Option 1: Use the 'setup_guide' tool — it generates an EIP-712 OperatorPermit that lets the renter (${renter}) authorize your current wallet (${account.address}) as operator. The renter signs the permit in their browser wallet, then the runner submits it on-chain.`,
|
|
264
|
+
`Option 2: The renter (${renter}) can call setOperator(${tokenId}, ${account.address}, <expiry_timestamp>) on AgentNFA contract at ${config.nfa} to directly authorize this wallet.`,
|
|
265
|
+
`Option 3: Go to https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety and set ${account.address} as the operator.`,
|
|
266
|
+
`Option 4: If you have access to the correct operator wallet (${operator}), set RUNNER_PRIVATE_KEY to that wallet's private key instead.`,
|
|
267
|
+
],
|
|
268
|
+
})
|
|
269
|
+
}],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
239
272
|
return { expired: false };
|
|
240
273
|
}
|
|
241
274
|
|