flyzto 2 周之前
父節點
當前提交
e89f61ba4c

+ 144 - 0
packages/pinnacle-sdk/index.js

@@ -0,0 +1,144 @@
+import axios from "axios";
+import { randomUUID } from "crypto";
+import { HttpsProxyAgent } from "https-proxy-agent";
+
+const PINNACLE_HOST = "https://api.pinnacle888.com";
+
+const axiosDefaultOptions = {
+  baseURL: "",
+  url: "",
+  method: "GET",
+  headers: {},
+  params: {},
+  data: {},
+  timeout: 10000,
+};
+
+const cleanUndefined = (obj) => {
+  return Object.fromEntries(
+    Object.entries(obj).filter(([, value]) => value !== undefined)
+  );
+}
+
+export const createPinnacleSdk = (config = {}) => {
+  const proxyAgent = config.httpProxy ? new HttpsProxyAgent(config.httpProxy) : undefined;
+
+  const pinnacleRequest = async (options, channel) => {
+    const { url, ...optionsRest } = options;
+    const { username, password } = config;
+    if (!url || !channel && (!username || !password)) {
+      throw new Error("url、username、password、channel is required");
+    }
+
+    const authHeader = channel
+      ? `Basic ${channel}`
+      : `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
+    const axiosConfig = {
+      ...axiosDefaultOptions,
+      ...optionsRest,
+      url,
+      baseURL: PINNACLE_HOST,
+      proxy: false,
+      ...(proxyAgent ? { httpsAgent: proxyAgent } : {}),
+      headers: {
+        ...axiosDefaultOptions.headers,
+        ...optionsRest.headers,
+        Authorization: authHeader,
+        Accept: "application/json",
+      },
+    };
+
+    return axios(axiosConfig).then(res => res.data);
+  }
+
+  const pinnacleGet = async (url, params, channel) => {
+    return pinnacleRequest({ url, params }, channel)
+    .catch(err => {
+      if (err.response?.data) {
+        err.data = err.response.data;
+        err.cause = err.response.status;
+      }
+      return Promise.reject(err);
+    });
+  }
+
+  const pinnaclePost = async (url, data, channel) => {
+    return pinnacleRequest({
+      url,
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      data,
+    }, channel);
+  }
+
+  const getLineInfo = async ({ info = {}, channel } = {}) => {
+    const {
+      leagueId, eventId, betType, handicap, team, side,
+      specialId, contestantId,
+      periodNumber = 0, oddsFormat = "Decimal", sportId = 29,
+    } = info;
+    let url = "/v2/line/";
+    let data = { sportId, leagueId, eventId, betType, handicap, periodNumber, team, side, oddsFormat };
+    if (specialId) {
+      url = "/v2/line/special";
+      data = { specialId, contestantId, oddsFormat };
+    }
+    data = cleanUndefined(data);
+    return pinnacleGet(url, data, channel)
+    .then(ret => ({ info: ret, line: data }));
+  }
+
+  const getAccountBalance = async (channel) => {
+    return pinnacleGet("/v1/client/balance", undefined, channel);
+  }
+
+  const placeOrder = async ({ info, line, stakeSize } = {}, channel) => {
+    const uuid = randomUUID();
+    if (line.specialId) {
+      const data = cleanUndefined({
+        oddsFormat: line.oddsFormat,
+        uniqueRequestId: uuid,
+        acceptBetterLine: true,
+        stake: stakeSize,
+        winRiskStake: "RISK",
+        lineId: info.lineId,
+        specialId: info.specialId,
+        contestantId: info.contestantId,
+      });
+      return pinnaclePost("/v4/bets/special", { bets: [data] }, channel)
+      .then(ret => ret.bets?.[0] ?? ret);
+    }
+
+    const data = cleanUndefined({
+      oddsFormat: line.oddsFormat,
+      uniqueRequestId: uuid,
+      acceptBetterLine: true,
+      stake: stakeSize,
+      winRiskStake: "RISK",
+      lineId: info.lineId,
+      altLineId: info.altLineId,
+      fillType: "NORMAL",
+      sportId: line.sportId,
+      eventId: line.eventId,
+      periodNumber: line.periodNumber,
+      betType: line.betType,
+      team: line.team,
+      side: line.side,
+      handicap: line.handicap,
+    });
+    return pinnaclePost("/v4/bets/place", data, channel);
+  }
+
+  return {
+    pinnacleRequest,
+    pinnacleGet,
+    pinnaclePost,
+    getLineInfo,
+    getAccountBalance,
+    placeOrder,
+  };
+}
+
+export default createPinnacleSdk;

+ 369 - 0
packages/pinnacle-sdk/package-lock.json

@@ -0,0 +1,369 @@
+{
+  "name": "@ppai/pinnacle-sdk",
+  "version": "0.0.1",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "@ppai/pinnacle-sdk",
+      "version": "0.0.1",
+      "dependencies": {
+        "axios": "^1.13.3",
+        "https-proxy-agent": "^7.0.6"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz",
+      "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/axios/node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/axios/node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz",
+      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    }
+  }
+}

+ 10 - 0
packages/pinnacle-sdk/package.json

@@ -0,0 +1,10 @@
+{
+  "name": "@ppai/pinnacle-sdk",
+  "version": "0.0.1",
+  "type": "module",
+  "main": "index.js",
+  "dependencies": {
+    "axios": "^1.13.3",
+    "https-proxy-agent": "^7.0.6"
+  }
+}

+ 573 - 0
packages/polymarket-sdk/index.js

@@ -0,0 +1,573 @@
+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;

+ 2285 - 0
packages/polymarket-sdk/package-lock.json

@@ -0,0 +1,2285 @@
+{
+  "name": "@ppai/polymarket-sdk",
+  "version": "0.0.1",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "@ppai/polymarket-sdk",
+      "version": "0.0.1",
+      "dependencies": {
+        "@polymarket/builder-relayer-client": "^0.0.9",
+        "@polymarket/builder-signing-sdk": "^1.0.0",
+        "@polymarket/clob-client-v2": "^1.0.6",
+        "axios": "^1.13.3",
+        "https-proxy-agent": "^7.0.6",
+        "qs": "^6.14.1",
+        "viem": "^2.50.3"
+      }
+    },
+    "node_modules/@adraffy/ens-normalize": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmmirror.com/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
+      "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
+      "license": "MIT"
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
+      "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
+      "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
+      "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
+      "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
+      "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
+      "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
+      "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
+      "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
+      "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
+      "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
+      "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
+      "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
+      "cpu": [
+        "loong64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
+      "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
+      "cpu": [
+        "mips64el"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
+      "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
+      "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
+      "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
+      "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
+      "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
+      "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
+      "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
+      "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
+      "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
+      "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
+      "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
+      "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
+      "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@ethersproject/abi": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/abi/-/abi-5.8.0.tgz",
+      "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/abstract-provider": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz",
+      "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/networks": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/web": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/abstract-signer": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz",
+      "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/address": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/address/-/address-5.8.0.tgz",
+      "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/rlp": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/base64": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/base64/-/base64-5.8.0.tgz",
+      "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/basex": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/basex/-/basex-5.8.0.tgz",
+      "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/bignumber": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/bignumber/-/bignumber-5.8.0.tgz",
+      "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "bn.js": "^5.2.1"
+      }
+    },
+    "node_modules/@ethersproject/bytes": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/bytes/-/bytes-5.8.0.tgz",
+      "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/constants": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/constants/-/constants-5.8.0.tgz",
+      "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/contracts": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/contracts/-/contracts-5.8.0.tgz",
+      "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abi": "^5.8.0",
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/hash": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/hash/-/hash-5.8.0.tgz",
+      "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/base64": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/hdnode": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/hdnode/-/hdnode-5.8.0.tgz",
+      "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/basex": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/pbkdf2": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0",
+        "@ethersproject/signing-key": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/wordlists": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/json-wallets": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz",
+      "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/hdnode": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/pbkdf2": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/random": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "aes-js": "3.0.0",
+        "scrypt-js": "3.0.1"
+      }
+    },
+    "node_modules/@ethersproject/keccak256": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/keccak256/-/keccak256-5.8.0.tgz",
+      "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "js-sha3": "0.8.0"
+      }
+    },
+    "node_modules/@ethersproject/logger": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/logger/-/logger-5.8.0.tgz",
+      "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/@ethersproject/networks": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/networks/-/networks-5.8.0.tgz",
+      "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/pbkdf2": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz",
+      "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/properties": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/properties/-/properties-5.8.0.tgz",
+      "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/providers": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/providers/-/providers-5.8.0.tgz",
+      "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/base64": "^5.8.0",
+        "@ethersproject/basex": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/networks": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/random": "^5.8.0",
+        "@ethersproject/rlp": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/web": "^5.8.0",
+        "bech32": "1.1.4",
+        "ws": "8.18.0"
+      }
+    },
+    "node_modules/@ethersproject/random": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/random/-/random-5.8.0.tgz",
+      "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/rlp": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/rlp/-/rlp-5.8.0.tgz",
+      "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/sha2": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/sha2/-/sha2-5.8.0.tgz",
+      "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "hash.js": "1.1.7"
+      }
+    },
+    "node_modules/@ethersproject/signing-key": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/signing-key/-/signing-key-5.8.0.tgz",
+      "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "bn.js": "^5.2.1",
+        "elliptic": "6.6.1",
+        "hash.js": "1.1.7"
+      }
+    },
+    "node_modules/@ethersproject/solidity": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/solidity/-/solidity-5.8.0.tgz",
+      "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/strings": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/strings/-/strings-5.8.0.tgz",
+      "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/transactions": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/transactions/-/transactions-5.8.0.tgz",
+      "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/rlp": "^5.8.0",
+        "@ethersproject/signing-key": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/units": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/units/-/units-5.8.0.tgz",
+      "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/wallet": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/wallet/-/wallet-5.8.0.tgz",
+      "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/hdnode": "^5.8.0",
+        "@ethersproject/json-wallets": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/random": "^5.8.0",
+        "@ethersproject/signing-key": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/wordlists": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/web": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/web/-/web-5.8.0.tgz",
+      "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/base64": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/wordlists": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/@ethersproject/wordlists/-/wordlists-5.8.0.tgz",
+      "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@noble/ciphers": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-1.3.0.tgz",
+      "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/curves": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmmirror.com/@noble/curves/-/curves-1.9.1.tgz",
+      "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "1.8.0"
+      },
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/hashes": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz",
+      "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@polymarket/builder-abstract-signer": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/@polymarket/builder-abstract-signer/-/builder-abstract-signer-0.0.1.tgz",
+      "integrity": "sha512-XuuxQQcXYtqQce8slhqiJQti1lVPr+xXC7M3lbetmNnsc9tlYdYRnojE+tcniCHg7VQ6dokcIu0eLYDtM5vdvQ==",
+      "dependencies": {
+        "ethers": "5.8.0",
+        "ts-node": "^9.1.1",
+        "typescript": "^5.8.3",
+        "viem": "^2.31.4"
+      }
+    },
+    "node_modules/@polymarket/builder-relayer-client": {
+      "version": "0.0.9",
+      "resolved": "https://registry.npmmirror.com/@polymarket/builder-relayer-client/-/builder-relayer-client-0.0.9.tgz",
+      "integrity": "sha512-7i5LSSpSFZiNuoLY623krWWEiT/jkxv76tueGTNOVy3ZND+2sJvOJk5+PQFgiws+LNa476UFOodkMEgqH5lSqw==",
+      "dependencies": {
+        "@polymarket/builder-abstract-signer": "0.0.1",
+        "@polymarket/builder-signing-sdk": "^0.0.8",
+        "axios": "^0.27.2",
+        "browser-or-node": "^3.0.0",
+        "ethers": "5.8.0",
+        "tsx": "^4.20.3",
+        "typescript": "^5.8.3",
+        "viem": "^2.31.4"
+      }
+    },
+    "node_modules/@polymarket/builder-relayer-client/node_modules/@polymarket/builder-signing-sdk": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmmirror.com/@polymarket/builder-signing-sdk/-/builder-signing-sdk-0.0.8.tgz",
+      "integrity": "sha512-rZLCFxEdYahl5FiJmhe22RDXysS1ibFJlWz4NT0s3itJRYq3XJzXXHXEZkAQplU+nIS1IlbbKjA4zDQaeCyYtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "^18.7.18",
+        "axios": "^1.12.2",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@polymarket/builder-relayer-client/node_modules/@polymarket/builder-signing-sdk/node_modules/axios": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz",
+      "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/@polymarket/builder-relayer-client/node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/@polymarket/builder-relayer-client/node_modules/axios": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-0.27.2.tgz",
+      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.14.9",
+        "form-data": "^4.0.0"
+      }
+    },
+    "node_modules/@polymarket/builder-relayer-client/node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/@polymarket/builder-signing-sdk": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/@polymarket/builder-signing-sdk/-/builder-signing-sdk-1.0.0.tgz",
+      "integrity": "sha512-LNHc8Ox+2ITM6+VIEH5LH3SVh9ScE2mOUAJI789YfyNqhF4n1cNulk35Hb6IZfy1xtNfcEfNotanPLa/U8dCaw==",
+      "license": "MIT",
+      "dependencies": {
+        "axios": "^1.12.2"
+      }
+    },
+    "node_modules/@polymarket/clob-client-v2": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmmirror.com/@polymarket/clob-client-v2/-/clob-client-v2-1.0.6.tgz",
+      "integrity": "sha512-Y6LefFNuxRs95fLq0m9W9EBjoM2w7EVgtsUGLydjlUFVGBeGhHOa8vll6+C6UZVnQEbdWE6i6hYRCwJE/ZVNKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/providers": "^5.8.0",
+        "@ethersproject/wallet": "^5.8.0",
+        "axios": "^1.0.0",
+        "browser-or-node": "^3.0.0",
+        "tslib": "^2.8.1",
+        "viem": "^2.46.3"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@scure/base": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmmirror.com/@scure/base/-/base-1.2.6.tgz",
+      "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip32": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmmirror.com/@scure/bip32/-/bip32-1.7.0.tgz",
+      "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "~1.9.0",
+        "@noble/hashes": "~1.8.0",
+        "@scure/base": "~1.2.5"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip39": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/@scure/bip39/-/bip39-1.6.0.tgz",
+      "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "~1.8.0",
+        "@scure/base": "~1.2.5"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "18.19.130",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz",
+      "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/abitype": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/abitype/-/abitype-1.2.3.tgz",
+      "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/wevm"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4",
+        "zod": "^3.22.0 || ^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "zod": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/aes-js": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/aes-js/-/aes-js-3.0.0.tgz",
+      "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==",
+      "license": "MIT"
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz",
+      "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/axios/node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/axios/node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/bech32": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/bech32/-/bech32-1.1.4.tgz",
+      "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==",
+      "license": "MIT"
+    },
+    "node_modules/bn.js": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-5.2.3.tgz",
+      "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
+      "license": "MIT"
+    },
+    "node_modules/brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+      "license": "MIT"
+    },
+    "node_modules/browser-or-node": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/browser-or-node/-/browser-or-node-3.0.0.tgz",
+      "integrity": "sha512-iczIdVJzGEYhP5DqQxYM9Hh7Ztpqqi+CXZpSmX8ALFs9ecXkQIeqRyM6TfxEfMVpwhl3dSuDvxdzzo9sUOIVBQ==",
+      "license": "MIT"
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "license": "MIT"
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/diff": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.4.tgz",
+      "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/elliptic": {
+      "version": "6.6.1",
+      "resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.6.1.tgz",
+      "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
+      "license": "MIT",
+      "dependencies": {
+        "bn.js": "^4.11.9",
+        "brorand": "^1.1.0",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.1",
+        "inherits": "^2.0.4",
+        "minimalistic-assert": "^1.0.1",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/elliptic/node_modules/bn.js": {
+      "version": "4.12.3",
+      "resolved": "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.3.tgz",
+      "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
+      "license": "MIT"
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.0.tgz",
+      "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.28.0",
+        "@esbuild/android-arm": "0.28.0",
+        "@esbuild/android-arm64": "0.28.0",
+        "@esbuild/android-x64": "0.28.0",
+        "@esbuild/darwin-arm64": "0.28.0",
+        "@esbuild/darwin-x64": "0.28.0",
+        "@esbuild/freebsd-arm64": "0.28.0",
+        "@esbuild/freebsd-x64": "0.28.0",
+        "@esbuild/linux-arm": "0.28.0",
+        "@esbuild/linux-arm64": "0.28.0",
+        "@esbuild/linux-ia32": "0.28.0",
+        "@esbuild/linux-loong64": "0.28.0",
+        "@esbuild/linux-mips64el": "0.28.0",
+        "@esbuild/linux-ppc64": "0.28.0",
+        "@esbuild/linux-riscv64": "0.28.0",
+        "@esbuild/linux-s390x": "0.28.0",
+        "@esbuild/linux-x64": "0.28.0",
+        "@esbuild/netbsd-arm64": "0.28.0",
+        "@esbuild/netbsd-x64": "0.28.0",
+        "@esbuild/openbsd-arm64": "0.28.0",
+        "@esbuild/openbsd-x64": "0.28.0",
+        "@esbuild/openharmony-arm64": "0.28.0",
+        "@esbuild/sunos-x64": "0.28.0",
+        "@esbuild/win32-arm64": "0.28.0",
+        "@esbuild/win32-ia32": "0.28.0",
+        "@esbuild/win32-x64": "0.28.0"
+      }
+    },
+    "node_modules/ethers": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmmirror.com/ethers/-/ethers-5.8.0.tgz",
+      "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abi": "5.8.0",
+        "@ethersproject/abstract-provider": "5.8.0",
+        "@ethersproject/abstract-signer": "5.8.0",
+        "@ethersproject/address": "5.8.0",
+        "@ethersproject/base64": "5.8.0",
+        "@ethersproject/basex": "5.8.0",
+        "@ethersproject/bignumber": "5.8.0",
+        "@ethersproject/bytes": "5.8.0",
+        "@ethersproject/constants": "5.8.0",
+        "@ethersproject/contracts": "5.8.0",
+        "@ethersproject/hash": "5.8.0",
+        "@ethersproject/hdnode": "5.8.0",
+        "@ethersproject/json-wallets": "5.8.0",
+        "@ethersproject/keccak256": "5.8.0",
+        "@ethersproject/logger": "5.8.0",
+        "@ethersproject/networks": "5.8.0",
+        "@ethersproject/pbkdf2": "5.8.0",
+        "@ethersproject/properties": "5.8.0",
+        "@ethersproject/providers": "5.8.0",
+        "@ethersproject/random": "5.8.0",
+        "@ethersproject/rlp": "5.8.0",
+        "@ethersproject/sha2": "5.8.0",
+        "@ethersproject/signing-key": "5.8.0",
+        "@ethersproject/solidity": "5.8.0",
+        "@ethersproject/strings": "5.8.0",
+        "@ethersproject/transactions": "5.8.0",
+        "@ethersproject/units": "5.8.0",
+        "@ethersproject/wallet": "5.8.0",
+        "@ethersproject/web": "5.8.0",
+        "@ethersproject/wordlists": "5.8.0"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+      "license": "MIT"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmmirror.com/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz",
+      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+      "license": "MIT",
+      "dependencies": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/isows": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/isows/-/isows-1.0.7.tgz",
+      "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "peerDependencies": {
+        "ws": "*"
+      }
+    },
+    "node_modules/js-sha3": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmmirror.com/js-sha3/-/js-sha3-0.8.0.tgz",
+      "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
+      "license": "MIT"
+    },
+    "node_modules/make-error": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz",
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+      "license": "ISC"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "license": "ISC"
+    },
+    "node_modules/minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+      "license": "MIT"
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/ox": {
+      "version": "0.14.26-386a343.0",
+      "resolved": "https://pkg.pr.new/ox@386a3439fe1ce76d237930f8c6e6bb493746069a",
+      "integrity": "sha512-OHHm9re1yVjiMN66GZ2JSGuqmvJPrk40zh3PIS/3I6prZLbt6U/zKlgW18eIIkO0Y/ZyySKr6D/4mUXjBmky1g==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@adraffy/ens-normalize": "^1.11.0",
+        "@noble/ciphers": "^1.3.0",
+        "@noble/curves": "1.9.1",
+        "@noble/hashes": "^1.8.0",
+        "@scure/bip32": "^1.7.0",
+        "@scure/bip39": "^1.6.0",
+        "abitype": "^1.2.3",
+        "eventemitter3": "5.0.1"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.15.2",
+      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz",
+      "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/scrypt-js": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/scrypt-js/-/scrypt-js-3.0.1.tgz",
+      "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==",
+      "license": "MIT"
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.21",
+      "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz",
+      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/ts-node": {
+      "version": "9.1.1",
+      "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-9.1.1.tgz",
+      "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==",
+      "license": "MIT",
+      "dependencies": {
+        "arg": "^4.1.0",
+        "create-require": "^1.1.0",
+        "diff": "^4.0.1",
+        "make-error": "^1.1.1",
+        "source-map-support": "^0.5.17",
+        "yn": "3.1.1"
+      },
+      "bin": {
+        "ts-node": "dist/bin.js",
+        "ts-node-script": "dist/bin-script.js",
+        "ts-node-transpile-only": "dist/bin-transpile.js",
+        "ts-script": "dist/bin-script-deprecated.js"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "typescript": ">=2.7"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/tsx": {
+      "version": "4.22.3",
+      "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.3.tgz",
+      "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==",
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "~0.28.0"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "license": "MIT"
+    },
+    "node_modules/viem": {
+      "version": "2.51.0",
+      "resolved": "https://registry.npmmirror.com/viem/-/viem-2.51.0.tgz",
+      "integrity": "sha512-8C0Ca+eEapXE29vHMUW59NqKENl1X4s9P6xSNC9Nvw6EvAeAhn/LNUlgztk6TOw7KN1Gzz5a/n9Wv4okUfmY9g==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "1.9.1",
+        "@noble/hashes": "1.8.0",
+        "@scure/bip32": "1.7.0",
+        "@scure/bip39": "1.6.0",
+        "abitype": "1.2.3",
+        "isows": "1.0.7",
+        "ox": "https://pkg.pr.new/ox@386a3439fe1ce76d237930f8c6e6bb493746069a",
+        "ws": "8.20.1"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/viem/node_modules/ws": {
+      "version": "8.20.1",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.1.tgz",
+      "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz",
+      "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    }
+  }
+}

+ 15 - 0
packages/polymarket-sdk/package.json

@@ -0,0 +1,15 @@
+{
+  "name": "@ppai/polymarket-sdk",
+  "version": "0.0.1",
+  "type": "module",
+  "main": "index.js",
+  "dependencies": {
+    "@polymarket/builder-relayer-client": "^0.0.9",
+    "@polymarket/builder-signing-sdk": "^1.0.0",
+    "@polymarket/clob-client-v2": "^1.0.6",
+    "axios": "^1.13.3",
+    "https-proxy-agent": "^7.0.6",
+    "qs": "^6.14.1",
+    "viem": "^2.50.3"
+  }
+}

+ 224 - 0
polymarket/libs/bestBidAskSizeWatcher.js

@@ -0,0 +1,224 @@
+import { EventEmitter } from "node:events";
+
+import { MarketWsClient } from "./polymarketClient.js";
+
+const normalizeTokenIds = (tokenIds) => {
+  if (!Array.isArray(tokenIds)) {
+    throw new Error("tokenIds must be an array", { cause: 400 });
+  }
+  return [...new Set(tokenIds.map(String).filter(Boolean))];
+};
+
+const normalizeSize = (size) => {
+  const value = Number(size);
+  return Number.isFinite(value) ? value : 0;
+};
+
+const applyLevels = (levels = [], target) => {
+  target.clear();
+  levels.forEach(({ price, size }) => {
+    if (!price) {
+      return;
+    }
+    const normalizedSize = normalizeSize(size);
+    if (normalizedSize > 0) {
+      target.set(String(price), String(size));
+    }
+  });
+};
+
+const updateLevel = (target, price, size) => {
+  if (!price) {
+    return;
+  }
+  if (normalizeSize(size) <= 0) {
+    target.delete(String(price));
+    return;
+  }
+  target.set(String(price), String(size));
+};
+
+const getBestLevel = (levels, compare) => {
+  let bestPrice;
+  let bestSize;
+
+  levels.forEach((size, price) => {
+    if (bestPrice === undefined || compare(Number(price), Number(bestPrice))) {
+      bestPrice = price;
+      bestSize = size;
+    }
+  });
+
+  if (bestPrice === undefined) {
+    return { price: null, size: null };
+  }
+  return { price: bestPrice, size: bestSize };
+};
+
+const parseSide = (side) => {
+  const normalizedSide = String(side || "").toLowerCase();
+  if (["ask", "asks", "sell"].includes(normalizedSide)) {
+    return "asks";
+  }
+  if (["bid", "bids", "buy"].includes(normalizedSide)) {
+    return "bids";
+  }
+  return "";
+};
+
+class BestBidAskSizeWatcher extends EventEmitter {
+  #tokenIds = [];
+  #client;
+  #books = new Map();
+  #started = false;
+
+  constructor(tokenIds = []) {
+    super();
+    this.#client = new MarketWsClient();
+    this.subscribe(tokenIds);
+  }
+
+  get tokenIds() {
+    return [...this.#tokenIds];
+  }
+
+  start() {
+    if (this.#started) {
+      return this;
+    }
+    this.#started = true;
+    this.#client.on("open", () => {
+      if (this.#tokenIds.length > 0) {
+        this.#client.subscribeToTokensIds(this.#tokenIds);
+      }
+      this.emit("open");
+    });
+    this.#client.on("message", data => this.#handleMessage(data));
+    this.#client.on("error", error => this.emit("error", error));
+    this.#client.on("close", event => this.emit("close", event));
+    this.#client.connect();
+    return this;
+  }
+
+  stop() {
+    this.#client.close();
+    this.#started = false;
+    return this;
+  }
+
+  subscribe(tokenIds) {
+    const ids = normalizeTokenIds(tokenIds);
+    const newIds = ids.filter(tokenId => !this.#books.has(tokenId));
+    if (newIds.length === 0) {
+      return this;
+    }
+
+    this.#tokenIds = [...this.#tokenIds, ...newIds];
+    newIds.forEach(tokenId => {
+      this.#books.set(tokenId, { asks: new Map(), bids: new Map() });
+    });
+
+    if (this.#started) {
+      this.#client.subscribeToTokensIds(newIds);
+    }
+    return this;
+  }
+
+  unsubscribe(tokenIds) {
+    const ids = normalizeTokenIds(tokenIds);
+    const removeIds = ids.filter(tokenId => this.#books.has(tokenId));
+    if (removeIds.length === 0) {
+      return this;
+    }
+
+    const removeIdSet = new Set(removeIds);
+    this.#tokenIds = this.#tokenIds.filter(tokenId => !removeIdSet.has(tokenId));
+    removeIds.forEach(tokenId => {
+      this.#books.delete(tokenId);
+    });
+
+    if (this.#started) {
+      this.#client.unsubscribeToTokensIds(removeIds);
+    }
+    return this;
+  }
+
+  snapshot() {
+    return Object.fromEntries(
+      this.#tokenIds.map(tokenId => [tokenId, this.#getBestBidAskSize(tokenId)])
+    );
+  }
+
+  #getBook(tokenId) {
+    const id = String(tokenId);
+    if (!this.#books.has(id)) {
+      this.#books.set(id, { asks: new Map(), bids: new Map() });
+    }
+    return this.#books.get(id);
+  }
+
+  #getBestBidAskSize(tokenId) {
+    const book = this.#getBook(tokenId);
+    const ask = getBestLevel(book.asks, (price, bestPrice) => price < bestPrice);
+    const bid = getBestLevel(book.bids, (price, bestPrice) => price > bestPrice);
+    return {
+      tokenId: String(tokenId),
+      best_ask: ask.price,
+      best_ask_size: ask.size,
+      best_bid: bid.price,
+      best_bid_size: bid.size,
+    };
+  }
+
+  #emitUpdate(tokenId) {
+    const data = this.#getBestBidAskSize(tokenId);
+    this.emit("update", data);
+    this.emit(String(tokenId), data);
+  }
+
+  #handleBook({ asset_id, asks = [], bids = [] }) {
+    const tokenId = String(asset_id);
+    if (!this.#books.has(tokenId)) {
+      return;
+    }
+    const book = this.#getBook(tokenId);
+    applyLevels(asks, book.asks);
+    applyLevels(bids, book.bids);
+    this.#emitUpdate(tokenId);
+  }
+
+  #handlePriceChanges(priceChanges = []) {
+    const changedTokenIds = new Set();
+    priceChanges.forEach(({ asset_id, price, size, side }) => {
+      const tokenId = String(asset_id);
+      if (!this.#books.has(tokenId)) {
+        return;
+      }
+      const parsedSide = parseSide(side);
+      if (!parsedSide) {
+        return;
+      }
+      const book = this.#getBook(tokenId);
+      updateLevel(book[parsedSide], price, size);
+      changedTokenIds.add(tokenId);
+    });
+    changedTokenIds.forEach(tokenId => this.#emitUpdate(tokenId));
+  }
+
+  #handleMessage(data) {
+    switch (data?.event_type) {
+      case "book":
+        this.#handleBook(data);
+        break;
+      case "price_change":
+        this.#handlePriceChanges(data.price_changes);
+        break;
+    }
+  }
+}
+
+export const watchBestBidAskSizes = (tokenIds) => {
+  return new BestBidAskSizeWatcher(tokenIds).start();
+};
+
+export default BestBidAskSizeWatcher;

+ 42 - 23
polymarket/libs/parseMarkets.js

@@ -78,7 +78,7 @@ const parseOutcomes = (outcomes, clobTokenIds, bestBid=0, bestAsk=0) => {
       best_ask = (1 - bestBid).toFixed(3);
       best_bid = (1 - bestAsk).toFixed(3);
     }
-    obj[key] = { id: ids[index], best_ask, best_bid };
+    obj[key] = { id: ids[index], best_ask, best_ask_size: 0, best_bid, best_bid_size: 0 };
     return obj;
   }, {});
 }
@@ -162,10 +162,14 @@ export const parseOddsAsk = (markets) => {
     if (key === 'moneyline') {
       Object.keys(marketData).forEach(side => {
         const askYes = +marketData[side].outcomes['Yes']['best_ask'];
-        const tokenYes = marketData[side].outcomes['Yes']['id'];
+        const askSizeYes = +marketData[side].outcomes['Yes']['best_ask_size'];
+        const maxYes = fixFloat(askYes * askSizeYes);
+        // const tokenYes = marketData[side].outcomes['Yes']['id'];
         const askNo = +marketData[side].outcomes['No']['best_ask'];
-        const tokenNo = marketData[side].outcomes['No']['id'];
-        const slug = marketData[side].market.slug;
+        const askSizeNo = +marketData[side].outcomes['No']['best_ask_size'];
+        const maxNo = fixFloat(askNo * askSizeNo);
+        // const tokenNo = marketData[side].outcomes['No']['id'];
+        // const slug = marketData[side].market.slug;
         if (askYes <= 0.1 || askNo <= 0.1) {
           return;
         }
@@ -189,18 +193,22 @@ export const parseOddsAsk = (markets) => {
             iorKeyNo = 'ior_moc';
             break;
         }
-        odds[iorKeyYes] = { v: iorYes, b: -feeYes, t: 1, ask: askYes, token: tokenYes, slug };
-        odds[iorKeyNo] = { v: iorNo, b: -feeNo, t: 1, ask: askNo, token: tokenNo, slug };
+        odds[iorKeyYes] = { v: iorYes, b: -feeYes, t: 1, ask: askYes, ask_size: askSizeYes, m: maxYes, /*token: tokenYes, slug */ };
+        odds[iorKeyNo] = { v: iorNo, b: -feeNo, t: 1, ask: askNo, ask_size: askSizeNo, m: maxNo, /*token: tokenNo, slug */ };
       });
     }
     else if (key === 'spreads') {
       Object.keys(marketData).forEach(handicap => {
         const ratio = +handicap;
         const askHome = +marketData[handicap].outcomes['Home']['best_ask'];
-        const tokenHome = marketData[handicap].outcomes['Home']['id'];
+        const askSizeHome = +marketData[handicap].outcomes['Home']['best_ask_size'];
+        const maxHome = fixFloat(askHome * askSizeHome);
+        // const tokenHome = marketData[handicap].outcomes['Home']['id'];
         const askAway = +marketData[handicap].outcomes['Away']['best_ask'];
-        const tokenAway = marketData[handicap].outcomes['Away']['id'];
-        const slug = marketData[handicap].market.slug;
+        const askSizeAway = +marketData[handicap].outcomes['Away']['best_ask_size'];
+        const maxAway = fixFloat(askAway * askSizeAway);
+        // const tokenAway = marketData[handicap].outcomes['Away']['id'];
+        // const slug = marketData[handicap].market.slug;
         if (askHome <= 0.1 || askAway <= 0.1) {
           return;
         }
@@ -208,17 +216,21 @@ export const parseOddsAsk = (markets) => {
         const iorAway = fixFloat(1 / askAway);
         const feeHome = parseAskFee(askHome);
         const feeAway = parseAskFee(askAway);
-        odds[`ior_r${ratioAccept(ratio)}h_${ratioString(ratio)}`] = { v: iorHome, b: -feeHome, t: 1, ask: askHome, token: tokenHome, slug };
-        odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, b: -feeAway, t: 1, ask: askAway, token: tokenAway, slug };
+        odds[`ior_r${ratioAccept(ratio)}h_${ratioString(ratio)}`] = { v: iorHome, b: -feeHome, t: 1, ask: askHome, ask_size: askSizeHome, m: maxHome, /*token: tokenHome, slug */ };
+        odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, b: -feeAway, t: 1, ask: askAway, ask_size: askSizeAway, m: maxAway, /*token: tokenAway, slug */ };
       });
     }
     else if (key === 'totals') {
       Object.keys(marketData).forEach(handicap => {
         const ratio = +handicap;
         const askOver = +marketData[handicap].outcomes['Over']['best_ask'];
-        const tokenOver = marketData[handicap].outcomes['Over']['id'];
+        const askSizeOver = +marketData[handicap].outcomes['Over']['best_ask_size'];
+        const maxOver = fixFloat(askOver * askSizeOver);
+        // const tokenOver = marketData[handicap].outcomes['Over']['id'];
         const askUnder = +marketData[handicap].outcomes['Under']['best_ask'];
-        const tokenUnder = marketData[handicap].outcomes['Under']['id'];
+        const askSizeUnder = +marketData[handicap].outcomes['Under']['best_ask_size'];
+        const maxUnder = fixFloat(askUnder * askSizeUnder);
+        // const tokenUnder = marketData[handicap].outcomes['Under']['id'];
         const slug = marketData[handicap].market.slug;
         if (askOver <= 0.1 || askUnder <= 0.1) {
           return;
@@ -227,8 +239,8 @@ export const parseOddsAsk = (markets) => {
         const iorUnder = fixFloat(1 / askUnder);
         const feeOver = parseAskFee(askOver);
         const feeUnder = parseAskFee(askUnder);
-        odds[`ior_ouc_${ratioString(ratio)}`] = { v: iorOver, b: -feeOver, t: 1, ask: askOver, token: tokenOver, slug };
-        odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, b: -feeUnder, t: 1, ask: askUnder, token: tokenUnder, slug };
+        odds[`ior_ouc_${ratioString(ratio)}`] = { v: iorOver, b: -feeOver, t: 1, ask: askOver, ask_size: askSizeOver, m: maxOver, /*token: tokenOver, slug */ };
+        odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, b: -feeUnder, t: 1, ask: askUnder, ask_size: askSizeUnder, m: maxUnder, /*token: tokenUnder, slug */ };
       });
     }
   });
@@ -251,9 +263,11 @@ export const parseOddsBid = (markets) => {
       Object.keys(marketData).forEach(side => {
         const askYes = +marketData[side].outcomes['Yes']['best_ask'];
         const bidYes = +marketData[side].outcomes['Yes']['best_bid'];
+        const bidSizeYes = +marketData[side].outcomes['Yes']['best_bid_size'];
         // const tokenYes = marketData[side].outcomes['Yes']['id'];
         const askNo = +marketData[side].outcomes['No']['best_ask'];
         const bidNo = +marketData[side].outcomes['No']['best_bid'];
+        const bidSizeNo = +marketData[side].outcomes['No']['best_bid_size'];
         // const tokenNo = marketData[side].outcomes['No']['id'];
         // const slug = marketData[side].market.slug;
         const tick_size = marketData[side].market.orderPriceMinTickSize;
@@ -282,8 +296,8 @@ export const parseOddsBid = (markets) => {
             iorKeyNo = 'ior_moc';
             break;
         }
-        odds[iorKeyYes] = { v: iorYes, b: rebateYes, t: 1, ask: askYes, bid: bidYes, bid_ex: bidPriceYes, tick_size, /*token: tokenYes, slug */ };
-        odds[iorKeyNo] = { v: iorNo, b: rebateNo, t: 1, ask: askNo, bid: bidNo, bid_ex: bidPriceNo, tick_size, /*token: tokenNo, slug */ };
+        odds[iorKeyYes] = { v: iorYes, b: rebateYes, t: 1, ask: askYes, bid: bidYes, bid_size: bidSizeYes, bid_ex: bidPriceYes, tick_size, /*token: tokenYes, slug */ };
+        odds[iorKeyNo] = { v: iorNo, b: rebateNo, t: 1, ask: askNo, bid: bidNo, bid_size: bidSizeNo, bid_ex: bidPriceNo, tick_size, /*token: tokenNo, slug */ };
       });
     }
     else if (key === 'spreads') {
@@ -291,9 +305,11 @@ export const parseOddsBid = (markets) => {
         const ratio = +handicap;
         const askHome = +marketData[handicap].outcomes['Home']['best_ask'];
         const bidHome = +marketData[handicap].outcomes['Home']['best_bid'];
+        const bidSizeHome = +marketData[handicap].outcomes['Home']['best_bid_size'];
         // const tokenHome = marketData[handicap].outcomes['Home']['id'];
         const askAway = +marketData[handicap].outcomes['Away']['best_ask'];
         const bidAway = +marketData[handicap].outcomes['Away']['best_bid'];
+        const bidSizeAway = +marketData[handicap].outcomes['Away']['best_bid_size'];
         // const tokenAway = marketData[handicap].outcomes['Away']['id'];
         // const slug = marketData[handicap].market.slug;
         const tick_size = marketData[handicap].market.orderPriceMinTickSize;
@@ -306,8 +322,8 @@ export const parseOddsBid = (markets) => {
         const iorAway = fixFloat(1 / bidPriceAway);
         const rebateHome = parseBidRebate(bidPriceHome);
         const rebateAway = parseBidRebate(bidPriceAway);
-        odds[`ior_r${ratioAccept(ratio)}h_${ratioString(ratio)}`] = { v: iorHome, b: rebateHome, t: 1, ask: askHome, bid: bidHome, bid_ex: bidPriceHome, tick_size, /*token: tokenHome, slug */ };
-        odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, b: rebateAway, t: 1, ask: askAway, bid: bidAway, bid_ex: bidPriceAway, tick_size, /*token: tokenAway, slug */ };
+        odds[`ior_r${ratioAccept(ratio)}h_${ratioString(ratio)}`] = { v: iorHome, b: rebateHome, t: 1, ask: askHome, bid: bidHome, bid_size: bidSizeHome, bid_ex: bidPriceHome, tick_size, /*token: tokenHome, slug */ };
+        odds[`ior_r${ratioAccept(-ratio)}c_${ratioString(ratio)}`] = { v: iorAway, b: rebateAway, t: 1, ask: askAway, bid: bidAway, bid_size: bidSizeAway, bid_ex: bidPriceAway, tick_size, /*token: tokenAway, slug */ };
       });
     }
     else if (key === 'totals') {
@@ -315,9 +331,11 @@ export const parseOddsBid = (markets) => {
         const ratio = +handicap;
         const askOver = +marketData[handicap].outcomes['Over']['best_ask'];
         const bidOver = +marketData[handicap].outcomes['Over']['best_bid'];
+        const bidSizeOver = +marketData[handicap].outcomes['Over']['best_bid_size'];
         // const tokenOver = marketData[handicap].outcomes['Over']['id'];
         const askUnder = +marketData[handicap].outcomes['Under']['best_ask'];
         const bidUnder = +marketData[handicap].outcomes['Under']['best_bid'];
+        const bidSizeUnder = +marketData[handicap].outcomes['Under']['best_bid_size'];
         // const tokenUnder = marketData[handicap].outcomes['Under']['id'];
         // const slug = marketData[handicap].market.slug;
         const tick_size = marketData[handicap].market.orderPriceMinTickSize;
@@ -330,8 +348,8 @@ export const parseOddsBid = (markets) => {
         const iorUnder = fixFloat(1 / bidPriceUnder);
         const rebateOver = parseBidRebate(bidPriceOver);
         const rebateUnder = parseBidRebate(bidPriceUnder);
-        odds[`ior_ouc_${ratioString(ratio)}`] = { v: iorOver, b: rebateOver, t: 1, ask: askOver, bid: bidOver, bid_ex: bidPriceOver, tick_size, /*token: tokenOver, slug */ };
-        odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, b: rebateUnder, t: 1, ask: askUnder, bid: bidUnder, bid_ex: bidPriceUnder, tick_size, /*token: tokenUnder, slug */ };
+        odds[`ior_ouc_${ratioString(ratio)}`] = { v: iorOver, b: rebateOver, t: 1, ask: askOver, bid: bidOver, bid_size: bidSizeOver, bid_ex: bidPriceOver, tick_size, /*token: tokenOver, slug */ };
+        odds[`ior_ouh_${ratioString(ratio)}`] = { v: iorUnder, b: rebateUnder, t: 1, ask: askUnder, bid: bidUnder, bid_size: bidSizeUnder, bid_ex: bidPriceUnder, tick_size, /*token: tokenUnder, slug */ };
       });
     }
   });
@@ -445,13 +463,14 @@ const parseMarketsData = (markets, teams) => {
  * @returns {Object}
  */
 const parseEvent = (event) => {
-  const { id, title, series, gameId, parentEventId, startTime } = event;
+  const { id, slug, title, series, gameId, parentEventId, startTime, sport: { sport } = {}} = event;
   const { teamHomeName, teamAwayName } = parseTeamData(title);
   const leagueId = series[0].id;
   const leagueName = series[0].title;
   const timestamp = new Date(startTime).getTime();
   return {
     id: +id,
+    sport, slug,
     gameId: gameId ? +gameId : undefined,
     parentEventId: parentEventId ? +parentEventId : undefined,
     leagueId: +leagueId,
@@ -563,4 +582,4 @@ const parseRatio = (ratioString) => {
     return { ior, id, message: 'market type data not found', cause: 400 };
   }
   return result;
- }
+ }

+ 25 - 166
polymarket/libs/pinnacleClient.js

@@ -1,186 +1,45 @@
-import axios from "axios";
-import { HttpsProxyAgent } from "https-proxy-agent";
-
-import { randomUUID } from 'crypto';
-
+import pinnacleSdk from "./pinnacleSdk.js";
 import Logs from "./logs.js";
 
-const axiosDefaultOptions = {
-  baseURL: "",
-  url: "",
-  method: "GET",
-  headers: {},
-  params: {},
-  data: {},
-  timeout: 10000,
-};
-
-export const pinnacleRequest = async (options, channel) => {
-
-  const { url, ...optionsRest } = options;
-  const username = process.env.PINNACLE_USERNAME;
-  const password = process.env.PINNACLE_PASSWORD;
-  if (!url || !channel && (!username || !password)) {
-    throw new Error("url、username、password、channel is required");
-  }
-
-  const authHeader = channel ? `Basic ${channel}` : `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
-  const axiosConfig = { ...axiosDefaultOptions, ...optionsRest, url, baseURL: "https://api.pinnacle888.com" };
-  Object.assign(axiosConfig.headers, {
-    "Authorization": authHeader,
-    "Accept": "application/json",
-  });
-  const proxy = process.env.NODE_HTTP_PROXY;
-  if (proxy) {
-    axiosConfig.proxy = false;
-    axiosConfig.httpsAgent = new HttpsProxyAgent(proxy);
-  }
-  Logs.outDev('pinnacle request', url, axiosConfig, { channel, username, password });
-  return axios(axiosConfig).then(res => {
-    return res.data;
-  });
+export const pinnacleRequest = (options, channel) => {
+  Logs.outDev("pinnacle request", options?.url, { channel });
+  return pinnacleSdk.pinnacleRequest(options, channel);
 }
 
-/**
- * Pinnacle API Get请求
- * @param {*} url
- * @param {*} params
- * @returns
- */
-export const pinnacleGet = async (url, params, channel) => {
-  return pinnacleRequest({
-    url,
-    params
-  }, channel)
+export const pinnacleGet = (url, params, channel) => {
+  return pinnacleSdk.pinnacleGet(url, params, channel)
   .catch(err => {
-    Logs.errDev('pinnacle get error', err);
-    if (err.response?.data) {
-      err.data = err.response.data;
-      err.cause = err.response.status;
-    }
+    Logs.errDev("pinnacle get error", err);
     return Promise.reject(err);
   });
 }
 
-/**
- * Pinnacle API Post请求
- * @param {*} url
- * @param {*} data
- * @returns
- */
-export const pinnaclePost = async (url, data, channel) => {
-  return pinnacleRequest({
-    url,
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
-    data
-  }, channel);
+export const pinnaclePost = (url, data, channel) => {
+  return pinnacleSdk.pinnaclePost(url, data, channel);
 }
 
-/**
- * 清理对象中的undefined值
- * @param {*} obj
- * @returns {Object}
- */
-const cleanUndefined = (obj) => {
-  return Object.fromEntries(
-    Object.entries(obj).filter(([, v]) => v !== undefined)
-  );
-}
-
-/**
- * 获取直盘线
- */
-export const getLineInfo = async ({ info = {}, channel }) => {
-  const {
-    leagueId, eventId, betType, handicap, team, side,
-    specialId, contestantId,
-    periodNumber=0, oddsFormat='Decimal', sportId=29
-  } = info;
-  let url = '/v2/line/';
-  let data = { sportId, leagueId, eventId, betType, handicap, periodNumber, team, side, oddsFormat };
-  if (specialId) {
-    url = '/v2/line/special';
-    data = { specialId, contestantId, oddsFormat };
-  }
-  data = cleanUndefined(data);
-  return pinnacleGet(url, data, channel)
-  .then(ret => ({ info: ret, line: data }))
+export const getLineInfo = ({ info = {}, channel } = {}) => {
+  return pinnacleSdk.getLineInfo({ info, channel })
   .catch(err => {
-    Logs.outDev('get line info error', err.data);
+    Logs.out("get line info error", err.data);
     Logs.errDev(err);
     return Promise.reject(err);
   });
 }
 
-/**
- * 获取账户余额
- */
-export const getAccountBalance = async () => {
-  return pinnacleGet('/v1/client/balance');
+export const getAccountBalance = (channel) => {
+  return pinnacleSdk.getAccountBalance(channel);
 }
 
-/**
- * 下注
- */
-export const placeOrder = async ({ info, line, stakeSize }, channel) => {
-  // return Promise.resolve({info, line, stakeSize});
-  const uuid = randomUUID()
-  if (line.specialId) {
-    const data = cleanUndefined({
-      oddsFormat: line.oddsFormat,
-      uniqueRequestId: uuid,
-      acceptBetterLine: true,
-      stake: stakeSize,
-      winRiskStake: 'RISK',
-      lineId: info.lineId,
-      specialId: info.specialId,
-      contestantId: info.contestantId,
-    });
-    Logs.outDev('pinnacle place order data', data);
-    return pinnaclePost('/v4/bets/special', { bets: [data] }, channel)
-    .then(ret => ret.bets?.[0] ?? ret)
-    .then(ret => {
-      Logs.outDev('pinnacle place order', ret, uuid);
-      return ret;
-    })
-    .catch(err => {
-      Logs.outDev('pinnacle place order error', err.data, uuid);
-      Logs.errDev(err);
-      return Promise.reject(err);
-    });
-  }
-  else {
-    const data = cleanUndefined({
-      oddsFormat: line.oddsFormat,
-      uniqueRequestId: uuid,
-      acceptBetterLine: true,
-      stake: stakeSize,
-      winRiskStake: 'RISK',
-      lineId: info.lineId,
-      altLineId: info.altLineId,
-      fillType: 'NORMAL',
-      sportId: line.sportId,
-      eventId: line.eventId,
-      periodNumber: line.periodNumber,
-      betType: line.betType,
-      team: line.team,
-      side: line.side,
-      handicap: line.handicap,
-    });
-    Logs.outDev('pinnacle place order data', data);
-    return pinnaclePost('/v4/bets/place', data, channel)
-    .then(ret => {
-      Logs.outDev('pinnacle place order', ret, uuid);
-      return ret;
-    })
-    .catch(err => {
-      Logs.outDev('pinnacle place order error', err.data, uuid);
-      Logs.errDev(err);
-      return Promise.reject(err);
-    });
-  }
+export const placeOrder = ({ info, line, stakeSize }, channel) => {
+  return pinnacleSdk.placeOrder({ info, line, stakeSize }, channel)
+  .then(ret => {
+    Logs.outDev("pinnacle place order", ret);
+    return ret;
+  })
+  .catch(err => {
+    Logs.out("pinnacle place order error", err.data);
+    Logs.errDev(err);
+    return Promise.reject(err);
+  });
 }
-

+ 11 - 0
polymarket/libs/pinnacleSdk.js

@@ -0,0 +1,11 @@
+import createPinnacleSdk from "@ppai/pinnacle-sdk";
+
+export const getPinnacleConfig = () => ({
+  username: process.env.PINNACLE_USERNAME,
+  password: process.env.PINNACLE_PASSWORD,
+  httpProxy: process.env.NODE_HTTP_PROXY,
+});
+
+export const pinnacleSdk = createPinnacleSdk(getPinnacleConfig());
+
+export default pinnacleSdk;

+ 27 - 735
polymarket/libs/polymarketClient.js

@@ -1,43 +1,20 @@
 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";
 
 import WebSocketClient from "./webSocketClient.js";
+import polymarketSdk from "./polymarketSdk.js";
+
+export const createClobClient = polymarketSdk.createClobClient;
+export const getSoccerSports = polymarketSdk.getSoccerSports;
+export const getEvents = polymarketSdk.getEvents;
+export const getOrderBook = polymarketSdk.getOrderBook;
+export const getMultipleOrderBooks = polymarketSdk.getMultipleOrderBooks;
+export const getBalanceAllowance = polymarketSdk.getBalanceAllowance;
+export const transferWallet = polymarketSdk.transferWallet;
+export const createLimitOrder = polymarketSdk.createLimitOrder;
+export const getOrder = polymarketSdk.getOrder;
+export const getOpenOrders = polymarketSdk.getOpenOrders;
 
-import Logs from "./logs.js";
-
-const NODE_HTTP_PROXY = process.env.NODE_HTTP_PROXY;
-const proxyAgent = NODE_HTTP_PROXY ? new HttpsProxyAgent(NODE_HTTP_PROXY) : undefined;
-if (NODE_HTTP_PROXY) {
-  axios.defaults.proxy = false;
-  axios.defaults.httpAgent = proxyAgent;
-  axios.defaults.httpsAgent = proxyAgent;
-}
-
-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";
-
-/**
- * axios 默认配置
- */
 const axiosDefaultOptions = {
   baseURL: "",
   url: "",
@@ -48,686 +25,6 @@ const axiosDefaultOptions = {
   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' })
-  };
-  return axios(mergedOptions).then(res => res.data);
-}
-
-/**
- * 请求市场数据
- * @param {*} options
- * @returns
- */
-const requestMarketData = async (options) => {
-  return clientRequest(options, GAMMA_HOST);
-}
-
-/**
- * 请求订单簿数据
- */
-const requestClobData = async (options) => {
-  return clientRequest(options, CLOB_HOST);
-}
-
-const getRequiredEnv = (key) => {
-  const value = process.env[key];
-  if (!value) {
-    throw new Error(`${key} is required`);
-  }
-  return value;
-}
-
-const normalizePrivateKey = (privateKey) => {
-  return privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
-}
-
-const getOptionalEnv = (key) => {
-  const value = process.env[key];
-  return value && value.trim() ? value.trim() : undefined;
-}
-
-const getPolymarketFunderAddress = (accountAddress) => {
-  return getOptionalEnv("POLYMARKET_DEPOSIT_WALLET_ADDRESS")
-    || getOptionalEnv("POLYMARKET_FUNDER_ADDRESS")
-    || accountAddress;
-}
-
-const getPolymarketSignatureType = (funderAddress, accountAddress) => {
-  const configuredType = getOptionalEnv("POLYMARKET_SIGNATURE_TYPE");
-  if (configuredType) {
-    const signatureType = SignatureTypeV2[configuredType] ?? Number(configuredType);
-    if (!Number.isInteger(signatureType) || !SignatureTypeV2[signatureType]) {
-      throw new Error(`POLYMARKET_SIGNATURE_TYPE is invalid: ${configuredType}`);
-    }
-    return signatureType;
-  }
-
-  return funderAddress.toLowerCase() === accountAddress.toLowerCase()
-    ? SignatureTypeV2.POLY_PROXY
-    : SignatureTypeV2.POLY_1271;
-}
-
-/**
- * 创建 viem HTTP transport
- * NODE_HTTP_PROXY 存在时通过 axios 代理请求 Polygon RPC
- * @param {string} rpcUrl
- * @returns {import("viem").HttpTransport}
- */
-const createViemHttpTransport = (rpcUrl) => {
-  if (!NODE_HTTP_PROXY) {
-    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,
-    });
-
-    return new Response(response.data, {
-      status: response.status,
-      statusText: response.statusText,
-      headers: response.headers,
-    });
-  };
-
-  return http(rpcUrl, { fetchFn });
-}
-
-export const createClobClient = () => {
-  const account = privateKeyToAccount(normalizePrivateKey(getRequiredEnv("POLYMARKET_PRIVATE_KEY")));
-  const signer = createWalletClient({
-    account,
-    chain: polygon,
-    transport: createViemHttpTransport(getOptionalEnv("POLYGON_RPC_URL")),
-  });
-  const funderAddress = getPolymarketFunderAddress(account.address);
-  const userApiCreds = {
-    key: getRequiredEnv("POLYMARKET_API_KEY"),
-    secret: getRequiredEnv("POLYMARKET_API_SECRET"),
-    passphrase: getRequiredEnv("POLYMARKET_API_PASSPHRASE"),
-  };
-
-  return new ClobClient({
-    host: CLOB_HOST,
-    chain: Chain.POLYGON,
-    signer,
-    creds: userApiCreds,
-    signatureType: getPolymarketSignatureType(funderAddress, account.address),
-    funderAddress,
-    throwOnError: true,
-  });
-}
-
-const createPolymarketContext = () => {
-  const rpcUrl = getOptionalEnv("POLYGON_RPC_URL");
-  const transport = createViemHttpTransport(rpcUrl);
-  const account = privateKeyToAccount(normalizePrivateKey(getRequiredEnv("POLYMARKET_PRIVATE_KEY")));
-  const signer = createWalletClient({ account, chain: polygon, transport });
-  const publicClient = createPublicClient({ chain: polygon, transport });
-  const creds = {
-    key: getRequiredEnv("POLYMARKET_API_KEY"),
-    secret: getRequiredEnv("POLYMARKET_API_SECRET"),
-    passphrase: getRequiredEnv("POLYMARKET_API_PASSPHRASE"),
-  };
-
-  return { account, signer, publicClient, creds };
-}
-
-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 createBuilderConfig = () => {
-  return new BuilderConfig({
-    localBuilderCreds: {
-      key: getRequiredEnv("POLYMARKET_BUILDER_API_KEY"),
-      secret: getRequiredEnv("POLYMARKET_BUILDER_SECRET"),
-      passphrase: getRequiredEnv("POLYMARKET_BUILDER_PASS_PHRASE"),
-    },
-  });
-}
-
-/**
- * 创建 Polymarket builder relayer 客户端
- * @param {Object} options
- * @param {Object} options.signer viem wallet client
- * @param {number} options.relayTxType relayer 交易类型
- * @returns {RelayClient}
- */
-const createRelayer = ({ signer, relayTxType = RelayerTxType.PROXY } = {}) => {
-  const relayerUrl = getOptionalEnv("POLYMARKET_RELAYER_URL") || "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;
-}
-
-/**
- * 获取 Polymarket proxy wallet 地址
- * @param {Object} options
- * @param {string} options.ownerAddress owner 钱包地址
- * @returns {string}
- */
-const getProxyWalletAddress = ({ ownerAddress }) => {
-  return getOptionalEnv("POLYMARKET_PROXY_WALLET_ADDRESS")
-    || deriveProxyWallet(ownerAddress, PROXY_FACTORY_ADDRESS);
-}
-
-/**
- * 获取 Polymarket deposit wallet 地址
- * @param {Object} options
- * @param {Object} options.signer viem wallet client
- * @returns {Promise<string>}
- */
-const getDepositWalletAddress = async ({ signer }) => {
-  const configuredAddress = getOptionalEnv("POLYMARKET_DEPOSIT_WALLET_ADDRESS");
-  if (configuredAddress) {
-    return configuredAddress;
-  }
-
-  const relayer = createRelayer({ signer });
-  return relayer.deriveDepositWalletAddress();
-}
-
-/**
- * 获取 pUSD 转账所需的钱包上下文
- * @returns {Promise<Object>} owner、signer、publicClient、relayer 和 proxy/deposit 钱包地址
- */
-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,
-  };
-}
-
-/**
- * 获取钱包原始 pUSD 余额
- * @param {Object} options
- * @param {Object} options.publicClient viem public client
- * @param {string} options.address 钱包地址
- * @returns {Promise<bigint>}
- */
-const getRawPusdBalance = ({ publicClient, address }) => {
-  return publicClient.readContract({
-    address: PUSD_ADDRESS,
-    abi: erc20Abi,
-    functionName: "balanceOf",
-    args: [address],
-  });
-}
-
-/**
- * 校验并转换转账金额为 pUSD 最小单位
- * @param {string|number} amount 转账数量
- * @returns {bigint}
- */
-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);
-}
-
-/**
- * 校验并标准化钱包转账方向
- * @param {Object} options
- * @param {string} options.from 来源钱包类型
- * @param {string} options.to 目标钱包类型
- * @returns {{from: "proxy"|"deposit", to: "proxy"|"deposit"}}
- */
-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 };
-}
-
-/**
- * 生成 pUSD ERC20 transfer 调用数据
- * @param {Object} options
- * @param {string} options.to 收款钱包地址
- * @param {bigint} options.amount pUSD 最小单位金额
- * @returns {string}
- */
-const createPusdTransferData = ({ to, amount }) => {
-  return encodeFunctionData({
-    abi: erc20Abi,
-    functionName: "transfer",
-    args: [to, amount],
-  });
-}
-
-/**
- * 统一格式化钱包转账返回结果
- * @param {Object} options
- * @returns {Object}
- */
-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,
-  };
-}
-
-/**
- * 校验来源钱包 pUSD 余额是否足够
- * @param {Object} options
- * @param {string} options.wallet 钱包类型
- * @param {bigint} options.balance 当前余额
- * @param {bigint} options.amount 转账金额
- */
-const ensureSufficientPusdBalance = ({ wallet, balance, amount }) => {
-  if (balance < amount) {
-    throw new Error(`Insufficient ${wallet} wallet pUSD balance: ${formatUnits(balance, PUSD_DECIMALS)}`, { cause: 400 });
-  }
-}
-
-/**
- * 根据转账方向提交 relayer 交易
- * @param {Object} options
- * @param {"proxy"|"deposit"} options.from 来源钱包类型
- * @param {"proxy"|"deposit"} options.to 目标钱包类型
- * @param {Object} options.context 转账钱包上下文
- * @param {string} options.data pUSD transfer 调用数据
- * @param {string|number} options.amount 展示用转账数量
- * @returns {Promise<Object>}
- */
-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),
-  );
-}
-
-/**
- * 执行 pUSD 钱包间转账的公共流程
- * @param {Object} options
- * @param {string|number} options.amount 转账数量
- * @param {"proxy"|"deposit"} options.from 来源钱包类型
- * @param {"proxy"|"deposit"} options.to 目标钱包类型
- * @returns {Promise<Object>}
- */
-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,
-  });
-}
-
-/**
- * 获取足球联赛
- * @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 })),
-  });
-}
-
-/**
- * 获取 USDC collateral 余额和授权信息
- */
-export 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;
-}
-
-/**
- * 在 Proxy wallet 和 Deposit wallet 之间转 pUSD
- * @param {Object} options
- * @param {string|number} options.amount 转账数量
- * @param {"proxy"|"deposit"} options.from 来源钱包类型
- * @param {"proxy"|"deposit"} options.to 目标钱包类型
- * @returns {Promise<Object>}
- */
-export const transferWallet = async ({ amount, from, to } = {}) => {
-  const direction = normalizeTransferDirection({ from, to });
-  return transferPusdBetweenWallets({
-    amount,
-    from: direction.from,
-    to: direction.to,
-  });
-}
-
-/**
- * 创建 Polymarket 限价挂单
- */
-export const createLimitOrder = async ({
-  tokenID,
-  price,
-  size,
-  side = Side.BUY,
-  tickSize = "0.01",
-  negRisk = false,
-  orderType = OrderType.GTC,
-  postOnly = true,
-  expiration,
-  deferExec = false,
-} = {}) => {
-  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();
-  const orderData = {
-    tokenID,
-    price: Number(price),
-    size: Number(size),
-    side,
-    ...(expiration ? { expiration: Number(expiration) } : {}),
-  };
-  const orderOptions = { tickSize, negRisk };
-
-  Logs.outDev('polymarket create limit order data', orderData, orderOptions, orderType, postOnly, deferExec);
-
-  return client.createAndPostOrder(orderData, orderOptions, orderType, postOnly, deferExec);
-}
-
-/**
- * 查询单个 Polymarket 订单
- * @param {string} orderID 订单 ID
- * @returns {Promise<Object>}
- */
-export const getOrder = async (orderID) => {
-  if (!orderID) {
-    throw new Error("orderID is required", { cause: 400 });
-  }
-
-  const client = createClobClient();
-  return client.getOrder(orderID);
-}
-
-/**
- * 查询 Polymarket 开放订单
- * @param {Object} options
- * @param {string} options.id 订单 ID
- * @param {string} options.market 市场 ID
- * @param {string} options.asset_id token/asset ID
- * @param {boolean} options.only_first_page 是否只查第一页
- * @param {string} options.next_cursor 分页 cursor
- * @returns {Promise<Array>}
- */
-export const getOpenOrders = async ({
-  id,
-  market,
-  asset_id,
-  only_first_page = false,
-  next_cursor,
-} = {}) => {
-  const client = createClobClient();
-  const params = {
-    ...(id ? { id } : {}),
-    ...(market ? { market } : {}),
-    ...(asset_id ? { asset_id } : {}),
-  };
-
-  return client.getOpenOrders(params, Boolean(only_first_page), next_cursor);
-}
-
 /**
  * 请求平台数据
  * @param {*} options
@@ -764,9 +61,9 @@ export const platformRequest = async (options) => {
 export const platformPost = async (url, data) => {
   return platformRequest({
     url,
-    method: 'POST',
+    method: "POST",
     headers: {
-      'Content-Type': 'application/json',
+      "Content-Type": "application/json",
     },
     data,
   });
@@ -779,7 +76,7 @@ export const platformPost = async (url, data) => {
  * @returns {Promise}
  */
 export const platformGet = async (url, params) => {
-  return platformRequest({ url, method: 'GET', params });
+  return platformRequest({ url, method: "GET", params });
 }
 
 /**
@@ -796,29 +93,24 @@ export class MarketWsClient extends WebSocketClient {
     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,
-    });
+    if (this.wsClient?.readyState === 1) {
+      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,
-    });
+    if (this.wsClient?.readyState === 1) {
+      this.send({
+        operation: "unsubscribe",
+        assets_ids: assetIds,
+      });
+    }
   }
 }

+ 22 - 0
polymarket/libs/polymarketSdk.js

@@ -0,0 +1,22 @@
+import createPolymarketSdk from "@ppai/polymarket-sdk";
+
+export const getPolymarketConfig = () => ({
+  privateKey: process.env.POLYMARKET_PRIVATE_KEY,
+  apiKey: process.env.POLYMARKET_API_KEY,
+  apiSecret: process.env.POLYMARKET_API_SECRET,
+  apiPassphrase: process.env.POLYMARKET_API_PASSPHRASE,
+  builderApiKey: process.env.POLYMARKET_BUILDER_API_KEY,
+  builderSecret: process.env.POLYMARKET_BUILDER_SECRET,
+  builderPassPhrase: process.env.POLYMARKET_BUILDER_PASS_PHRASE,
+  depositWalletAddress: process.env.POLYMARKET_DEPOSIT_WALLET_ADDRESS,
+  funderAddress: process.env.POLYMARKET_FUNDER_ADDRESS,
+  proxyWalletAddress: process.env.POLYMARKET_PROXY_WALLET_ADDRESS,
+  signatureType: process.env.POLYMARKET_SIGNATURE_TYPE,
+  relayerUrl: process.env.POLYMARKET_RELAYER_URL,
+  polygonRpcUrl: process.env.POLYGON_RPC_URL,
+  httpProxy: process.env.NODE_HTTP_PROXY,
+});
+
+export const polymarketSdk = createPolymarketSdk(getPolymarketConfig());
+
+export default polymarketSdk;

+ 28 - 59
polymarket/libs/syncData.js

@@ -7,7 +7,8 @@ import Logs from "./logs.js";
 import { setData } from "./cache.js";
 import getDateInTimezone from "./getDateInTimezone.js";
 
-import { getSoccerSports, getEvents, platformPost, platformGet, MarketWsClient } from "./polymarketClient.js";
+import { getEvents, platformPost } from "./polymarketClient.js";
+import BestBidAskSizeWatcher from "./bestBidAskSizeWatcher.js";
 import { parseMarkets, parseOddsAsk, parseOddsBid, parseIorDetail } from "./parseMarkets.js";
 
 const __filename = fileURLToPath(import.meta.url);
@@ -25,7 +26,7 @@ const GLOBAL_DATA = {
   clobTokenMap: {},
   filteredLeagues: [],
   relatedGames: [],
-  marketWsClient: null,
+  marketWatcher: null,
   wsClientData: null,
   lastChangeTime: 0,
 };
@@ -99,6 +100,7 @@ const GLOBAL_DATA = {
 //   })
 //   .catch(error => {
 //     Logs.out('failed to update leagues and games list', error.message);
+//     Logs.errDev(error);
 //   });
 // }
 
@@ -114,7 +116,8 @@ const GLOBAL_DATA = {
 //     GLOBAL_DATA.relatedGames = relatedGames.map(item => item.id);
 //   })
 //   .catch(error => {
-//     Logs.out('failed to update related games', error.message);
+//     Logs.out('failed to update related games', error.message);\
+//     Logs.errDev(error);
 //   })
 //   .finally(() => {
 //     setTimeout(() => {
@@ -133,6 +136,10 @@ const getMarketsData = async () => {
   const tomorrowDateMinus4 = getDateInTimezone(-4, Date.now()+24*60*60*1000);
   const tomorrowGmtMinus4EndTime = new Date(`${tomorrowDateMinus4} 23:59:59 GMT-4`).getTime();
   const endDateMax = new Date(tomorrowGmtMinus4EndTime).toISOString();
+  // const todayDateMinus4 = getDateInTimezone(-4, Date.now() + 24*60*60*1000);
+  // const todayGmtMinus4EndTime = new Date(`${todayDateMinus4} 23:59:59 GMT-4`).getTime();
+  // const startDateMax = new Date(todayGmtMinus4EndTime).toISOString();
+  Logs.outDev('getMarketsData', endDateMin, endDateMax);
   return getEvents({ endDateMin, endDateMax })
   .then(events => {
     const { eventsData } = GLOBAL_DATA;
@@ -216,19 +223,20 @@ const updateGamesMarkets = () => {
   Logs.outDev('updateGamesMarkets');
   getMarketsData()
   .then(clobTokenUpdate => {
-    const { marketWsClient } = GLOBAL_DATA;
+    const { marketWatcher } = GLOBAL_DATA;
     const { add, remove } = clobTokenUpdate;
     if (add.length > 0) {
       Logs.outDev('subscribeToTokensIds', add);
-      marketWsClient?.subscribeToTokensIds(add);
+      marketWatcher?.subscribe(add);
     }
     if (remove.length > 0) {
       Logs.outDev('unsubscribeToTokensIds', remove);
-      marketWsClient?.unsubscribeToTokensIds(remove);
+      marketWatcher?.unsubscribe(remove);
     }
   })
   .catch(error => {
     Logs.out('failed to update games markets', error.message);
+    Logs.errDev(error);
   })
   .finally(() => {
     setTimeout(() => {
@@ -282,7 +290,8 @@ const updateOdds = async () => {
 const updateOddsLoop = () => {
   updateOdds()
   .catch(error => {
-    Logs.err('failed to update odds', error.message);
+    Logs.out('failed to update odds', error.message);
+    Logs.errDev(error);
   })
   .finally(() => {
     setTimeout(() => {
@@ -291,46 +300,14 @@ const updateOddsLoop = () => {
   });
 }
 
-/**
- * 处理价格变化
- * @param {*} priceChanges
- */
-const marketPriceChange = (priceChanges) => {
-  let changed = false;
-  priceChanges.forEach(priceChange => {
-    const { asset_id, best_ask, best_bid } = priceChange;
-    const { clobTokenMap } = GLOBAL_DATA;
-    const clobToken = clobTokenMap[asset_id];
-    if (!clobToken) {
-      return;
-    }
-    Object.assign(clobToken, { best_ask, best_bid });
-    changed = true;
-  });
-  if (changed) {
-    GLOBAL_DATA.lastChangeTime = Date.now();
-  }
-}
-
-/**
- * 处理市场数据
- */
-const marketBook = (data) => {
-  const { asks, bids, asset_id } = data;
+const updateClobTokenBestBidAskSize = (data) => {
+  const { tokenId, best_ask, best_ask_size, best_bid, best_bid_size } = data;
   const { clobTokenMap } = GLOBAL_DATA;
-  const clobToken = clobTokenMap[asset_id];
+  const clobToken = clobTokenMap[tokenId];
   if (!clobToken) {
     return;
   }
-  const bestAskItem = asks?.reduce((minItem, current) => {
-    return Number(current.price) < Number(minItem.price) ? current : minItem;
-  }, asks?.[0] ?? { price: '1' });
-  const best_ask = bestAskItem?.price ?? '0.001';
-  const bestBidItem = bids?.reduce((maxItem, current) => {
-    return Number(current.price) > Number(maxItem.price) ? current : maxItem;
-  }, bids?.[0] ?? { price: '0' });
-  const best_bid = bestBidItem?.price ?? '0';
-  Object.assign(clobToken, { best_ask, best_bid });
+  Object.assign(clobToken, { best_ask, best_ask_size, best_bid, best_bid_size });
   GLOBAL_DATA.lastChangeTime = Date.now();
 }
 
@@ -338,32 +315,24 @@ const marketBook = (data) => {
  * 启动同步市场数据
  */
 export const startSyncMarketsData = () => {
-  const marketWsClient = new MarketWsClient();
-  GLOBAL_DATA.marketWsClient = marketWsClient;
-  marketWsClient.connect();
-  marketWsClient.on('open', () => {
+  const marketWatcher = new BestBidAskSizeWatcher();
+  GLOBAL_DATA.marketWatcher = marketWatcher;
+  marketWatcher.on('open', () => {
     // updateRelatedGames();
     updateGamesMarkets();
     syncMarketsData();
     updateOddsLoop();
   });
-  marketWsClient.on('message', data => {
-    // Logs.outDev('message data:', data);
-    switch(data.event_type) {
-      case 'price_change':
-        marketPriceChange(data.price_changes);
-        break;
-      case 'book':
-        marketBook(data);
-        break
-    }
+  marketWatcher.on('update', data => {
+    updateClobTokenBestBidAskSize(data);
   });
-  marketWsClient.on('error', error => {
+  marketWatcher.on('error', error => {
     Logs.err('error', error);
   });
-  marketWsClient.on('close', event => {
+  marketWatcher.on('close', event => {
     Logs.outDev('close', event);
   });
+  marketWatcher.start();
 }
 
 /**

+ 4 - 1
polymarket/main.js

@@ -54,9 +54,12 @@ app.use((req, res, next) => {
     return res.status(200).json(response);
   }
   res.sendError = (err) => {
-    if (err.cause === 400 || err.status === 400) {
+    if (err.cause === 400) {
       return res.badRequest(err.data, err.message);
     }
+    else if (err.response) {
+      return res.serverError(err.response.data, err.response.statusText);
+    }
     return res.serverError(err.data, err.message);
   }
   next();

+ 31 - 0
polymarket/package-lock.json

@@ -12,6 +12,8 @@
         "@polymarket/builder-relayer-client": "^0.0.9",
         "@polymarket/builder-signing-sdk": "^1.0.0",
         "@polymarket/clob-client-v2": "^1.0.6",
+        "@ppai/pinnacle-sdk": "file:../packages/pinnacle-sdk",
+        "@ppai/polymarket-sdk": "file:../packages/polymarket-sdk",
         "axios": "^1.13.3",
         "dayjs": "^1.11.19",
         "dotenv": "^17.4.2",
@@ -22,6 +24,27 @@
         "ws": "^8.19.0"
       }
     },
+    "../packages/pinnacle-sdk": {
+      "name": "@ppai/pinnacle-sdk",
+      "version": "0.0.1",
+      "dependencies": {
+        "axios": "^1.13.3",
+        "https-proxy-agent": "^7.0.6"
+      }
+    },
+    "../packages/polymarket-sdk": {
+      "name": "@ppai/polymarket-sdk",
+      "version": "0.0.1",
+      "dependencies": {
+        "@polymarket/builder-relayer-client": "^0.0.9",
+        "@polymarket/builder-signing-sdk": "^1.0.0",
+        "@polymarket/clob-client-v2": "^1.0.6",
+        "axios": "^1.13.3",
+        "https-proxy-agent": "^7.0.6",
+        "qs": "^6.14.1",
+        "viem": "^2.50.3"
+      }
+    },
     "node_modules/@adraffy/ens-normalize": {
       "version": "1.11.1",
       "resolved": "https://registry.npmmirror.com/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
@@ -1324,6 +1347,14 @@
         "node": ">=20.10"
       }
     },
+    "node_modules/@ppai/pinnacle-sdk": {
+      "resolved": "../packages/pinnacle-sdk",
+      "link": true
+    },
+    "node_modules/@ppai/polymarket-sdk": {
+      "resolved": "../packages/polymarket-sdk",
+      "link": true
+    },
     "node_modules/@scure/base": {
       "version": "1.2.6",
       "resolved": "https://registry.npmmirror.com/@scure/base/-/base-1.2.6.tgz",

+ 2 - 0
polymarket/package.json

@@ -16,6 +16,8 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
+    "@ppai/pinnacle-sdk": "file:../packages/pinnacle-sdk",
+    "@ppai/polymarket-sdk": "file:../packages/polymarket-sdk",
     "@polymarket/builder-relayer-client": "^0.0.9",
     "@polymarket/builder-signing-sdk": "^1.0.0",
     "@polymarket/clob-client-v2": "^1.0.6",

+ 18 - 9
polymarket/routes/trading.js

@@ -24,7 +24,8 @@ router.get('/balance/:wallet', (req, res) => {
   getBalanceAllowance({ wallet })
   .then(data => res.sendSuccess(data))
   .catch(error => {
-    Logs.errDev('get balance allowance error', error);
+    Logs.out('get balance allowance error', error.message);
+    Logs.errDev(error);
     return res.sendError(error);
   });
 });
@@ -44,7 +45,8 @@ router.post('/wallet/transfer', (req, res) => {
   transferWallet({ amount, from, to })
   .then(data => res.sendSuccess(data))
   .catch(error => {
-    Logs.errDev('transfer wallet error', error);
+    Logs.out('transfer wallet error', error.message);
+    Logs.errDev(error);
     return res.sendError(error);
   });
 });
@@ -54,7 +56,8 @@ router.get('/get_ior_info/:id/:ior', (req, res) => {
   getIorInfo(ior, id)
   .then(data => res.sendSuccess(data))
   .catch(error => {
-    Logs.errDev('get ior info error', error);
+    Logs.out('get ior info error', error.message);
+    Logs.errDev(error);
     return res.sendError(error);
   });
 });
@@ -63,7 +66,8 @@ router.get('/orderbook/:tokenId', (req, res) => {
   getOrderBook(req.params.tokenId)
   .then(data => res.sendSuccess(data))
   .catch(error => {
-    Logs.errDev('get order book error', error);
+    Logs.out('get order book error', error.message);
+    Logs.errDev(error);
     return res.sendError(error);
   });
 });
@@ -72,7 +76,8 @@ router.post('/get_line_info', (req, res) => {
   getLineInfo(req.body)
   .then(data => res.sendSuccess(data))
   .catch(error => {
-    Logs.errDev('get line info error', error);
+    Logs.out('get line info error', error.message);
+    Logs.errDev(error);
     return res.sendError(error);
   });
 });
@@ -85,7 +90,8 @@ router.post('/orderbooks', (req, res) => {
   getMultipleOrderBooks(tokenIds)
   .then(data => res.sendSuccess(data))
   .catch(error => {
-    Logs.errDev('get multiple order books error', error);
+    Logs.out('get multiple order books error', error.message);
+    Logs.errDev(error);
     return res.sendError(error);
   });
 });
@@ -119,7 +125,8 @@ router.post('/orders/limit', (req, res) => {
   })
   .then(data => res.sendSuccess(data))
   .catch(error => {
-    Logs.errDev('create limit order error', error);
+    Logs.out('create limit order error', error.message);
+    Logs.errDev(error);
     return res.sendError(error);
   });
 });
@@ -143,7 +150,8 @@ router.get('/orders/open', (req, res) => {
   })
   .then(data => res.sendSuccess(data))
   .catch(error => {
-    Logs.errDev('get open orders error', error);
+    Logs.out('get open orders error', error.message);
+    Logs.errDev(error);
     return res.sendError(error);
   });
 });
@@ -152,7 +160,8 @@ router.get('/orders/:orderID', (req, res) => {
   getOrder(req.params.orderID)
   .then(data => res.sendSuccess(data))
   .catch(error => {
-    Logs.errDev('get order error', error);
+    Logs.out('get order error', error.message);
+    Logs.errDev(error);
     return res.sendError(error);
   });
 });

+ 2 - 2
server/libs/getGamesRelations.js

@@ -4,8 +4,8 @@ const getGameData = (game, hasOdds=false) => {
   if (!game) {
     return null;
   }
-  const { id, leagueId, leagueName, teamHomeName, teamAwayName, timestamp, odds, evtime } = game;
-  const gameData = { id, leagueId, leagueName, teamHomeName, teamAwayName, timestamp };
+  const { id, sport, slug, leagueId, leagueName, teamHomeName, teamAwayName, timestamp, odds, evtime } = game;
+  const gameData = { id, sport, slug, leagueId, leagueName, teamHomeName, teamAwayName, timestamp };
   if (hasOdds) {
     gameData.odds = odds;
     gameData.evtime = evtime;

+ 11 - 0
server/libs/pinnacleSdk.js

@@ -0,0 +1,11 @@
+import createPinnacleSdk from "@ppai/pinnacle-sdk";
+
+export const getPinnacleConfig = () => ({
+  username: process.env.PINNACLE_USERNAME,
+  password: process.env.PINNACLE_PASSWORD,
+  httpProxy: process.env.NODE_HTTP_PROXY,
+});
+
+export const pinnacleSdk = createPinnacleSdk(getPinnacleConfig());
+
+export default pinnacleSdk;

+ 22 - 0
server/libs/polymarketSdk.js

@@ -0,0 +1,22 @@
+import createPolymarketSdk from "@ppai/polymarket-sdk";
+
+export const getPolymarketConfig = () => ({
+  privateKey: process.env.POLYMARKET_PRIVATE_KEY,
+  apiKey: process.env.POLYMARKET_API_KEY,
+  apiSecret: process.env.POLYMARKET_API_SECRET,
+  apiPassphrase: process.env.POLYMARKET_API_PASSPHRASE,
+  builderApiKey: process.env.POLYMARKET_BUILDER_API_KEY,
+  builderSecret: process.env.POLYMARKET_BUILDER_SECRET,
+  builderPassPhrase: process.env.POLYMARKET_BUILDER_PASS_PHRASE,
+  depositWalletAddress: process.env.POLYMARKET_DEPOSIT_WALLET_ADDRESS,
+  funderAddress: process.env.POLYMARKET_FUNDER_ADDRESS,
+  proxyWalletAddress: process.env.POLYMARKET_PROXY_WALLET_ADDRESS,
+  signatureType: process.env.POLYMARKET_SIGNATURE_TYPE,
+  relayerUrl: process.env.POLYMARKET_RELAYER_URL,
+  polygonRpcUrl: process.env.POLYGON_RPC_URL,
+  httpProxy: process.env.NODE_HTTP_PROXY,
+});
+
+export const polymarketSdk = createPolymarketSdk(getPolymarketConfig());
+
+export default polymarketSdk;

+ 3 - 2
server/main.js

@@ -80,9 +80,10 @@ app.use((req, res, next) => {
     else if (err.cause == 404) {
       return res.notFound(err.data, err.message);
     }
-    else {
-      return res.serverError(err.data, err.message);
+    else if (err.response) {
+      return res.serverError(err.response.data, err.response.statusText);
     }
+    return res.serverError(err.data, err.message);
   }
   next();
 });

+ 9 - 1
server/models/Games.js

@@ -10,6 +10,7 @@ import {
   getPolymarketBalanceAllowance,
   getPolymarketOpenOrders,
   getPolymarketOrder,
+  getPinnacleBalance,
   transferPolymarketWallet,
 } from "./Markets.js";
 
@@ -270,6 +271,13 @@ export const getOpenOrders = async ({
   });
 }
 
+/**
+ * 获取Pinnacle账户余额信息
+ */
+export const getPinnacleAccountBalance = async ({ channel } = {}) => {
+  return getPinnacleBalance({ channel });
+}
+
 /**
  * 清理过期关系
  */
@@ -292,5 +300,5 @@ export default {
   getLeagues, setLeaguesRelation, removeLeaguesRelation, getLeaguesRelations,
   getGames, setGamesRelation, removeGamesRelation, getGamesRelations,
   getSolutions, getSolutionIorsInfo, betSolution, getOrders, removeOrder, getOrder, getOpenOrders,
-  getPolymarketBalanceAllowance, transferPolymarketWallet,
+  getPolymarketBalanceAllowance, getPinnacleAccountBalance, transferPolymarketWallet,
 };

+ 1 - 0
server/models/Markets.bak.js

@@ -452,6 +452,7 @@ export const getSoulutionBetResult = async ({ iors, iorsInfo, stakeLimit, stake=
   })).then(results => {
     return results.sort((a, b) => a[1] - b[1]).map(item => item[0]);
   }).catch(error => {
+    Logs.out('get soulution bet result error', error.message);
     Logs.errDev(error);
     return Promise.reject(error);
   });

+ 60 - 28
server/models/Markets.js

@@ -1,6 +1,8 @@
 import Logs from "../libs/logs.js";
 
 import { platformGet, platformPost } from "../libs/platformRequest.js";
+import pinnacleSdk from "../libs/pinnacleSdk.js";
+import polymarketSdk from "../libs/polymarketSdk.js";
 import eventSolutions from '../triangle/eventSolutions.js';
 
 const MAKER_FEE_RATE = 0.03;
@@ -86,7 +88,8 @@ const getPolymarketIorInfo = async (ior, id) => {
   return platformGet(`http://127.0.0.1:9021/api/trading/get_ior_info/${id}/${ior}`)
   .then(res => res.data)
   .catch(err => {
-    Logs.errDev('get polymarket ior info error', err);
+    Logs.out('get polymarket ior info error', err.message);
+    Logs.errDev(err);
     return Promise.reject(err);
   });
 }
@@ -101,7 +104,8 @@ const getPinnacleIorInfo = async (ior, id) => {
   return platformGet(`https://cb.long.bid/api/trading/get_ior_info/${id}/${ior}`)
   .then(res => res.data)
   .catch(err => {
-    Logs.errDev('get pinnacle ior info error', err);
+    Logs.out('get pinnacle ior info error', err.message);
+    Logs.errDev(err);
     return Promise.reject(err);
   });
 }
@@ -116,6 +120,12 @@ export const getPlatformIorInfo = async (ior, platform, id) => {
     },
     pinnacle() {
       return getPinnacleIorInfo(ior, id);
+    },
+    obsports() {
+      return {}
+    },
+    huangguan() {
+      return {}
     }
   }
   const result = await getInfo[platform]?.();
@@ -129,10 +139,10 @@ export const getPlatformIorInfo = async (ior, platform, id) => {
 const getPolymarketIorDetailInfo = async (info) => {
   Logs.outDev('get polymarket ior detail info', info);
   const { id } = info;
-  return platformGet(`http://127.0.0.1:9021/api/trading/orderbook/${id}`)
-  .then(res => res.data)
+  return polymarketSdk.getOrderBook(id)
   .catch(err => {
-    Logs.errDev('get polymarket ior detail info error', err);
+    Logs.out('get polymarket ior detail info error', err.message);
+    Logs.errDev(err);
     return Promise.reject(err);
   });
 }
@@ -142,10 +152,10 @@ const getPolymarketIorDetailInfo = async (info) => {
  */
 const getPinnacleIorDetailInfo = async (info, channel) => {
   Logs.outDev('get pinnacle ior detail info', { info, channel });
-  return platformPost(`http://127.0.0.1:9021/api/trading/get_line_info`, { info, channel })
-  .then(res => res.data)
+  return pinnacleSdk.getLineInfo({ info, channel })
   .catch(err => {
-    Logs.errDev('get pinnacle ior detail info error', err);
+    Logs.out('get pinnacle ior detail info error', err.message);
+    Logs.errDev(err);
     return Promise.reject(err);
   });
 }
@@ -164,6 +174,12 @@ export const getPlatformIorsDetailInfo = async (ior, platform, id) => {
     },
     pinnacle() {
       return getPinnacleIorDetailInfo(info);
+    },
+    obsports() {
+      return {}
+    },
+    huangguan() {
+      return {}
     }
   }
   return getInfo[platform]?.();
@@ -263,10 +279,10 @@ export const getSolutionByLatestIors = (iorsInfo, cross_type, retry=false) => {
  * 创建Polymarket限价挂单 买单
  */
 export const createPolymarketLimitBuyOrder = async ({ tokenID, price, size, tickSize = "0.01", negRisk = false } = {}) => {
-  return platformPost(`http://127.0.0.1:9021/api/trading/orders/limit`, { tokenID, price, size, tickSize, negRisk })
-  .then(res => res.data)
+  return polymarketSdk.createLimitOrder({ tokenID, price, size, tickSize, negRisk })
   .catch(err => {
-    Logs.errDev('create polymarket limit buy order error', err);
+    Logs.out('create polymarket limit buy order error', err.message);
+    Logs.errDev(err);
     return Promise.reject(err);
   });
 }
@@ -281,10 +297,10 @@ export const getPolymarketOrder = async ({ orderID } = {}) => {
   if (!orderID) {
     throw new Error('orderID is required', { cause: 400 });
   }
-  return platformGet(`http://127.0.0.1:9021/api/trading/orders/${orderID}`)
-  .then(res => res.data)
+  return polymarketSdk.getOrder(orderID)
   .catch(err => {
-    Logs.errDev('get polymarket order error', err);
+    Logs.out('get polymarket order error', err.message);
+    Logs.errDev(err);
     return Promise.reject(err);
   });
 }
@@ -306,16 +322,16 @@ export const getPolymarketOpenOrders = async ({
   only_first_page = false,
   next_cursor,
 } = {}) => {
-  return platformGet(`http://127.0.0.1:9021/api/trading/orders/open`, {
-    ...(id ? { id } : {}),
-    ...(market ? { market } : {}),
-    ...(asset_id ? { asset_id } : {}),
-    ...(only_first_page ? { only_first_page } : {}),
-    ...(next_cursor ? { next_cursor } : {}),
+  return polymarketSdk.getOpenOrders({
+    id,
+    market,
+    asset_id,
+    only_first_page,
+    next_cursor,
   })
-  .then(res => res.data)
   .catch(err => {
-    Logs.errDev('get polymarket open orders error', err);
+    Logs.out('get polymarket open orders error', err.message);
+    Logs.errDev(err);
     return Promise.reject(err);
   });
 }
@@ -329,10 +345,26 @@ export const getPolymarketBalanceAllowance = async ({ wallet = "both" } = {}) =>
   if (!wallet || !["both", "proxy", "deposit"].includes(wallet)) {
     throw new Error('invalid wallet', { cause: 400 });
   }
-  return platformGet(`http://127.0.0.1:9021/api/trading/balance/${wallet}`)
-  .then(res => res.data)
+  return polymarketSdk.getBalanceAllowance({ wallet })
   .catch(err => {
-    Logs.errDev('get polymarket balance allowance error', err);
+    Logs.out('get polymarket balance allowance error', err.message);
+    Logs.errDev(err);
+    return Promise.reject(err);
+  });
+}
+
+/**
+ * 获取Pinnacle账户余额信息
+ */
+export const getPinnacleBalance = async ({ channel } = {}) => {
+  return pinnacleSdk.getAccountBalance(channel)
+  .then(balance => ({
+    ...balance,
+    account: process.env.PINNACLE_USERNAME,
+  }))
+  .catch(err => {
+    Logs.out('get pinnacle balance error', err.message);
+    Logs.errDev(err);
     return Promise.reject(err);
   });
 }
@@ -359,10 +391,10 @@ export const transferPolymarketWallet = async ({ amount, from, to } = {}) => {
   if (from === to) {
     throw new Error('from and to cannot be the same', { cause: 400 });
   }
-  return platformPost(`http://127.0.0.1:9021/api/trading/wallet/transfer`, { amount, from, to })
-  .then(res => res.data)
+  return polymarketSdk.transferWallet({ amount, from, to })
   .catch(err => {
-    Logs.errDev('transfer polymarket wallet error', err);
+    Logs.out('transfer polymarket wallet error', err.message);
+    Logs.errDev(err);
     return Promise.reject(err);
   });
 }

+ 8 - 4
server/models/Platforms.js

@@ -48,8 +48,10 @@ triangleData.registerRequest('solutions', solutions => {
   });
   if (changed.update.length || changed.add.length || changed.remove.length) {
     Store.set('solutions', solutions);
-    const profitableSolutions = solutions.filter(solution => solution.sol.win_profit_rate > 0);
-    Logs.outDev('profitable solutions', profitableSolutions);
+    // const profitableSolutions = solutions.filter(solution => solution.sol.win_profit_rate > 0);
+    // if (profitableSolutions.length) {
+    //   Logs.outDev('profitable solutions', profitableSolutions);
+    // }
   }
 });
 
@@ -199,7 +201,8 @@ const updateObossOdds = () => {
     return Promise.reject(new Error(`status code ${res.statusCode}`));
   })
   .catch(error => {
-    Logs.err('failed to update oboss odds', error.message);
+    Logs.out('failed to update oboss odds', error.message);
+    Logs.errDev(error);
   })
   .finally(() => {
     setTimeout(() => {
@@ -268,7 +271,8 @@ const updateGamesRelations = () => {
     return Promise.reject(new Error(res.message));
   })
   .catch(error => {
-    Logs.err('failed to update games relations', error.message);
+    Logs.out('failed to update games relations', error.message);
+    Logs.errDev(error);
   })
   .finally(() => {
     setTimeout(() => {

+ 59 - 28
server/package-lock.json

@@ -10,6 +10,8 @@
       "license": "ISC",
       "dependencies": {
         "@google-cloud/translate": "^9.3.0",
+        "@ppai/pinnacle-sdk": "file:../packages/pinnacle-sdk",
+        "@ppai/polymarket-sdk": "file:../packages/polymarket-sdk",
         "axios": "^1.13.5",
         "cookie-parser": "^1.4.7",
         "dayjs": "^1.11.19",
@@ -19,6 +21,27 @@
         "mongoose": "^9.6.2"
       }
     },
+    "../packages/pinnacle-sdk": {
+      "name": "@ppai/pinnacle-sdk",
+      "version": "0.0.1",
+      "dependencies": {
+        "axios": "^1.13.3",
+        "https-proxy-agent": "^7.0.6"
+      }
+    },
+    "../packages/polymarket-sdk": {
+      "name": "@ppai/polymarket-sdk",
+      "version": "0.0.1",
+      "dependencies": {
+        "@polymarket/builder-relayer-client": "^0.0.9",
+        "@polymarket/builder-signing-sdk": "^1.0.0",
+        "@polymarket/clob-client-v2": "^1.0.6",
+        "axios": "^1.13.3",
+        "https-proxy-agent": "^7.0.6",
+        "qs": "^6.14.1",
+        "viem": "^2.50.3"
+      }
+    },
     "node_modules/@google-cloud/common": {
       "version": "6.0.0",
       "resolved": "https://registry.npmmirror.com/@google-cloud/common/-/common-6.0.0.tgz",
@@ -160,6 +183,14 @@
         "node": ">=14"
       }
     },
+    "node_modules/@ppai/pinnacle-sdk": {
+      "resolved": "../packages/pinnacle-sdk",
+      "link": true
+    },
+    "node_modules/@ppai/polymarket-sdk": {
+      "resolved": "../packages/polymarket-sdk",
+      "link": true
+    },
     "node_modules/@protobufjs/aspromise": {
       "version": "1.1.2",
       "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -270,15 +301,6 @@
         "node": ">= 0.6"
       }
     },
-    "node_modules/agent-base": {
-      "version": "7.1.4",
-      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
-      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 14"
-      }
-    },
     "node_modules/ansi-regex": {
       "version": "6.2.2",
       "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -951,9 +973,9 @@
       }
     },
     "node_modules/follow-redirects": {
-      "version": "1.15.11",
-      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
-      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "version": "1.16.0",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
       "funding": [
         {
           "type": "individual",
@@ -1077,6 +1099,28 @@
         "node": ">=18"
       }
     },
+    "node_modules/gaxios/node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/gaxios/node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/gcp-metadata": {
       "version": "8.1.2",
       "resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
@@ -1344,19 +1388,6 @@
         "node": ">= 6.0.0"
       }
     },
-    "node_modules/https-proxy-agent": {
-      "version": "7.0.6",
-      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
-      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
-      "license": "MIT",
-      "dependencies": {
-        "agent-base": "^7.1.2",
-        "debug": "4"
-      },
-      "engines": {
-        "node": ">= 14"
-      }
-    },
     "node_modules/iconv-lite": {
       "version": "0.7.2",
       "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz",
@@ -1886,9 +1917,9 @@
       }
     },
     "node_modules/qs": {
-      "version": "6.14.1",
-      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz",
-      "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+      "version": "6.15.2",
+      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz",
+      "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
       "license": "BSD-3-Clause",
       "dependencies": {
         "side-channel": "^1.1.0"

+ 2 - 0
server/package.json

@@ -12,6 +12,8 @@
   "license": "ISC",
   "dependencies": {
     "@google-cloud/translate": "^9.3.0",
+    "@ppai/pinnacle-sdk": "file:../packages/pinnacle-sdk",
+    "@ppai/polymarket-sdk": "file:../packages/polymarket-sdk",
     "axios": "^1.13.5",
     "cookie-parser": "^1.4.7",
     "dayjs": "^1.11.19",

+ 13 - 1
server/routes/games.js

@@ -28,7 +28,8 @@ router.post('/remove_leagues_relation', (req, res) => {
   Games.removeLeaguesRelation(id)
   .then(() => {
     res.sendSuccess();
-  }).catch(err => {
+  })
+  .catch(err => {
     res.sendError(err);
   });
 });
@@ -186,6 +187,17 @@ router.get('/get_polymarket_balance_allowance/:wallet', (req, res) => {
   });
 });
 
+router.get('/get_pinnacle_balance', (req, res) => {
+  const { channel } = req.query;
+  Games.getPinnacleAccountBalance({ channel })
+  .then(balance => {
+    res.sendSuccess(balance);
+  })
+  .catch(err => {
+    res.sendError(err);
+  });
+});
+
 router.post('/transfer_polymarket_wallet', (req, res) => {
   const { amount, from, to } = req.body;
   Games.transferPolymarketWallet({ amount, from, to })

+ 4 - 0
server/triangle/iorKeys.js

@@ -349,9 +349,13 @@ const iorKeys = {
   // ],
   'R:0': [
     ['ior_rh_05', 'ior_rac_05', '-', 'la_wa_rv'],
+    ['ior_rh_05', 'ior_moh', '-', 'la_wa_rv'],
     ['ior_rc_05', 'ior_rah_05', '-', 'la_wa_rv'],
+    ['ior_rc_05', 'ior_moc', '-', 'la_wa_rv'],
     ['ior_mh', 'ior_rac_05', '-', 'la_wa_rv'],
+    ['ior_mh', 'ior_moh', '-', 'la_wa_rv'],
     ['ior_mc', 'ior_rah_05', '-', 'la_wa_rv'],
+    ['ior_mc', 'ior_moc', '-', 'la_wa_rv'],
 
     ['ior_rh_15', 'ior_rac_15', '-', 'la_wa_rv'],
     ['ior_rc_15', 'ior_rah_15', '-', 'la_wa_rv'],

+ 3 - 2
server/triangle/trangleCalc.js

@@ -2,7 +2,7 @@ import crypto from 'crypto';
 import iorKeys from './iorKeys.js';
 import eventSolutions from './eventSolutions.js';
 
-const IS_BID = process.env.PPAI_RUN_MODE == 'BID';
+// const IS_BID = process.env.PPAI_RUN_MODE == 'BID';
 
 /**
  * 精确浮点数字
@@ -73,7 +73,8 @@ const getOptimalSelections = (odds, rules) => {
       const cartesian = cartesianOdds(selection);
       cartesian.forEach(iors => {
         const pmIors = iors.filter(ior => ior.p == 'polymarket');
-        const pmPass = IS_BID ? pmIors.length == 1 : pmIors.length >= 1;
+        // const pmPass = IS_BID ? pmIors.length == 1 : pmIors.length >= 1;
+        const pmPass = pmIors.length == 1;
         const iorsCount = new Set(iors.filter(ior => ior.p !== 'no').map(ior => ior.p));
         if (pmPass && iorsCount.size > 1) {
           validOptions.push(iors);

+ 1 - 1
web/src/main.vue

@@ -74,7 +74,7 @@ const handleLogout = async () => {
   </header>
 
   <router-view v-slot="{ Component }">
-    <keep-alive :include="['wallet', 'games', 'leagues', 'locales']">
+    <keep-alive :include="['orders', 'wallet', 'games', 'leagues', 'locales']">
       <component :is="Component" />
     </keep-alive>
   </router-view>

+ 82 - 16
web/src/views/home.vue

@@ -10,13 +10,15 @@ const games = ref(null);
 const refreshTimer = ref(null);
 const minProfitRate = ref(0);
 const loading = ref(false);
+const iorInfoModalOpen = ref(false);
+const iorInfoDetail = ref(null);
 const minProfitRateStorageKey = 'home:min_profit_rate';
 
 const platformLabels = {
   polymarket: 'Polymarket',
-  pinnacle: 'Pinnacle',
+  pinnacle: '平博',
   huangguan: '皇冠',
-  obsports: 'OB',
+  obsports: 'OB体育',
 };
 
 const sideLabels = ['A', 'B', 'C'];
@@ -55,6 +57,8 @@ const getSolutionIorsInfo = (sid) => {
   .then(res => {
     if (res.data.statusCode === 200) {
       console.log(res.data.data);
+      iorInfoDetail.value = res.data.data;
+      iorInfoModalOpen.value = true;
     }
     else {
       throw new Error(res.data.message);
@@ -66,6 +70,10 @@ const getSolutionIorsInfo = (sid) => {
   });
 }
 
+const closeIorInfoModal = () => {
+  iorInfoModalOpen.value = false;
+};
+
 const betSolution = (sid, stake=0) => {
   console.log('betSolution', sid, stake);
   api.get('/api/games/bet_solution', { params: { sid, stake } })
@@ -179,7 +187,26 @@ const solutionsFiltered = computed(() => {
     || polymarket.teamAwayName?.toLowerCase().includes(searchValue)
     || pinnacle.leagueName?.toLowerCase().includes(searchValue)
     || pinnacle.teamHomeName?.toLowerCase().includes(searchValue)
-    || pinnacle.teamAwayName?.toLowerCase().includes(searchValue);
+    || pinnacle.teamAwayName?.toLowerCase().includes(searchValue)
+    || item.solutions?.some(solution => String(solution.sid ?? '').toLowerCase().includes(searchValue));
+  }).map(item => {
+    if (!searchValue) {
+      return item;
+    }
+
+    const matchedSolutions = item.solutions?.filter(solution => {
+      return String(solution.sid ?? '').toLowerCase().includes(searchValue);
+    }) ?? [];
+    if (!matchedSolutions.length) {
+      return item;
+    }
+
+    return {
+      ...item,
+      solutions: matchedSolutions,
+      bestProfitRate: matchedSolutions[0]?.sol?.win_profit_rate,
+      solutionCount: matchedSolutions.length,
+    };
   });
 });
 
@@ -267,17 +294,22 @@ onUnmounted(() => {
               <span>RID: {{ item.id }}</span>
             </div>
           </div>
-          <div class="solution-stack">
-            <div
+          <a-tabs class="solution-tabs">
+            <a-tab-pane
               v-for="(solution, index) in item.solutions"
-              :key="solution.sid"
-              class="solution-panel"
+              :key="String(solution.sid)"
             >
+              <template #tab>
+                <span class="solution-tab-title">
+                  <span>#{{ index + 1 }}</span>
+                  <span :class="`profit-${getProfitColor(solution.sol?.win_profit_rate)}`">
+                    {{ formatRate(solution.sol?.win_profit_rate) }}
+                  </span>
+                </span>
+              </template>
+              <div class="solution-panel">
               <div class="solution-header">
                 <div class="solution-title">
-                  <a-tag :color="getProfitColor(solution.sol?.win_profit_rate)">
-                    #{{ index + 1 }} {{ formatRate(solution.sol?.win_profit_rate) }}
-                  </a-tag>
                   <span>{{ solution.cross }}</span>
                   <span>{{ solution.rule }}</span>
                 </div>
@@ -304,7 +336,10 @@ onUnmounted(() => {
                   class="metric-item"
                 >
                   <span>{{ metric.label }}</span>
-                  <strong>{{ metric.value }}</strong>
+                  <a-tooltip v-if="metric.label === 'SID'" :title="metric.value">
+                    <strong>{{ metric.value }}</strong>
+                  </a-tooltip>
+                  <strong v-else>{{ metric.value }}</strong>
                 </div>
               </div>
 
@@ -338,12 +373,23 @@ onUnmounted(() => {
                   <pre>{{ formatJson(solution) }}</pre>
                 </a-collapse-panel>
               </a-collapse>
-            </div>
-          </div>
+              </div>
+            </a-tab-pane>
+          </a-tabs>
         </a-list-item>
       </template>
     </a-list>
   </div>
+
+  <a-modal
+    v-model:open="iorInfoModalOpen"
+    title="盘口详情"
+    width="900px"
+    :footer="null"
+    @cancel="closeIorInfoModal"
+  >
+    <pre class="json-detail">{{ formatJson(iorInfoDetail) }}</pre>
+  </a-modal>
 </template>
 
 <style lang="scss" scoped>
@@ -390,9 +436,24 @@ onUnmounted(() => {
   color: #666;
   font-size: 12px;
 }
-.solution-stack {
-  display: grid;
-  gap: 12px;
+.solution-tabs {
+  :deep(.ant-tabs-nav) {
+    margin-bottom: 12px;
+  }
+}
+.solution-tab-title {
+  display: inline-flex;
+  gap: 6px;
+  align-items: center;
+}
+.profit-green {
+  color: #389e0d;
+}
+.profit-red {
+  color: #cf1322;
+}
+.profit-default {
+  color: rgba(0, 0, 0, 0.65);
 }
 .solution-panel {
   display: grid;
@@ -475,4 +536,9 @@ pre {
   border: 1px solid rgba(5, 5, 5, 0.06);
   border-radius: 4px;
 }
+.json-detail {
+  max-height: 70vh;
+  padding: 12px;
+  border-radius: 6px;
+}
 </style>

+ 43 - 14
web/src/views/orders.vue

@@ -10,28 +10,44 @@ const deletingId = ref('');
 const orders = ref([]);
 
 const columns = [
-  { title: '状态', dataIndex: 'status', key: 'status', width: 110 },
-  { title: '方向', dataIndex: 'side', key: 'side', width: 90 },
-  { title: '价格(pUSD)', dataIndex: 'price', key: 'price', width: 110 },
+  { title: '状态', dataIndex: 'status', key: 'status', width: 60 },
+  { title: '方向', dataIndex: 'side', key: 'side', width: 60 },
+  { title: '价格(pUSD)', dataIndex: 'price', key: 'price', width: 100 },
   { title: '数量', dataIndex: 'size', key: 'size', width: 100 },
-  { title: '投入(pUSD)', dataIndex: 'stake', key: 'stake', width: 120 },
-  { title: '收益率', dataIndex: 'profitRate', key: 'profitRate', width: 110 },
+  { title: '投入(pUSD)', dataIndex: 'stake', key: 'stake', width: 100 },
+  { title: '收益率', dataIndex: 'profitRate', key: 'profitRate', width: 100 },
+  { title: '比赛', dataIndex: 'gameName', key: 'gameName', width: 240 },
+  { title: '盘口', dataIndex: 'marketKeys', key: 'marketKeys', width: 80 },
   { title: '订单ID', dataIndex: 'polymarketOrderID', key: 'polymarketOrderID', width: 150 },
-  { title: '市场', dataIndex: 'market', key: 'market', width: 150 },
-  { title: 'Asset ID', dataIndex: 'assetId', key: 'assetId', width: 150 },
   { title: 'SID', dataIndex: 'sid', key: 'sid', width: 90 },
   { title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170 },
   { title: '操作', dataIndex: 'actions', key: 'actions', width: 90, fixed: 'right' },
 ];
 
+const getGameName = (gameRelation = {}) => {
+  const game = gameRelation.platforms?.polymarket || {};
+  const home = game.localesTeamHomeName || game.teamHomeName || '-';
+  const away = game.localesTeamAwayName || game.teamAwayName || '-';
+  return `${home} vs ${away}`;
+};
+
+const getPolymarketGameUrl = (gameRelation = {}) => {
+  const { sport, slug } = gameRelation.platforms?.polymarket || {};
+  if (!sport || !slug) {
+    return '';
+  }
+  return `https://polymarket.com/zh/sports/${sport}/${slug}`;
+};
+
+const getMarketKeys = (cpr = []) => {
+  return cpr.find(item => item?.p === 'polymarket')?.k || '-';
+};
+
 const tableRows = computed(() => {
   return orders.value.map(item => {
     const polymarketOrder = item.polymarketOrder ?? {};
     const cpr = item.cpr ?? [];
     const polymarketCpr = cpr.find(row => row?.p === 'polymarket') ?? {};
-    const polymarketInfo = Array.isArray(item.iorsInfo)
-      ? item.iorsInfo[cpr.findIndex(row => row?.p === 'polymarket')] ?? {}
-      : {};
 
     return {
       ...item,
@@ -40,8 +56,9 @@ const tableRows = computed(() => {
       price: polymarketOrder.price ?? polymarketCpr.bid_ex ?? '-',
       size: item.size ?? polymarketOrder.size ?? polymarketOrder.original_size ?? '-',
       profitRate: item.sol?.win_profit_rate,
-      market: polymarketInfo.market || polymarketOrder.market || '-',
-      assetId: polymarketInfo.asset_id || polymarketOrder.asset_id || '-',
+      gameName: getGameName(item.gameRelation),
+      gameUrl: getPolymarketGameUrl(item.gameRelation),
+      marketKeys: getMarketKeys(cpr),
     };
   });
 });
@@ -159,7 +176,7 @@ onMounted(() => {
       :columns="columns"
       :data-source="tableRows"
       :loading="loading"
-      :scroll="{ x: 1800, y: 'calc(100vh - 215px)' }"
+      :scroll="{ x: 1200, y: 'calc(100vh - 215px)' }"
       size="small"
       bordered
     >
@@ -172,7 +189,19 @@ onMounted(() => {
             {{ text || '-' }}
           </a-tag>
         </template>
-        <template v-else-if="column.key === 'sid' || column.key === 'polymarketOrderID' || column.key === 'market' || column.key === 'assetId'">
+        <template v-else-if="column.key === 'gameName'">
+          <a
+            v-if="record.gameUrl"
+            class="ellipsis"
+            :href="record.gameUrl"
+            target="_blank"
+            rel="noopener noreferrer"
+          >
+            {{ text || '-' }}
+          </a>
+          <span v-else class="ellipsis">{{ text || '-' }}</span>
+        </template>
+        <template v-else-if="column.key === 'sid' || column.key === 'polymarketOrderID' || column.key === 'marketKeys'">
           <a-tooltip :title="text">
             <span class="mono ellipsis">{{ text || '-' }}</span>
           </a-tooltip>

+ 159 - 70
web/src/views/wallet.vue

@@ -5,10 +5,12 @@ import { ReloadOutlined, SwapOutlined } from '@ant-design/icons-vue';
 import api from '@/libs/api';
 
 const walletType = ref('both');
-const loading = ref(false);
+const polymarketLoading = ref(false);
+const pinnacleLoading = ref(false);
 const transferLoading = ref(false);
 const transferModalOpen = ref(false);
 const wallets = ref([]);
+const pinnacleBalance = ref(null);
 const transferForm = ref({
   amount: '',
   from: 'proxy',
@@ -27,6 +29,13 @@ const columns = [
   { title: 'CLOB 授权', dataIndex: 'clobAllowanceSummary', key: 'clobAllowanceSummary', width: 180 },
 ];
 
+const pinnacleColumns = [
+  { title: '账户', dataIndex: 'account', key: 'account', width: 100 },
+  { title: '币种', dataIndex: 'currency', key: 'currency', width: 160 },
+  { title: '余额', dataIndex: 'availableBalance', key: 'availableBalance', width: 160 },
+  { title: '未结算', dataIndex: 'outstandingTransactions', key: 'outstandingTransactions', width: 160 },
+];
+
 const walletOptions = [
   { label: '全部', value: 'both' },
   { label: 'Deposit', value: 'deposit' },
@@ -126,12 +135,18 @@ const displayWallets = computed(() => {
   });
 });
 
+const displayPinnacleBalance = computed(() => {
+  return pinnacleBalance.value
+    ? [{ ...pinnacleBalance.value, key: pinnacleBalance.value.account || pinnacleBalance.value.clientId || 'pinnacle' }]
+    : [];
+});
+
 const formatJson = (value) => {
   return JSON.stringify(value ?? {}, null, 2);
 };
 
-const refresh = () => {
-  loading.value = true;
+const refreshPolymarket = () => {
+  polymarketLoading.value = true;
   api.get(`/api/games/get_polymarket_balance_allowance/${walletType.value}`)
   .then(res => {
     if (res.data.statusCode === 200) {
@@ -146,12 +161,37 @@ const refresh = () => {
     console.error(err);
   })
   .finally(() => {
-    loading.value = false;
+    polymarketLoading.value = false;
   });
 };
 
+const refreshPinnacle = () => {
+  pinnacleLoading.value = true;
+  api.get('/api/games/get_pinnacle_balance')
+  .then(res => {
+    if (res.data.statusCode === 200) {
+      pinnacleBalance.value = res.data.data;
+    }
+    else {
+      throw new Error(res.data.message);
+    }
+  })
+  .catch(err => {
+    message.error(err.response?.data?.message ?? err.message);
+    console.error(err);
+  })
+  .finally(() => {
+    pinnacleLoading.value = false;
+  });
+};
+
+const refresh = () => {
+  refreshPolymarket();
+  refreshPinnacle();
+};
+
 const onWalletTypeChange = () => {
-  refresh();
+  refreshPolymarket();
 };
 
 const openTransferModal = () => {
@@ -195,7 +235,7 @@ const submitTransfer = () => {
       message.success('转账已提交');
       transferModalOpen.value = false;
       transferForm.value.amount = '';
-      refresh();
+      refreshPolymarket();
     }
     else {
       throw new Error(res.data.message);
@@ -218,18 +258,7 @@ onMounted(() => {
 <template>
   <a-page-header title="钱包">
     <template #extra>
-      <a-segmented
-        v-model:value="walletType"
-        :options="walletOptions"
-        @change="onWalletTypeChange"
-      />
-      <a-button type="primary" @click="openTransferModal">
-        <template #icon>
-          <swap-outlined />
-        </template>
-        转账
-      </a-button>
-      <a-button :loading="loading" @click="refresh">
+      <a-button :loading="polymarketLoading || pinnacleLoading" @click="refresh">
         <template #icon>
           <reload-outlined />
         </template>
@@ -239,63 +268,99 @@ onMounted(() => {
   </a-page-header>
 
   <div class="wallet-container">
-    <a-table
-      :columns="columns"
-      :data-source="displayWallets"
-      :loading="loading"
-      :pagination="false"
-      size="small"
-      bordered
-    >
-      <template #bodyCell="{ column, record, text }">
-        <template v-if="column.key === 'type'">
-          <a-tag :color="record.type === 'deposit' ? 'blue' : 'green'">
-            {{ record.type }}
-          </a-tag>
-        </template>
-        <template v-else-if="column.key === 'address'">
-          <span class="address">{{ text }}</span>
-        </template>
-        <template v-else-if="column.key === 'clobAllowanceSummary'">
-          <a-tooltip :title="record.clobAllowanceSummary.description">
-            <a-tag :color="record.clobAllowanceSummary.color">
-              {{ record.clobAllowanceSummary.text }}
+    <section class="wallet-section">
+      <div class="section-title">
+        <h3>Polymarket 钱包</h3>
+        <div class="section-actions">
+          <a-segmented
+            v-model:value="walletType"
+            :options="walletOptions"
+            @change="onWalletTypeChange"
+          />
+          <a-button type="primary" @click="openTransferModal">
+            <template #icon>
+              <swap-outlined />
+            </template>
+            转账
+          </a-button>
+        </div>
+      </div>
+      <a-table
+        :columns="columns"
+        :data-source="displayWallets"
+        :loading="polymarketLoading"
+        :pagination="false"
+        size="small"
+        bordered
+      >
+        <template #bodyCell="{ column, record, text }">
+          <template v-if="column.key === 'type'">
+            <a-tag :color="record.type === 'deposit' ? 'blue' : 'green'">
+              {{ record.type }}
             </a-tag>
-          </a-tooltip>
+          </template>
+          <template v-else-if="column.key === 'address'">
+            <span class="address">{{ text }}</span>
+          </template>
+          <template v-else-if="column.key === 'clobAllowanceSummary'">
+            <a-tooltip :title="record.clobAllowanceSummary.description">
+              <a-tag :color="record.clobAllowanceSummary.color">
+                {{ record.clobAllowanceSummary.text }}
+              </a-tag>
+            </a-tooltip>
+          </template>
         </template>
-      </template>
-      <template #expandedRowRender="{ record }">
-        <div class="wallet-detail">
-          <div>
-            <strong>Owner</strong>
-            <span class="address">{{ record.owner }}</span>
-          </div>
-          <div>
-            <strong>CLOB 授权明细</strong>
-            <div class="allowance-list">
-              <div
-                v-for="item in record.allowanceRows"
-                :key="item.spender"
-                class="allowance-item"
-              >
-                <span class="address">{{ item.spender }}</span>
-                <a-tag :color="item.authorized ? 'green' : 'red'">
-                  {{ item.authorized ? item.label : '未授权' }}
-                </a-tag>
+        <template #expandedRowRender="{ record }">
+          <div class="wallet-detail">
+            <div>
+              <strong>Owner</strong>
+              <span class="address">{{ record.owner }}</span>
+            </div>
+            <div>
+              <strong>CLOB 授权明细</strong>
+              <div class="allowance-list">
+                <div
+                  v-for="item in record.allowanceRows"
+                  :key="item.spender"
+                  class="allowance-item"
+                >
+                  <span class="address">{{ item.spender }}</span>
+                  <a-tag :color="item.authorized ? 'green' : 'red'">
+                    {{ item.authorized ? item.label : '未授权' }}
+                  </a-tag>
+                </div>
+                <a-empty
+                  v-if="!record.allowanceRows.length"
+                  description="无授权数据"
+                />
               </div>
-              <a-empty
-                v-if="!record.allowanceRows.length"
-                description="无授权数据"
-              />
+            </div>
+            <div>
+              <strong>CLOB 详情</strong>
+              <pre>{{ formatJson(record.clobBalanceAllowance) }}</pre>
             </div>
           </div>
-          <div>
-            <strong>CLOB 详情</strong>
-            <pre>{{ formatJson(record.clobBalanceAllowance) }}</pre>
-          </div>
-        </div>
-      </template>
-    </a-table>
+        </template>
+      </a-table>
+    </section>
+
+    <section class="wallet-section">
+      <div class="section-title">
+        <h3>Pinnacle 账户</h3>
+      </div>
+      <a-table
+        :columns="pinnacleColumns"
+        :data-source="displayPinnacleBalance"
+        :loading="pinnacleLoading"
+        :pagination="false"
+        size="small"
+        bordered
+      >
+        <template #expandedRowRender="{ record }">
+          <pre>{{ formatJson(record) }}</pre>
+        </template>
+      </a-table>
+    </section>
   </div>
 
   <a-modal
@@ -342,8 +407,32 @@ onMounted(() => {
 .wallet-container {
   height: calc(100vh - 126px);
   padding: 15px;
+  overflow: auto;
   border-top: 1px solid rgba(5, 5, 5, 0.06);
 }
+.wallet-section {
+  display: grid;
+  gap: 10px;
+  margin-bottom: 18px;
+}
+.section-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  h3 {
+    margin: 0;
+    font-size: 15px;
+    font-weight: 600;
+  }
+}
+.section-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+  justify-content: flex-end;
+}
 .address {
   font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
   font-size: 12px;