diff --git a/src/index.ts b/src/index.ts index dead092..0a52a5b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs"; import { FastMCP } from "fastmcp"; import * as tools from "./tools/index.js"; +import { log } from "./util/log.js"; const packageJson = JSON.parse( readFileSync(new URL("../package.json", import.meta.url), "utf8"), @@ -42,12 +43,10 @@ async function main() { server.addTool(tools.redeemPositionsTool); server.addTool(tools.getPositionsTool); - process.stderr.write( - "Trading features enabled (POLYMARKET_PRIVATE_KEY is configured)\n", - ); + log("Trading features enabled (POLYMARKET_PRIVATE_KEY is configured)"); } else { - process.stderr.write( - "Read-only mode: Set POLYMARKET_PRIVATE_KEY environment variable to enable trading features\n", + log( + "Read-only mode: Set POLYMARKET_PRIVATE_KEY environment variable to enable trading features", ); } diff --git a/src/services/approvals.ts b/src/services/approvals.ts index dc53270..5bf6aaa 100644 --- a/src/services/approvals.ts +++ b/src/services/approvals.ts @@ -6,6 +6,7 @@ import { utils, Wallet, } from "ethers"; +import { log } from "../util/log.js"; import { getConfig, POLYGON_ADDRESSES } from "./config.js"; export type ApprovalCheck = { @@ -226,7 +227,7 @@ export class PolymarketApprovals { if (current.missing.includes(key)) { const hash = await this.sendTx(fn, nonce++, waitConfs); txHashes.push(hash); - process.stderr.write(`Approved ${label}: ${hash}\n`); + log(`Approved ${label}: ${hash}`); } } diff --git a/src/services/redemption.ts b/src/services/redemption.ts index 86c2671..608a545 100644 --- a/src/services/redemption.ts +++ b/src/services/redemption.ts @@ -4,6 +4,7 @@ */ import { type BigNumber, Contract, providers, Wallet } from "ethers"; +import { log } from "../util/log.js"; import { getConfig, POLYGON_ADDRESSES } from "./config.js"; // Parent collection ID for Polymarket (constant) @@ -174,7 +175,7 @@ export class PolymarketRedemption { "No CTF tokens to redeem. Balance is 0 - position may have already been redeemed.", }; } - process.stderr.write(`Token balance: ${tokenBalance.toString()}\n`); + log(`Token balance: ${tokenBalance.toString()}`); } // Check if market is resolved @@ -218,9 +219,9 @@ export class PolymarketRedemption { const amounts: [bigint, bigint] = outcomeIndex === 0 ? [tokenBalance, 0n] : [0n, tokenBalance]; - process.stderr.write(`Redeeming negRisk position:` + "\n"); - process.stderr.write(` Condition ID: ${conditionIdBytes32}\n`); - process.stderr.write(` Amounts: [${amounts[0]}, ${amounts[1]}]\n`); + log(`Redeeming negRisk position:`); + log(` Condition ID: ${conditionIdBytes32}`); + log(` Amounts: [${amounts[0]}, ${amounts[1]}]`); const negRiskAdapter = this.getNegRiskAdapterContract(); tx = await negRiskAdapter.redeemPositions(conditionIdBytes32, amounts, { @@ -238,11 +239,9 @@ export class PolymarketRedemption { }; } - process.stderr.write(`Redeeming CTF position:` + "\n"); - process.stderr.write(` Condition ID: ${conditionIdBytes32}\n`); - process.stderr.write( - ` Winning index sets: [${winningIndexSets.join(", ")}]\n`, - ); + log(`Redeeming CTF position:`); + log(` Condition ID: ${conditionIdBytes32}`); + log(` Winning index sets: [${winningIndexSets.join(", ")}]`); const ctf = this.getCtfContract(); tx = await ctf.redeemPositions( @@ -256,7 +255,7 @@ export class PolymarketRedemption { ); } - process.stderr.write(`Transaction submitted: ${tx.hash}\n`); + log(`Transaction submitted: ${tx.hash}`); // Wait for confirmation const receipt = await tx.wait(1); diff --git a/src/services/trading.ts b/src/services/trading.ts index afee56c..e51d636 100644 --- a/src/services/trading.ts +++ b/src/services/trading.ts @@ -8,6 +8,7 @@ import type { } from "@polymarket/clob-client"; import { ClobClient, OrderType, Side } from "@polymarket/clob-client"; import { providers, Wallet } from "ethers"; +import { log } from "../util/log.js"; import { PolymarketApprovals } from "./approvals.js"; import { getConfig } from "./config.js"; @@ -112,11 +113,11 @@ export class PolymarketTrading { cfg.funderAddress, ); - process.stderr.write("Polymarket trading client initialized\n"); - process.stderr.write(` - Signer: ${await ethersSigner.getAddress()}\n`); - process.stderr.write(` - Signature Type: ${cfg.signatureType}\n`); + log("Polymarket trading client initialized"); + log(` - Signer: ${await ethersSigner.getAddress()}`); + log(` - Signature Type: ${cfg.signatureType}`); if (cfg.funderAddress) { - process.stderr.write(` - Funder/Proxy: ${cfg.funderAddress}\n`); + log(` - Funder/Proxy: ${cfg.funderAddress}`); } } @@ -253,12 +254,12 @@ export class PolymarketTrading { const client = this.getClient(); - process.stderr.write(`Placing ${args.side} order:\n`); - process.stderr.write(` Token: ${args.tokenId}\n`); - process.stderr.write(` Price: ${args.price}\n`); - process.stderr.write(` Size: ${args.size}\n`); - process.stderr.write( - ` Market: negRisk=${marketParams.negRisk}, tickSize=${marketParams.tickSize}\n`, + log(`Placing ${args.side} order:`); + log(` Token: ${args.tokenId}`); + log(` Price: ${args.price}`); + log(` Size: ${args.size}`); + log( + ` Market: negRisk=${marketParams.negRisk}, tickSize=${marketParams.tickSize}`, ); return client.createAndPostOrder( @@ -304,11 +305,11 @@ export class PolymarketTrading { const client = this.getClient(); - process.stderr.write(`Placing ${args.side} market order:\n`); - process.stderr.write(` Token: ${args.tokenId}\n`); - process.stderr.write(` Amount: ${args.amount}\n`); - process.stderr.write( - ` Market: negRisk=${marketParams.negRisk}, tickSize=${marketParams.tickSize}\n`, + log(`Placing ${args.side} market order:`); + log(` Token: ${args.tokenId}`); + log(` Amount: ${args.amount}`); + log( + ` Market: negRisk=${marketParams.negRisk}, tickSize=${marketParams.tickSize}`, ); return client.createAndPostMarketOrder( diff --git a/src/tools/cancel-all-orders.ts b/src/tools/cancel-all-orders.ts index 2b89836..7c68f0c 100644 --- a/src/tools/cancel-all-orders.ts +++ b/src/tools/cancel-all-orders.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { ApprovalRequiredError } from "../services/approvals.js"; import { tradeApi } from "../services/trading.js"; +import { withApprovalGuard } from "../util/with-approval-guard.js"; const cancelAllOrdersSchema = z.object({}); @@ -8,15 +8,6 @@ export const cancelAllOrdersTool = { name: "cancel_all_orders", description: "Cancel all open orders for the authenticated account.", parameters: cancelAllOrdersSchema, - execute: async (_args: z.infer) => { - try { - const result = await tradeApi.cancelAllOrders(); - return JSON.stringify(result, null, 2); - } catch (err) { - if (err instanceof ApprovalRequiredError) { - return JSON.stringify(err, null, 2); - } - throw err; - } - }, + execute: async (_args: z.infer) => + withApprovalGuard(() => tradeApi.cancelAllOrders()), }; diff --git a/src/tools/cancel-order.ts b/src/tools/cancel-order.ts index df154fc..5024c1b 100644 --- a/src/tools/cancel-order.ts +++ b/src/tools/cancel-order.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { ApprovalRequiredError } from "../services/approvals.js"; import { tradeApi } from "../services/trading.js"; +import { withApprovalGuard } from "../util/with-approval-guard.js"; const cancelOrderSchema = z.object({ orderId: z.string().describe("The unique identifier of the order to cancel"), @@ -10,15 +10,6 @@ export const cancelOrderTool = { name: "cancel_order", description: "Cancel a specific order by its ID.", parameters: cancelOrderSchema, - execute: async (args: z.infer) => { - try { - const result = await tradeApi.cancelOrder(args.orderId); - return JSON.stringify(result, null, 2); - } catch (err) { - if (err instanceof ApprovalRequiredError) { - return JSON.stringify(err, null, 2); - } - throw err; - } - }, + execute: async (args: z.infer) => + withApprovalGuard(() => tradeApi.cancelOrder(args.orderId)), }; diff --git a/src/tools/get-balance-allowance.ts b/src/tools/get-balance-allowance.ts index e4c0b2f..7ffa816 100644 --- a/src/tools/get-balance-allowance.ts +++ b/src/tools/get-balance-allowance.ts @@ -1,7 +1,7 @@ import { AssetType } from "@polymarket/clob-client"; import { z } from "zod"; -import { ApprovalRequiredError } from "../services/approvals.js"; import { tradeApi } from "../services/trading.js"; +import { withApprovalGuard } from "../util/with-approval-guard.js"; const getBalanceAllowanceSchema = z.object({ assetType: z @@ -23,15 +23,6 @@ export const getBalanceAllowanceTool = { asset_type: AssetType[args.assetType], }; if (args.tokenID) params.token_id = args.tokenID; - - try { - const result = await tradeApi.getBalanceAllowance(params); - return JSON.stringify(result, null, 2); - } catch (err) { - if (err instanceof ApprovalRequiredError) { - return JSON.stringify(err, null, 2); - } - throw err; - } + return withApprovalGuard(() => tradeApi.getBalanceAllowance(params)); }, }; diff --git a/src/tools/place-market-order.ts b/src/tools/place-market-order.ts index f251402..3a1ce46 100644 --- a/src/tools/place-market-order.ts +++ b/src/tools/place-market-order.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { ApprovalRequiredError } from "../services/approvals.js"; import { tradeApi } from "../services/trading.js"; +import { withApprovalGuard } from "../util/with-approval-guard.js"; const placeMarketOrderSchema = z.object({ tokenId: z.string().describe("The token ID of the market outcome to trade"), @@ -24,20 +24,13 @@ export const placeMarketOrderTool = { description: "Place a market order that executes immediately at current market price. IMPORTANT: For BUY orders, amount is the dollar amount ($USD) you want to spend. For SELL orders, amount is the number of shares to sell. Example: amount=5, side=BUY means 'spend $5 to buy shares at market price'. Minimum $1 for BUY orders.", parameters: placeMarketOrderSchema, - execute: async (args: z.infer) => { - try { - const result = await tradeApi.placeMarketOrder({ + execute: async (args: z.infer) => + withApprovalGuard(() => + tradeApi.placeMarketOrder({ tokenId: args.tokenId, amount: args.amount, side: args.side, ...(args.orderType && { orderType: args.orderType }), - }); - return JSON.stringify(result, null, 2); - } catch (err) { - if (err instanceof ApprovalRequiredError) { - return JSON.stringify(err, null, 2); - } - throw err; - } - }, + }), + ), }; diff --git a/src/tools/place-order.ts b/src/tools/place-order.ts index d40d28b..fe4bd89 100644 --- a/src/tools/place-order.ts +++ b/src/tools/place-order.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { ApprovalRequiredError } from "../services/approvals.js"; import { tradeApi } from "../services/trading.js"; +import { withApprovalGuard } from "../util/with-approval-guard.js"; const placeOrderSchema = z.object({ tokenId: z.string().describe("The token ID of the market outcome to trade"), @@ -31,21 +31,14 @@ export const placeOrderTool = { description: "Place a limit order on Polymarket at a specific price. Specify the number of shares (size) and price (0-1). For both BUY and SELL, you specify the number of shares you want to trade. Example: size=10, price=0.6 means buy/sell 10 shares at $0.60 per share (total: $6).", parameters: placeOrderSchema, - execute: async (args: z.infer) => { - try { - const result = await tradeApi.placeOrder({ + execute: async (args: z.infer) => + withApprovalGuard(() => + tradeApi.placeOrder({ tokenId: args.tokenId, price: args.price, size: args.size, side: args.side, ...(args.orderType && { orderType: args.orderType }), - }); - return JSON.stringify(result, null, 2); - } catch (err) { - if (err instanceof ApprovalRequiredError) { - return JSON.stringify(err, null, 2); - } - throw err; - } - }, + }), + ), }; diff --git a/src/tools/update-balance-allowance.ts b/src/tools/update-balance-allowance.ts index 83a56a3..918ba64 100644 --- a/src/tools/update-balance-allowance.ts +++ b/src/tools/update-balance-allowance.ts @@ -1,7 +1,7 @@ import { AssetType } from "@polymarket/clob-client"; import { z } from "zod"; -import { ApprovalRequiredError } from "../services/approvals.js"; import { tradeApi } from "../services/trading.js"; +import { withApprovalGuard } from "../util/with-approval-guard.js"; const updateBalanceAllowanceSchema = z.object({ assetType: z @@ -23,19 +23,12 @@ export const updateBalanceAllowanceTool = { asset_type: AssetType[args.assetType], }; if (args.tokenID) params.token_id = args.tokenID; - - try { + return withApprovalGuard(async () => { await tradeApi.updateBalanceAllowance(params); - return JSON.stringify( - { success: true, message: "Balance allowance updated successfully" }, - null, - 2, - ); - } catch (err) { - if (err instanceof ApprovalRequiredError) { - return JSON.stringify(err, null, 2); - } - throw err; - } + return { + success: true, + message: "Balance allowance updated successfully", + }; + }); }, }; diff --git a/src/util/log.ts b/src/util/log.ts new file mode 100644 index 0000000..6af4be3 --- /dev/null +++ b/src/util/log.ts @@ -0,0 +1,3 @@ +export function log(message: string): void { + process.stderr.write(`${message}\n`); +} diff --git a/src/util/with-approval-guard.ts b/src/util/with-approval-guard.ts new file mode 100644 index 0000000..5683c3d --- /dev/null +++ b/src/util/with-approval-guard.ts @@ -0,0 +1,15 @@ +import { ApprovalRequiredError } from "../services/approvals.js"; + +export async function withApprovalGuard( + fn: () => Promise, +): Promise { + try { + const result = await fn(); + return JSON.stringify(result, null, 2); + } catch (err) { + if (err instanceof ApprovalRequiredError) { + return JSON.stringify(err, null, 2); + } + throw err; + } +}