import axios from "axios"; import qs from "qs"; import { HttpsProxyAgent } from "https-proxy-agent"; import { ClobClient, Side, OrderType } from "@polymarket/clob-client"; import { Wallet } from "ethers"; import WebSocketClient from "./WebSocketClient.js"; import Logs from "./logs.js"; /** * axios 默认配置 */ const axiosDefaultOptions = { baseURL: "", url: "", method: "GET", headers: {}, params: {}, data: {}, timeout: 10000, }; /** * 通用请求 * @param {*} options * @param {*} baseURL * @returns */ const clientRequest = async (options, baseURL) => { const { url } = options; if (!url || !baseURL) { throw new Error("url and baseURL are required"); } const mergedOptions = { ...axiosDefaultOptions, ...options, baseURL, paramsSerializer: params => qs.stringify(params, { arrayFormat: 'repeat' }) }; const proxy = process.env.NODE_HTTP_PROXY; if (proxy) { mergedOptions.proxy = false; mergedOptions.httpsAgent = new HttpsProxyAgent(proxy); } return axios(mergedOptions).then(res => res.data); } /** * 请求市场数据 * @param {*} options * @returns */ const requestMarketData = async (options) => { return clientRequest(options, "https://gamma-api.polymarket.com"); } /** * 请求订单簿数据 */ const requestClobData = async (options) => { return clientRequest(options, "https://clob.polymarket.com"); } /** * 获取足球联赛 * @returns {Promise} */ export const getSoccerSports = async () => { return requestMarketData({ url: "/sports" }) .then(sportsData => { return sportsData.filter(item => { const { tags } = item; const tagIds = tags.split(",").map(item => +item); return tagIds.includes(100350); }); }); } /** * 获取赛事数据 * @param {Object} options * @param {number} options.limit * @param {number} options.tag_id * @param {boolean} options.active * @param {boolean} options.closed * @param {string} options.endDateMin * @param {string} options.endDateMax * @returns {Promise} */ export const getEvents = async ({ limit = 500, tag_id = 100350, active = true, closed = false, endDateMin = "", endDateMax = "", } = {}) => { return requestMarketData({ url: "/events", params: { limit, tag_id, active, closed, end_date_min: endDateMin, end_date_max: endDateMax, } }).then(events => events.filter(item => !!item.series)); } /** * 获取订单簿数据 */ export const getOrderBook = async (tokenId) => { return requestClobData({ url: "/book", params: { token_id: tokenId, } }); } /** * 批量获取订单簿数据 * @param {Array} tokenIds * @returns {Promise} */ export const getMultipleOrderBooks = async (tokenIds) => { return requestClobData({ url: "/books", method: 'POST', headers: { 'Content-Type': 'application/json', }, data: tokenIds.map(tokenId => ({ token_id: tokenId })), }); } /** * 下注 */ export const placeOrder = async (info) => { // return Promise.resolve(info); const { asset_id, bestPrice, stakeSize, tick_size: tickSize, neg_risk: negRisk } = info; const HOST = "https://clob.polymarket.com"; const CHAIN_ID = 137; // Polygon mainnet const signer = new Wallet(process.env.POLYMARKET_PRIVATE_KEY); const userApiCreds = { key: process.env.POLYMARKET_API_KEY, secret: process.env.POLYMARKET_API_SECRET, passphrase: process.env.POLYMARKET_API_PASSPHRASE, }; const SIGNATURE_TYPE = 0; const FUNDER_ADDRESS = signer.address; // Logs.outDev('clob client init', HOST, CHAIN_ID, signer.address, userApiCreds, SIGNATURE_TYPE, FUNDER_ADDRESS); const client = new ClobClient( HOST, CHAIN_ID, signer, userApiCreds, SIGNATURE_TYPE, FUNDER_ADDRESS ); const orderData = { tokenID: asset_id, price: bestPrice, size: stakeSize, side: Side.BUY, } const orderOptions = { tickSize, negRisk } Logs.outDev('polymarket place order data', orderData, orderOptions, OrderType.FOK); // return client.getBalanceAllowance({ asset_type: 'COLLATERAL' }) return client.createAndPostOrder(orderData, orderOptions, OrderType.FOK) .then(res => { // if (res.error) { // return Promise.reject(new Error(res.error)); // } return res; }); // return Promise.resolve(info); // return Promise.reject(new Error('polymarket place order not implemented', { cause: 400 })); } /** * 请求平台数据 * @param {*} options * @returns {Promise} */ export const platformRequest = async (options) => { const { url } = options; if (!url) { throw new Error("url is required"); } const mergedOptions = { ...axiosDefaultOptions, ...options, baseURL: "http://127.0.0.1:9020", }; return axios(mergedOptions).then(res => res.data); } /** * 请求平台 POST 数据 * @param {string} url * @param {Object} data * @returns {Promise} */ export const platformPost = async (url, data) => { return platformRequest({ url, method: 'POST', headers: { 'Content-Type': 'application/json', }, data, }); } /** * 请求平台 GET 数据 * @param {string} url * @param {Object} params * @returns {Promise} */ export const platformGet = async (url, params) => { return platformRequest({ url, method: 'GET', params }); } /** * 市场 WebSocket 客户端 */ export class MarketWsClient extends WebSocketClient { #assetIds = []; constructor() { let agent; const proxy = process.env.NODE_HTTP_PROXY; if (proxy) { agent = new HttpsProxyAgent(proxy); } super("wss://ws-subscriptions-clob.polymarket.com/ws/market", { agent }); } connect() { super.connect(); this.on('open', () => { if (this.#assetIds.length > 0) { this.subscribeToTokensIds(this.#assetIds); } }); } subscribeToTokensIds(assetIds) { this.#assetIds = [...new Set([...this.#assetIds, ...assetIds])]; this.send({ operation: "subscribe", assets_ids: assetIds, }); } unsubscribeToTokensIds(assetIds) { const assetIdsSet = new Set(assetIds); this.#assetIds = this.#assetIds.filter(id => !assetIdsSet.has(id)); this.send({ operation: "unsubscribe", assets_ids: assetIds, }); } }