import axios from "axios"; import qs from "qs"; import { HttpsProxyAgent } from "https-proxy-agent"; import { AssetType, Chain, ClobClient, OrderType, Side, SignatureTypeV2 } from "@polymarket/clob-client-v2"; import { BuilderConfig } from "@polymarket/builder-signing-sdk"; import { deriveProxyWallet, RelayerTxType, RelayClient } from "@polymarket/builder-relayer-client"; import { createPublicClient, createWalletClient, encodeFunctionData, erc20Abi, formatUnits, http, parseUnits, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { polygon } from "viem/chains"; export { OrderType, Side, SignatureTypeV2 }; const CHAIN_ID = 137; const GAMMA_HOST = "https://gamma-api.polymarket.com"; const CLOB_HOST = "https://clob.polymarket.com"; const PUSD_ADDRESS = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"; const PUSD_DECIMALS = 6; const PROXY_FACTORY_ADDRESS = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"; const axiosDefaultOptions = { baseURL: "", url: "", method: "GET", headers: {}, params: {}, data: {}, timeout: 10000, }; const clientRequest = async (options, baseURL, proxyAgent) => { const { url } = options; if (!url || !baseURL) { throw new Error("url and baseURL are required"); } return axios({ ...axiosDefaultOptions, ...options, baseURL, proxy: false, ...(proxyAgent ? { httpAgent: proxyAgent, httpsAgent: proxyAgent } : {}), paramsSerializer: params => qs.stringify(params, { arrayFormat: "repeat" }), }).then(res => res.data); } const getRequiredConfig = (config, key) => { const value = config[key]; if (!value) { throw new Error(`${key} is required`); } return value; } const normalizePrivateKey = (privateKey) => { return privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`; } const getOptionalConfig = (config, key) => { const value = config[key]; return value && String(value).trim() ? String(value).trim() : undefined; } const getPolymarketFunderAddress = (config, accountAddress) => { return getOptionalConfig(config, "depositWalletAddress") || getOptionalConfig(config, "funderAddress") || accountAddress; } const getPolymarketSignatureType = (config, funderAddress, accountAddress) => { const configuredType = getOptionalConfig(config, "signatureType"); if (configuredType) { const signatureType = SignatureTypeV2[configuredType] ?? Number(configuredType); if (!Number.isInteger(signatureType) || !SignatureTypeV2[signatureType]) { throw new Error(`signatureType is invalid: ${configuredType}`); } return signatureType; } return funderAddress.toLowerCase() === accountAddress.toLowerCase() ? SignatureTypeV2.POLY_PROXY : SignatureTypeV2.POLY_1271; } const createViemHttpTransport = (rpcUrl, proxyAgent) => { if (!proxyAgent) { return rpcUrl ? http(rpcUrl) : http(); } const fetchFn = async (url, init = {}) => { const headers = Object.fromEntries(new Headers(init.headers ?? {}).entries()); const response = await axios({ url, method: init.method || "POST", headers, data: init.body, transformResponse: [data => data], responseType: "text", validateStatus: () => true, proxy: false, httpAgent: proxyAgent, httpsAgent: proxyAgent, }); return new Response(response.data, { status: response.status, statusText: response.statusText, headers: response.headers, }); }; return http(rpcUrl, { fetchFn }); } export const createPolymarketSdk = (config = {}) => { const proxyAgent = config.httpProxy ? new HttpsProxyAgent(config.httpProxy) : undefined; const requestMarketData = (options) => clientRequest(options, GAMMA_HOST, proxyAgent); const requestClobData = (options) => clientRequest(options, CLOB_HOST, proxyAgent); const createClobClient = () => { const account = privateKeyToAccount(normalizePrivateKey(getRequiredConfig(config, "privateKey"))); const signer = createWalletClient({ account, chain: polygon, transport: createViemHttpTransport(getOptionalConfig(config, "polygonRpcUrl"), proxyAgent), }); const funderAddress = getPolymarketFunderAddress(config, account.address); const userApiCreds = { key: getRequiredConfig(config, "apiKey"), secret: getRequiredConfig(config, "apiSecret"), passphrase: getRequiredConfig(config, "apiPassphrase"), }; return new ClobClient({ host: CLOB_HOST, chain: Chain.POLYGON, signer, creds: userApiCreds, signatureType: getPolymarketSignatureType(config, funderAddress, account.address), funderAddress, throwOnError: true, }); } const createPolymarketContext = () => { const transport = createViemHttpTransport(getOptionalConfig(config, "polygonRpcUrl"), proxyAgent); const account = privateKeyToAccount(normalizePrivateKey(getRequiredConfig(config, "privateKey"))); const signer = createWalletClient({ account, chain: polygon, transport }); const publicClient = createPublicClient({ chain: polygon, transport }); const creds = { key: getRequiredConfig(config, "apiKey"), secret: getRequiredConfig(config, "apiSecret"), passphrase: getRequiredConfig(config, "apiPassphrase"), }; return { account, signer, publicClient, creds }; } const createBuilderConfig = () => { return new BuilderConfig({ localBuilderCreds: { key: getRequiredConfig(config, "builderApiKey"), secret: getRequiredConfig(config, "builderSecret"), passphrase: getRequiredConfig(config, "builderPassPhrase"), }, }); } const createRelayer = ({ signer, relayTxType = RelayerTxType.PROXY } = {}) => { const relayerUrl = getOptionalConfig(config, "relayerUrl") || "https://relayer-v2.polymarket.com"; const relayer = new RelayClient(relayerUrl, CHAIN_ID, signer, createBuilderConfig(), relayTxType); if (proxyAgent) { relayer.httpClient.instance.defaults.proxy = false; relayer.httpClient.instance.defaults.httpAgent = proxyAgent; relayer.httpClient.instance.defaults.httpsAgent = proxyAgent; } return relayer; } const getProxyWalletAddress = ({ ownerAddress }) => { return getOptionalConfig(config, "proxyWalletAddress") || deriveProxyWallet(ownerAddress, PROXY_FACTORY_ADDRESS); } const getDepositWalletAddress = async ({ signer }) => { const configuredAddress = getOptionalConfig(config, "depositWalletAddress"); if (configuredAddress) { return configuredAddress; } const relayer = createRelayer({ signer }); return relayer.deriveDepositWalletAddress(); } const normalizeWalletMode = (wallet = "both") => { const value = wallet.toLowerCase(); if (!["deposit", "proxy", "both"].includes(value)) { throw new Error("wallet must be deposit, proxy, or both", { cause: 400 }); } return value; } const createBalanceClobClient = ({ signer, creds, funderAddress, signatureType }) => { return new ClobClient({ host: CLOB_HOST, chain: Chain.POLYGON, signer, creds, signatureType, funderAddress, throwOnError: true, }); } const getChainPusdBalance = async ({ publicClient, address }) => { const balance = await publicClient.readContract({ address: PUSD_ADDRESS, abi: erc20Abi, functionName: "balanceOf", args: [address], }); return formatUnits(balance, PUSD_DECIMALS); } const getClobBalanceAllowance = async ({ signer, creds, funderAddress, signatureType }) => { const client = createBalanceClobClient({ signer, creds, funderAddress, signatureType }); return client.getBalanceAllowance({ asset_type: AssetType.COLLATERAL }); } const getRawPusdBalance = ({ publicClient, address }) => { return publicClient.readContract({ address: PUSD_ADDRESS, abi: erc20Abi, functionName: "balanceOf", args: [address], }); } const normalizeTransferAmount = (amount) => { if (!amount || !Number.isFinite(Number(amount)) || Number(amount) <= 0) { throw new Error("amount must be greater than 0", { cause: 400 }); } return parseUnits(String(amount), PUSD_DECIMALS); } const normalizeTransferDirection = ({ from, to } = {}) => { const normalizedFrom = String(from || "").toLowerCase(); const normalizedTo = String(to || "").toLowerCase(); if ( !["proxy", "deposit"].includes(normalizedFrom) || !["proxy", "deposit"].includes(normalizedTo) || normalizedFrom === normalizedTo ) { throw new Error("Transfer direction must be proxy -> deposit or deposit -> proxy", { cause: 400 }); } return { from: normalizedFrom, to: normalizedTo }; } const createPusdTransferData = ({ to, amount }) => { return encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [to, amount], }); } const buildTransferResult = ({ owner, from, to, sourceAddress, destinationAddress, amount, sourceBalance, transactionID, transactionHash, confirmed, }) => { return { owner, pUSD: PUSD_ADDRESS, from, to, sourceAddress, destinationAddress, amount: String(amount), sourceBalance: formatUnits(sourceBalance, PUSD_DECIMALS), transactionID, transactionHash, confirmed, }; } const ensureSufficientPusdBalance = ({ wallet, balance, amount }) => { if (balance < amount) { throw new Error(`Insufficient ${wallet} wallet pUSD balance: ${formatUnits(balance, PUSD_DECIMALS)}`, { cause: 400 }); } } const getTransferWalletContext = async () => { const { account, signer, publicClient } = createPolymarketContext(); const proxyRelayer = createRelayer({ signer, relayTxType: RelayerTxType.PROXY }); const proxyWalletAddress = getProxyWalletAddress({ ownerAddress: account.address }); const depositWalletAddress = await getDepositWalletAddress({ signer }); return { account, signer, publicClient, proxyRelayer, proxyWalletAddress, depositWalletAddress, }; } const executePusdTransfer = ({ from, to, context, data, amount, }) => { if (from === "proxy" && to === "deposit") { return context.proxyRelayer.execute( [{ to: PUSD_ADDRESS, data, value: "0", }], `transfer ${amount} pUSD from proxy to deposit wallet`, ); } const depositRelayer = createRelayer({ signer: context.signer }); return depositRelayer.executeDepositWalletBatch( [{ target: PUSD_ADDRESS, data, value: "0", }], context.depositWalletAddress, String(Math.floor(Date.now() / 1000) + 600), ); } const transferPusdBetweenWallets = async ({ amount, from, to } = {}) => { const transferAmount = normalizeTransferAmount(amount); const context = await getTransferWalletContext(); const sourceAddress = from === "proxy" ? context.proxyWalletAddress : context.depositWalletAddress; const destinationAddress = to === "proxy" ? context.proxyWalletAddress : context.depositWalletAddress; const sourceBalance = await getRawPusdBalance({ publicClient: context.publicClient, address: sourceAddress, }); ensureSufficientPusdBalance({ wallet: from, balance: sourceBalance, amount: transferAmount, }); const data = createPusdTransferData({ to: destinationAddress, amount: transferAmount, }); const response = await executePusdTransfer({ from, to, context, data, amount, }); const confirmed = await response.wait(); return buildTransferResult({ owner: context.account.address, from, to, sourceAddress, destinationAddress, amount, sourceBalance, transactionID: response.transactionID, transactionHash: response.transactionHash || response.hash, confirmed, }); } 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); }); }); } 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)); } const getOrderBook = async (tokenId) => { return requestClobData({ url: "/book", params: { token_id: tokenId, } }); } const getMultipleOrderBooks = async (tokenIds) => { return requestClobData({ url: "/books", method: "POST", headers: { "Content-Type": "application/json", }, data: tokenIds.map(tokenId => ({ token_id: tokenId })), }); } const getBalanceAllowance = async ({ wallet = "both" } = {}) => { const walletMode = normalizeWalletMode(wallet); const { account, signer, publicClient, creds } = createPolymarketContext(); const wallets = []; if (walletMode === "proxy" || walletMode === "both") { wallets.push({ type: "proxy", address: getProxyWalletAddress({ ownerAddress: account.address }), signatureType: SignatureTypeV2.POLY_PROXY, }); } if (walletMode === "deposit" || walletMode === "both") { wallets.push({ type: "deposit", address: await getDepositWalletAddress({ signer }), signatureType: SignatureTypeV2.POLY_1271, }); } const results = []; for (const item of wallets) { const [chainPusdBalance, clobBalanceAllowance] = await Promise.all([ getChainPusdBalance({ publicClient, address: item.address }), getClobBalanceAllowance({ signer, creds, funderAddress: item.address, signatureType: item.signatureType, }), ]); results.push({ type: item.type, owner: account.address, address: item.address, signatureType: SignatureTypeV2[item.signatureType], chainPusdBalance, clobBalanceAllowance, }); } return results; } const transferWallet = async ({ amount, from, to } = {}) => { const direction = normalizeTransferDirection({ from, to }); return transferPusdBetweenWallets({ amount, from: direction.from, to: direction.to, }); } const createLimitOrder = async ({ tokenID, price, size, side = Side.BUY, tickSize = "0.01", negRisk = false, orderType = OrderType.GTC, postOnly = true, expiration, deferExec = false, } = {}) => { return Promise.reject(new Error("not implemented", { cause: 501 })); if (!tokenID) { throw new Error("tokenID is required", { cause: 400 }); } if (!Number.isFinite(Number(price))) { throw new Error("price is required", { cause: 400 }); } if (!Number.isFinite(Number(size))) { throw new Error("size is required", { cause: 400 }); } if (orderType !== OrderType.GTC && orderType !== OrderType.GTD) { throw new Error(`orderType must be ${OrderType.GTC} or ${OrderType.GTD}`, { cause: 400 }); } const client = createClobClient(); return client.createAndPostOrder({ tokenID, price: Number(price), size: Number(size), side, ...(expiration ? { expiration: Number(expiration) } : {}), }, { tickSize, negRisk }, orderType, postOnly, deferExec); } const getOrder = async (orderID) => { if (!orderID) { throw new Error("orderID is required", { cause: 400 }); } const client = createClobClient(); return client.getOrder(orderID); } const getOpenOrders = async ({ id, market, asset_id, only_first_page = false, next_cursor, } = {}) => { const client = createClobClient(); return client.getOpenOrders({ ...(id ? { id } : {}), ...(market ? { market } : {}), ...(asset_id ? { asset_id } : {}), }, Boolean(only_first_page), next_cursor); } return { createClobClient, getSoccerSports, getEvents, getOrderBook, getMultipleOrderBooks, getBalanceAllowance, transferWallet, createLimitOrder, getOrder, getOpenOrders, }; } export default createPolymarketSdk;