commit 7c7fad45f312845f0116fda2cc2e1766ab0b98ff Author: direkturcrypto Date: Sun Feb 22 15:38:13 2026 +0700 feat: initial polymarket copy trade tool - Watcher: polls Data API for trader activity - Executor: buy/sell with market orders + retry logic - Position manager: JSON-based state tracking - Auto-sell: limit orders at profit target - Redeemer: check & redeem winning positions on-chain - Config: env-based settings with validation - DRY_RUN mode for safe testing diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6128155 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Wallet +PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE +WALLET_ADDRESS=0xYOUR_WALLET_ADDRESS_HERE + +# Polymarket API Credentials (optional, auto-derived if not set) +CLOB_API_KEY= +CLOB_API_SECRET= +CLOB_API_PASSPHRASE= + +# Trader to Copy +TRADER_ADDRESS=0xTRADER_ADDRESS_TO_COPY + +# Trade Sizing +# "percentage" = % of trader's trade size, "balance" = % of own balance +SIZE_MODE=percentage +SIZE_PERCENT=50 +MIN_TRADE_SIZE=1 + +# Auto Sell +AUTO_SELL_ENABLED=true +AUTO_SELL_PROFIT_PERCENT=10 + +# Sell Mode when copying trader's sell +# "market" = sell at market price, "limit" = sell at trader's avg sell price +SELL_MODE=market + +# Polling Intervals (in seconds) +POLL_INTERVAL=15 +REDEEM_INTERVAL=60 + +# Dry Run (set to true to simulate without executing trades) +DRY_RUN=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf8f74e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +data/*.json +.DS_Store diff --git a/AGENT.MD b/AGENT.MD new file mode 100644 index 0000000..7414768 --- /dev/null +++ b/AGENT.MD @@ -0,0 +1,64 @@ +# AGENT.MD — Polymarket Copy Trade Tool + +Dokumentasi untuk AI Agent yang akan melanjutkan atau memodifikasi project ini. + +## Overview + +Tool untuk copy trade otomatis dari trader di Polymarket. Dibangun dengan Node.js (ESM), menggunakan Polymarket CLOB SDK. + +## Architecture + +- **Config** (`src/config/index.js`): Semua settings dari `.env`, validasi required fields +- **Client** (`src/services/client.js`): Inisialisasi `ClobClient` dari `@polymarket/clob-client`, auto-derive API creds, cek balance USDC.e on-chain +- **Watcher** (`src/services/watcher.js`): Polling `data-api.polymarket.com/activity?user={address}` untuk deteksi trade baru. Dedup via `processed_trades.json` +- **Executor** (`src/services/executor.js`): Execute buy (market FOK order + retry) dan sell (market/limit). Sizing mode: percentage of trader size atau percentage of own balance +- **Position** (`src/services/position.js`): CRUD posisi di `positions.json`. Prevent duplicate buy per conditionId (1 market = 1 buy only) +- **AutoSell** (`src/services/autoSell.js`): Setelah buy filled, place GTC limit sell di `avgBuyPrice * (1 + profitPercent/100)`. Rounded ke tick size yang valid +- **Redeemer** (`src/services/redeemer.js`): Check resolved markets via Gamma API + on-chain CTF `payoutDenominator`. Redeem via CTF `redeemPositions` +- **State** (`src/utils/state.js`): Atomic JSON file writes (tmp file + rename) ke folder `data/` +- **Logger** (`src/utils/logger.js`): Timestamped, color-coded, emoji-prefixed console logging + +## Key APIs Used + +| API | Base URL | Auth | Purpose | +|-----|----------|------|---------| +| Gamma API | `gamma-api.polymarket.com` | No | Market info, resolution status | +| Data API | `data-api.polymarket.com` | No | Trader activity, positions | +| CLOB API | `clob.polymarket.com` | Yes (L2) | Place/cancel orders | +| Polygon RPC | `polygon-rpc.com` | No | USDC balance, CTF redeem | + +## Dependencies + +- `@polymarket/clob-client` — Official Polymarket CLOB SDK (uses ethers v5 internally) +- `ethers@5` — Wallet signing, contract interaction +- `dotenv` — Environment variable loading +- `nodemon` (dev) — Auto-reload on file changes (ignores `data/*.json`) + +## State Files (data/) + +- `positions.json` — Active positions: `{ [conditionId]: { tokenId, shares, avgBuyPrice, ... } }` +- `processed_trades.json` — Already-handled trade IDs (max 500): `{ tradeIds: [...] }` + +## Trade Flow + +1. Watcher detects new BUY → check no existing position → calculate size → check balance → market buy (FOK + retry) → save position → auto-sell if enabled +2. Watcher detects new SELL → check position exists → cancel auto-sell order → market/limit sell → remove position +3. Redeemer loop → check resolved markets → redeem CTF on-chain → remove position + +## Important Notes + +- Module type: ESM (`"type": "module"` in package.json) +- USDC on Polygon = USDC.e (`0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`) +- CTF Contract = `0x4D97DCd97eC945f40cF65F87097ACe5EA0476045` +- Neg Risk CTF = `0xC5d563A36AE78145C45a50134d48A1215220f80a` +- Signature type 0 = EOA wallet +- Tick sizes: 0.1, 0.01, 0.001, 0.0001 +- Market orders use FOK (fill-or-kill), limit orders use GTC (good-til-cancelled) +- Data API activity response fields vary — code handles multiple field name conventions + +## Known Limitations + +- No WebSocket support yet (polling only) +- No partial fill tracking for market orders +- Redeem requires MATIC for gas +- No proxy wallet support (EOA only, signature type 0) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7f8a49 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# 🎯 Polymarket Copy Trade Tool + +Auto-copy trades dari trader manapun di Polymarket. + +## Features + +- 👀 **Watch Trader** — Monitor aktivitas trading dari address wallet tertentu +- 📊 **Copy Buy** — Otomatis buy ketika trader buy, dengan sizing yang bisa di-setting +- 📉 **Copy Sell** — Otomatis sell ketika trader sell (market / limit) +- 💰 **Auto Sell** — Pasang limit sell otomatis setelah buy filled (sesuai target profit %) +- 🏆 **Auto Redeem** — Cek dan redeem posisi yang sudah WIN secara berkala +- 🔄 **Smart Position** — 1 market hanya buy 1x, tidak duplikat +- ✅ **Balance Check** — Cek saldo sebelum trade +- 🧪 **Dry Run Mode** — Test tanpa eksekusi trade sungguhan + +## Setup + +### 1. Clone & Install + +```bash +git clone +cd polymarket-copy +npm install +``` + +### 2. Configure Environment + +```bash +cp .env.example .env +``` + +Edit `.env` dengan setting Anda: + +| Variable | Description | Default | +|---|---|---| +| `PRIVATE_KEY` | Private key wallet Polygon | (required) | +| `WALLET_ADDRESS` | Address wallet Anda | (required) | +| `TRADER_ADDRESS` | Address trader yang mau di-copy | (required) | +| `SIZE_MODE` | `percentage` (dari size trader) atau `balance` (dari balance sendiri) | `percentage` | +| `SIZE_PERCENT` | Persentase sizing | `50` | +| `MIN_TRADE_SIZE` | Minimum trade dalam USDC | `1` | +| `AUTO_SELL_ENABLED` | Aktifkan auto-sell | `true` | +| `AUTO_SELL_PROFIT_PERCENT` | Target profit % untuk auto-sell | `10` | +| `SELL_MODE` | `market` atau `limit` saat copy sell | `market` | +| `POLL_INTERVAL` | Interval polling (detik) | `15` | +| `REDEEM_INTERVAL` | Interval cek redeem (detik) | `60` | +| `DRY_RUN` | Mode simulasi tanpa real trade | `true` | + +### 3. Run + +```bash +# Development (auto-reload) +npm run dev + +# Production +npm start +``` + +## How It Works + +``` +┌─────────────────────────────────────────────┐ +│ WATCHER LOOP │ +│ Poll Data API setiap N detik │ +│ → Cek trade baru dari trader │ +├─────────────────┬───────────────────────────┤ +│ NEW BUY │ NEW SELL │ +│ │ │ +│ ✓ Cek posisi │ ✓ Cek ada posisi? │ +│ ✓ Cek balance │ ✓ Cancel auto-sell │ +│ ✓ Market order │ ✓ Market/Limit sell │ +│ ✓ Retry loop │ ✓ Retry loop │ +│ ✓ Auto-sell │ ✓ Remove position │ +│ ✓ Save posisi │ │ +├─────────────────┴───────────────────────────┤ +│ REDEEMER LOOP │ +│ Cek berkala posisi yang sudah WIN │ +│ → Redeem on-chain via CTF contract │ +└─────────────────────────────────────────────┘ +``` + +## Folder Structure + +``` +polymarket-copy/ +├── src/ +│ ├── config/index.js — Environment vars & settings +│ ├── services/ +│ │ ├── client.js — CLOB client init & balance check +│ │ ├── watcher.js — Poll trader activity +│ │ ├── executor.js — Buy & sell logic +│ │ ├── position.js — Position management +│ │ ├── autoSell.js — Auto limit sell +│ │ └── redeemer.js — Redeem winning positions +│ ├── utils/ +│ │ ├── logger.js — Color-coded logging +│ │ └── state.js — JSON state management +│ └── index.js — Main entry point +├── data/ — Runtime state (gitignored) +├── .env.example +├── .gitignore +└── package.json +``` + +## Important Notes + +- ⚠️ **Test dengan DRY_RUN=true** terlebih dahulu +- ⚠️ **Gunakan SIZE_PERCENT kecil** untuk percobaan awal +- ⚠️ **Private key jangan di-commit** — sudah ada di .gitignore +- Butuh USDC.e di Polygon untuk trading +- Butuh sedikit MATIC untuk gas fee (redeem positions) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cdcb4eb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1832 @@ +{ + "name": "polymarket-copy", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "polymarket-copy", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@polymarket/clob-client": "^5.2.4", + "dotenv": "^17.3.1", + "ethers": "^5.8.0" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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-signing-sdk": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@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/clob-client": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@polymarket/clob-client/-/clob-client-5.2.4.tgz", + "integrity": "sha512-kBTEEHNE111FmD4NKcHwRIL7DmCYqIu1iHTpbkUrwh08cjUSuz4caFUAYbyKiVFmyknSUblKmcAZnNdbY+KV5Q==", + "license": "MIT", + "dependencies": { + "@ethersproject/providers": "^5.7.2", + "@ethersproject/units": "^5.7.0", + "@ethersproject/wallet": "^5.7.0", + "@polymarket/builder-signing-sdk": "^0.0.8", + "@polymarket/order-utils": "^3.0.1", + "axios": "^1.0.0", + "browser-or-node": "^2.1.1", + "ethers": "^5.7.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=20.10" + } + }, + "node_modules/@polymarket/order-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymarket/order-utils/-/order-utils-3.0.1.tgz", + "integrity": "sha512-XVcVladfGtC/VmboMkcszqYs82rvath/0XFWqzIFfq8O4atVOU8ykPOGJ2ZfodBPcXETzL+2u1rcepLMLKu9AQ==", + "license": "MIT", + "dependencies": { + "@ethersproject/providers": "^5.7.2", + "@ethersproject/wallet": "^5.7.0", + "ethers": "^5.7.1", + "tslib": "^2.4.0", + "viem": "^2.31.4" + }, + "engines": { + "node": ">=20.10", + "yarn": ">=1" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/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.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/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.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/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/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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.npmjs.org/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.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "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.npmjs.org/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.npmjs.org/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.npmjs.org/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/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/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-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/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.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ox": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.12.4.tgz", + "integrity": "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q==", + "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/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/viem": { + "version": "2.46.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.46.2.tgz", + "integrity": "sha512-w8Qv5Vyo7TfXcH3vgmxRa1NRvzJCDy2aSGSRsJn3503nC/qVbgEQ+n3aj/CkqWXbloudZh97h5o5aQrQSVGy0w==", + "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": "0.12.4", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "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.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "peer": true, + "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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..11bdd73 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "polymarket-copy", + "version": "1.0.0", + "description": "Polymarket Copy Trade Tool - Auto copy trades from any trader", + "main": "src/index.js", + "type": "module", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon --ignore 'data/*.json' src/index.js" + }, + "keywords": ["polymarket", "copy-trade", "crypto"], + "author": "direkturcrypto", + "license": "ISC", + "dependencies": { + "@polymarket/clob-client": "^4.7.3", + "dotenv": "^16.4.7", + "ethers": "^5.8.0" + }, + "devDependencies": { + "nodemon": "^3.1.9" + } +} diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 0000000..cb2de1b --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,62 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +const config = { + // Wallet + privateKey: process.env.PRIVATE_KEY, + walletAddress: process.env.WALLET_ADDRESS, + + // Polymarket API (optional, auto-derived if empty) + clobApiKey: process.env.CLOB_API_KEY || '', + clobApiSecret: process.env.CLOB_API_SECRET || '', + clobApiPassphrase: process.env.CLOB_API_PASSPHRASE || '', + + // Polymarket endpoints + clobHost: 'https://clob.polymarket.com', + gammaHost: 'https://gamma-api.polymarket.com', + dataHost: 'https://data-api.polymarket.com', + chainId: 137, + + // Trader to copy + traderAddress: process.env.TRADER_ADDRESS, + + // Trade sizing + sizeMode: process.env.SIZE_MODE || 'percentage', // "percentage" | "balance" + sizePercent: parseFloat(process.env.SIZE_PERCENT || '50'), + minTradeSize: parseFloat(process.env.MIN_TRADE_SIZE || '1'), + + // Auto sell + autoSellEnabled: process.env.AUTO_SELL_ENABLED === 'true', + autoSellProfitPercent: parseFloat(process.env.AUTO_SELL_PROFIT_PERCENT || '10'), + + // Sell mode when copying sell + sellMode: process.env.SELL_MODE || 'market', // "market" | "limit" + + // Polling intervals (seconds) + pollInterval: parseInt(process.env.POLL_INTERVAL || '15', 10) * 1000, + redeemInterval: parseInt(process.env.REDEEM_INTERVAL || '60', 10) * 1000, + + // Dry run + dryRun: process.env.DRY_RUN === 'true', + + // Retry settings + maxRetries: 5, + retryDelay: 3000, +}; + +// Validation +export function validateConfig() { + const required = ['privateKey', 'walletAddress', 'traderAddress']; + const missing = required.filter((key) => !config[key]); + if (missing.length > 0) { + throw new Error(`Missing required config: ${missing.join(', ')}. Check your .env file.`); + } + if (!['percentage', 'balance'].includes(config.sizeMode)) { + throw new Error(`Invalid SIZE_MODE: ${config.sizeMode}. Use "percentage" or "balance".`); + } + if (!['market', 'limit'].includes(config.sellMode)) { + throw new Error(`Invalid SELL_MODE: ${config.sellMode}. Use "market" or "limit".`); + } +} + +export default config; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b81ba89 --- /dev/null +++ b/src/index.js @@ -0,0 +1,142 @@ +import config, { validateConfig } from './config/index.js'; +import { initClient, getUsdcBalance } from './services/client.js'; +import { checkNewTrades, markTradeProcessed } from './services/watcher.js'; +import { executeBuy, executeSell } from './services/executor.js'; +import { checkAndRedeemPositions } from './services/redeemer.js'; +import { getOpenPositions } from './services/position.js'; +import logger from './utils/logger.js'; + +// ASCII Art Banner +function showBanner() { + console.log(` +\x1b[36m╔══════════════════════════════════════════════════════╗ +║ 🎯 POLYMARKET COPY TRADE TOOL 🎯 ║ +║ Auto-copy trades from any trader ║ +╚══════════════════════════════════════════════════════╝\x1b[0m + `); +} + +// Show current settings +function showSettings() { + logger.info('=== Settings ==='); + logger.info(`Trader: ${config.traderAddress}`); + logger.info(`Size Mode: ${config.sizeMode} (${config.sizePercent}%)`); + logger.info(`Min Trade Size: $${config.minTradeSize}`); + logger.info(`Auto Sell: ${config.autoSellEnabled ? `ON (${config.autoSellProfitPercent}% profit)` : 'OFF'}`); + logger.info(`Sell Mode: ${config.sellMode}`); + logger.info(`Poll Interval: ${config.pollInterval / 1000}s`); + logger.info(`Redeem Interval: ${config.redeemInterval / 1000}s`); + logger.info(`Dry Run: ${config.dryRun ? 'YES (no real trades)' : 'NO (live trading!)'}`); + logger.info('================'); +} + +// Main watcher loop +async function watcherLoop() { + try { + logger.watch('Checking trader activity...'); + const newTrades = await checkNewTrades(); + + if (newTrades.length === 0) { + logger.watch('No new trades from trader'); + return; + } + + logger.watch(`Found ${newTrades.length} new trade(s) from trader`); + + for (const trade of newTrades) { + try { + if (trade.type === 'BUY') { + await executeBuy(trade); + } else if (trade.type === 'SELL') { + await executeSell(trade); + } + } catch (err) { + logger.error(`Error processing trade ${trade.id}:`, err.message); + } + + // Mark as processed regardless of success/failure + markTradeProcessed(trade.id); + } + } catch (err) { + logger.error('Watcher loop error:', err.message); + } +} + +// Redeemer loop +async function redeemerLoop() { + try { + await checkAndRedeemPositions(); + } catch (err) { + logger.error('Redeemer loop error:', err.message); + } +} + +// Main +async function main() { + showBanner(); + + // Validate config + try { + validateConfig(); + } catch (err) { + logger.error(err.message); + process.exit(1); + } + + showSettings(); + + // Initialize client + try { + await initClient(); + } catch (err) { + logger.error('Failed to initialize client:', err.message); + process.exit(1); + } + + // Show balance + try { + const balance = await getUsdcBalance(); + logger.money(`USDC.e Balance: $${balance.toFixed(2)}`); + } catch (err) { + logger.warn('Could not fetch balance:', err.message); + } + + // Show existing positions + const positions = getOpenPositions(); + if (positions.length > 0) { + logger.info(`Existing positions: ${positions.length}`); + positions.forEach((p) => { + logger.info(` - ${p.market} | ${p.shares} shares @ $${p.avgBuyPrice}`); + }); + } + + logger.success('Bot started! Watching trader activity...'); + logger.info('Press Ctrl+C to stop'); + + // Start loops + // Initial run + await watcherLoop(); + await redeemerLoop(); + + // Interval loops + const watcherInterval = setInterval(watcherLoop, config.pollInterval); + const redeemerInterval = setInterval(redeemerLoop, config.redeemInterval); + + // Graceful shutdown + const shutdown = () => { + logger.info('Shutting down...'); + clearInterval(watcherInterval); + clearInterval(redeemerInterval); + logger.info('Goodbye! 👋'); + process.exit(0); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +main().catch((err) => { + logger.error('Fatal error:', err.message); + console.error(err); + process.exit(1); +}); diff --git a/src/services/autoSell.js b/src/services/autoSell.js new file mode 100644 index 0000000..6567afd --- /dev/null +++ b/src/services/autoSell.js @@ -0,0 +1,63 @@ +import { Side, OrderType } from '@polymarket/clob-client'; +import config from '../config/index.js'; +import { getClient } from './client.js'; +import { updatePosition } from './position.js'; +import logger from '../utils/logger.js'; + +/** + * Place an auto-sell limit order at profit target above avg buy price + * @param {string} conditionId - Market condition ID + * @param {string} tokenId - CLOB token ID + * @param {number} shares - Number of shares to sell + * @param {number} avgBuyPrice - Average buy price + * @param {Object} marketOpts - { tickSize, negRisk } + */ +export async function placeAutoSell(conditionId, tokenId, shares, avgBuyPrice, marketOpts) { + try { + const profitMultiplier = 1 + config.autoSellProfitPercent / 100; + let sellPrice = avgBuyPrice * profitMultiplier; + + // Round to tick size + const tickSize = parseFloat(marketOpts.tickSize); + sellPrice = Math.round(sellPrice / tickSize) * tickSize; + + // Clamp between 0.01 and 0.99 + sellPrice = Math.max(0.01, Math.min(0.99, sellPrice)); + + // Format to proper decimal places + const decimals = marketOpts.tickSize.split('.')[1]?.length || 2; + sellPrice = parseFloat(sellPrice.toFixed(decimals)); + + logger.trade(`Auto-sell: ${shares} shares @ $${sellPrice} (${config.autoSellProfitPercent}% above avg buy $${avgBuyPrice.toFixed(4)})`); + + if (config.dryRun) { + logger.info('[DRY RUN] Would place limit sell order'); + updatePosition(conditionId, { sellOrderId: 'dry-run-order' }); + return; + } + + const client = getClient(); + const response = await client.createAndPostOrder( + { + tokenID: tokenId, + price: sellPrice, + size: shares, + side: Side.SELL, + }, + { + tickSize: marketOpts.tickSize, + negRisk: marketOpts.negRisk, + }, + OrderType.GTC, + ); + + if (response && response.success) { + logger.success(`Auto-sell order placed: ${response.orderID} @ $${sellPrice}`); + updatePosition(conditionId, { sellOrderId: response.orderID }); + } else { + logger.warn(`Auto-sell failed: ${response?.errorMsg || 'Unknown'}`); + } + } catch (err) { + logger.error('Failed to place auto-sell:', err.message); + } +} diff --git a/src/services/client.js b/src/services/client.js new file mode 100644 index 0000000..5d18068 --- /dev/null +++ b/src/services/client.js @@ -0,0 +1,81 @@ +import { ClobClient } from '@polymarket/clob-client'; +import { Wallet } from 'ethers'; +import config from '../config/index.js'; +import logger from '../utils/logger.js'; + +let clobClient = null; +let signer = null; + +/** + * Initialize the Polymarket CLOB client + * Auto-derives API credentials if not provided in .env + */ +export async function initClient() { + logger.info('Initializing Polymarket CLOB client...'); + + signer = new Wallet(config.privateKey); + const walletAddress = signer.address; + logger.info(`Wallet address: ${walletAddress}`); + + // Step 1: Create temp client to derive API credentials + let apiCreds; + if (config.clobApiKey && config.clobApiSecret && config.clobApiPassphrase) { + apiCreds = { + key: config.clobApiKey, + secret: config.clobApiSecret, + passphrase: config.clobApiPassphrase, + }; + logger.info('Using API credentials from .env'); + } else { + const tempClient = new ClobClient(config.clobHost, config.chainId, signer); + apiCreds = await tempClient.createOrDeriveApiKey(); + logger.info('API credentials derived successfully'); + } + + // Step 2: Initialize full trading client + clobClient = new ClobClient( + config.clobHost, + config.chainId, + signer, + apiCreds, + 0, // Signature type: 0 = EOA + config.walletAddress || walletAddress, // Funder address + ); + + logger.success('CLOB client initialized'); + return clobClient; +} + +/** + * Get the initialized CLOB client + */ +export function getClient() { + if (!clobClient) { + throw new Error('CLOB client not initialized. Call initClient() first.'); + } + return clobClient; +} + +/** + * Get the signer wallet + */ +export function getSigner() { + if (!signer) { + throw new Error('Signer not initialized. Call initClient() first.'); + } + return signer; +} + +/** + * Get USDC.e balance on Polygon for the wallet + */ +export async function getUsdcBalance() { + const { ethers } = await import('ethers'); + const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com'); + const usdcAddress = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'; // USDC.e on Polygon + const abi = ['function balanceOf(address) view returns (uint256)']; + const usdc = new ethers.Contract(usdcAddress, abi, provider); + const funderAddress = config.walletAddress || signer.address; + const balance = await usdc.balanceOf(funderAddress); + return parseFloat(ethers.utils.formatUnits(balance, 6)); +} diff --git a/src/services/executor.js b/src/services/executor.js new file mode 100644 index 0000000..5e803c1 --- /dev/null +++ b/src/services/executor.js @@ -0,0 +1,319 @@ +import { Side, OrderType } from '@polymarket/clob-client'; +import config from '../config/index.js'; +import { getClient, getUsdcBalance } from './client.js'; +import { hasPosition, addPosition, getPosition, updatePosition, removePosition } from './position.js'; +import { fetchMarketByTokenId } from './watcher.js'; +import { placeAutoSell } from './autoSell.js'; +import logger from '../utils/logger.js'; + +/** + * Calculate trade size based on settings + * @param {number} traderSize - Trader's trade size in USDC + * @returns {number} Our trade size in USDC + */ +async function calculateTradeSize(traderSize) { + if (config.sizeMode === 'percentage') { + // % of trader's trade size + return traderSize * (config.sizePercent / 100); + } else if (config.sizeMode === 'balance') { + // % of our own balance + const balance = await getUsdcBalance(); + return balance * (config.sizePercent / 100); + } + return 0; +} + +/** + * Get market options (tick size and neg risk) for a token + */ +async function getMarketOptions(tokenId) { + const client = getClient(); + try { + // Try to get from market info + const marketInfo = await fetchMarketByTokenId(tokenId); + if (marketInfo) { + return { + tickSize: String(marketInfo.minimum_tick_size || '0.01'), + negRisk: marketInfo.neg_risk || false, + conditionId: marketInfo.condition_id || '', + question: marketInfo.question || '', + }; + } + } catch (err) { + logger.warn('Failed to get market info, using defaults:', err.message); + } + + // Fallback: try SDK methods + try { + const tickSize = await client.getTickSize(tokenId); + const negRisk = await client.getNegRisk(tokenId); + return { tickSize: String(tickSize), negRisk, conditionId: '', question: '' }; + } catch (err) { + logger.warn('Failed to get tick size from SDK, using default 0.01'); + return { tickSize: '0.01', negRisk: false, conditionId: '', question: '' }; + } +} + +/** + * Execute a BUY trade (copy trader's buy) + * @param {Object} trade - Trade info from watcher + */ +export async function executeBuy(trade) { + const { tokenId, conditionId, market, price, size } = trade; + + // Check if already have position for this market + if (hasPosition(conditionId)) { + logger.warn(`Already have position for: ${market || conditionId}. Skipping buy.`); + return; + } + + // Calculate our trade size + const tradeSize = await calculateTradeSize(size * price); // trader's USDC amount + if (tradeSize < config.minTradeSize) { + logger.warn(`Trade size $${tradeSize.toFixed(2)} below minimum $${config.minTradeSize}. Skipping.`); + return; + } + + // Check balance + const balance = await getUsdcBalance(); + if (balance < tradeSize) { + logger.error(`Insufficient balance: $${balance.toFixed(2)} < $${tradeSize.toFixed(2)} needed`); + return; + } + + // Get market options + const marketOpts = await getMarketOptions(tokenId); + const effectiveConditionId = conditionId || marketOpts.conditionId; + + // Double check no position exists + if (effectiveConditionId && hasPosition(effectiveConditionId)) { + logger.warn(`Already have position for: ${market || effectiveConditionId}. Skipping buy.`); + return; + } + + logger.trade(`BUY ${market || tokenId} | Size: $${tradeSize.toFixed(2)} | Trader price: ${price}`); + + if (config.dryRun) { + logger.info('[DRY RUN] Would place market buy order'); + // Still record position in dry run for testing + addPosition({ + conditionId: effectiveConditionId, + tokenId, + market: market || marketOpts.question || tokenId, + shares: tradeSize / price, + avgBuyPrice: price, + totalCost: tradeSize, + outcome: trade.outcome, + }); + return; + } + + // Place market order with retries + const client = getClient(); + let filled = false; + let totalSharesFilled = 0; + let totalCostFilled = 0; + + for (let attempt = 1; attempt <= config.maxRetries; attempt++) { + try { + const remainingAmount = tradeSize - totalCostFilled; + if (remainingAmount < config.minTradeSize) break; + + logger.info(`Buy attempt ${attempt}/${config.maxRetries} | Amount: $${remainingAmount.toFixed(2)}`); + + // Use FAK (fill-and-kill) to get what's available, then retry remainder + const response = await client.createAndPostMarketOrder( + { + tokenID: tokenId, + side: Side.BUY, + amount: remainingAmount, + price: Math.min(price * 1.05, 0.99), // 5% slippage allowance, max 0.99 + }, + { + tickSize: marketOpts.tickSize, + negRisk: marketOpts.negRisk, + }, + OrderType.FOK, + ); + + if (response && response.success) { + logger.success(`Order placed: ${response.orderID} | Status: ${response.status}`); + + // Check if fully filled by trying to get trade info + const takingAmount = parseFloat(response.takingAmount || '0'); + const makingAmount = parseFloat(response.makingAmount || '0'); + + if (takingAmount > 0 || makingAmount > 0) { + totalSharesFilled += takingAmount || (remainingAmount / price); + totalCostFilled += makingAmount || remainingAmount; + filled = true; + break; // FOK either fills fully or cancels + } else { + filled = true; + totalSharesFilled = tradeSize / price; + totalCostFilled = tradeSize; + break; + } + } else { + logger.warn(`Order not filled. Error: ${response?.errorMsg || 'Unknown'}`); + } + } catch (err) { + logger.error(`Buy attempt ${attempt} failed:`, err.message); + } + + // Wait before retry + if (attempt < config.maxRetries) { + await new Promise((r) => setTimeout(r, config.retryDelay)); + } + } + + if (!filled || totalCostFilled === 0) { + logger.error(`Failed to fill buy order for ${market || tokenId} after ${config.maxRetries} attempts`); + return; + } + + // Calculate avg buy price + const avgBuyPrice = totalSharesFilled > 0 ? totalCostFilled / totalSharesFilled : price; + + // Record position + addPosition({ + conditionId: effectiveConditionId, + tokenId, + market: market || marketOpts.question || tokenId, + shares: totalSharesFilled, + avgBuyPrice, + totalCost: totalCostFilled, + outcome: trade.outcome, + }); + + // Auto-sell if enabled + if (config.autoSellEnabled) { + await placeAutoSell(effectiveConditionId, tokenId, totalSharesFilled, avgBuyPrice, marketOpts); + } +} + +/** + * Execute a SELL trade (copy trader's sell) + * @param {Object} trade - Trade info from watcher + */ +export async function executeSell(trade) { + const { tokenId, conditionId, market, price } = trade; + + // Get market options to resolve conditionId + let effectiveConditionId = conditionId; + let marketOpts; + if (!effectiveConditionId) { + marketOpts = await getMarketOptions(tokenId); + effectiveConditionId = marketOpts.conditionId; + } + + // Check if we have a position + const position = getPosition(effectiveConditionId); + if (!position) { + logger.warn(`No position found for: ${market || effectiveConditionId}. Skipping sell.`); + return; + } + + if (position.status === 'selling' || position.status === 'sold') { + logger.warn(`Position already ${position.status}: ${market || effectiveConditionId}. Skipping.`); + return; + } + + logger.trade(`SELL ${position.market} | Shares: ${position.shares} | Trader price: ${price}`); + + if (config.dryRun) { + logger.info('[DRY RUN] Would place sell order'); + removePosition(effectiveConditionId); + return; + } + + // Cancel existing auto-sell order if any + if (position.sellOrderId) { + try { + const client = getClient(); + await client.cancelOrder(position.sellOrderId); + logger.info(`Cancelled auto-sell order: ${position.sellOrderId}`); + } catch (err) { + logger.warn(`Failed to cancel auto-sell: ${err.message}`); + } + } + + updatePosition(effectiveConditionId, { status: 'selling' }); + + if (!marketOpts) { + marketOpts = await getMarketOptions(tokenId); + } + + const client = getClient(); + let filled = false; + + for (let attempt = 1; attempt <= config.maxRetries; attempt++) { + try { + if (config.sellMode === 'market') { + // Market sell (FOK) + logger.info(`Sell attempt ${attempt}/${config.maxRetries} (market) | Shares: ${position.shares}`); + + const response = await client.createAndPostMarketOrder( + { + tokenID: tokenId, + side: Side.SELL, + amount: position.shares, + price: Math.max(price * 0.95, 0.01), // 5% slippage, min 0.01 + }, + { + tickSize: marketOpts.tickSize, + negRisk: marketOpts.negRisk, + }, + OrderType.FOK, + ); + + if (response && response.success) { + logger.success(`Sell order placed: ${response.orderID}`); + filled = true; + break; + } else { + logger.warn(`Sell not filled: ${response?.errorMsg || 'Unknown'}`); + } + } else { + // Limit sell at trader's sell price + logger.info(`Sell attempt ${attempt}/${config.maxRetries} (limit) | Price: ${price}`); + + const response = await client.createAndPostOrder( + { + tokenID: tokenId, + price: price, + size: position.shares, + side: Side.SELL, + }, + { + tickSize: marketOpts.tickSize, + negRisk: marketOpts.negRisk, + }, + OrderType.GTC, + ); + + if (response && response.success) { + logger.success(`Limit sell placed: ${response.orderID} @ $${price}`); + filled = true; + break; + } else { + logger.warn(`Limit sell failed: ${response?.errorMsg || 'Unknown'}`); + } + } + } catch (err) { + logger.error(`Sell attempt ${attempt} failed:`, err.message); + } + + if (attempt < config.maxRetries) { + await new Promise((r) => setTimeout(r, config.retryDelay)); + } + } + + if (filled) { + removePosition(effectiveConditionId); + logger.money(`Position sold: ${position.market}`); + } else { + updatePosition(effectiveConditionId, { status: 'open' }); + logger.error(`Failed to sell ${position.market} after ${config.maxRetries} attempts`); + } +} diff --git a/src/services/position.js b/src/services/position.js new file mode 100644 index 0000000..d203625 --- /dev/null +++ b/src/services/position.js @@ -0,0 +1,112 @@ +import { readState, writeState } from '../utils/state.js'; +import logger from '../utils/logger.js'; + +const POSITIONS_FILE = 'positions.json'; + +/** + * Get all current positions + * @returns {Object} Map of conditionId -> position data + */ +export function getPositions() { + return readState(POSITIONS_FILE, {}); +} + +/** + * Check if we already have a position for this market (conditionId) + * @param {string} conditionId + * @returns {boolean} + */ +export function hasPosition(conditionId) { + const positions = getPositions(); + return !!positions[conditionId]; +} + +/** + * Add a new position after buy is filled + * @param {Object} params + * @param {string} params.conditionId - Market condition ID + * @param {string} params.tokenId - CLOB token ID + * @param {string} params.market - Market question/title + * @param {number} params.shares - Number of shares bought + * @param {number} params.avgBuyPrice - Average buy price + * @param {number} params.totalCost - Total USDC spent + * @param {string} params.outcome - YES/NO outcome + * @param {string} [params.sellOrderId] - Auto-sell order ID if placed + */ +export function addPosition({ + conditionId, + tokenId, + market, + shares, + avgBuyPrice, + totalCost, + outcome, + sellOrderId, +}) { + const positions = getPositions(); + positions[conditionId] = { + conditionId, + tokenId, + market, + shares, + avgBuyPrice, + totalCost, + outcome: outcome || '', + sellOrderId: sellOrderId || null, + status: 'open', // open, selling, sold, redeemed + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + writeState(POSITIONS_FILE, positions); + logger.success(`Position added: ${market} | ${shares} shares @ $${avgBuyPrice}`); +} + +/** + * Update a position + * @param {string} conditionId + * @param {Object} updates - Fields to update + */ +export function updatePosition(conditionId, updates) { + const positions = getPositions(); + if (positions[conditionId]) { + positions[conditionId] = { + ...positions[conditionId], + ...updates, + updatedAt: new Date().toISOString(), + }; + writeState(POSITIONS_FILE, positions); + } +} + +/** + * Remove a position (after sell or redeem) + * @param {string} conditionId + */ +export function removePosition(conditionId) { + const positions = getPositions(); + if (positions[conditionId]) { + const market = positions[conditionId].market; + delete positions[conditionId]; + writeState(POSITIONS_FILE, positions); + logger.info(`Position removed: ${market}`); + } +} + +/** + * Get position by conditionId + * @param {string} conditionId + * @returns {Object|null} + */ +export function getPosition(conditionId) { + const positions = getPositions(); + return positions[conditionId] || null; +} + +/** + * Get all open positions as an array + * @returns {Array} + */ +export function getOpenPositions() { + const positions = getPositions(); + return Object.values(positions).filter((p) => p.status === 'open'); +} diff --git a/src/services/redeemer.js b/src/services/redeemer.js new file mode 100644 index 0000000..86c5e8f --- /dev/null +++ b/src/services/redeemer.js @@ -0,0 +1,148 @@ +import { ethers } from 'ethers'; +import config from '../config/index.js'; +import { getOpenPositions, updatePosition, removePosition } from './position.js'; +import logger from '../utils/logger.js'; + +// Contract addresses on Polygon +const CTF_ADDRESS = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; +const NEG_RISK_CTF_ADDRESS = '0xC5d563A36AE78145C45a50134d48A1215220f80a'; +const USDC_ADDRESS = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'; + +// CTF ABI (minimal for redeemPositions & balanceOf) +const CTF_ABI = [ + 'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)', + 'function balanceOf(address owner, uint256 tokenId) view returns (uint256)', + 'function payoutNumerators(bytes32 conditionId, uint256 outcomeIndex) view returns (uint256)', + 'function payoutDenominator(bytes32 conditionId) view returns (uint256)', +]; + +/** + * Check if a market has been resolved and our position is a winner + * @param {string} conditionId + * @returns {Object|null} { resolved, won } + */ +async function checkMarketResolution(conditionId) { + try { + // Check via Gamma API + const url = `${config.gammaHost}/markets?condition_id=${conditionId}`; + const response = await fetch(url); + if (!response.ok) return null; + + const markets = await response.json(); + if (!markets || markets.length === 0) return null; + + const market = markets[0]; + return { + resolved: market.closed || market.resolved || false, + active: market.active, + endDate: market.end_date_iso, + resolutionSource: market.resolution_source, + question: market.question, + }; + } catch (err) { + logger.error('Failed to check market resolution:', err.message); + return null; + } +} + +/** + * Check on-chain if a position (token) has value (payout available) + */ +async function checkOnChainPayout(conditionId) { + try { + const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com'); + const ctf = new ethers.Contract(CTF_ADDRESS, CTF_ABI, provider); + + const denominator = await ctf.payoutDenominator(conditionId); + if (denominator.isZero()) return { resolved: false, payouts: [] }; + + // Check payouts for both outcomes (YES=0, NO=1) + const payouts = []; + for (let i = 0; i < 2; i++) { + const numerator = await ctf.payoutNumerators(conditionId, i); + payouts.push(numerator.toNumber() / denominator.toNumber()); + } + + return { resolved: true, payouts }; + } catch (err) { + // If payoutDenominator is 0 or reverts, market not resolved + return { resolved: false, payouts: [] }; + } +} + +/** + * Redeem winning position on-chain + */ +async function redeemPosition(conditionId, isNegRisk = false) { + try { + const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com'); + const wallet = new ethers.Wallet(config.privateKey, provider); + const ctfAddress = isNegRisk ? NEG_RISK_CTF_ADDRESS : CTF_ADDRESS; + const ctf = new ethers.Contract(ctfAddress, CTF_ABI, wallet); + + const parentCollectionId = ethers.constants.HashZero; + const indexSets = [1, 2]; // Both outcomes + + logger.info(`Redeeming position for conditionId: ${conditionId}`); + + const tx = await ctf.redeemPositions( + USDC_ADDRESS, + parentCollectionId, + conditionId, + indexSets, + { gasLimit: 300000 }, + ); + + logger.info(`Redeem tx sent: ${tx.hash}`); + const receipt = await tx.wait(); + logger.success(`Redeem confirmed in block ${receipt.blockNumber}`); + + return true; + } catch (err) { + logger.error('Failed to redeem position:', err.message); + return false; + } +} + +/** + * Check all open positions for redeemable (resolved & won) markets + */ +export async function checkAndRedeemPositions() { + const positions = getOpenPositions(); + if (positions.length === 0) return; + + logger.info(`Checking ${positions.length} position(s) for redemption...`); + + for (const position of positions) { + try { + // Check via API first + const resolution = await checkMarketResolution(position.conditionId); + + if (!resolution || !resolution.resolved) { + // Try on-chain check as fallback + const onChain = await checkOnChainPayout(position.conditionId); + if (!onChain.resolved) continue; + + // Check if our outcome won + // Determine outcome index (0=YES, 1=NO based on token position) + logger.info(`Market resolved on-chain: ${position.market} | Payouts: ${onChain.payouts}`); + } else { + logger.info(`Market resolved: ${position.market}`); + } + + if (config.dryRun) { + logger.info(`[DRY RUN] Would redeem position: ${position.market}`); + continue; + } + + // Attempt to redeem + const success = await redeemPosition(position.conditionId); + if (success) { + removePosition(position.conditionId); + logger.money(`Redeemed: ${position.market}`); + } + } catch (err) { + logger.error(`Error checking position ${position.market}:`, err.message); + } + } +} diff --git a/src/services/watcher.js b/src/services/watcher.js new file mode 100644 index 0000000..80729a4 --- /dev/null +++ b/src/services/watcher.js @@ -0,0 +1,137 @@ +import config from '../config/index.js'; +import logger from '../utils/logger.js'; +import { readState, writeState } from '../utils/state.js'; + +const PROCESSED_FILE = 'processed_trades.json'; + +/** + * Fetch trader's recent activity from Data API + * @returns {Array} List of trade activities + */ +async function fetchTraderActivity() { + const url = `${config.dataHost}/activity?user=${config.traderAddress}`; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Data API returned ${response.status}`); + } + return await response.json(); + } catch (err) { + logger.error('Failed to fetch trader activity:', err.message); + return []; + } +} + +/** + * Get list of already processed trade IDs + */ +function getProcessedTrades() { + return readState(PROCESSED_FILE, { tradeIds: [] }); +} + +/** + * Mark a trade as processed + */ +function markTradeProcessed(tradeId) { + const data = getProcessedTrades(); + data.tradeIds.push(tradeId); + // Keep only last 500 trade IDs to prevent unbounded growth + if (data.tradeIds.length > 500) { + data.tradeIds = data.tradeIds.slice(-500); + } + writeState(PROCESSED_FILE, data); +} + +/** + * Check for new trades from the watched trader + * @returns {Array} New trades to process: { id, type, tokenId, conditionId, market, price, size, timestamp, side } + */ +export async function checkNewTrades() { + const activities = await fetchTraderActivity(); + const processed = getProcessedTrades(); + + if (!Array.isArray(activities) || activities.length === 0) { + return []; + } + + const newTrades = []; + + for (const activity of activities) { + // Skip already processed + const tradeId = activity.id || activity.transaction_hash || `${activity.timestamp}_${activity.asset}`; + if (processed.tradeIds.includes(tradeId)) { + continue; + } + + // Only process filled trades (buys and sells) + const type = activity.type?.toUpperCase(); + if (!['BUY', 'SELL'].includes(type)) { + // Mark non-buy/sell as processed so we don't re-check + markTradeProcessed(tradeId); + continue; + } + + // Extract trade info + const trade = { + id: tradeId, + type, // BUY or SELL + tokenId: activity.asset || activity.token_id || '', + conditionId: activity.condition_id || activity.conditionId || '', + market: activity.title || activity.question || activity.market || '', + price: parseFloat(activity.price || '0'), + size: parseFloat(activity.size || activity.amount || '0'), + side: activity.side || type, + timestamp: activity.timestamp || activity.created_at || new Date().toISOString(), + outcome: activity.outcome || '', + proxyWalletAddress: activity.proxyWalletAddress || '', + }; + + // Need tokenId to trade + if (!trade.tokenId) { + logger.warn(`Skipping trade without tokenId: ${tradeId}`); + markTradeProcessed(tradeId); + continue; + } + + newTrades.push(trade); + } + + return newTrades; +} + +/** + * Mark trade as processed after handling + */ +export { markTradeProcessed }; + +/** + * Fetch market info from Gamma API by condition ID + */ +export async function fetchMarketInfo(conditionId) { + try { + const url = `${config.gammaHost}/markets?condition_id=${conditionId}`; + const response = await fetch(url); + if (!response.ok) return null; + const markets = await response.json(); + return markets && markets.length > 0 ? markets[0] : null; + } catch (err) { + logger.error('Failed to fetch market info:', err.message); + return null; + } +} + +/** + * Fetch market info by token ID (CLOB token) + */ +export async function fetchMarketByTokenId(tokenId) { + try { + const url = `${config.gammaHost}/markets?clob_token_ids=${tokenId}`; + const response = await fetch(url); + if (!response.ok) return null; + const markets = await response.json(); + return markets && markets.length > 0 ? markets[0] : null; + } catch (err) { + logger.error('Failed to fetch market by tokenId:', err.message); + return null; + } +} diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..d515e48 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,34 @@ +const COLORS = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', +}; + +function timestamp() { + return new Date().toISOString().replace('T', ' ').substring(0, 19); +} + +function formatMsg(level, color, emoji, ...args) { + const ts = timestamp(); + const prefix = `${COLORS.dim}[${ts}]${COLORS.reset} ${color}${emoji} ${level}${COLORS.reset}`; + console.log(prefix, ...args); +} + +const logger = { + info: (...args) => formatMsg('INFO', COLORS.blue, 'ℹ️ ', ...args), + success: (...args) => formatMsg('SUCCESS', COLORS.green, '✅', ...args), + warn: (...args) => formatMsg('WARN', COLORS.yellow, '⚠️ ', ...args), + error: (...args) => formatMsg('ERROR', COLORS.red, '❌', ...args), + trade: (...args) => formatMsg('TRADE', COLORS.magenta, '📊', ...args), + watch: (...args) => formatMsg('WATCH', COLORS.cyan, '👀', ...args), + money: (...args) => formatMsg('MONEY', COLORS.green, '💰', ...args), +}; + +export default logger; diff --git a/src/utils/state.js b/src/utils/state.js new file mode 100644 index 0000000..8c5e8ba --- /dev/null +++ b/src/utils/state.js @@ -0,0 +1,48 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DATA_DIR = path.join(__dirname, '..', '..', 'data'); + +// Ensure data directory exists +if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); +} + +/** + * Read JSON state file + * @param {string} filename - File name (e.g., "positions.json") + * @param {*} defaultValue - Default value if file doesn't exist + * @returns {*} Parsed JSON data + */ +export function readState(filename, defaultValue = {}) { + const filePath = path.join(DATA_DIR, filename); + try { + if (fs.existsSync(filePath)) { + const data = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(data); + } + } catch (err) { + console.error(`Error reading state file ${filename}:`, err.message); + } + return defaultValue; +} + +/** + * Write JSON state file (atomic write via temp file) + * @param {string} filename - File name (e.g., "positions.json") + * @param {*} data - Data to write + */ +export function writeState(filename, data) { + const filePath = path.join(DATA_DIR, filename); + const tempPath = filePath + '.tmp'; + try { + fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8'); + fs.renameSync(tempPath, filePath); + } catch (err) { + console.error(`Error writing state file ${filename}:`, err.message); + // Clean up temp file if rename failed + try { fs.unlinkSync(tempPath); } catch (_) { } + } +}