@potswap_io/mcp 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +114 -3
- package/dist/api.d.ts +41 -0
- package/dist/api.js +91 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +24 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -0
- package/dist/limits.d.ts +13 -0
- package/dist/limits.js +23 -0
- package/dist/redact.d.ts +5 -0
- package/dist/redact.js +23 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +39 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +201 -0
- package/dist/shared/errors.d.ts +5 -0
- package/dist/shared/errors.js +24 -0
- package/dist/shared/fee.d.ts +24 -0
- package/dist/shared/fee.js +42 -0
- package/dist/shared/lib-types.d.ts +42 -0
- package/dist/shared/lib-types.js +1 -0
- package/dist/shared/risk.d.ts +15 -0
- package/dist/shared/risk.js +35 -0
- package/dist/shared/types.d.ts +50 -0
- package/dist/shared/types.js +1 -0
- package/dist/strategies/engine.d.ts +40 -0
- package/dist/strategies/engine.js +100 -0
- package/dist/strategies/lock.d.ts +7 -0
- package/dist/strategies/lock.js +30 -0
- package/dist/strategies/scheduler.d.ts +38 -0
- package/dist/strategies/scheduler.js +219 -0
- package/dist/strategies/store.d.ts +26 -0
- package/dist/strategies/store.js +53 -0
- package/dist/strategies/tools.d.ts +10 -0
- package/dist/strategies/tools.js +143 -0
- package/dist/strategies/types.d.ts +68 -0
- package/dist/strategies/types.js +2 -0
- package/dist/trade.d.ts +86 -0
- package/dist/trade.js +83 -0
- package/dist/wallet.d.ts +10 -0
- package/dist/wallet.js +48 -0
- package/package.json +58 -6
package/dist/trade.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Swap execution shared by the `swap` tool (P10) and the strategy scheduler (P11).
|
|
2
|
+
// One path: quote → limits → fresh order as taker → local signature → execute.
|
|
3
|
+
import { VersionedTransaction } from '@solana/web3.js';
|
|
4
|
+
import { NO_WALLET, SOL_MINT } from './config.js';
|
|
5
|
+
import { checkLimits } from './limits.js';
|
|
6
|
+
import { explorer, fromRaw, toRaw } from './format.js';
|
|
7
|
+
import { describeOrderError, friendlyError } from './shared/errors.js';
|
|
8
|
+
import { feeBpsFor } from './shared/fee.js';
|
|
9
|
+
export async function buildQuote(api, a) {
|
|
10
|
+
const [inTok, outTok, cfg, px] = await Promise.all([
|
|
11
|
+
api.token(a.inputMint), api.token(a.outputMint), api.config(), api.prices([a.inputMint]).catch(() => ({})),
|
|
12
|
+
]);
|
|
13
|
+
const inAmountRaw = toRaw(a.amount, inTok.decimals);
|
|
14
|
+
// PRD P8.12–P8.15: the fee tier depends on the USD value of the input at quote time.
|
|
15
|
+
const inputUsd = px[a.inputMint] ? a.amount * px[a.inputMint] : undefined;
|
|
16
|
+
const feeDecision = feeBpsFor(cfg.fee, inputUsd);
|
|
17
|
+
const o = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: inAmountRaw, slippageBps: a.slippageBps, feeBps: feeDecision.bps });
|
|
18
|
+
if (o.errorMessage && !o.outAmount)
|
|
19
|
+
throw new Error(`${describeOrderError(o)} No funds were debited.`);
|
|
20
|
+
const outAmount = fromRaw(o.outAmount, outTok.decimals);
|
|
21
|
+
return {
|
|
22
|
+
inputMint: a.inputMint, inputSymbol: inTok.symbol, outputMint: a.outputMint, outputSymbol: outTok.symbol,
|
|
23
|
+
inAmount: a.amount, inAmountRaw, outAmount, price: outAmount / a.amount,
|
|
24
|
+
priceImpactPct: o.priceImpactPct !== undefined ? Math.abs(Number(o.priceImpactPct)) : undefined,
|
|
25
|
+
slippageBps: o.slippageBps, feeBps: o.platformFee?.feeBps ?? o.feeBps ?? 0,
|
|
26
|
+
feeTier: cfg.referralAccount ? feeDecision.tier : null, feeLabel: cfg.referralAccount ? feeDecision.label : null,
|
|
27
|
+
scheduledFeeBps: feeDecision.bps, inputUsd, router: o.router, requestId: o.requestId, expiresAt: o.expireAt ?? null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function executeSwap(ctx, a) {
|
|
31
|
+
const { api, config, signer } = ctx;
|
|
32
|
+
if (!signer)
|
|
33
|
+
return { kind: 'no-wallet', message: NO_WALLET };
|
|
34
|
+
try {
|
|
35
|
+
const quote = await buildQuote(api, a);
|
|
36
|
+
const prices = await api.prices([SOL_MINT, a.inputMint]).catch(() => ({}));
|
|
37
|
+
let balanceUi;
|
|
38
|
+
try {
|
|
39
|
+
balanceUi = (await ctx.balances(signer.publicKey))[a.inputMint] ?? 0;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
balanceUi = undefined;
|
|
43
|
+
}
|
|
44
|
+
const inputUsd = prices[a.inputMint] ? a.amount * prices[a.inputMint] : undefined;
|
|
45
|
+
const solEquivalent = a.inputMint === SOL_MINT ? a.amount : inputUsd !== undefined && prices[SOL_MINT] ? inputUsd / prices[SOL_MINT] : undefined;
|
|
46
|
+
const refusal = checkLimits({
|
|
47
|
+
amountUi: a.amount, inputMint: a.inputMint, inputSymbol: quote.inputSymbol, isSol: a.inputMint === SOL_MINT,
|
|
48
|
+
inputUsd, solUsd: prices[SOL_MINT], slippageBps: a.slippageBps,
|
|
49
|
+
maxTradeSol: config.maxTradeSol, maxSlippageBps: config.maxSlippageBps, balanceUi,
|
|
50
|
+
});
|
|
51
|
+
if (refusal)
|
|
52
|
+
return { kind: 'refused', message: refusal, quote };
|
|
53
|
+
if (a.extraSolCap && solEquivalent !== undefined && solEquivalent > a.extraSolCap.remainingSol) {
|
|
54
|
+
return { kind: 'refused', message: `${a.extraSolCap.label} No funds were debited.`, quote };
|
|
55
|
+
}
|
|
56
|
+
if (!a.confirm)
|
|
57
|
+
return { kind: 'quote-only', quote };
|
|
58
|
+
if (config.dryRun)
|
|
59
|
+
return { kind: 'dry-run', quote };
|
|
60
|
+
const order = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: quote.inAmountRaw, taker: signer.publicKey, slippageBps: a.slippageBps, feeBps: quote.scheduledFeeBps });
|
|
61
|
+
if (!order.transaction)
|
|
62
|
+
return { kind: 'failed', message: `${describeOrderError(order)} No funds were debited.`, quote };
|
|
63
|
+
const tx = VersionedTransaction.deserialize(Buffer.from(order.transaction, 'base64'));
|
|
64
|
+
signer.sign(tx);
|
|
65
|
+
const res = await api.execute(Buffer.from(tx.serialize()).toString('base64'), order.requestId);
|
|
66
|
+
if (res.status !== 'Success') {
|
|
67
|
+
return { kind: 'failed', message: friendlyError(res.error || `Swap failed (code ${res.code})`), quote, signature: res.signature, explorerUrl: res.signature ? explorer(res.signature) : undefined };
|
|
68
|
+
}
|
|
69
|
+
const outDec = (await api.token(a.outputMint)).decimals;
|
|
70
|
+
const outAmount = fromRaw(res.totalOutputAmount ?? order.outAmount, outDec);
|
|
71
|
+
const feeBps = order.platformFee?.feeBps ?? order.feeBps ?? 0;
|
|
72
|
+
const feeUsd = inputUsd !== undefined ? inputUsd * feeBps / 10_000 : undefined;
|
|
73
|
+
const cfg = await api.config();
|
|
74
|
+
const result = {
|
|
75
|
+
signature: res.signature, explorerUrl: explorer(res.signature), inAmount: a.amount, inputSymbol: quote.inputSymbol,
|
|
76
|
+
outAmount, outputSymbol: quote.outputSymbol, router: order.router, feeBps, feeUsd, pointsAdded: cfg.referralAccount ? feeUsd : 0, solEquivalent,
|
|
77
|
+
};
|
|
78
|
+
return { kind: 'ok', result, quote };
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return { kind: 'failed', message: friendlyError(e instanceof Error ? e.message : String(e)) };
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/wallet.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type VersionedTransaction } from '@solana/web3.js';
|
|
2
|
+
export interface Signer {
|
|
3
|
+
publicKey: string;
|
|
4
|
+
sign(tx: VersionedTransaction): VersionedTransaction;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseSecret(raw: string): Uint8Array;
|
|
7
|
+
export declare function loadSigner(opts: {
|
|
8
|
+
privateKey?: string;
|
|
9
|
+
keypairPath?: string;
|
|
10
|
+
}): Signer | null;
|
package/dist/wallet.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// PRD P10.2/P10.9 — load the agent's keypair; the secret never leaves this module.
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { Keypair } from '@solana/web3.js';
|
|
4
|
+
import bs58 from 'bs58';
|
|
5
|
+
import { registerSecret } from './redact.js';
|
|
6
|
+
export function parseSecret(raw) {
|
|
7
|
+
const s = raw.trim();
|
|
8
|
+
if (s.startsWith('[')) {
|
|
9
|
+
const arr = JSON.parse(s);
|
|
10
|
+
if (!Array.isArray(arr) || arr.length !== 64 || !arr.every((n) => Number.isInteger(n) && n >= 0 && n < 256))
|
|
11
|
+
throw new Error('Keypair JSON must be an array of 64 bytes');
|
|
12
|
+
return Uint8Array.from(arr);
|
|
13
|
+
}
|
|
14
|
+
let bytes;
|
|
15
|
+
try {
|
|
16
|
+
bytes = bs58.decode(s);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error('Private key is not valid base58 or a JSON byte array');
|
|
20
|
+
}
|
|
21
|
+
if (bytes.length === 64)
|
|
22
|
+
return bytes;
|
|
23
|
+
if (bytes.length === 32)
|
|
24
|
+
return Keypair.fromSeed(bytes).secretKey;
|
|
25
|
+
throw new Error(`Private key has ${bytes.length} bytes; expected 64 (or a 32-byte seed)`);
|
|
26
|
+
}
|
|
27
|
+
export function loadSigner(opts) {
|
|
28
|
+
let raw = opts.privateKey;
|
|
29
|
+
if (!raw && opts.keypairPath) {
|
|
30
|
+
try {
|
|
31
|
+
raw = readFileSync(opts.keypairPath, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error(`Cannot read keypair file at SOLANA_KEYPAIR_PATH`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!raw)
|
|
38
|
+
return null;
|
|
39
|
+
const kp = Keypair.fromSecretKey(parseSecret(raw));
|
|
40
|
+
// Register every encoding the secret could leak in, not just the one supplied.
|
|
41
|
+
registerSecret(raw.trim());
|
|
42
|
+
registerSecret(bs58.encode(kp.secretKey));
|
|
43
|
+
registerSecret(JSON.stringify(Array.from(kp.secretKey)));
|
|
44
|
+
return {
|
|
45
|
+
publicKey: kp.publicKey.toBase58(),
|
|
46
|
+
sign(tx) { tx.sign([kp]); return tx; },
|
|
47
|
+
};
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@potswap_io/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@potswap_io/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Potswap MCP server: best-route Solana swaps, DCA / rebalance / take-profit strategies for AI agents, non-custodial, earns POT.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"potswap-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/server.js",
|
|
11
|
+
"types": "dist/server.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"dev": "tsx src/index.ts",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"sync-shared": "node scripts/sync-shared.mjs",
|
|
26
|
+
"prepublishOnly": "npm run sync-shared && npm run build"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/potswap-io/mcp.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://www.potswap.io",
|
|
33
|
+
"keywords": [
|
|
34
|
+
"mcp",
|
|
35
|
+
"solana",
|
|
36
|
+
"swap",
|
|
37
|
+
"jupiter",
|
|
38
|
+
"ai-agent",
|
|
39
|
+
"potswap",
|
|
40
|
+
"model-context-protocol"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
44
|
+
"@solana/web3.js": "^1.99.0",
|
|
45
|
+
"bs58": "^6.0.0",
|
|
46
|
+
"zod": "^4.6.7"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^26.6.5",
|
|
50
|
+
"tsx": "^4.23.15",
|
|
51
|
+
"typescript": "^7.0.2",
|
|
52
|
+
"vite": "^8.3.5",
|
|
53
|
+
"vitest": "^5.0.5"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/potswap-io/mcp/issues"
|
|
57
|
+
}
|
|
58
|
+
}
|