intent-swap-mcp 0.1.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.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Price feed — CoinGecko free API, no key required.
3
+ */
4
+
5
+ const COINGECKO_IDS: Record<string, string> = {
6
+ ETH: "ethereum",
7
+ WETH: "weth",
8
+ WBTC: "wrapped-bitcoin",
9
+ BTC: "bitcoin",
10
+ USDC: "usd-coin",
11
+ USDT: "tether",
12
+ DAI: "dai",
13
+ ARB: "arbitrum",
14
+ UNI: "uniswap",
15
+ LINK: "chainlink",
16
+ MATIC: "matic-network",
17
+ OP: "optimism",
18
+ PEPE: "pepe",
19
+ SHIB: "shiba-inu",
20
+ AAVE: "aave",
21
+ CRV: "curve-dao-token",
22
+ };
23
+
24
+ // Simple in-memory cache (60s TTL)
25
+ const priceCache: Map<string, { price: number; ts: number }> = new Map();
26
+ const CACHE_TTL = 60_000;
27
+
28
+ export async function getTokenPrice(symbol: string): Promise<number | null> {
29
+ const id = COINGECKO_IDS[symbol.toUpperCase()];
30
+ if (!id) return null;
31
+
32
+ const cached = priceCache.get(symbol);
33
+ if (cached && Date.now() - cached.ts < CACHE_TTL) return cached.price;
34
+
35
+ try {
36
+ const res = await fetch(
37
+ `https://api.coingecko.com/api/v3/simple/price?ids=${id}&vs_currencies=usd`,
38
+ { signal: AbortSignal.timeout(5000) }
39
+ );
40
+ const data = (await res.json()) as Record<string, { usd: number }>;
41
+ const price = data[id]?.usd ?? null;
42
+ if (price !== null) priceCache.set(symbol, { price, ts: Date.now() });
43
+ return price;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export async function getSupportedTokens() {
50
+ const symbols = Object.keys(COINGECKO_IDS);
51
+ const ids = Object.values(COINGECKO_IDS).join(",");
52
+
53
+ let prices: Record<string, number> = {};
54
+ try {
55
+ const res = await fetch(
56
+ `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`,
57
+ { signal: AbortSignal.timeout(5000) }
58
+ );
59
+ const data = (await res.json()) as Record<string, { usd: number }>;
60
+ for (const [sym, id] of Object.entries(COINGECKO_IDS)) {
61
+ if (data[id]?.usd) prices[sym] = data[id].usd;
62
+ }
63
+ } catch {
64
+ // return without prices
65
+ }
66
+
67
+ return symbols.map((sym) => ({
68
+ symbol: sym,
69
+ coingeckoId: COINGECKO_IDS[sym],
70
+ priceUsd: prices[sym] ?? null,
71
+ }));
72
+ }
package/src/uniswap.ts ADDED
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Uniswap V3 quote + tx builder.
3
+ * Uses public RPC — no API key required.
4
+ *
5
+ * Quote: calls QuoterV2 on-chain
6
+ * Build: generates SwapRouter02 calldata via viem
7
+ */
8
+
9
+ import { createPublicClient, http, parseUnits, formatUnits, encodeFunctionData, type Address } from "viem";
10
+ import { mainnet, arbitrum } from "viem/chains";
11
+
12
+ // ─── Chain config ──────────────────────────────────────────────────────────
13
+
14
+ const CHAINS = {
15
+ mainnet: {
16
+ chain: mainnet,
17
+ rpc: "https://ethereum.publicnode.com",
18
+ quoterV2: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e" as Address,
19
+ swapRouter: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45" as Address,
20
+ },
21
+ arbitrum: {
22
+ chain: arbitrum,
23
+ rpc: "https://arbitrum-one.publicnode.com",
24
+ quoterV2: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e" as Address,
25
+ swapRouter: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45" as Address,
26
+ },
27
+ };
28
+
29
+ // ─── Token registry ────────────────────────────────────────────────────────
30
+
31
+ interface TokenInfo {
32
+ address: Address;
33
+ decimals: number;
34
+ isNative?: boolean;
35
+ }
36
+
37
+ const TOKENS_MAINNET: Record<string, TokenInfo> = {
38
+ ETH: { address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18, isNative: true },
39
+ WETH: { address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", decimals: 18 },
40
+ USDC: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", decimals: 6 },
41
+ USDT: { address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", decimals: 6 },
42
+ DAI: { address: "0x6B175474E89094C44Da98b954EedeAC495271d0F", decimals: 18 },
43
+ WBTC: { address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", decimals: 8 },
44
+ UNI: { address: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", decimals: 18 },
45
+ LINK: { address: "0x514910771AF9Ca656af840dff83E8264EcF986CA", decimals: 18 },
46
+ ARB: { address: "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1", decimals: 18 },
47
+ AAVE: { address: "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", decimals: 18 },
48
+ CRV: { address: "0xD533a949740bb3306d119CC777fa900bA034cd52", decimals: 18 },
49
+ };
50
+
51
+ // ─── ABIs (minimal) ────────────────────────────────────────────────────────
52
+
53
+ const QUOTER_V2_ABI = [
54
+ {
55
+ name: "quoteExactInputSingle",
56
+ type: "function",
57
+ stateMutability: "nonpayable",
58
+ inputs: [
59
+ {
60
+ name: "params",
61
+ type: "tuple",
62
+ components: [
63
+ { name: "tokenIn", type: "address" },
64
+ { name: "tokenOut", type: "address" },
65
+ { name: "amountIn", type: "uint256" },
66
+ { name: "fee", type: "uint24" },
67
+ { name: "sqrtPriceLimitX96", type: "uint160" },
68
+ ],
69
+ },
70
+ ],
71
+ outputs: [
72
+ { name: "amountOut", type: "uint256" },
73
+ { name: "sqrtPriceX96After", type: "uint160" },
74
+ { name: "initializedTicksCrossed", type: "uint32" },
75
+ { name: "gasEstimate", type: "uint256" },
76
+ ],
77
+ },
78
+ ] as const;
79
+
80
+ const SWAP_ROUTER_ABI = [
81
+ {
82
+ name: "exactInputSingle",
83
+ type: "function",
84
+ stateMutability: "payable",
85
+ inputs: [
86
+ {
87
+ name: "params",
88
+ type: "tuple",
89
+ components: [
90
+ { name: "tokenIn", type: "address" },
91
+ { name: "tokenOut", type: "address" },
92
+ { name: "fee", type: "uint24" },
93
+ { name: "recipient", type: "address" },
94
+ { name: "amountIn", type: "uint256" },
95
+ { name: "amountOutMinimum", type: "uint256" },
96
+ { name: "sqrtPriceLimitX96", type: "uint160" },
97
+ ],
98
+ },
99
+ ],
100
+ outputs: [{ name: "amountOut", type: "uint256" }],
101
+ },
102
+ ] as const;
103
+
104
+ // ─── Fee tiers to try ──────────────────────────────────────────────────────
105
+
106
+ const FEE_TIERS = [500, 3000, 10000] as const; // 0.05%, 0.3%, 1%
107
+
108
+ // ─── Helpers ───────────────────────────────────────────────────────────────
109
+
110
+ function getTokenInfo(symbol: string): TokenInfo {
111
+ const info = TOKENS_MAINNET[symbol.toUpperCase()];
112
+ if (!info) throw new Error(`Unsupported token: ${symbol}`);
113
+ return info;
114
+ }
115
+
116
+ function slippageBpsToMultiplier(bps: number): bigint {
117
+ // amountOutMinimum = amountOut * (10000 - bps) / 10000
118
+ return BigInt(10000 - bps);
119
+ }
120
+
121
+ // ─── Quote ─────────────────────────────────────────────────────────────────
122
+
123
+ export interface SwapQuote {
124
+ fromToken: string;
125
+ toToken: string;
126
+ amountIn: number;
127
+ amountOut: number;
128
+ feeTier: number;
129
+ priceImpactPct: number | null;
130
+ gasEstimate: number;
131
+ rateFormatted: string;
132
+ }
133
+
134
+ export async function getSwapQuote(params: {
135
+ fromToken: string;
136
+ toToken: string;
137
+ amount: number;
138
+ slippageBps?: number;
139
+ }): Promise<SwapQuote> {
140
+ const { fromToken, toToken, amount } = params;
141
+ const tokenIn = getTokenInfo(fromToken);
142
+ const tokenOut = getTokenInfo(toToken);
143
+
144
+ const client = createPublicClient({
145
+ chain: CHAINS.mainnet.chain,
146
+ transport: http(CHAINS.mainnet.rpc),
147
+ });
148
+
149
+ const amountIn = parseUnits(amount.toString(), tokenIn.decimals);
150
+
151
+ // Try fee tiers, pick best output
152
+ let bestOut: bigint | null = null;
153
+ let bestFee = 3000;
154
+ let bestGas = 0;
155
+
156
+ for (const fee of FEE_TIERS) {
157
+ try {
158
+ const result = await client.readContract({
159
+ address: CHAINS.mainnet.quoterV2,
160
+ abi: QUOTER_V2_ABI,
161
+ functionName: "quoteExactInputSingle",
162
+ args: [
163
+ {
164
+ tokenIn: tokenIn.address,
165
+ tokenOut: tokenOut.address,
166
+ amountIn,
167
+ fee,
168
+ sqrtPriceLimitX96: 0n,
169
+ },
170
+ ],
171
+ });
172
+ const [amountOut, , , gasEstimate] = result as [bigint, bigint, number, bigint];
173
+ if (bestOut === null || amountOut > bestOut) {
174
+ bestOut = amountOut;
175
+ bestFee = fee;
176
+ bestGas = Number(gasEstimate);
177
+ }
178
+ } catch {
179
+ // fee tier not available, try next
180
+ }
181
+ }
182
+
183
+ if (bestOut === null) {
184
+ throw new Error(`No liquidity found for ${fromToken}→${toToken}`);
185
+ }
186
+
187
+ const amountOut = Number(formatUnits(bestOut, tokenOut.decimals));
188
+ const rate = amountOut / amount;
189
+
190
+ return {
191
+ fromToken: fromToken.toUpperCase(),
192
+ toToken: toToken.toUpperCase(),
193
+ amountIn: amount,
194
+ amountOut,
195
+ feeTier: bestFee,
196
+ priceImpactPct: null, // would need pool state to calculate precisely
197
+ gasEstimate: bestGas,
198
+ rateFormatted: `1 ${fromToken.toUpperCase()} = ${rate.toFixed(6)} ${toToken.toUpperCase()}`,
199
+ };
200
+ }
201
+
202
+ // ─── Build TX ──────────────────────────────────────────────────────────────
203
+
204
+ export interface SwapTx {
205
+ to: Address;
206
+ data: string;
207
+ value: string; // hex, ETH value in wei
208
+ fromToken: string;
209
+ toToken: string;
210
+ amountIn: number;
211
+ estimatedAmountOut: number;
212
+ feeTier: number;
213
+ slippageBps: number;
214
+ summary: string;
215
+ warning: string;
216
+ }
217
+
218
+ export async function buildSwapTx(params: {
219
+ fromToken: string;
220
+ toToken: string;
221
+ amount: number;
222
+ recipientAddress: string;
223
+ slippageBps?: number;
224
+ }): Promise<SwapTx> {
225
+ const { fromToken, toToken, amount, recipientAddress, slippageBps = 50 } = params;
226
+
227
+ // Get quote first
228
+ const quote = await getSwapQuote({ fromToken, toToken, amount, slippageBps });
229
+
230
+ const tokenIn = getTokenInfo(fromToken);
231
+ const tokenOut = getTokenInfo(toToken);
232
+
233
+ const amountIn = parseUnits(amount.toString(), tokenIn.decimals);
234
+ const amountOutRaw = parseUnits(quote.amountOut.toString(), tokenOut.decimals);
235
+ const amountOutMinimum = (amountOutRaw * slippageBpsToMultiplier(slippageBps)) / 10000n;
236
+
237
+ const calldata = encodeFunctionData({
238
+ abi: SWAP_ROUTER_ABI,
239
+ functionName: "exactInputSingle",
240
+ args: [
241
+ {
242
+ tokenIn: tokenIn.address,
243
+ tokenOut: tokenOut.address,
244
+ fee: quote.feeTier,
245
+ recipient: recipientAddress as Address,
246
+ amountIn,
247
+ amountOutMinimum,
248
+ sqrtPriceLimitX96: 0n,
249
+ },
250
+ ],
251
+ });
252
+
253
+ const isNativeIn = tokenIn.isNative === true;
254
+
255
+ return {
256
+ to: CHAINS.mainnet.swapRouter,
257
+ data: calldata,
258
+ value: isNativeIn ? `0x${amountIn.toString(16)}` : "0x0",
259
+ fromToken: fromToken.toUpperCase(),
260
+ toToken: toToken.toUpperCase(),
261
+ amountIn: amount,
262
+ estimatedAmountOut: quote.amountOut,
263
+ feeTier: quote.feeTier,
264
+ slippageBps,
265
+ summary: `Swap ${amount} ${fromToken.toUpperCase()} → ~${quote.amountOut.toFixed(4)} ${toToken.toUpperCase()} (slippage: ${slippageBps / 100}%)`,
266
+ warning:
267
+ "This transaction must be signed and broadcast by the user's wallet. " +
268
+ "If fromToken is an ERC-20 (not ETH), the user must first approve the SwapRouter to spend their tokens.",
269
+ };
270
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }