transfer.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import 'dotenv/config';
  2. import axios from "axios";
  3. import { HttpsProxyAgent } from "https-proxy-agent";
  4. import { BuilderConfig } from "@polymarket/builder-signing-sdk";
  5. import {
  6. deriveProxyWallet,
  7. RelayerTxType,
  8. RelayClient,
  9. } from "@polymarket/builder-relayer-client";
  10. import {
  11. createPublicClient,
  12. createWalletClient,
  13. encodeFunctionData,
  14. erc20Abi,
  15. formatUnits,
  16. http,
  17. maxUint256,
  18. parseUnits,
  19. } from "viem";
  20. import { privateKeyToAccount } from "viem/accounts";
  21. import { polygon } from "viem/chains";
  22. const CHAIN_ID = 137;
  23. const PUSD_ADDRESS = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB";
  24. const PUSD_DECIMALS = 6;
  25. const PROXY_FACTORY_ADDRESS = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
  26. const DEFAULT_CLOB_SPENDERS = [
  27. "0xE111180000d2663C0091e4f400237545B87B996B",
  28. "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296",
  29. "0xe2222d279d744050d28e00520010520000310F59",
  30. ];
  31. const NODE_HTTP_PROXY = process.env.NODE_HTTP_PROXY;
  32. const proxyAgent = NODE_HTTP_PROXY ? new HttpsProxyAgent(NODE_HTTP_PROXY) : undefined;
  33. if (NODE_HTTP_PROXY) {
  34. axios.defaults.proxy = false;
  35. axios.defaults.httpAgent = proxyAgent;
  36. axios.defaults.httpsAgent = proxyAgent;
  37. }
  38. const getRequiredEnv = (key) => {
  39. const value = process.env[key];
  40. if (!value) {
  41. throw new Error(`${key} is required`);
  42. }
  43. return value;
  44. }
  45. const getOptionalEnv = (key) => {
  46. const value = process.env[key];
  47. return value && value.trim() ? value.trim() : undefined;
  48. }
  49. const normalizePrivateKey = (privateKey) => {
  50. return privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
  51. }
  52. const getArgValue = (name) => {
  53. const prefix = `${name}=`;
  54. const arg = process.argv.slice(2).find(item => item.startsWith(prefix));
  55. return arg ? arg.slice(prefix.length) : undefined;
  56. }
  57. const normalizeDirection = () => {
  58. const from = (getArgValue("--from") || process.env.TRANSFER_FROM || "proxy").toLowerCase();
  59. const to = (getArgValue("--to") || process.env.TRANSFER_TO || "deposit").toLowerCase();
  60. if (!["proxy", "deposit"].includes(from) || !["proxy", "deposit"].includes(to) || from === to) {
  61. throw new Error("Transfer direction must be proxy -> deposit or deposit -> proxy");
  62. }
  63. return { from, to };
  64. }
  65. const normalizeAction = () => {
  66. const action = (getArgValue("--action") || process.env.TRANSFER_ACTION || "transfer").toLowerCase();
  67. if (!["transfer", "approve"].includes(action)) {
  68. throw new Error("action must be transfer or approve");
  69. }
  70. return action;
  71. }
  72. const normalizeWalletMode = () => {
  73. const wallet = (getArgValue("--wallet") || process.env.APPROVE_WALLET || "deposit").toLowerCase();
  74. if (!["proxy", "deposit", "both"].includes(wallet)) {
  75. throw new Error("wallet must be proxy, deposit, or both");
  76. }
  77. return wallet;
  78. }
  79. const getClobSpenders = () => {
  80. const configured = getArgValue("--spenders") || process.env.CLOB_SPENDERS;
  81. if (!configured) {
  82. return DEFAULT_CLOB_SPENDERS;
  83. }
  84. const spenders = configured.split(",").map(item => item.trim()).filter(Boolean);
  85. if (!spenders.length) {
  86. throw new Error("CLOB spenders cannot be empty");
  87. }
  88. return spenders;
  89. }
  90. const action = normalizeAction();
  91. const amount = getArgValue("--amount") || process.env.TRANSFER_AMOUNT;
  92. if (action === "transfer" && (!amount || Number(amount) <= 0)) {
  93. throw new Error("Transfer amount is required. Example: npm run transfer -- --amount=10");
  94. }
  95. const direction = action === "transfer" ? normalizeDirection() : undefined;
  96. const walletMode = action === "approve" ? normalizeWalletMode() : undefined;
  97. const clobSpenders = getClobSpenders();
  98. const execute = process.argv.includes("--execute");
  99. const relayerUrl = getOptionalEnv("POLYMARKET_RELAYER_URL") || "https://relayer-v2.polymarket.com";
  100. const rpcUrl = getOptionalEnv("POLYGON_RPC_URL");
  101. const account = privateKeyToAccount(normalizePrivateKey(getRequiredEnv("POLYMARKET_PRIVATE_KEY")));
  102. const walletTransport = rpcUrl ? http(rpcUrl) : http();
  103. const signer = createWalletClient({ account, chain: polygon, transport: walletTransport });
  104. const publicClient = createPublicClient({ chain: polygon, transport: walletTransport });
  105. const builderConfig = new BuilderConfig({
  106. localBuilderCreds: {
  107. key: getRequiredEnv("POLYMARKET_BUILDER_API_KEY"),
  108. secret: getRequiredEnv("POLYMARKET_BUILDER_SECRET"),
  109. passphrase: getRequiredEnv("POLYMARKET_BUILDER_PASS_PHRASE"),
  110. },
  111. });
  112. const createRelayer = (relayTxType = RelayerTxType.PROXY) => {
  113. const relayer = new RelayClient(relayerUrl, CHAIN_ID, signer, builderConfig, relayTxType);
  114. if (proxyAgent) {
  115. relayer.httpClient.instance.defaults.proxy = false;
  116. relayer.httpClient.instance.defaults.httpAgent = proxyAgent;
  117. relayer.httpClient.instance.defaults.httpsAgent = proxyAgent;
  118. }
  119. return relayer;
  120. }
  121. const proxyRelayer = createRelayer(RelayerTxType.PROXY);
  122. const proxyWalletAddress = getOptionalEnv("POLYMARKET_PROXY_WALLET_ADDRESS")
  123. || deriveProxyWallet(account.address, PROXY_FACTORY_ADDRESS);
  124. const depositWalletAddress = getOptionalEnv("POLYMARKET_DEPOSIT_WALLET_ADDRESS")
  125. || await proxyRelayer.deriveDepositWalletAddress();
  126. const [proxyBalance, depositBalance] = await Promise.all([
  127. publicClient.readContract({
  128. address: PUSD_ADDRESS,
  129. abi: erc20Abi,
  130. functionName: "balanceOf",
  131. args: [proxyWalletAddress],
  132. }),
  133. publicClient.readContract({
  134. address: PUSD_ADDRESS,
  135. abi: erc20Abi,
  136. functionName: "balanceOf",
  137. args: [depositWalletAddress],
  138. }),
  139. ]);
  140. const baseResult = {
  141. owner: account.address,
  142. proxyWalletAddress,
  143. depositWalletAddress,
  144. pUSD: PUSD_ADDRESS,
  145. proxyBalance: formatUnits(proxyBalance, PUSD_DECIMALS),
  146. depositBalance: formatUnits(depositBalance, PUSD_DECIMALS),
  147. execute,
  148. };
  149. const createApproveData = (spender) => {
  150. return encodeFunctionData({
  151. abi: erc20Abi,
  152. functionName: "approve",
  153. args: [spender, maxUint256],
  154. });
  155. }
  156. const runApprove = async () => {
  157. const wallets = [];
  158. if (walletMode === "proxy" || walletMode === "both") {
  159. wallets.push({
  160. type: "proxy",
  161. address: proxyWalletAddress,
  162. calls: clobSpenders.map(spender => ({
  163. to: PUSD_ADDRESS,
  164. data: createApproveData(spender),
  165. value: "0",
  166. })),
  167. });
  168. }
  169. if (walletMode === "deposit" || walletMode === "both") {
  170. wallets.push({
  171. type: "deposit",
  172. address: depositWalletAddress,
  173. calls: clobSpenders.map(spender => ({
  174. target: PUSD_ADDRESS,
  175. data: createApproveData(spender),
  176. value: "0",
  177. })),
  178. });
  179. }
  180. console.log(JSON.stringify({
  181. ...baseResult,
  182. action,
  183. wallet: walletMode,
  184. spenders: clobSpenders,
  185. approvals: wallets.map(item => ({
  186. type: item.type,
  187. address: item.address,
  188. spenderCount: item.calls.length,
  189. })),
  190. }, null, 2));
  191. if (!execute) {
  192. console.log(`Dry run only. Add --execute to submit CLOB approvals for ${walletMode} wallet.`);
  193. return;
  194. }
  195. const results = [];
  196. for (const wallet of wallets) {
  197. const response = wallet.type === "proxy"
  198. ? await proxyRelayer.execute(wallet.calls, "approve pUSD for CLOB")
  199. : await createRelayer().executeDepositWalletBatch(
  200. wallet.calls,
  201. depositWalletAddress,
  202. String(Math.floor(Date.now() / 1000) + 600),
  203. );
  204. const confirmed = await response.wait();
  205. results.push({
  206. type: wallet.type,
  207. address: wallet.address,
  208. transactionID: response.transactionID,
  209. transactionHash: response.transactionHash || response.hash,
  210. confirmed,
  211. });
  212. }
  213. console.log(JSON.stringify({ results }, null, 2));
  214. }
  215. const runTransfer = async () => {
  216. const transferAmount = parseUnits(amount, PUSD_DECIMALS);
  217. const result = {
  218. ...baseResult,
  219. action,
  220. from: direction.from,
  221. to: direction.to,
  222. sourceAddress: direction.from === "proxy" ? proxyWalletAddress : depositWalletAddress,
  223. destinationAddress: direction.to === "deposit" ? depositWalletAddress : proxyWalletAddress,
  224. amount,
  225. };
  226. console.log(JSON.stringify(result, null, 2));
  227. const sourceBalance = direction.from === "proxy" ? proxyBalance : depositBalance;
  228. if (sourceBalance < transferAmount) {
  229. throw new Error(`Insufficient ${direction.from} wallet pUSD balance: ${formatUnits(sourceBalance, PUSD_DECIMALS)}`);
  230. }
  231. if (!execute) {
  232. console.log(`Dry run only. Add --execute to submit the ${direction.from} -> ${direction.to} transfer.`);
  233. return;
  234. }
  235. const data = encodeFunctionData({
  236. abi: erc20Abi,
  237. functionName: "transfer",
  238. args: [result.destinationAddress, transferAmount],
  239. });
  240. const deadline = String(Math.floor(Date.now() / 1000) + 600);
  241. const depositRelayer = createRelayer();
  242. const response = direction.from === "proxy"
  243. ? await proxyRelayer.execute(
  244. [{
  245. to: PUSD_ADDRESS,
  246. data,
  247. value: "0",
  248. }],
  249. `transfer ${amount} pUSD from proxy to deposit wallet`,
  250. )
  251. : await depositRelayer.executeDepositWalletBatch(
  252. [{
  253. target: PUSD_ADDRESS,
  254. data,
  255. value: "0",
  256. }],
  257. depositWalletAddress,
  258. deadline,
  259. );
  260. const confirmed = await response.wait();
  261. console.log(JSON.stringify({
  262. transactionID: response.transactionID,
  263. transactionHash: response.transactionHash || response.hash,
  264. confirmed,
  265. }, null, 2));
  266. }
  267. if (action === "approve") {
  268. await runApprove();
  269. }
  270. else {
  271. await runTransfer();
  272. }