index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. import axios from "axios";
  2. import qs from "qs";
  3. import { HttpsProxyAgent } from "https-proxy-agent";
  4. import { AssetType, Chain, ClobClient, OrderType, Side, SignatureTypeV2 } from "@polymarket/clob-client-v2";
  5. import { BuilderConfig } from "@polymarket/builder-signing-sdk";
  6. import { deriveProxyWallet, RelayerTxType, RelayClient } from "@polymarket/builder-relayer-client";
  7. import {
  8. createPublicClient,
  9. createWalletClient,
  10. encodeFunctionData,
  11. erc20Abi,
  12. formatUnits,
  13. http,
  14. parseUnits,
  15. } from "viem";
  16. import { privateKeyToAccount } from "viem/accounts";
  17. import { polygon } from "viem/chains";
  18. export { OrderType, Side, SignatureTypeV2 };
  19. const CHAIN_ID = 137;
  20. const GAMMA_HOST = "https://gamma-api.polymarket.com";
  21. const CLOB_HOST = "https://clob.polymarket.com";
  22. const PUSD_ADDRESS = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB";
  23. const PUSD_DECIMALS = 6;
  24. const PROXY_FACTORY_ADDRESS = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
  25. const axiosDefaultOptions = {
  26. baseURL: "",
  27. url: "",
  28. method: "GET",
  29. headers: {},
  30. params: {},
  31. data: {},
  32. timeout: 10000,
  33. };
  34. const clientRequest = async (options, baseURL, proxyAgent) => {
  35. const { url } = options;
  36. if (!url || !baseURL) {
  37. throw new Error("url and baseURL are required");
  38. }
  39. return axios({
  40. ...axiosDefaultOptions,
  41. ...options,
  42. baseURL,
  43. proxy: false,
  44. ...(proxyAgent ? { httpAgent: proxyAgent, httpsAgent: proxyAgent } : {}),
  45. paramsSerializer: params => qs.stringify(params, { arrayFormat: "repeat" }),
  46. }).then(res => res.data);
  47. }
  48. const getRequiredConfig = (config, key) => {
  49. const value = config[key];
  50. if (!value) {
  51. throw new Error(`${key} is required`);
  52. }
  53. return value;
  54. }
  55. const normalizePrivateKey = (privateKey) => {
  56. return privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
  57. }
  58. const getOptionalConfig = (config, key) => {
  59. const value = config[key];
  60. return value && String(value).trim() ? String(value).trim() : undefined;
  61. }
  62. const getPolymarketFunderAddress = (config, accountAddress) => {
  63. return getOptionalConfig(config, "depositWalletAddress")
  64. || getOptionalConfig(config, "funderAddress")
  65. || accountAddress;
  66. }
  67. const getPolymarketSignatureType = (config, funderAddress, accountAddress) => {
  68. const configuredType = getOptionalConfig(config, "signatureType");
  69. if (configuredType) {
  70. const signatureType = SignatureTypeV2[configuredType] ?? Number(configuredType);
  71. if (!Number.isInteger(signatureType) || !SignatureTypeV2[signatureType]) {
  72. throw new Error(`signatureType is invalid: ${configuredType}`);
  73. }
  74. return signatureType;
  75. }
  76. return funderAddress.toLowerCase() === accountAddress.toLowerCase()
  77. ? SignatureTypeV2.POLY_PROXY
  78. : SignatureTypeV2.POLY_1271;
  79. }
  80. const createViemHttpTransport = (rpcUrl, proxyAgent) => {
  81. if (!proxyAgent) {
  82. return rpcUrl ? http(rpcUrl) : http();
  83. }
  84. const fetchFn = async (url, init = {}) => {
  85. const headers = Object.fromEntries(new Headers(init.headers ?? {}).entries());
  86. const response = await axios({
  87. url,
  88. method: init.method || "POST",
  89. headers,
  90. data: init.body,
  91. transformResponse: [data => data],
  92. responseType: "text",
  93. validateStatus: () => true,
  94. proxy: false,
  95. httpAgent: proxyAgent,
  96. httpsAgent: proxyAgent,
  97. });
  98. return new Response(response.data, {
  99. status: response.status,
  100. statusText: response.statusText,
  101. headers: response.headers,
  102. });
  103. };
  104. return http(rpcUrl, { fetchFn });
  105. }
  106. export const createPolymarketSdk = (config = {}) => {
  107. const proxyAgent = config.httpProxy ? new HttpsProxyAgent(config.httpProxy) : undefined;
  108. const requestMarketData = (options) => clientRequest(options, GAMMA_HOST, proxyAgent);
  109. const requestClobData = (options) => clientRequest(options, CLOB_HOST, proxyAgent);
  110. const createClobClient = () => {
  111. const account = privateKeyToAccount(normalizePrivateKey(getRequiredConfig(config, "privateKey")));
  112. const signer = createWalletClient({
  113. account,
  114. chain: polygon,
  115. transport: createViemHttpTransport(getOptionalConfig(config, "polygonRpcUrl"), proxyAgent),
  116. });
  117. const funderAddress = getPolymarketFunderAddress(config, account.address);
  118. const userApiCreds = {
  119. key: getRequiredConfig(config, "apiKey"),
  120. secret: getRequiredConfig(config, "apiSecret"),
  121. passphrase: getRequiredConfig(config, "apiPassphrase"),
  122. };
  123. return new ClobClient({
  124. host: CLOB_HOST,
  125. chain: Chain.POLYGON,
  126. signer,
  127. creds: userApiCreds,
  128. signatureType: getPolymarketSignatureType(config, funderAddress, account.address),
  129. funderAddress,
  130. throwOnError: true,
  131. });
  132. }
  133. const createPolymarketContext = () => {
  134. const transport = createViemHttpTransport(getOptionalConfig(config, "polygonRpcUrl"), proxyAgent);
  135. const account = privateKeyToAccount(normalizePrivateKey(getRequiredConfig(config, "privateKey")));
  136. const signer = createWalletClient({ account, chain: polygon, transport });
  137. const publicClient = createPublicClient({ chain: polygon, transport });
  138. const creds = {
  139. key: getRequiredConfig(config, "apiKey"),
  140. secret: getRequiredConfig(config, "apiSecret"),
  141. passphrase: getRequiredConfig(config, "apiPassphrase"),
  142. };
  143. return { account, signer, publicClient, creds };
  144. }
  145. const createBuilderConfig = () => {
  146. return new BuilderConfig({
  147. localBuilderCreds: {
  148. key: getRequiredConfig(config, "builderApiKey"),
  149. secret: getRequiredConfig(config, "builderSecret"),
  150. passphrase: getRequiredConfig(config, "builderPassPhrase"),
  151. },
  152. });
  153. }
  154. const createRelayer = ({ signer, relayTxType = RelayerTxType.PROXY } = {}) => {
  155. const relayerUrl = getOptionalConfig(config, "relayerUrl") || "https://relayer-v2.polymarket.com";
  156. const relayer = new RelayClient(relayerUrl, CHAIN_ID, signer, createBuilderConfig(), relayTxType);
  157. if (proxyAgent) {
  158. relayer.httpClient.instance.defaults.proxy = false;
  159. relayer.httpClient.instance.defaults.httpAgent = proxyAgent;
  160. relayer.httpClient.instance.defaults.httpsAgent = proxyAgent;
  161. }
  162. return relayer;
  163. }
  164. const getProxyWalletAddress = ({ ownerAddress }) => {
  165. return getOptionalConfig(config, "proxyWalletAddress")
  166. || deriveProxyWallet(ownerAddress, PROXY_FACTORY_ADDRESS);
  167. }
  168. const getDepositWalletAddress = async ({ signer }) => {
  169. const configuredAddress = getOptionalConfig(config, "depositWalletAddress");
  170. if (configuredAddress) {
  171. return configuredAddress;
  172. }
  173. const relayer = createRelayer({ signer });
  174. return relayer.deriveDepositWalletAddress();
  175. }
  176. const normalizeWalletMode = (wallet = "both") => {
  177. const value = wallet.toLowerCase();
  178. if (!["deposit", "proxy", "both"].includes(value)) {
  179. throw new Error("wallet must be deposit, proxy, or both", { cause: 400 });
  180. }
  181. return value;
  182. }
  183. const createBalanceClobClient = ({ signer, creds, funderAddress, signatureType }) => {
  184. return new ClobClient({
  185. host: CLOB_HOST,
  186. chain: Chain.POLYGON,
  187. signer,
  188. creds,
  189. signatureType,
  190. funderAddress,
  191. throwOnError: true,
  192. });
  193. }
  194. const getChainPusdBalance = async ({ publicClient, address }) => {
  195. const balance = await publicClient.readContract({
  196. address: PUSD_ADDRESS,
  197. abi: erc20Abi,
  198. functionName: "balanceOf",
  199. args: [address],
  200. });
  201. return formatUnits(balance, PUSD_DECIMALS);
  202. }
  203. const getClobBalanceAllowance = async ({ signer, creds, funderAddress, signatureType }) => {
  204. const client = createBalanceClobClient({ signer, creds, funderAddress, signatureType });
  205. return client.getBalanceAllowance({ asset_type: AssetType.COLLATERAL });
  206. }
  207. const getRawPusdBalance = ({ publicClient, address }) => {
  208. return publicClient.readContract({
  209. address: PUSD_ADDRESS,
  210. abi: erc20Abi,
  211. functionName: "balanceOf",
  212. args: [address],
  213. });
  214. }
  215. const normalizeTransferAmount = (amount) => {
  216. if (!amount || !Number.isFinite(Number(amount)) || Number(amount) <= 0) {
  217. throw new Error("amount must be greater than 0", { cause: 400 });
  218. }
  219. return parseUnits(String(amount), PUSD_DECIMALS);
  220. }
  221. const normalizeTransferDirection = ({ from, to } = {}) => {
  222. const normalizedFrom = String(from || "").toLowerCase();
  223. const normalizedTo = String(to || "").toLowerCase();
  224. if (
  225. !["proxy", "deposit"].includes(normalizedFrom)
  226. || !["proxy", "deposit"].includes(normalizedTo)
  227. || normalizedFrom === normalizedTo
  228. ) {
  229. throw new Error("Transfer direction must be proxy -> deposit or deposit -> proxy", { cause: 400 });
  230. }
  231. return { from: normalizedFrom, to: normalizedTo };
  232. }
  233. const createPusdTransferData = ({ to, amount }) => {
  234. return encodeFunctionData({
  235. abi: erc20Abi,
  236. functionName: "transfer",
  237. args: [to, amount],
  238. });
  239. }
  240. const buildTransferResult = ({
  241. owner,
  242. from,
  243. to,
  244. sourceAddress,
  245. destinationAddress,
  246. amount,
  247. sourceBalance,
  248. transactionID,
  249. transactionHash,
  250. confirmed,
  251. }) => {
  252. return {
  253. owner,
  254. pUSD: PUSD_ADDRESS,
  255. from,
  256. to,
  257. sourceAddress,
  258. destinationAddress,
  259. amount: String(amount),
  260. sourceBalance: formatUnits(sourceBalance, PUSD_DECIMALS),
  261. transactionID,
  262. transactionHash,
  263. confirmed,
  264. };
  265. }
  266. const ensureSufficientPusdBalance = ({ wallet, balance, amount }) => {
  267. if (balance < amount) {
  268. throw new Error(`Insufficient ${wallet} wallet pUSD balance: ${formatUnits(balance, PUSD_DECIMALS)}`, { cause: 400 });
  269. }
  270. }
  271. const getTransferWalletContext = async () => {
  272. const { account, signer, publicClient } = createPolymarketContext();
  273. const proxyRelayer = createRelayer({ signer, relayTxType: RelayerTxType.PROXY });
  274. const proxyWalletAddress = getProxyWalletAddress({ ownerAddress: account.address });
  275. const depositWalletAddress = await getDepositWalletAddress({ signer });
  276. return {
  277. account,
  278. signer,
  279. publicClient,
  280. proxyRelayer,
  281. proxyWalletAddress,
  282. depositWalletAddress,
  283. };
  284. }
  285. const executePusdTransfer = ({
  286. from,
  287. to,
  288. context,
  289. data,
  290. amount,
  291. }) => {
  292. if (from === "proxy" && to === "deposit") {
  293. return context.proxyRelayer.execute(
  294. [{
  295. to: PUSD_ADDRESS,
  296. data,
  297. value: "0",
  298. }],
  299. `transfer ${amount} pUSD from proxy to deposit wallet`,
  300. );
  301. }
  302. const depositRelayer = createRelayer({ signer: context.signer });
  303. return depositRelayer.executeDepositWalletBatch(
  304. [{
  305. target: PUSD_ADDRESS,
  306. data,
  307. value: "0",
  308. }],
  309. context.depositWalletAddress,
  310. String(Math.floor(Date.now() / 1000) + 600),
  311. );
  312. }
  313. const transferPusdBetweenWallets = async ({ amount, from, to } = {}) => {
  314. const transferAmount = normalizeTransferAmount(amount);
  315. const context = await getTransferWalletContext();
  316. const sourceAddress = from === "proxy"
  317. ? context.proxyWalletAddress
  318. : context.depositWalletAddress;
  319. const destinationAddress = to === "proxy"
  320. ? context.proxyWalletAddress
  321. : context.depositWalletAddress;
  322. const sourceBalance = await getRawPusdBalance({
  323. publicClient: context.publicClient,
  324. address: sourceAddress,
  325. });
  326. ensureSufficientPusdBalance({
  327. wallet: from,
  328. balance: sourceBalance,
  329. amount: transferAmount,
  330. });
  331. const data = createPusdTransferData({
  332. to: destinationAddress,
  333. amount: transferAmount,
  334. });
  335. const response = await executePusdTransfer({
  336. from,
  337. to,
  338. context,
  339. data,
  340. amount,
  341. });
  342. const confirmed = await response.wait();
  343. return buildTransferResult({
  344. owner: context.account.address,
  345. from,
  346. to,
  347. sourceAddress,
  348. destinationAddress,
  349. amount,
  350. sourceBalance,
  351. transactionID: response.transactionID,
  352. transactionHash: response.transactionHash || response.hash,
  353. confirmed,
  354. });
  355. }
  356. const getSoccerSports = async () => {
  357. return requestMarketData({ url: "/sports" })
  358. .then(sportsData => {
  359. return sportsData.filter(item => {
  360. const { tags } = item;
  361. const tagIds = tags.split(",").map(item => +item);
  362. return tagIds.includes(100350);
  363. });
  364. });
  365. }
  366. const getEvents = async ({
  367. limit = 500, tag_id = 100350, active = true,
  368. closed = false, endDateMin = "", endDateMax = "",
  369. } = {}) => {
  370. return requestMarketData({
  371. url: "/events",
  372. params: {
  373. limit, tag_id, active, closed,
  374. end_date_min: endDateMin,
  375. end_date_max: endDateMax,
  376. }
  377. }).then(events => events.filter(item => !!item.series));
  378. }
  379. const getOrderBook = async (tokenId) => {
  380. return requestClobData({
  381. url: "/book",
  382. params: {
  383. token_id: tokenId,
  384. }
  385. });
  386. }
  387. const getMultipleOrderBooks = async (tokenIds) => {
  388. return requestClobData({
  389. url: "/books",
  390. method: "POST",
  391. headers: {
  392. "Content-Type": "application/json",
  393. },
  394. data: tokenIds.map(tokenId => ({ token_id: tokenId })),
  395. });
  396. }
  397. const getBalanceAllowance = async ({ wallet = "both" } = {}) => {
  398. const walletMode = normalizeWalletMode(wallet);
  399. const { account, signer, publicClient, creds } = createPolymarketContext();
  400. const wallets = [];
  401. if (walletMode === "proxy" || walletMode === "both") {
  402. wallets.push({
  403. type: "proxy",
  404. address: getProxyWalletAddress({ ownerAddress: account.address }),
  405. signatureType: SignatureTypeV2.POLY_PROXY,
  406. });
  407. }
  408. if (walletMode === "deposit" || walletMode === "both") {
  409. wallets.push({
  410. type: "deposit",
  411. address: await getDepositWalletAddress({ signer }),
  412. signatureType: SignatureTypeV2.POLY_1271,
  413. });
  414. }
  415. const results = [];
  416. for (const item of wallets) {
  417. const [chainPusdBalance, clobBalanceAllowance] = await Promise.all([
  418. getChainPusdBalance({ publicClient, address: item.address }),
  419. getClobBalanceAllowance({
  420. signer,
  421. creds,
  422. funderAddress: item.address,
  423. signatureType: item.signatureType,
  424. }),
  425. ]);
  426. results.push({
  427. type: item.type,
  428. owner: account.address,
  429. address: item.address,
  430. signatureType: SignatureTypeV2[item.signatureType],
  431. chainPusdBalance,
  432. clobBalanceAllowance,
  433. });
  434. }
  435. return results;
  436. }
  437. const transferWallet = async ({ amount, from, to } = {}) => {
  438. const direction = normalizeTransferDirection({ from, to });
  439. return transferPusdBetweenWallets({
  440. amount,
  441. from: direction.from,
  442. to: direction.to,
  443. });
  444. }
  445. const createLimitOrder = async ({
  446. tokenID,
  447. price,
  448. size,
  449. side = Side.BUY,
  450. tickSize = "0.01",
  451. negRisk = false,
  452. orderType = OrderType.GTC,
  453. postOnly = true,
  454. expiration,
  455. deferExec = false,
  456. } = {}) => {
  457. return Promise.reject(new Error("not implemented", { cause: 501 }));
  458. if (!tokenID) {
  459. throw new Error("tokenID is required", { cause: 400 });
  460. }
  461. if (!Number.isFinite(Number(price))) {
  462. throw new Error("price is required", { cause: 400 });
  463. }
  464. if (!Number.isFinite(Number(size))) {
  465. throw new Error("size is required", { cause: 400 });
  466. }
  467. if (orderType !== OrderType.GTC && orderType !== OrderType.GTD) {
  468. throw new Error(`orderType must be ${OrderType.GTC} or ${OrderType.GTD}`, { cause: 400 });
  469. }
  470. const client = createClobClient();
  471. return client.createAndPostOrder({
  472. tokenID,
  473. price: Number(price),
  474. size: Number(size),
  475. side,
  476. ...(expiration ? { expiration: Number(expiration) } : {}),
  477. }, { tickSize, negRisk }, orderType, postOnly, deferExec);
  478. }
  479. const getOrder = async (orderID) => {
  480. if (!orderID) {
  481. throw new Error("orderID is required", { cause: 400 });
  482. }
  483. const client = createClobClient();
  484. return client.getOrder(orderID);
  485. }
  486. const getOpenOrders = async ({
  487. id,
  488. market,
  489. asset_id,
  490. only_first_page = false,
  491. next_cursor,
  492. } = {}) => {
  493. const client = createClobClient();
  494. return client.getOpenOrders({
  495. ...(id ? { id } : {}),
  496. ...(market ? { market } : {}),
  497. ...(asset_id ? { asset_id } : {}),
  498. }, Boolean(only_first_page), next_cursor);
  499. }
  500. return {
  501. createClobClient,
  502. getSoccerSports,
  503. getEvents,
  504. getOrderBook,
  505. getMultipleOrderBooks,
  506. getBalanceAllowance,
  507. transferWallet,
  508. createLimitOrder,
  509. getOrder,
  510. getOpenOrders,
  511. };
  512. }
  513. export default createPolymarketSdk;