Skip to content

Commit 51307ef

Browse files
authored
Feat: add tool to call contract function and track staked tokens (#25)
1 parent ab4e2ae commit 51307ef

File tree

8 files changed

+296
-574
lines changed

8 files changed

+296
-574
lines changed

package-lock.json

Lines changed: 16 additions & 571 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { privateKeyToAccount } from "viem/accounts";
99
import dotenv from "dotenv";
1010
import { privateKeySchema } from "./tools/hyper-evm/sendFunds/schemas.js";
1111
import * as hyper from "@nktkas/hyperliquid";
12-
1312
dotenv.config();
1413

1514
export const hyperEvmConfig = defineChain({

src/main.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@ import {
55
ListToolsRequestSchema,
66
type CallToolRequest,
77
} from "@modelcontextprotocol/sdk/types.js";
8+
89
import {
910
GET_BALANCE_TOOL,
1011
GET_LATEST_BLOCK_TOOL,
1112
DEPLOY_CONTRACTS_TOOL,
1213
SEND_FUNDS_TOOL,
1314
GET_TRANSACTION_RECEIPT_TOOL,
1415
GET_TOKEN_BALANCE_TOOL,
16+
GET_LOGS_TOOL,
17+
CALL_CONTRACT_FUNCTION,
1518
STAKE_TOOL,
1619
UNSTAKE_TOOL,
17-
GET_LOGS_TOOL,
1820
GET_HISTORICAL_ORDERS_TOOL,
21+
TRACK_STAKED_TOKENS,
1922
} from "./tools/tools.js";
2023
import { getBalance } from "./tools/hyper-evm/getBalance/index.js";
2124
import { getLatestBlock } from "./tools/hyper-evm/getBlockNumber/index.js";
2225
import { deployContracts } from "./tools/hyper-evm/deployContracts/index.js";
2326
import type { DeployContractsInput } from "./tools/hyper-evm/deployContracts/schemas.js";
27+
import { callContracts } from "./tools/hyper-evm/callContracts/index.js";
28+
import { CallContractSchema } from "./tools/hyper-evm/callContracts/schema.js";
2429
import { sendFunds } from "./tools/hyper-evm/sendFunds/index.js";
2530
import { sendFundsInputSchema } from "./tools/hyper-evm/sendFunds/schemas.js";
2631
import { getTransactionReceipt } from "./tools/hyper-evm/getTransactionReceipt/index.js";
@@ -37,9 +42,12 @@ import {
3742
} from "./tools/hyper-evm/handleStake/schemas.js";
3843
import { getLogs } from "./tools/hyper-evm/getLogs/index.js";
3944
import { getHistoricalOrders } from "./tools/hypercore/getHistoricalOrders/index.js";
45+
import { getStakedtokens } from "./tools/hypercore/trackstakedtokens/index.js";
46+
import { StakedInputSchema } from "./tools/hypercore/trackstakedtokens/schema.js";
4047

4148
async function main() {
4249
console.error("Starting Hyperliquid MCP server...");
50+
4351
const server = new Server(
4452
{
4553
name: "hyperliquid",
@@ -68,6 +76,54 @@ async function main() {
6876
return balance;
6977
}
7078

79+
case "call_contract_function": {
80+
try {
81+
const { contractAddress, functionName, abi, functionArgs } =
82+
args as {
83+
contractAddress: string;
84+
functionName: string;
85+
abi: any;
86+
functionArgs?: any[];
87+
};
88+
89+
const validatedInput = CallContractSchema.parse({
90+
contractAddress,
91+
functionName,
92+
abi,
93+
functionArgs,
94+
});
95+
96+
const result = await callContracts(validatedInput);
97+
98+
return {
99+
content: [
100+
{
101+
type: "text",
102+
text: JSON.stringify(
103+
result,
104+
(_, v) => (typeof v === "bigint" ? v.toString() : v),
105+
2
106+
),
107+
},
108+
],
109+
};
110+
} catch (validationError) {
111+
console.error("Validation error:", validationError);
112+
return {
113+
content: [
114+
{
115+
type: "text",
116+
text: `Error: ${
117+
validationError instanceof Error
118+
? validationError.message
119+
: String(validationError)
120+
}`,
121+
},
122+
],
123+
};
124+
}
125+
}
126+
71127
case "deploy_contracts": {
72128
const input = args as DeployContractsInput;
73129
const result = await deployContracts(input);
@@ -151,9 +207,19 @@ async function main() {
151207
return result;
152208
}
153209

210+
case "track_staked_tokens": {
211+
const input = args as {
212+
userAddress: string;
213+
isTestnet: boolean | string;
214+
};
215+
const validatedInput = StakedInputSchema.parse(input);
216+
const result = await getStakedtokens(validatedInput);
217+
return result;
218+
}
219+
154220
default: {
155221
throw new Error(
156-
`Tool '${name}' not found. Available tools: get_latest_block, get_balance, deploy_contracts, send_funds, get_transaction_receipt, get_token_balance, stake, unstake`
222+
`Tool '${name}' not found. Available tools: get_latest_block, get_balance, deploy_contracts, send_funds, get_transaction_receipt, get_token_balance, stake, unstake, get_logs, call_contract_function, track_staked_tokens`
157223
);
158224
}
159225
}
@@ -181,10 +247,12 @@ async function main() {
181247
SEND_FUNDS_TOOL,
182248
GET_TRANSACTION_RECEIPT_TOOL,
183249
GET_TOKEN_BALANCE_TOOL,
250+
CALL_CONTRACT_FUNCTION,
184251
STAKE_TOOL,
185252
UNSTAKE_TOOL,
186253
GET_LOGS_TOOL,
187254
GET_HISTORICAL_ORDERS_TOOL,
255+
TRACK_STAKED_TOKENS,
188256
],
189257
};
190258
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
encodeFunctionData,
3+
type Abi,
4+
type AbiFunction,
5+
isAddress,
6+
} from "viem";
7+
import { createWalletClient, http } from "viem";
8+
import { publicClient, walletClient } from "../../../config.js";
9+
import { hyperEvmConfig } from "../../../config.js";
10+
import type { GetContractDetails } from "./schema.js";
11+
import { privateKeySchema } from "../sendFunds/schemas.js";
12+
import { privateKeyToAccount } from "viem/accounts";
13+
14+
export async function callContracts(contractDetails: GetContractDetails) {
15+
try {
16+
const address = contractDetails.contractAddress;
17+
if (!isAddress(address)) {
18+
throw new Error(`Invalid HyperEVM address: ${address}`);
19+
}
20+
21+
let abi: Abi;
22+
if (typeof contractDetails.abi === "string") {
23+
try {
24+
abi = JSON.parse(contractDetails.abi) as Abi;
25+
} catch (error) {
26+
throw new Error(`Invalid ABI string: ${error}`);
27+
}
28+
} else {
29+
abi = contractDetails.abi as Abi;
30+
}
31+
32+
const functionAbi = abi.find(
33+
(item): item is AbiFunction =>
34+
"type" in item &&
35+
item.type === "function" &&
36+
"name" in item &&
37+
item.name === contractDetails.functionName
38+
);
39+
40+
if (!functionAbi) {
41+
throw new Error(
42+
`Function ${contractDetails.functionName} not found in ABI`
43+
);
44+
}
45+
46+
if (
47+
functionAbi.stateMutability === "view" ||
48+
functionAbi.stateMutability === "pure"
49+
) {
50+
const callParams: any = {
51+
address: contractDetails.contractAddress,
52+
abi,
53+
functionName: contractDetails.functionName,
54+
};
55+
56+
if (functionAbi.inputs && functionAbi.inputs.length > 0) {
57+
if (
58+
!contractDetails.functionArgs ||
59+
contractDetails.functionArgs.length !== functionAbi.inputs.length
60+
) {
61+
throw Error(
62+
`Function ${contractDetails.functionName} expects ${functionAbi.inputs.length} arguments, ` +
63+
`${contractDetails.functionArgs?.length || 0} provided`
64+
);
65+
}
66+
callParams.args = contractDetails.functionArgs;
67+
}
68+
69+
return await publicClient.readContract(callParams);
70+
}
71+
72+
const callParams: any = {
73+
address: contractDetails.contractAddress,
74+
abi,
75+
functionName: contractDetails.functionName,
76+
};
77+
78+
if (functionAbi.inputs && functionAbi.inputs.length > 0) {
79+
if (
80+
!contractDetails.functionArgs ||
81+
contractDetails.functionArgs.length !== functionAbi.inputs.length
82+
) {
83+
throw Error(
84+
`Function ${contractDetails.functionName} expects ${functionAbi.inputs.length} arguments, ` +
85+
`${contractDetails.functionArgs?.length || 0} provided`
86+
);
87+
}
88+
callParams.args = contractDetails.functionArgs;
89+
}
90+
91+
return await walletClient.writeContract(callParams);
92+
} catch (error) {
93+
console.log(error);
94+
throw Error(
95+
`Failed to call contract function: ${error instanceof Error ? error.message : String(error)}`
96+
);
97+
}
98+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { z } from "zod";
2+
import { isAddress } from "viem";
3+
4+
export const CallContractSchema = z.object({
5+
contractAddress: z.string().refine(address => isAddress(address), {
6+
message: "Must be a valid HyperEVM address (0x format, 42 characters)",
7+
}),
8+
functionName: z.string().min(1, "Function name cannot be empty"),
9+
abi: z.union([z.string(), z.array(z.any())]),
10+
functionArgs: z.preprocess(val => {
11+
if (typeof val === "string") {
12+
try {
13+
return JSON.parse(val);
14+
} catch {
15+
return [val];
16+
}
17+
}
18+
return val;
19+
}, z.array(z.any()).optional()),
20+
});
21+
22+
export type GetContractDetails = z.infer<typeof CallContractSchema>;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Hyperliquid } from "hyperliquid";
2+
import type { GetStakedInput } from "./schema.js";
3+
4+
export async function getStakedtokens(stackingInputDetails: GetStakedInput) {
5+
try {
6+
const address = stackingInputDetails.userAddress;
7+
const isTestnet = stackingInputDetails.isTestnet;
8+
9+
const sdk = new Hyperliquid({ testnet: isTestnet });
10+
11+
const delegatorrwards = await sdk.info.getDelegatorRewards(address);
12+
const delegatorsummary = await sdk.info.getDelegatorSummary(address);
13+
14+
const safeStringify = (obj: unknown) =>
15+
JSON.stringify(
16+
obj,
17+
(_, v) => (typeof v === "bigint" ? v.toString() : v),
18+
2
19+
);
20+
21+
return {
22+
content: [
23+
{
24+
type: "text",
25+
text: `Delegator rewards for ${address}:\n${safeStringify(delegatorrwards)}\n\nDelegator summary:\n${safeStringify(delegatorsummary)}`,
26+
},
27+
],
28+
};
29+
} catch (error) {
30+
throw new Error(
31+
`Failed to track staked token status: ${error instanceof Error ? error.message : String(error)}`
32+
);
33+
}
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { z } from "zod";
2+
import { isAddress } from "viem";
3+
4+
export const StakedInputSchema = z.object({
5+
userAddress: z
6+
.string()
7+
.refine(address => isAddress(address), {
8+
message: "Must be a valid Ethereum address (0x format)",
9+
})
10+
.describe(
11+
"The address of the validator to stake to (must be a valid Ethereum address starting with 0x)."
12+
),
13+
isTestnet: z
14+
.union([
15+
z.boolean(),
16+
z.string().transform(val => {
17+
if (val === "true" || val === "1") {
18+
return true;
19+
}
20+
if (val === "false" || val === "0") {
21+
return false;
22+
}
23+
throw new Error(
24+
"isTestnet must be a boolean or string representation of boolean"
25+
);
26+
}),
27+
])
28+
.describe(
29+
"Set to true if staking on testnet, false for mainnet. Accepts boolean or string 'true'/'false'."
30+
),
31+
});
32+
33+
export type GetStakedInput = z.infer<typeof StakedInputSchema>;

src/tools/tools.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
22
import { getBalanceInputSchema } from "./hyper-evm/getBalance/schemas.js";
3+
import { CallContractSchema } from "./hyper-evm/callContracts/schema.js";
34
import { deployContractsSchema } from "./hyper-evm/deployContracts/schemas.js";
45
import { sendFundsInputSchema } from "./hyper-evm/sendFunds/schemas.js";
56
import { getTransactionReceiptInputSchema } from "./hyper-evm/getTransactionReceipt/schemas.js";
@@ -10,6 +11,7 @@ import {
1011
} from "./hyper-evm/handleStake/schemas.js";
1112
import { getLogsInputSchema } from "./hyper-evm/getLogs/schemas.js";
1213
import { getOrdersInputSchema } from "./hypercore/getHistoricalOrders/schemas.js";
14+
import { StakedInputSchema } from "./hypercore/trackstakedtokens/schema.js";
1315

1416
export const GET_BALANCE_TOOL: Tool = {
1517
name: "get_balance",
@@ -31,6 +33,16 @@ export const GET_LATEST_BLOCK_TOOL: Tool = {
3133
},
3234
};
3335

36+
export const CALL_CONTRACT_FUNCTION: Tool = {
37+
name: "call_contract_function",
38+
description: "Call a contract function on HyperEVM",
39+
inputSchema: {
40+
type: "object",
41+
properties: CallContractSchema.shape,
42+
required: ["contractAddress", "functionName", "abi"],
43+
},
44+
};
45+
3446
export const DEPLOY_CONTRACTS_TOOL: Tool = {
3547
name: "deploy_contracts",
3648
description: "Deploy a contract",
@@ -112,3 +124,14 @@ export const GET_HISTORICAL_ORDERS_TOOL: Tool = {
112124
required: ["userAddress"],
113125
},
114126
};
127+
128+
export const TRACK_STAKED_TOKENS: Tool = {
129+
name: "track_staked_tokens",
130+
description:
131+
"Get the rewards and summary of a user address present on hyperliquid",
132+
inputSchema: {
133+
type: "object",
134+
properties: StakedInputSchema.shape,
135+
required: ["userAddress", "isTestnet"],
136+
},
137+
};

0 commit comments

Comments
 (0)