commit 8e500d94373eb4c977a64bf574a1bd365538254d Author: warproxxx Date: Mon Mar 31 12:16:51 2025 -0400 Repo is ready diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8f0f8b2 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Polymarket Authentication +PK=your_private_key_here +BROWSER_ADDRESS=your_wallet_address_here + +# Google Sheets (for data_updater) +SPREADSHEET_URL=your_google_spreadsheet_url_here \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f095dae --- /dev/null +++ b/.gitignore @@ -0,0 +1,469 @@ +output.txt +data/ +credentials.json +*/credentials.json +.env +.env.* +positions/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + + + +.env +env/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..474194c --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# Poly-Maker + +A market making bot for Polymarket prediction markets. This bot automates the process of providing liquidity to markets on Polymarket by maintaining orders on both sides of the book with configurable parameters. + +## Overview + +Poly-Maker is a comprehensive solution for automated market making on Polymarket. It includes: + +- Real-time order book monitoring via WebSockets +- Position management with risk controls +- Customizable trade parameters fetched from Google Sheets +- Automated position merging functionality +- Sophisticated spread and price management + +## Structure + +The repository consists of several interconnected modules: + +- `poly_data`: Core data management and market making logic +- `poly_merger`: Utility for merging positions (based on open-source Polymarket code) +- `poly_stats`: Account statistics tracking +- `poly_utils`: Shared utility functions +- `data_updater`: Separate module for collecting market information + +## Requirements + +- Python 3.8+ +- Node.js (for poly_merger) +- Google Sheets API credentials +- Polymarket account and API credentials + +## Installation + +1. Clone the repository: +``` +git clone https://github.com/yourusername/poly-maker.git +cd poly-maker +``` + +2. Install Python dependencies: +``` +pip install -r requirements.txt +``` + +3. Install Node.js dependencies for the merger: +``` +cd poly_merger +npm install +cd .. +``` + +4. Set up your environment variables by copying the example file: +``` +cp .env.example .env +``` + +5. Edit the `.env` file with your credentials: +- `PK`: Your private key for Polymarket +- `BROWSER_ADDRESS`: Your wallet address +- `SPREADSHEET_URL`: URL to your Google Sheets configuration + +6. Create a Google Service Account and download credentials: +``` +cp data_updater/credentials.json.example credentials.json +``` +Edit the credentials.json with your Google service account details. + +## Configuration + +The bot is configured via a Google Spreadsheet with several worksheets: + +- **Selected Markets**: Markets you want to trade +- **All Markets**: Database of all markets on Polymarket +- **Hyperparameters**: Configuration parameters for the trading logic + +## Usage + +### Data Collection + +Before running the main bot, you need to gather market data: + +``` +cd data_updater +python find_markets.py +``` + +This will populate your Google Sheet with available markets. + +### Running the Bot + +Start the market maker: + +``` +python main.py +``` + +The bot will: +1. Connect to Polymarket API +2. Subscribe to market data via WebSockets +3. Place and manage orders according to your configuration +4. Monitor and merge positions when beneficial + +## Notes on poly_merger + +The `poly_merger` module is a particularly powerful utility that handles position merging on Polymarket. It's built on open-source Polymarket code and provides a smooth way to consolidate positions, reducing gas fees and improving capital efficiency. + +## Important Notes + +- This code interacts with real markets and can potentially lose real money +- Test thoroughly with small amounts before deploying with significant capital +- The `data_updater` is technically a separate repository but is included here for convenience +- The `find_markets.py` script in the data_updater is essential for the bot to get market information + +## License + +MIT \ No newline at end of file diff --git a/data_updater/README.md b/data_updater/README.md new file mode 100644 index 0000000..ea04c4a --- /dev/null +++ b/data_updater/README.md @@ -0,0 +1,78 @@ +# Polymarket Market Maker Data Updater + +This tool analyzes Polymarket prediction markets to identify profitable market making opportunities. It fetches market data, calculates potential rewards, analyzes price volatility, and updates a Google Spreadsheet with the results. + +## Features + +- Fetches all available Polymarket markets +- Calculates rewards based on market maker formulas +- Analyzes price volatility over different time windows +- Updates Google Sheets with market opportunities sorted by profitability +- Automatically runs every 5 minutes + +## Prerequisites + +- Python 3.7+ +- A Google Cloud project with Sheets API enabled +- Polymarket account with an API key + +## Setup + +1. Clone the repository +2. Install dependencies: + ``` + pip install -r requirements.txt + ``` +3. Create a `.env` file with your private key (see example below) +4. Create a Google service account and download `credentials.json` +5. Set up your Google Spreadsheet with the following worksheets: + - "All Markets" + - "Volatility Markets" + - "Full Markets" + - "Selected Markets" + +## Running the Tool + +``` +python update_markets.py +``` + +This will start the continuous update process, refreshing market data every 5 minutes. + +## Configuration Files + +### .env Example + +``` +PK=your_polygon_wallet_private_key +SPREADSHEET_URL=https://docs.google.com/spreadsheets/d/your_spreadsheet_id/edit +``` + +### credentials.json Example + +This is a Google service account credentials file. Create one at the [Google Cloud Console](https://console.cloud.google.com/): + +```json +{ + "type": "service_account", + "project_id": "your-project-id", + "private_key_id": "abc123...", + "private_key": "-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n", + "client_email": "your-service-account@your-project-id.iam.gserviceaccount.com", + "client_id": "123456789", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account%40your-project-id.iam.gserviceaccount.com" +} +``` + +## Security Notes + +- Keep your private key and credentials.json secure +- Never commit sensitive files to public repositories +- Use environment variables for sensitive information + +## Disclaimer + +This tool is for educational purposes only. Trading cryptocurrency involves significant risk. This is not financial advice. \ No newline at end of file diff --git a/data_updater/credentials.json.example b/data_updater/credentials.json.example new file mode 100644 index 0000000..148503a --- /dev/null +++ b/data_updater/credentials.json.example @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "your-project-id", + "private_key_id": "abc123...", + "private_key": "-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n", + "client_email": "your-service-account@your-project-id.iam.gserviceaccount.com", + "client_id": "123456789", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account%40your-project-id.iam.gserviceaccount.com" +} \ No newline at end of file diff --git a/data_updater/erc20ABI.json b/data_updater/erc20ABI.json new file mode 100644 index 0000000..37f31f9 --- /dev/null +++ b/data_updater/erc20ABI.json @@ -0,0 +1,279 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "guy", + "type": "address" + }, + { + "name": "wad", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "src", + "type": "address" + }, + { + "name": "dst", + "type": "address" + }, + { + "name": "wad", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "wad", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "dst", + "type": "address" + }, + { + "name": "wad", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "deposit", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address" + }, + { + "indexed": true, + "name": "guy", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address" + }, + { + "indexed": true, + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address" + }, + { + "indexed": false, + "name": "wad", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + } +] \ No newline at end of file diff --git a/data_updater/find_markets.py b/data_updater/find_markets.py new file mode 100644 index 0000000..cc177b2 --- /dev/null +++ b/data_updater/find_markets.py @@ -0,0 +1,322 @@ +import pandas as pd +import numpy as np +import os +import requests +import time +import warnings +warnings.filterwarnings("ignore") + + +if not os.path.exists('data'): + os.makedirs('data') + +def get_sel_df(spreadsheet, sheet_name='Selected Markets'): + try: + wk2 = spreadsheet.worksheet(sheet_name) + sel_df = pd.DataFrame(wk2.get_all_records()) + sel_df = sel_df[sel_df['question'] != ""].reset_index(drop=True) + return sel_df + except: + return pd.DataFrame() + +def get_all_markets(client): + cursor = "" + all_markets = [] + + while True: + try: + markets = client.get_sampling_markets(next_cursor = cursor) + markets_df = pd.DataFrame(markets['data']) + + + cursor = markets['next_cursor'] + + + + all_markets.append(markets_df) + + if cursor is None: + break + except: + break + + all_df = pd.concat(all_markets) + all_df = all_df.reset_index(drop=True) + + return all_df + +def get_bid_ask_range(ret, TICK_SIZE): + bid_from = ret['midpoint'] - ret['max_spread'] / 100 + bid_to = ret['best_ask'] #Although bid to this high up will change bid_from because of changing midpoint, take optimistic approach + + if bid_to == 0: + bid_to = ret['midpoint'] + + if bid_to - TICK_SIZE > ret['midpoint']: + bid_to = ret['best_bid'] + (TICK_SIZE + 0.1 * TICK_SIZE) + + if bid_from > bid_to: + bid_from = bid_to - (TICK_SIZE + 0.1 * TICK_SIZE) + + ask_to = ret['midpoint'] + ret['max_spread'] / 100 + ask_from = ret['best_bid'] + + if ask_from == 0: + ask_from = ret['midpoint'] + + if ask_from + TICK_SIZE < ret['midpoint']: + ask_from = ret['best_ask'] - (TICK_SIZE + 0.1 * TICK_SIZE) + + if ask_from > ask_to: + ask_to = ask_from + (TICK_SIZE + 0.1 * TICK_SIZE) + + bid_from = round(bid_from, 3) + bid_to = round(bid_to, 3) + ask_from = round(ask_from, 3) + ask_to = round(ask_to, 3) + + if bid_from < 0: + bid_from = 0 + + if ask_from < 0: + ask_from = 0 + + return bid_from, bid_to, ask_from, ask_to + + +def generate_numbers(start, end, TICK_SIZE): + # Calculate the starting point, rounding up to the next hundredth if not an exact multiple of TICK_SIZE + rounded_start = (int(start * 100) + 1) / 100 if start * 100 % 1 != 0 else start + TICK_SIZE + + # Calculate the ending point, rounding down to the nearest hundredth + rounded_end = int(end * 100) / 100 + + # Generate numbers from rounded_start to rounded_end, ensuring they fall strictly within the original bounds + numbers = [] + current = rounded_start + while current < end: + numbers.append(current) + current += TICK_SIZE + current = round(current, len(str(TICK_SIZE).split('.')[1])) # Rounding to avoid floating point imprecision + + return numbers + +def add_formula_params(curr_df, midpoint, v, daily_reward): + curr_df['s'] = (curr_df['price'] - midpoint).abs() + curr_df['S'] = ((v - curr_df['s']) / v) ** 2 + curr_df['100'] = 1/curr_df['price'] * 100 + + curr_df['size'] = curr_df['size'] + curr_df['100'] + + curr_df['Q'] = curr_df['S'] * curr_df['size'] + curr_df['reward_per_100'] = (curr_df['Q'] / curr_df['Q'].sum()) * daily_reward / 2 / curr_df['size'] * curr_df['100'] + return curr_df + +def process_single_row(row, client): + ret = {} + ret['question'] = row['question'] + ret['neg_risk'] = row['neg_risk'] + + ret['answer1'] = row['tokens'][0]['outcome'] + ret['answer2'] = row['tokens'][1]['outcome'] + + ret['min_size'] = row['rewards']['min_size'] + ret['max_spread'] = row['rewards']['max_spread'] + + token1 = row['tokens'][0]['token_id'] + token2 = row['tokens'][1]['token_id'] + + rate = 0 + for rate_info in row['rewards']['rates']: + if rate_info['asset_address'].lower() == '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'.lower(): + rate = rate_info['rewards_daily_rate'] + break + + ret['rewards_daily_rate'] = rate + book = client.get_order_book(token1) + + bids = pd.DataFrame() + asks = pd.DataFrame() + + try: + bids = pd.DataFrame(book.bids).astype(float) + except: + pass + + try: + asks = pd.DataFrame(book.asks).astype(float) + except: + pass + + + try: + ret['best_bid'] = bids.iloc[-1]['price'] + except: + ret['best_bid'] = 0 + + try: + ret['best_ask'] = asks.iloc[-1]['price'] + except: + ret['best_ask'] = 0 + + ret['midpoint'] = (ret['best_bid'] + ret['best_ask']) / 2 + + TICK_SIZE = row['minimum_tick_size'] + ret['tick_size'] = TICK_SIZE + + bid_from, bid_to, ask_from, ask_to = get_bid_ask_range(ret, TICK_SIZE) + v = round((ret['max_spread'] / 100), 2) + + bids_df = pd.DataFrame() + bids_df['price'] = generate_numbers(bid_from, bid_to, TICK_SIZE) + + asks_df = pd.DataFrame() + asks_df['price'] = generate_numbers(ask_from, ask_to, TICK_SIZE) + + try: + bids_df = bids_df.merge(bids, on='price', how='left').fillna(0) + except: + bids_df = pd.DataFrame() + + try: + asks_df = asks_df.merge(asks, on='price', how='left').fillna(0) + except: + asks_df = pd.DataFrame() + + best_bid_reward = 0 + ret_bid = pd.DataFrame() + + try: + ret_bid = add_formula_params(bids_df, ret['midpoint'], v, rate) + best_bid_reward = round(ret_bid['reward_per_100'].max(), 2) + except: + pass + + best_ask_reward = 0 + ret_ask = pd.DataFrame() + + try: + ret_ask = add_formula_params(asks_df, ret['midpoint'], v, rate) + best_ask_reward = round(ret_ask['reward_per_100'].max(), 2) + except: + pass + + ret['bid_reward_per_100'] = best_bid_reward + ret['ask_reward_per_100'] = best_ask_reward + + ret['sm_reward_per_100'] = round((best_bid_reward + best_ask_reward) / 2, 2) + ret['gm_reward_per_100'] = round((best_bid_reward * best_ask_reward) ** 0.5, 2) + + ret['end_date_iso'] = row['end_date_iso'] + ret['market_slug'] = row['market_slug'] + ret['token1'] = token1 + ret['token2'] = token2 + ret['condition_id'] = row['condition_id'] + + return ret + + +def get_all_results(all_df, client): + all_results = [] + + for idx, row in all_df.iterrows(): + + try: + if idx % 10 == 0: + print(f'{idx} of {len(all_df)}') + + time.sleep(1) + result = process_single_row(row, client) + all_results.append(result) + except: + print("error fetching market") + + return all_results + +def get_combined_markets(new_df, new_markets, sel_df): + + if len(sel_df) > 0: + old_markets = new_df[new_df['question'].isin(sel_df['question'])] + all_markets = pd.concat([old_markets, new_markets]) + else: + all_markets = new_markets + + all_markets = all_markets.drop_duplicates('question') + + all_markets = all_markets.sort_values('gm_reward_per_100', ascending=False) + return all_markets + +import concurrent.futures + +def calculate_annualized_volatility(df, hours): + end_time = df['t'].max() + start_time = end_time - pd.Timedelta(hours=hours) + window_df = df[df['t'] >= start_time] + volatility = window_df['log_return'].std() + annualized_volatility = volatility * np.sqrt(60 * 24 * 252) + return round(annualized_volatility, 2) + +def add_volatility(row): + res = requests.get(f'https://clob.polymarket.com/prices-history?interval=1m&market={row["token1"]}&fidelity=10') + price_df = pd.DataFrame(res.json()['history']) + price_df['t'] = pd.to_datetime(price_df['t'], unit='s') + price_df['p'] = price_df['p'].round(2) + + price_df.to_csv(f'data/{row["token1"]}.csv', index=False) + + price_df['log_return'] = np.log(price_df['p'] / price_df['p'].shift(1)) + + row_dict = row.copy() + + stats = { + '1_hour': calculate_annualized_volatility(price_df, 1), + '3_hour': calculate_annualized_volatility(price_df, 3), + '6_hour': calculate_annualized_volatility(price_df, 6), + '12_hour': calculate_annualized_volatility(price_df, 12), + '24_hour': calculate_annualized_volatility(price_df, 24), + '7_day': calculate_annualized_volatility(price_df, 24 * 7), + '14_day': calculate_annualized_volatility(price_df, 24 * 14), + '30_day': calculate_annualized_volatility(price_df, 24 * 30), + 'volatility_price': price_df['p'].iloc[-1] + } + + new_dict = {**row_dict, **stats} + return new_dict + +def add_volatility_to_df(df): + + results = [] + df = df.reset_index(drop=True) + + for idx, row in df.iterrows(): + try: + if idx % 10 == 0: + print(f'{idx} of {len(df)}') + + ret = add_volatility(row.to_dict()) + time.sleep(1) + results.append(ret) + except: + print("Error fetching volatility") + + return pd.DataFrame(results) + + +def get_markets(all_results, sel_df, maker_reward=1): + new_df = pd.DataFrame(all_results) + new_df['spread'] = abs(new_df['best_ask'] - new_df['best_bid']) + new_df = new_df.sort_values('rewards_daily_rate', ascending=False) + new_df[' '] = '' + + new_df = new_df[['question', 'answer1', 'answer2', 'neg_risk', 'spread', 'best_bid', 'best_ask', 'rewards_daily_rate', 'bid_reward_per_100', 'ask_reward_per_100', 'gm_reward_per_100', 'sm_reward_per_100', 'min_size', 'max_spread', 'tick_size', 'market_slug', 'token1', 'token2', 'condition_id']] + new_df = new_df.replace([np.inf, -np.inf], 0) + all_data = new_df.copy() + s_df = new_df.copy() + + + making_markets = s_df[~new_df['question'].isin(sel_df['question'])] + making_markets = making_markets.sort_values('gm_reward_per_100', ascending=False) + making_markets = making_markets[making_markets['gm_reward_per_100'] >= maker_reward] + all_markets = get_combined_markets(new_df, making_markets, sel_df) + + return all_data, all_markets diff --git a/data_updater/google_utils.py b/data_updater/google_utils.py new file mode 100644 index 0000000..09d3de7 --- /dev/null +++ b/data_updater/google_utils.py @@ -0,0 +1,13 @@ +from google.oauth2.service_account import Credentials +import gspread +import os + + +def get_spreadsheet(): + """Get the main Google Spreadsheet using credentials and URL from environment variables""" + scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"] + + credentials = Credentials.from_service_account_file('credentials.json', scopes=scope) + client = gspread.authorize(credentials) + spreadsheet = client.open_by_url(os.getenv("SPREADSHEET_URL")) + return spreadsheet \ No newline at end of file diff --git a/data_updater/requirements.txt b/data_updater/requirements.txt new file mode 100644 index 0000000..9aba353 --- /dev/null +++ b/data_updater/requirements.txt @@ -0,0 +1,4 @@ +py-clob-client +gspread +gspread-dataframe +pandas diff --git a/data_updater/trading_utils.py b/data_updater/trading_utils.py new file mode 100644 index 0000000..2612547 --- /dev/null +++ b/data_updater/trading_utils.py @@ -0,0 +1,136 @@ +from py_clob_client.constants import POLYGON +from py_clob_client.client import ClobClient +from py_clob_client.clob_types import OrderArgs, BalanceAllowanceParams, AssetType +from py_clob_client.order_builder.constants import BUY + +from web3.constants import MAX_INT +from web3 import Web3 +from web3.middleware import geth_poa_middleware + +import json + +from dotenv import load_dotenv +load_dotenv() + +import time + +import os + +def get_clob_client(): + host = "https://clob.polymarket.com" + key = os.getenv("PK") + chain_id = POLYGON + + if key is None: + print("Environment variable 'PK' cannot be found") + return None + + + try: + client = ClobClient(host, key=key, chain_id=chain_id) + api_creds = client.create_or_derive_api_creds() + client.set_api_creds(api_creds) + return client + except Exception as ex: + print("Error creating clob client") + print("________________") + print(ex) + return None + + +def approveContracts(): + web3 = Web3(Web3.HTTPProvider("https://polygon-rpc.com")) + web3.middleware_onion.inject(geth_poa_middleware, layer=0) + wallet = web3.eth.account.privateKeyToAccount(os.getenv("PK")) + + + with open('erc20ABI.json', 'r') as file: + erc20_abi = json.load(file) + + ctf_address = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" + erc1155_set_approval = """[{"inputs": [{ "internalType": "address", "name": "operator", "type": "address" },{ "internalType": "bool", "name": "approved", "type": "bool" }],"name": "setApprovalForAll","outputs": [],"stateMutability": "nonpayable","type": "function"}]""" + + usdc_contract = web3.eth.contract(address="0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", abi=erc20_abi) # usdc.e + ctf_contract = web3.eth.contract(address=ctf_address, abi=erc1155_set_approval) + + + for address in ['0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E', '0xC5d563A36AE78145C45a50134d48A1215220f80a', '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296']: + usdc_nonce = web3.eth.getTransactionCount( wallet.address ) + raw_usdc_txn = usdc_contract.functions.approve(address, int(MAX_INT, 0)).build_transaction({ + "chainId": 137, + "from": wallet.address, + "nonce": usdc_nonce + }) + signed_usdc_txn = web3.eth.account.sign_transaction(raw_usdc_txn, private_key=os.getenv("PK")) + usdc_tx_receipt = web3.eth.wait_for_transaction_receipt(signed_usdc_txn, 600) + + + print(f'USDC Transaction for {address} returned {usdc_tx_receipt}') + time.sleep(1) + + ctf_nonce = web3.eth.getTransactionCount( wallet.address ) + + raw_ctf_approval_txn = ctf_contract.functions.setApprovalForAll(address, True).buildTransaction({ + "chainId": 137, + "from": wallet.address, + "nonce": ctf_nonce + }) + + signed_ctf_approval_tx = web3.eth.account.sign_transaction(raw_ctf_approval_txn, private_key=os.getenv("PK")) + send_ctf_approval_tx = web3.eth.send_raw_transaction(signed_ctf_approval_tx.rawTransaction) + ctf_approval_tx_receipt = web3.eth.wait_for_transaction_receipt(send_ctf_approval_tx, 600) + + print(f'CTF Transaction for {address} returned {ctf_approval_tx_receipt}') + time.sleep(1) + + + + nonce = web3.eth.getTransactionCount( wallet.address ) + raw_txn_2 = usdc_contract.functions.approve("0xC5d563A36AE78145C45a50134d48A1215220f80a", int(MAX_INT, 0)).build_transaction({ + "chainId": 137, + "from": wallet.address, + "nonce": nonce + }) + signed_txn_2 = web3.eth.account.sign_transaction(raw_txn_2, private_key=os.getenv("PK")) + send_txn_2 = web3.eth.send_raw_transaction(signed_txn_2.rawTransaction) + + + nonce = web3.eth.getTransactionCount( wallet.address ) + raw_txn_3 = usdc_contract.functions.approve("0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296", int(MAX_INT, 0)).build_transaction({ + "chainId": 137, + "from": wallet.address, + "nonce": nonce + }) + signed_txn_3 = web3.eth.account.sign_transaction(raw_txn_3, private_key=os.getenv("PK")) + send_txn_3 = web3.eth.send_raw_transaction(signed_txn_3.rawTransaction) + + +def market_action( marketId, action, price, size ): + order_args = OrderArgs( + price=price, + size=size, + side=action, + token_id=marketId, + ) + signed_order = get_clob_client().create_order(order_args) + + try: + resp = get_clob_client().post_order(signed_order) + print(resp) + except Exception as ex: + print(ex) + pass + + +def get_position(marketId): + client = get_clob_client() + position_res = client.get_balance_allowance( + BalanceAllowanceParams( + asset_type=AssetType.CONDITIONAL, + token_id=marketId + ) + ) + orderBook = client.get_order_book(marketId) + price = float(orderBook.bids[-1].price) + shares = int(position_res['balance']) / 1e6 + return shares * price \ No newline at end of file diff --git a/data_updater/update_markets.py b/data_updater/update_markets.py new file mode 100644 index 0000000..dbd3ae2 --- /dev/null +++ b/data_updater/update_markets.py @@ -0,0 +1,130 @@ +import time +import pandas as pd +from trading_utils import get_clob_client +from google_utils import get_spreadsheet +from find_markets import get_sel_df, get_all_markets, get_all_results, get_markets, add_volatility_to_df +from gspread_dataframe import set_with_dataframe +import traceback + +# Initialize global variables +spreadsheet = get_spreadsheet() +client = get_clob_client() + +wk_all = spreadsheet.worksheet("All Markets") +wk_vol = spreadsheet.worksheet("Volatility Markets") + +sel_df = get_sel_df(spreadsheet, "Selected Markets") + +def update_sheet(data, worksheet): + all_values = worksheet.get_all_values() + existing_num_rows = len(all_values) + existing_num_cols = len(all_values[0]) if all_values else 0 + + num_rows, num_cols = data.shape + max_rows = max(num_rows, existing_num_rows) + max_cols = max(num_cols, existing_num_cols) + + # Create a DataFrame with the maximum size and fill it with empty strings + padded_data = pd.DataFrame('', index=range(max_rows), columns=range(max_cols)) + + # Update the padded DataFrame with the original data and its columns + padded_data.iloc[:num_rows, :num_cols] = data.values + padded_data.columns = list(data.columns) + [''] * (max_cols - num_cols) + + # Update the sheet with the padded DataFrame, including column headers + set_with_dataframe(worksheet, padded_data, include_index=False, include_column_header=True, resize=True) + +def sort_df(df): + # Calculate the mean and standard deviation for each column + mean_gm = df['gm_reward_per_100'].mean() + std_gm = df['gm_reward_per_100'].std() + + mean_volatility = df['volatility_sum'].mean() + std_volatility = df['volatility_sum'].std() + + # Standardize the columns + df['std_gm_reward_per_100'] = (df['gm_reward_per_100'] - mean_gm) / std_gm + df['std_volatility_sum'] = (df['volatility_sum'] - mean_volatility) / std_volatility + + # Define a custom scoring function for best_bid and best_ask + def proximity_score(value): + if 0.1 <= value <= 0.25: + return (0.25 - value) / 0.15 + elif 0.75 <= value <= 0.9: + return (value - 0.75) / 0.15 + else: + return 0 + + df['bid_score'] = df['best_bid'].apply(proximity_score) + df['ask_score'] = df['best_ask'].apply(proximity_score) + + # Create a composite score (higher is better for rewards, lower is better for volatility, with proximity scores) + df['composite_score'] = ( + df['std_gm_reward_per_100'] - + df['std_volatility_sum'] + + df['bid_score'] + + df['ask_score'] + ) + + # Sort by the composite score in descending order + sorted_df = df.sort_values(by='composite_score', ascending=False) + + # Drop the intermediate columns used for calculation + sorted_df = sorted_df.drop(columns=['std_gm_reward_per_100', 'std_volatility_sum', 'bid_score', 'ask_score', 'composite_score']) + + return sorted_df + +def fetch_and_process_data(): + global spreadsheet, client, wk_all, wk_vol, sel_df + + spreadsheet = get_spreadsheet() + client = get_clob_client() + + wk_all = spreadsheet.worksheet("All Markets") + wk_vol = spreadsheet.worksheet("Volatility Markets") + wk_full = spreadsheet.worksheet("Full Markets") + + sel_df = get_sel_df(spreadsheet, "Selected Markets") + + + all_df = get_all_markets(client) + all_results = get_all_results(all_df, client) + m_data, all_markets = get_markets(all_results, sel_df, maker_reward=0.75) + + print(f'{pd.to_datetime("now")}: Fetched all markets data of length {len(all_markets)}.') + new_df = add_volatility_to_df(all_markets) + new_df['volatility_sum'] = new_df['24_hour'] + new_df['7_day'] + new_df['14_day'] + + new_df = new_df.sort_values('volatility_sum', ascending=True) + new_df['volatilty/reward'] = ((new_df['gm_reward_per_100'] / new_df['volatility_sum']).round(2)).astype(str) + + new_df = new_df[['question', 'answer1', 'answer2', 'spread', 'rewards_daily_rate', 'gm_reward_per_100', 'sm_reward_per_100', 'bid_reward_per_100', 'ask_reward_per_100', 'volatility_sum', 'volatilty/reward', 'min_size', '1_hour', '3_hour', '6_hour', '12_hour', '24_hour', '7_day', '30_day', + 'best_bid', 'best_ask', 'volatility_price', 'max_spread', 'tick_size', + 'neg_risk', 'market_slug', 'token1', 'token2', 'condition_id']] + + + volatility_df = new_df.copy() + volatility_df = volatility_df[new_df['volatility_sum'] < 20] + # volatility_df = sort_df(volatility_df) + volatility_df = volatility_df.sort_values('gm_reward_per_100', ascending=False) + + new_df = new_df.sort_values('gm_reward_per_100', ascending=False) + + + print(f'{pd.to_datetime("now")}: Fetched select market of length {len(new_df)}.') + + if len(new_df) > 50: + update_sheet(new_df, wk_all) + update_sheet(volatility_df, wk_vol) + update_sheet(m_data, wk_full) + else: + print(f'{pd.to_datetime("now")}: Not updating sheet because of length {len(new_df)}.') + +if __name__ == "__main__": + while True: + try: + fetch_and_process_data() + time.sleep(60 * 5) # Sleep for 5 minutes (300 seconds) + except Exception as e: + traceback.print_exc() + print(str(e)) diff --git a/main.py b/main.py new file mode 100644 index 0000000..290d0a9 --- /dev/null +++ b/main.py @@ -0,0 +1,112 @@ +import gc # Garbage collection +import time # Time functions +import asyncio # Asynchronous I/O +import traceback # Exception handling +import threading # Thread management + +from poly_data.polymarket_client import PolymarketClient +from poly_data.data_utils import update_markets, update_positions, update_orders +from poly_data.websocket_handlers import connect_market_websocket, connect_user_websocket +import poly_data.global_state as global_state +from poly_data.data_processing import remove_from_performing + +def update_once(): + """ + Initialize the application state by fetching market data, positions, and orders. + """ + update_markets() # Get market information from Google Sheets + update_positions() # Get current positions from Polymarket + update_orders() # Get current orders from Polymarket + +def remove_from_pending(): + """ + Clean up stale trades that have been pending for too long (>15 seconds). + This prevents the system from getting stuck on trades that may have failed. + """ + try: + current_time = time.time() + + # Iterate through all performing trades + for col in list(global_state.performing.keys()): + for trade_id in list(global_state.performing[col]): + + try: + # If trade has been pending for more than 15 seconds, remove it + if current_time - global_state.performing_timestamps[col].get(trade_id, current_time) > 15: + print(f"Removing stale entry {trade_id} from {col} after 15 seconds") + remove_from_performing(col, trade_id) + print("After removing: ", global_state.performing, global_state.performing_timestamps) + except: + print("Error in remove_from_pending") + print(traceback.format_exc()) + except: + print("Error in remove_from_pending") + print(traceback.format_exc()) + +def update_periodically(): + """ + Background thread function that periodically updates market data, positions and orders. + - Positions and orders are updated every 5 seconds + - Market data is updated every 30 seconds (every 6 cycles) + - Stale pending trades are removed each cycle + """ + i = 1 + while True: + time.sleep(5) # Update every 5 seconds + + try: + # Clean up stale trades + remove_from_pending() + + # Update positions and orders every cycle + update_positions(avgOnly=True) # Only update average price, not position size + update_orders() + + # Update market data every 6th cycle (30 seconds) + if i % 6 == 0: + update_markets() + i = 1 + + gc.collect() # Force garbage collection to free memory + i += 1 + except: + print("Error in update_periodically") + print(traceback.format_exc()) + +async def main(): + """ + Main application entry point. Initializes client, data, and manages websocket connections. + """ + # Initialize client + global_state.client = PolymarketClient() + + # Initialize state and fetch initial data + global_state.all_tokens = [] + update_once() + print("After initial updates: ", global_state.orders, global_state.positions) + + print("\n") + print(f'There are {len(global_state.df)} market, {len(global_state.positions)} positions and {len(global_state.orders)} orders. Starting positions: {global_state.positions}') + + # Start background update thread + update_thread = threading.Thread(target=update_periodically, daemon=True) + update_thread.start() + + # Main loop - maintain websocket connections + while True: + try: + # Connect to market and user websockets simultaneously + await asyncio.gather( + connect_market_websocket(global_state.all_tokens), + connect_user_websocket() + ) + print("Reconnecting to the websocket") + except: + print("Error in main loop") + print(traceback.format_exc()) + + await asyncio.sleep(1) + gc.collect() # Clean up memory + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/poly_data/CONSTANTS.py b/poly_data/CONSTANTS.py new file mode 100644 index 0000000..85b3a2b --- /dev/null +++ b/poly_data/CONSTANTS.py @@ -0,0 +1,3 @@ +# Minimum position size to trigger position merging +# Positions smaller than this will be ignored to save on gas costs +MIN_MERGE_SIZE = 20 \ No newline at end of file diff --git a/poly_data/__init__.py b/poly_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/poly_data/abis.py b/poly_data/abis.py new file mode 100644 index 0000000..961a457 --- /dev/null +++ b/poly_data/abis.py @@ -0,0 +1,3 @@ +erc20_abi = """[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"}]""" +NegRiskAdapterABI = """[{"inputs":[{"internalType":"bytes32","name":"_conditionId","type":"bytes32"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"splitPosition","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_conditionId","type":"bytes32"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"mergePositions","outputs":[],"stateMutability":"nonpayable","type":"function"}]""" +ConditionalTokenABI = """[{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"id","type":"uint256"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"collateralToken","type":"address"},{"name":"parentCollectionId","type":"bytes32"},{"name":"conditionId","type":"bytes32"},{"name":"indexSets","type":"uint256[]"}],"name":"redeemPositions","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"},{"name":"","type":"uint256"}],"name":"payoutNumerators","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"ids","type":"uint256[]"},{"name":"values","type":"uint256[]"},{"name":"data","type":"bytes"}],"name":"safeBatchTransferFrom","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"collateralToken","type":"address"},{"name":"collectionId","type":"bytes32"}],"name":"getPositionId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"name":"owners","type":"address[]"},{"name":"ids","type":"uint256[]"}],"name":"balanceOfBatch","outputs":[{"name":"","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"collateralToken","type":"address"},{"name":"parentCollectionId","type":"bytes32"},{"name":"conditionId","type":"bytes32"},{"name":"partition","type":"uint256[]"},{"name":"amount","type":"uint256"}],"name":"splitPosition","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"oracle","type":"address"},{"name":"questionId","type":"bytes32"},{"name":"outcomeSlotCount","type":"uint256"}],"name":"getConditionId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"name":"parentCollectionId","type":"bytes32"},{"name":"conditionId","type":"bytes32"},{"name":"indexSet","type":"uint256"}],"name":"getCollectionId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"collateralToken","type":"address"},{"name":"parentCollectionId","type":"bytes32"},{"name":"conditionId","type":"bytes32"},{"name":"partition","type":"uint256[]"},{"name":"amount","type":"uint256"}],"name":"mergePositions","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"operator","type":"address"},{"name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"questionId","type":"bytes32"},{"name":"payouts","type":"uint256[]"}],"name":"reportPayouts","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"conditionId","type":"bytes32"}],"name":"getOutcomeSlotCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"oracle","type":"address"},{"name":"questionId","type":"bytes32"},{"name":"outcomeSlotCount","type":"uint256"}],"name":"prepareCondition","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"payoutDenominator","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"id","type":"uint256"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"conditionId","type":"bytes32"},{"indexed":true,"name":"oracle","type":"address"},{"indexed":true,"name":"questionId","type":"bytes32"},{"indexed":false,"name":"outcomeSlotCount","type":"uint256"}],"name":"ConditionPreparation","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"conditionId","type":"bytes32"},{"indexed":true,"name":"oracle","type":"address"},{"indexed":true,"name":"questionId","type":"bytes32"},{"indexed":false,"name":"outcomeSlotCount","type":"uint256"},{"indexed":false,"name":"payoutNumerators","type":"uint256[]"}],"name":"ConditionResolution","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"stakeholder","type":"address"},{"indexed":false,"name":"collateralToken","type":"address"},{"indexed":true,"name":"parentCollectionId","type":"bytes32"},{"indexed":true,"name":"conditionId","type":"bytes32"},{"indexed":false,"name":"partition","type":"uint256[]"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"PositionSplit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"stakeholder","type":"address"},{"indexed":false,"name":"collateralToken","type":"address"},{"indexed":true,"name":"parentCollectionId","type":"bytes32"},{"indexed":true,"name":"conditionId","type":"bytes32"},{"indexed":false,"name":"partition","type":"uint256[]"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"PositionsMerge","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"redeemer","type":"address"},{"indexed":true,"name":"collateralToken","type":"address"},{"indexed":true,"name":"parentCollectionId","type":"bytes32"},{"indexed":false,"name":"conditionId","type":"bytes32"},{"indexed":false,"name":"indexSets","type":"uint256[]"},{"indexed":false,"name":"payout","type":"uint256"}],"name":"PayoutRedemption","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"operator","type":"address"},{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"id","type":"uint256"},{"indexed":false,"name":"value","type":"uint256"}],"name":"TransferSingle","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"operator","type":"address"},{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"ids","type":"uint256[]"},{"indexed":false,"name":"values","type":"uint256[]"}],"name":"TransferBatch","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"operator","type":"address"},{"indexed":false,"name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"string"},{"indexed":true,"name":"id","type":"uint256"}],"name":"URI","type":"event"}]""" \ No newline at end of file diff --git a/poly_data/data_processing.py b/poly_data/data_processing.py new file mode 100644 index 0000000..567ce1e --- /dev/null +++ b/poly_data/data_processing.py @@ -0,0 +1,144 @@ +import json +from sortedcontainers import SortedDict +import poly_data.global_state as global_state +import poly_data.CONSTANTS as CONSTANTS + +from trading import perform_trade +import time +import asyncio +from poly_data.data_utils import set_position, set_order, update_positions + +def process_book_data(asset, json_data): + global_state.all_data[asset] = { + 'bids': SortedDict(), + 'asks': SortedDict() + } + + global_state.all_data[asset]['bids'].update({float(entry['price']): float(entry['size']) for entry in json_data['bids']}) + global_state.all_data[asset]['asks'].update({float(entry['price']): float(entry['size']) for entry in json_data['asks']}) + +def process_price_change(asset, side, price_level, new_size): + if side == 'bids': + book = global_state.all_data[asset]['bids'] + else: + book = global_state.all_data[asset]['asks'] + + if new_size == 0: + if price_level in book: + del book[price_level] + else: + book[price_level] = new_size + +def process_data(json_data, trade=True): + event_type = json_data['event_type'] + asset = json_data['market'] + + if event_type == 'book': + process_book_data(asset, json_data) + + if trade: + asyncio.create_task(perform_trade(asset)) + + elif event_type == 'price_change': + side = 'bids' if json_data['side'] == 'BUY' else 'asks' + price_level = float(json_data['price']) + new_size = float(json_data['size']) + process_price_change(asset, side, price_level, new_size) + + if trade: + asyncio.create_task(perform_trade(asset)) + + + # pretty_print(f'Received book update for {asset}:', global_state.all_data[asset]) + +def add_to_performing(col, id): + if col not in global_state.performing: + global_state.performing[col] = set() + + if col not in global_state.performing_timestamps: + global_state.performing_timestamps[col] = {} + + # Add the trade ID and track its timestamp + global_state.performing[col].add(id) + global_state.performing_timestamps[col][id] = time.time() + +def remove_from_performing(col, id): + if col in global_state.performing: + global_state.performing[col].discard(id) + + if col in global_state.performing_timestamps: + global_state.performing_timestamps[col].pop(id, None) + +def process_user_data(row): + market = row['market'] + + side = row['side'].lower() + token = row['asset_id'] + + if token in global_state.REVERSE_TOKENS: + col = token + "_" + side + + if row['event_type'] == 'trade': + size = 0 + price = 0 + maker_outcome = "" + taker_outcome = row['outcome'] + + is_user_maker = False + for maker_order in row['maker_orders']: + if maker_order['maker_address'].lower() == global_state.client.browser_wallet.lower(): + print("User is maker") + size = float(maker_order['matched_amount']) + price = float(maker_order['price']) + + is_user_maker = True + maker_outcome = maker_order['outcome'] #this is curious + + if maker_outcome == taker_outcome: + side = 'buy' if side == 'sell' else 'sell' #need to reverse as we reverse token too + else: + token = global_state.REVERSE_TOKENS[token] + + if not is_user_maker: + size = float(row['size']) + price = float(row['price']) + print("User is taker") + + print("TRADE EVENT FOR: ", row['market'], "ID: ", row['id'], "STATUS: ", row['status'], " SIDE: ", row['side'], " MAKER OUTCOME: ", maker_outcome, " TAKER OUTCOME: ", taker_outcome, " PROCESSED SIDE: ", side, " SIZE: ", size) + + + if row['status'] == 'CONFIRMED' or row['status'] == 'FAILED' : + if row['status'] == 'FAILED': + print(f"Trade failed for {token}, decreasing") + asyncio.create_task(asyncio.sleep(2)) + update_positions() + else: + remove_from_performing(col, row['id']) + print("Confirmed. Performing is ", len(global_state.performing[col])) + print("Last trade update is ", global_state.last_trade_update) + print("Performing is ", global_state.performing) + print("Performing timestamps is ", global_state.performing_timestamps) + + asyncio.create_task(perform_trade(market)) + + elif row['status'] == 'MATCHED': + add_to_performing(col, row['id']) + + print("Matched. Performing is ", len(global_state.performing[col])) + set_position(token, side, size, price) + print("Position after matching is ", global_state.positions[str(token)]) + print("Last trade update is ", global_state.last_trade_update) + print("Performing is ", global_state.performing) + print("Performing timestamps is ", global_state.performing_timestamps) + asyncio.create_task(perform_trade(market)) + elif row['status'] == 'MINED': + remove_from_performing(col, row['id']) + + elif row['event_type'] == 'order': + print("ORDER EVENT FOR: ", row['market'], " STATUS: ", row['status'], " TYPE: ", row['type'], " SIDE: ", side, " ORIGINAL SIZE: ", row['original_size'], " SIZE MATCHED: ", row['size_matched']) + + set_order(token, side, float(row['original_size']) - float(row['size_matched']), row['price']) + asyncio.create_task(perform_trade(market)) + + else: + print(f"User date received for {market} but its not in") \ No newline at end of file diff --git a/poly_data/data_utils.py b/poly_data/data_utils.py new file mode 100644 index 0000000..0ff03a4 --- /dev/null +++ b/poly_data/data_utils.py @@ -0,0 +1,169 @@ +import poly_data.global_state as global_state +from poly_data.utils import get_sheet_df +import time +import poly_data.global_state as global_state + +#sth here seems to be removing the position +def update_positions(avgOnly=False): + pos_df = global_state.client.get_all_positions() + + for idx, row in pos_df.iterrows(): + asset = str(row['asset']) + + if asset in global_state.positions: + position = global_state.positions[asset].copy() + else: + position = {'size': 0, 'avgPrice': 0} + + position['avgPrice'] = row['avgPrice'] + + if not avgOnly: + position['size'] = row['size'] + else: + + for col in [f"{asset}_sell", f"{asset}_buy"]: + #need to review this + if col not in global_state.performing or not isinstance(global_state.performing[col], set) or len(global_state.performing[col]) == 0: + try: + old_size = position['size'] + except: + old_size = 0 + + if asset in global_state.last_trade_update: + if time.time() - global_state.last_trade_update[asset] < 5: + print(f"Skipping update for {asset} because last trade update was less than 5 seconds ago") + continue + + if old_size != row['size']: + print(f"No trades are pending. Updating position from {old_size} to {row['size']} and avgPrice to {row['avgPrice']} using API") + + position['size'] = row['size'] + else: + print(f"ALERT: Skipping update for {asset} because there are trades pending for {col} looking like {global_state.performing[col]}") + + global_state.positions[asset] = position + +def get_position(token): + token = str(token) + if token in global_state.positions: + return global_state.positions[token] + else: + return {'size': 0, 'avgPrice': 0} + +def set_position(token, side, size, price, source='websocket'): + token = str(token) + size = float(size) + price = float(price) + + global_state.last_trade_update[token] = time.time() + + if side.lower() == 'sell': + size *= -1 + + if token in global_state.positions: + + prev_price = global_state.positions[token]['avgPrice'] + prev_size = global_state.positions[token]['size'] + + + if size > 0: + if prev_size == 0: + # Starting a new position + avgPrice_new = price + else: + # Buying more; update average price + avgPrice_new = (prev_price * prev_size + price * size) / (prev_size + size) + elif size < 0: + # Selling; average price remains the same + avgPrice_new = prev_price + else: + # No change in position + avgPrice_new = prev_price + + + global_state.positions[token]['size'] += size + global_state.positions[token]['avgPrice'] = avgPrice_new + else: + global_state.positions[token] = {'size': size, 'avgPrice': price} + + print(f"Updated position from {source}, set to ", global_state.positions[token]) + +def update_orders(): + all_orders = global_state.client.get_all_orders() + + orders = {} + + if len(all_orders) > 0: + for token in all_orders['asset_id'].unique(): + + if token not in orders: + orders[str(token)] = {'buy': {'price': 0, 'size': 0}, 'sell': {'price': 0, 'size': 0}} + + curr_orders = all_orders[all_orders['asset_id'] == str(token)] + + if len(curr_orders) > 0: + sel_orders = {} + sel_orders['buy'] = curr_orders[curr_orders['side'] == 'BUY'] + sel_orders['sell'] = curr_orders[curr_orders['side'] == 'SELL'] + + for type in ['buy', 'sell']: + curr = sel_orders[type] + + if len(curr) > 1: + print("Multiple orders found, cancelling") + global_state.client.cancel_all_asset(token) + orders[str(token)] = {'buy': {'price': 0, 'size': 0}, 'sell': {'price': 0, 'size': 0}} + elif len(curr) == 1: + orders[str(token)][type]['price'] = float(curr.iloc[0]['price']) + orders[str(token)][type]['size'] = float(curr.iloc[0]['original_size'] - curr.iloc[0]['size_matched']) + + global_state.orders = orders + +def get_order(token): + token = str(token) + if token in global_state.orders: + + if 'buy' not in global_state.orders[token]: + global_state.orders[token]['buy'] = {'price': 0, 'size': 0} + + if 'sell' not in global_state.orders[token]: + global_state.orders[token]['sell'] = {'price': 0, 'size': 0} + + return global_state.orders[token] + else: + return {'buy': {'price': 0, 'size': 0}, 'sell': {'price': 0, 'size': 0}} + +def set_order(token, side, size, price): + curr = {} + curr = {side: {'price': 0, 'size': 0}} + + curr[side]['size'] = float(size) + curr[side]['price'] = float(price) + + global_state.orders[str(token)] = curr + print("Updated order, set to ", curr) + + + +def update_markets(): + received_df, received_params = get_sheet_df() + + if len(received_df) > 0: + global_state.df, global_state.params = received_df.copy(), received_params + + for idx, row in global_state.df.iterrows(): + for col in ['token1', 'token2']: + row[col] = str(row[col]) + + if row['token1'] not in global_state.all_tokens: + global_state.all_tokens.append(row['token1']) + + if row['token1'] not in global_state.REVERSE_TOKENS: + global_state.REVERSE_TOKENS[row['token1']] = row['token2'] + + if row['token2'] not in global_state.REVERSE_TOKENS: + global_state.REVERSE_TOKENS[row['token2']] = row['token1'] + + for col2 in [f"{row['token1']}_buy", f"{row['token1']}_sell", f"{row['token2']}_buy", f"{row['token2']}_sell"]: + if col2 not in global_state.performing: + global_state.performing[col2] = set() \ No newline at end of file diff --git a/poly_data/global_state.py b/poly_data/global_state.py new file mode 100644 index 0000000..4bcb940 --- /dev/null +++ b/poly_data/global_state.py @@ -0,0 +1,49 @@ +import threading +import pandas as pd + +# ============ Market Data ============ + +# List of all tokens being tracked +all_tokens = [] + +# Mapping between tokens in the same market (YES->NO, NO->YES) +REVERSE_TOKENS = {} + +# Order book data for all markets +all_data = {} + +# Market configuration data from Google Sheets +df = None + +# ============ Client & Parameters ============ + +# Polymarket client instance +client = None + +# Trading parameters from Google Sheets +params = {} + +# Lock for thread-safe trading operations +lock = threading.Lock() + +# ============ Trading State ============ + +# Tracks trades that have been matched but not yet mined +# Format: {"token_side": {trade_id1, trade_id2, ...}} +performing = {} + +# Timestamps for when trades were added to performing +# Used to clear stale trades +performing_timestamps = {} + +# Timestamps for when positions were last updated +last_trade_update = {} + +# Current open orders for each token +# Format: {token_id: {'buy': {price, size}, 'sell': {price, size}}} +orders = {} + +# Current positions for each token +# Format: {token_id: {'size': float, 'avgPrice': float}} +positions = {} + diff --git a/poly_data/polymarket_client.py b/poly_data/polymarket_client.py new file mode 100644 index 0000000..9cf11d2 --- /dev/null +++ b/poly_data/polymarket_client.py @@ -0,0 +1,320 @@ +from dotenv import load_dotenv # Environment variable management +import os # Operating system interface + +# Polymarket API client libraries +from py_clob_client.client import ClobClient +from py_clob_client.clob_types import OrderArgs, BalanceAllowanceParams, AssetType, PartialCreateOrderOptions +from py_clob_client.constants import POLYGON + +# Web3 libraries for blockchain interaction +from web3 import Web3 +from web3.middleware import geth_poa_middleware +from eth_account import Account + +import requests # HTTP requests +import pandas as pd # Data analysis +import json # JSON processing +import subprocess # For calling external processes + +from py_clob_client.clob_types import OpenOrderParams + +# Smart contract ABIs +from poly_data.abis import NegRiskAdapterABI, ConditionalTokenABI, erc20_abi + +# Load environment variables +load_dotenv() + + +class PolymarketClient: + """ + Client for interacting with Polymarket's API and smart contracts. + + This class provides methods for: + - Creating and managing orders + - Querying order book data + - Checking balances and positions + - Merging positions + + The client connects to both the Polymarket API and the Polygon blockchain. + """ + + def __init__(self, pk='default') -> None: + """ + Initialize the Polymarket client with API and blockchain connections. + + Args: + pk (str, optional): Private key identifier, defaults to 'default' + """ + host="https://clob.polymarket.com" + + # Get credentials from environment variables + key=os.getenv("PK") + browser_address = os.getenv("BROWSER_ADDRESS") + + # Don't print sensitive wallet information + print("Initializing Polymarket client...") + chain_id=POLYGON + self.browser_wallet=Web3.toChecksumAddress(browser_address) + + # Initialize the Polymarket API client + self.client = ClobClient( + host=host, + key=key, + chain_id=chain_id, + funder=self.browser_wallet, + signature_type=2 + ) + + # Set up API credentials + self.creds = self.client.create_or_derive_api_creds() + self.client.set_api_creds(creds=self.creds) + + # Initialize Web3 connection to Polygon + web3 = Web3(Web3.HTTPProvider("https://polygon-rpc.com")) + web3.middleware_onion.inject(geth_poa_middleware, layer=0) + + # Set up USDC contract for balance checks + self.usdc_contract = web3.eth.contract( + address="0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + abi=erc20_abi + ) + + # Store key contract addresses + self.addresses = { + 'neg_risk_adapter': '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296', + 'collateral': '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + 'conditional_tokens': '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045' + } + + # Initialize contract interfaces + self.neg_risk_adapter = web3.eth.contract( + address=self.addresses['neg_risk_adapter'], + abi=NegRiskAdapterABI + ) + + self.conditional_tokens = web3.eth.contract( + address=self.addresses['conditional_tokens'], + abi=ConditionalTokenABI + ) + + self.web3 = web3 + + + def create_order(self, marketId, action, price, size, neg_risk=False): + """ + Create and submit a new order to the Polymarket order book. + + Args: + marketId (str): ID of the market token to trade + action (str): "BUY" or "SELL" + price (float): Order price (0-1 range for prediction markets) + size (float): Order size in USDC + neg_risk (bool, optional): Whether this is a negative risk market. Defaults to False. + + Returns: + dict: Response from the API containing order details, or empty dict on error + """ + # Create order parameters + order_args = OrderArgs( + token_id=str(marketId), + price=price, + size=size, + side=action + ) + + signed_order = None + + # Handle regular vs negative risk markets differently + if neg_risk == False: + signed_order = self.client.create_order(order_args) + else: + signed_order = self.client.create_order(order_args, options=PartialCreateOrderOptions(neg_risk=True)) + + try: + # Submit the signed order to the API + resp = self.client.post_order(signed_order) + return resp + except Exception as ex: + print(ex) + return {} + + def get_order_book(self, market): + """ + Get the current order book for a specific market. + + Args: + market (str): Market ID to query + + Returns: + tuple: (bids_df, asks_df) - DataFrames containing bid and ask orders + """ + orderBook = self.client.get_order_book(market) + return pd.DataFrame(orderBook.bids).astype(float), pd.DataFrame(orderBook.asks).astype(float) + + + def get_usdc_balance(self): + """ + Get the USDC balance of the connected wallet. + + Returns: + float: USDC balance in decimal format + """ + return self.usdc_contract.functions.balanceOf(self.browser_wallet).call() / 10**6 + + def get_pos_balance(self): + """ + Get the total value of all positions for the connected wallet. + + Returns: + float: Total position value in USDC + """ + res = requests.get(f'https://data-api.polymarket.com/value?user={self.browser_wallet}') + return float(res.json()['value']) + + def get_total_balance(self): + """ + Get the combined value of USDC balance and all positions. + + Returns: + float: Total account value in USDC + """ + return self.get_usdc_balance() + self.get_pos_balance() + + def get_all_positions(self): + """ + Get all positions for the connected wallet across all markets. + + Returns: + DataFrame: All positions with details like market, size, avgPrice + """ + res = requests.get(f'https://data-api.polymarket.com/positions?user={self.browser_wallet}') + return pd.DataFrame(res.json()) + + def get_raw_position(self, tokenId): + """ + Get the raw token balance for a specific market outcome token. + + Args: + tokenId (int): Token ID to query + + Returns: + int: Raw token amount (before decimal conversion) + """ + return int(self.conditional_tokens.functions.balanceOf(self.browser_wallet, int(tokenId)).call()) + + def get_position(self, tokenId): + """ + Get both raw and formatted position size for a token. + + Args: + tokenId (int): Token ID to query + + Returns: + tuple: (raw_position, shares) - Raw token amount and decimal shares + Shares less than 1 are treated as 0 to avoid dust amounts + """ + raw_position = self.get_raw_position(tokenId) + shares = float(raw_position / 1e6) + + # Ignore very small positions (dust) + if shares < 1: + shares = 0 + + return raw_position, shares + + def get_all_orders(self): + """ + Get all open orders for the connected wallet. + + Returns: + DataFrame: All open orders with their details + """ + orders_df = pd.DataFrame(self.client.get_orders()) + + # Convert numeric columns to float + for col in ['original_size', 'size_matched', 'price']: + if col in orders_df.columns: + orders_df[col] = orders_df[col].astype(float) + + return orders_df + + def get_market_orders(self, market): + """ + Get all open orders for a specific market. + + Args: + market (str): Market ID to query + + Returns: + DataFrame: Open orders for the specified market + """ + orders_df = pd.DataFrame(self.client.get_orders(OpenOrderParams( + market=market, + ))) + + # Convert numeric columns to float + for col in ['original_size', 'size_matched', 'price']: + if col in orders_df.columns: + orders_df[col] = orders_df[col].astype(float) + + return orders_df + + + def cancel_all_asset(self, asset_id): + """ + Cancel all orders for a specific asset token. + + Args: + asset_id (str): Asset token ID + """ + self.client.cancel_market_orders(asset_id=str(asset_id)) + + + + def cancel_all_market(self, marketId): + """ + Cancel all orders in a specific market. + + Args: + marketId (str): Market ID + """ + self.client.cancel_market_orders(market=marketId) + + + def merge_positions(self, amount_to_merge, condition_id, is_neg_risk_market): + """ + Merge positions in a market to recover collateral. + + This function calls the external poly_merger Node.js script to execute + the merge operation on-chain. When you hold both YES and NO positions + in the same market, merging them recovers your USDC. + + Args: + amount_to_merge (int): Raw token amount to merge (before decimal conversion) + condition_id (str): Market condition ID + is_neg_risk_market (bool): Whether this is a negative risk market + + Returns: + str: Transaction hash or output from the merge script + + Raises: + Exception: If the merge operation fails + """ + amount_to_merge_str = str(amount_to_merge) + + # Prepare the command to run the JavaScript script + node_command = f'node poly_merger/merge.js {amount_to_merge_str} {condition_id} {"true" if is_neg_risk_market else "false"}' + print(node_command) + + # Run the command and capture the output + result = subprocess.run(node_command, shell=True, capture_output=True, text=True) + + # Check if there was an error + if result.returncode != 0: + print("Error:", result.stderr) + raise Exception(f"Error in merging positions: {result.stderr}") + + print("Done merging") + + # Return the transaction hash or output + return result.stdout \ No newline at end of file diff --git a/poly_data/trading_utils.py b/poly_data/trading_utils.py new file mode 100644 index 0000000..25b84ee --- /dev/null +++ b/poly_data/trading_utils.py @@ -0,0 +1,148 @@ +import math +from poly_data.data_utils import update_positions +import poly_data.global_state as global_state + +# def get_avgPrice(position, assetId): +# curr_global = global_state.all_positions[global_state.all_positions['asset'] == str(assetId)] +# api_position_size = 0 +# api_avgPrice = 0 + +# if len(curr_global) > 0: +# c_row = curr_global.iloc[0] +# api_avgPrice = round(c_row['avgPrice'], 2) +# api_position_size = c_row['size'] + +# if position > 0: +# if abs((api_position_size - position)/position * 100) > 5: +# print("Updating global positions") +# update_positions() + +# try: +# c_row = curr_global.iloc[0] +# api_avgPrice = round(c_row['avgPrice'], 2) +# api_position_size = c_row['size'] +# except: +# return 0 +# return api_avgPrice + +def get_best_bid_ask_deets(market, name, size, deviation_threshold=0.05): + + best_bid, best_bid_size, second_best_bid, second_best_bid_size, top_bid = find_best_price_with_size(global_state.all_data[market]['bids'], size, reverse=True) + best_ask, best_ask_size, second_best_ask, second_best_ask_size, top_ask = find_best_price_with_size(global_state.all_data[market]['asks'], size, reverse=False) + + mid_price = (best_bid + best_ask) / 2 + + bid_sum_within_n_percent = sum(size for price, size in global_state.all_data[market]['bids'].items() if best_bid <= price <= mid_price * (1 + deviation_threshold)) + ask_sum_within_n_percent = sum(size for price, size in global_state.all_data[market]['asks'].items() if mid_price * (1 - deviation_threshold) <= price <= best_ask) + + if name == 'token2': + best_bid, second_best_bid, top_bid, best_ask, second_best_ask, top_ask = 1 - best_ask, 1 - second_best_ask, 1 - top_ask, 1 - best_bid, 1 - second_best_bid, 1 - top_bid + best_bid_size, second_best_bid_size, best_ask_size, second_best_ask_size = best_ask_size, second_best_ask_size, best_bid_size, second_best_bid_size + bid_sum_within_n_percent, ask_sum_within_n_percent = ask_sum_within_n_percent, bid_sum_within_n_percent + + + + #return as dictionary + return { + 'best_bid': best_bid, + 'best_bid_size': best_bid_size, + 'second_best_bid': second_best_bid, + 'second_best_bid_size': second_best_bid_size, + 'top_bid': top_bid, + 'best_ask': best_ask, + 'best_ask_size': best_ask_size, + 'second_best_ask': second_best_ask, + 'second_best_ask_size': second_best_ask_size, + 'top_ask': top_ask, + 'bid_sum_within_n_percent': bid_sum_within_n_percent, + 'ask_sum_within_n_percent': ask_sum_within_n_percent + } + + +def find_best_price_with_size(price_dict, min_size, reverse=False): + lst = list(price_dict.items()) + + if reverse: + lst.reverse() + + best_price, best_size = None, None + second_best_price, second_best_size = None, None + top_price = None + set_best = False + + for price, size in lst: + if top_price is None: + top_price = price + + if set_best: + second_best_price, second_best_size = price, size + break + + if size > min_size: + if best_price is None: + best_price, best_size = price, size + set_best = True + + return best_price, best_size, second_best_price, second_best_size, top_price + +def get_order_prices(best_bid, best_bid_size, top_bid, best_ask, best_ask_size, top_ask, avgPrice, row): + + bid_price = best_bid + row['tick_size'] + ask_price = best_ask - row['tick_size'] + + if best_bid_size < row['min_size'] * 1.5: + bid_price = best_bid + + if best_ask_size < 250 * 1.5: + ask_price = best_ask + + + if bid_price >= top_ask: + bid_price = top_bid + + if ask_price <= top_bid: + ask_price = top_ask + + if bid_price == ask_price: + bid_price = top_bid + ask_price = top_ask + + # if ask_price <= avgPrice: + # if avgPrice - ask_price <= (row['max_spread']*1.7/100): + # ask_price = avgPrice + + #temp for sleep + if ask_price <= avgPrice and avgPrice > 0: + ask_price = avgPrice + + return bid_price, ask_price + + + + +def round_down(number, decimals): + factor = 10 ** decimals + return math.floor(number * factor) / factor + +def round_up(number, decimals): + factor = 10 ** decimals + return math.ceil(number * factor) / factor + +def get_buy_sell_amount(position, bid_price, row): + buy_amount = 0 + sell_amount = 0 + + sell_amount = position + buy_amount = row['trade_size'] - position + + if buy_amount > 0.7 * row['min_size'] and buy_amount < row['min_size']: + buy_amount = row['min_size'] + + if bid_price < 0.1: + + if row['multiplier'] != '': + print(f"Multiplying buy amount by {int(row['multiplier'])}") + buy_amount = buy_amount * int(row['multiplier']) + + return buy_amount, sell_amount + diff --git a/poly_data/utils.py b/poly_data/utils.py new file mode 100644 index 0000000..a0a059a --- /dev/null +++ b/poly_data/utils.py @@ -0,0 +1,34 @@ +import json +from poly_utils.google_utils import get_spreadsheet +import pandas as pd + +def pretty_print(txt, dic): + print("\n", txt, json.dumps(dic, indent=4)) + +def get_sheet_df(): + + all = 'All Markets' + sel = 'Selected Markets' + + spreadsheet = get_spreadsheet() + + wk = spreadsheet.worksheet(sel) + df = pd.DataFrame(wk.get_all_records()) + df = df[df['question'] != ""].reset_index(drop=True) + + wk2 = spreadsheet.worksheet(all) + df2 = pd.DataFrame(wk2.get_all_records()) + df2 = df2[df2['question'] != ""].reset_index(drop=True) + + + result = df.merge(df2, on='question', how='inner') + + wk_p = spreadsheet.worksheet('Hyperparameters') + records = wk_p.get_all_records() + hyperparams, current_type = {}, None + + for r in records: + current_type = r['type'] or current_type + hyperparams.setdefault(current_type, {})[r['param']] = r['value'] + + return result, hyperparams diff --git a/poly_data/websocket_handlers.py b/poly_data/websocket_handlers.py new file mode 100644 index 0000000..1fcf6b4 --- /dev/null +++ b/poly_data/websocket_handlers.py @@ -0,0 +1,98 @@ +import asyncio # Asynchronous I/O +import json # JSON handling +import websockets # WebSocket client +import traceback # Exception handling + +from poly_data.data_processing import process_data, process_user_data +import poly_data.global_state as global_state + +async def connect_market_websocket(chunk): + """ + Connect to Polymarket's market WebSocket API and process market updates. + + This function: + 1. Establishes a WebSocket connection to the Polymarket API + 2. Subscribes to updates for a specified list of market tokens + 3. Processes incoming order book and price updates + + Args: + chunk (list): List of token IDs to subscribe to + + Notes: + If the connection is lost, the function will exit and the main loop will + attempt to reconnect after a short delay. + """ + uri = "wss://ws-subscriptions-clob.polymarket.com/ws/market" + async with websockets.connect(uri, ping_interval=5, ping_timeout=None) as websocket: + # Prepare and send subscription message + message = {"assets_ids": chunk} + await websocket.send(json.dumps(message)) + + print("\n") + print(f"Sent market subscription message: {message}") + + try: + # Process incoming market data indefinitely + while True: + message = await websocket.recv() + json_data = json.loads(message) + # Process order book updates and trigger trading as needed + process_data(json_data) + except websockets.ConnectionClosed: + print("Connection closed in market websocket") + print(traceback.format_exc()) + except Exception as e: + print(f"Exception in market websocket: {e}") + print(traceback.format_exc()) + finally: + # Brief delay before attempting to reconnect + await asyncio.sleep(5) + +async def connect_user_websocket(): + """ + Connect to Polymarket's user WebSocket API and process order/trade updates. + + This function: + 1. Establishes a WebSocket connection to the Polymarket user API + 2. Authenticates using API credentials + 3. Processes incoming order and trade updates for the user + + Notes: + If the connection is lost, the function will exit and the main loop will + attempt to reconnect after a short delay. + """ + uri = "wss://ws-subscriptions-clob.polymarket.com/ws/user" + + async with websockets.connect(uri, ping_interval=5, ping_timeout=None) as websocket: + # Prepare authentication message with API credentials + message = { + "type": "user", + "auth": { + "apiKey": global_state.client.client.creds.api_key, + "secret": global_state.client.client.creds.api_secret, + "passphrase": global_state.client.client.creds.api_passphrase + } + } + + # Send authentication message + await websocket.send(json.dumps(message)) + + print("\n") + print(f"Sent user subscription message") + + try: + # Process incoming user data indefinitely + while True: + message = await websocket.recv() + json_data = json.loads(message) + # Process trade and order updates + process_user_data(json_data) + except websockets.ConnectionClosed: + print("Connection closed in user websocket") + print(traceback.format_exc()) + except Exception as e: + print(f"Exception in user websocket: {e}") + print(traceback.format_exc()) + finally: + # Brief delay before attempting to reconnect + await asyncio.sleep(5) \ No newline at end of file diff --git a/poly_merger/README.md b/poly_merger/README.md new file mode 100644 index 0000000..045e159 --- /dev/null +++ b/poly_merger/README.md @@ -0,0 +1,36 @@ +# Poly-Merger + +A utility for merging Polymarket positions efficiently. This tool helps in consolidating opposite positions in the same market, allowing you to: + +1. Reduce gas costs +2. Free up capital +3. Simplify position management + +## How It Works + +The merger tool interacts with Polymarket's smart contracts to combine opposite positions in binary markets. When you hold both YES and NO shares in the same market, this tool will merge them to recover your USDC. + +## Usage + +The merger is invoked through the main Poly-Maker bot when position merging conditions are met, but you can also use it independently: + +``` +node merge.js [amount_to_merge] [condition_id] [is_neg_risk_market] +``` + +Example: +``` +node merge.js 1000000 1234567 true +``` + +This would merge 1 USDC worth of opposing positions in market 1234567, which is a negative risk market. + +## Prerequisites + +- Node.js +- ethers.js v5.x +- A .env file with your Polygon network private key + +## Notes + +This implementation is based on open-source Polymarket code but has been optimized for automated market making operations. \ No newline at end of file diff --git a/poly_merger/merge.js b/poly_merger/merge.js new file mode 100644 index 0000000..83596ea --- /dev/null +++ b/poly_merger/merge.js @@ -0,0 +1,136 @@ +/** + * Poly-Merger: Position Merging Utility for Polymarket + * + * This script handles merging of YES and NO positions in Polymarket prediction markets + * to recover collateral. It works with both regular and negative risk markets. + * + * The merger supports Gnosis Safe wallets through the safe-helpers.js utility. + * + * Usage: + * node merge.js [amountToMerge] [conditionId] [isNegRiskMarket] + * + * Example: + * node merge.js 1000000 12345 true + */ + +const { ethers } = require('ethers'); +const { resolve } = require('path'); +const { signAndExecuteSafeTransaction } = require('./safe-helpers'); +const { safeAbi } = require('./safeAbi'); + +// Load environment variables +require('dotenv').config() + +// Connect to Polygon network +const provider = new ethers.providers.JsonRpcProvider("https://polygon.llamarpc.com"); +const privateKey = process.env.PK; +const wallet = new ethers.Wallet(privateKey, provider); + +// Polymarket contract addresses +const addresses = { + // Adapter contract for negative risk markets + neg_risk_adapter: '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296', + // USDC token contract on Polygon + collateral: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + // Main conditional tokens contract for prediction markets + conditional_tokens: '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045' +}; + +// Minimal ABIs for the contracts we interact with +const negRiskAdapterAbi = [ + "function mergePositions(bytes32 conditionId, uint256 amount)" +]; + +const conditionalTokensAbi = [ + "function mergePositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] partition, uint256 amount)" +]; + +/** + * Merges YES and NO positions in a Polymarket prediction market to recover USDC collateral. + * + * This function handles both regular and negative risk markets via different contract calls. + * It uses the Gnosis Safe wallet infrastructure for secure transaction execution. + * + * @param {string|number} amountToMerge - Raw amount of tokens to merge (typically expressed in raw units, e.g., 1000000 = 1 USDC) + * @param {string|number} conditionId - The market's condition ID + * @param {boolean} isNegRiskMarket - Whether this is a negative risk market (uses different contract) + * @returns {string} The transaction hash of the merge operation + */ +async function mergePositions(amountToMerge, conditionId, isNegRiskMarket) { + // Log parameters for debugging + console.log(amountToMerge, conditionId, isNegRiskMarket); + + // Prepare transaction parameters + const nonce = await provider.getTransactionCount(wallet.address); + const gasPrice = await provider.getGasPrice(); + const gasLimit = 10000000; // Set high gas limit to ensure transaction completes + + let tx; + // Different contract calls for different market types + if (isNegRiskMarket) { + // For negative risk markets, use the adapter contract + const negRiskAdapter = new ethers.Contract(addresses.neg_risk_adapter, negRiskAdapterAbi, wallet); + tx = await negRiskAdapter.populateTransaction.mergePositions(conditionId, amountToMerge); + } else { + // For regular markets, use the conditional tokens contract directly + const conditionalTokens = new ethers.Contract(addresses.conditional_tokens, conditionalTokensAbi, wallet); + tx = await conditionalTokens.populateTransaction.mergePositions( + addresses.collateral, // USDC contract + ethers.constants.HashZero, // Parent collection ID (0 for top-level markets) + conditionId, // Market ID + [1, 2], // Partition (indexes of outcomes to merge) + amountToMerge // Amount to merge + ); + } + + // Prepare full transaction object + const transaction = { + ...tx, + chainId: 137, // Polygon chain ID + gasPrice: gasPrice, + gasLimit: gasLimit, + nonce: nonce + }; + + // Get the Safe address from environment variables + const safeAddress = process.env.BROWSER_ADDRESS; + const safe = new ethers.Contract(safeAddress, safeAbi, wallet); + + // Execute the transaction through the Safe + console.log("Signing Transaction") + const txResponse = await signAndExecuteSafeTransaction( + wallet, + safe, + transaction.to, + transaction.data, + { + gasPrice: transaction.gasPrice, + gasLimit: transaction.gasLimit + } + ); + + console.log("Sent transaction. Waiting for response") + const txReceipt = await txResponse.wait(); + + console.log("merge positions " + txReceipt.transactionHash); + return txReceipt.transactionHash; +} + +// Parse command line arguments +const args = process.argv.slice(2); + +// Amount of tokens to merge (in raw units, e.g., 1000000 = 1 USDC) +const amountToMerge = args[0]; + +// The market's condition ID +const conditionId = args[1]; + +// Whether this is a negative risk market (true/false) +const isNegRiskMarket = args[2] === 'true'; + +// Execute the merge operation and handle any errors +mergePositions(amountToMerge, conditionId, isNegRiskMarket) + .catch(error => { + console.error("Error merging positions:", error); + process.exit(1); + }); \ No newline at end of file diff --git a/poly_merger/package-lock.json b/poly_merger/package-lock.json new file mode 100644 index 0000000..a7d73c4 --- /dev/null +++ b/poly_merger/package-lock.json @@ -0,0 +1,1339 @@ +{ + "name": "poly_live", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "poly_live", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "dotenv": "^16.4.5", + "ethers": "^5.6.3" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.6.1.tgz", + "integrity": "sha512-0cqssYh6FXjlwKWBmLm3+zH2BNARoS5u/hxbz+LpQmcDB3w0W553h2btWui1/uZp2GBM/SI3KniTuMcYyHpA5w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.6.0.tgz", + "integrity": "sha512-oPMFlKLN+g+y7a79cLK3WiLcjWFnZQtXWgnLAbHZcN3s7L4v90UHpTOrLk+m3yr0gt+/h9STTM6zrr7PM8uoRw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/networks": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/web": "^5.6.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.6.0.tgz", + "integrity": "sha512-WOqnG0NJKtI8n0wWZPReHtaLkDByPL67tn4nBaDAhmVq8sjHTPbCdz4DRhVu/cfTOvfy9w3iq5QZ7BX7zw56BQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.6.0.tgz", + "integrity": "sha512-6nvhYXjbXsHPS+30sHZ+U4VMagFC/9zAk6Gd/h3S21YW4+yfb0WfRtaAIZ4kfM4rrVwqiy284LP0GtL5HXGLxQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/rlp": "^5.6.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.6.0.tgz", + "integrity": "sha512-2Neq8wxJ9xHxCF9TUgmKeSh9BXJ6OAxWfeGWvbauPh8FuHEjamgHilllx8KkSd5ErxyHIX7Xv3Fkcud2kY9ezw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.6.0.tgz", + "integrity": "sha512-qN4T+hQd/Md32MoJpc69rOwLYRUXwjTlhHDIeUkUmiN/JyWkkLLMoG0TqvSQKNqZOMgN5stbUYN6ILC+eD7MEQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/properties": "^5.6.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.6.0.tgz", + "integrity": "sha512-VziMaXIUHQlHJmkv1dlcd6GY2PmT0khtAqaMctCIDogxkrarMzA9L94KN1NeXqqOfFD6r0sJT3vCTOFSmZ07DA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "bn.js": "^4.11.9" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.6.1.tgz", + "integrity": "sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.6.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.6.0.tgz", + "integrity": "sha512-SrdaJx2bK0WQl23nSpV/b1aq293Lh0sUaZT/yYKPDKn4tlAbkH96SPJwIhwSwTsoQQZxuh1jnqsKwyymoiBdWA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.6.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.6.0.tgz", + "integrity": "sha512-74Ge7iqTDom0NX+mux8KbRUeJgu1eHZ3iv6utv++sLJG80FVuU9HnHeKVPfjd9s3woFhaFoQGf3B3iH/FrQmgw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "^5.6.0", + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/transactions": "^5.6.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.6.0.tgz", + "integrity": "sha512-fFd+k9gtczqlr0/BruWLAu7UAOas1uRRJvOR84uDf4lNZ+bTkGl366qvniUZHKtlqxBRU65MkOobkmvmpHU+jA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.6.0.tgz", + "integrity": "sha512-61g3Jp3nwDqJcL/p4nugSyLrpl/+ChXIOtCEM8UDmWeB3JCAt5FoLdOMXQc3WWkc0oM2C0aAn6GFqqMcS/mHTw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/basex": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/pbkdf2": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/sha2": "^5.6.0", + "@ethersproject/signing-key": "^5.6.0", + "@ethersproject/strings": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/wordlists": "^5.6.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.6.0.tgz", + "integrity": "sha512-fmh86jViB9r0ibWXTQipxpAGMiuxoqUf78oqJDlCAJXgnJF024hOOX7qVgqsjtbeoxmcLwpPsXNU0WEe/16qPQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/hdnode": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/pbkdf2": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/random": "^5.6.0", + "@ethersproject/strings": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.6.0.tgz", + "integrity": "sha512-tk56BJ96mdj/ksi7HWZVWGjCq0WVl/QvfhFQNeL8fxhBlGoP+L80uDCiQcpJPd+2XxkivS3lwRm3E0CXTfol0w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.6.0.tgz", + "integrity": "sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ] + }, + "node_modules/@ethersproject/networks": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.6.2.tgz", + "integrity": "sha512-9uEzaJY7j5wpYGTojGp8U89mSsgQLc40PCMJLMCnFXTs7nhBveZ0t7dbqWUNrepWTszDbFkYD6WlL8DKx5huHA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.6.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.6.0.tgz", + "integrity": "sha512-Wu1AxTgJo3T3H6MIu/eejLFok9TYoSdgwRr5oGY1LTLfmGesDoSx05pemsbrPT2gG4cQME+baTSCp5sEo2erZQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/sha2": "^5.6.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.6.0.tgz", + "integrity": "sha512-szoOkHskajKePTJSZ46uHUWWkbv7TzP2ypdEK6jGMqJaEt2sb0jCgfBo0gH0m2HBpRixMuJ6TBRaQCF7a9DoCg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.6.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.3.tgz", + "integrity": "sha512-PKTEZXjdAhGK/+hUwRLQqlGrwTRrK4D0FHEP2V9klUF8C6+AWfHkA3AoLI+a8oq5CUZoRAH0nMecYGkKxUjeSw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/basex": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/networks": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/random": "^5.6.0", + "@ethersproject/rlp": "^5.6.0", + "@ethersproject/sha2": "^5.6.0", + "@ethersproject/strings": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/web": "^5.6.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "node_modules/@ethersproject/random": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.6.0.tgz", + "integrity": "sha512-si0PLcLjq+NG/XHSZz90asNf+YfKEqJGVdxoEkSukzbnBgC8rydbgbUgBbBGLeHN4kAJwUFEKsu3sCXT93YMsw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.6.0.tgz", + "integrity": "sha512-dz9WR1xpcTL+9DtOT/aDO+YyxSSdO8YIS0jyZwHHSlAmnxA6cKU3TrTd4Xc/bHayctxTgGLYNuVVoiXE4tTq1g==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.6.0.tgz", + "integrity": "sha512-1tNWCPFLu1n3JM9t4/kytz35DkuF9MxqkGGEHNauEbaARdm2fafnOyw1s0tIQDPKF/7bkP1u3dbrmjpn5CelyA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.6.0.tgz", + "integrity": "sha512-S+njkhowmLeUu/r7ir8n78OUKx63kBdMCPssePS89So1TH4hZqnWFsThEd/GiXYp9qMxVrydf7KdM9MTGPFukA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "bn.js": "^4.11.9", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.6.0.tgz", + "integrity": "sha512-YwF52vTNd50kjDzqKaoNNbC/r9kMDPq3YzDWmsjFTRBcIF1y4JCQJ8gB30wsTfHbaxgxelI5BfxQSxD/PbJOww==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/sha2": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.6.0.tgz", + "integrity": "sha512-uv10vTtLTZqrJuqBZR862ZQjTIa724wGPWQqZrofaPI/kUsf53TBG0I0D+hQ1qyNtllbNzaW+PDPHHUI6/65Mg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/logger": "^5.6.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.6.0.tgz", + "integrity": "sha512-4HX+VOhNjXHZyGzER6E/LVI2i6lf9ejYeWD6l4g50AdmimyuStKc39kvKf1bXWQMg7QNVh+uC7dYwtaZ02IXeg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/rlp": "^5.6.0", + "@ethersproject/signing-key": "^5.6.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.6.0.tgz", + "integrity": "sha512-tig9x0Qmh8qbo1w8/6tmtyrm/QQRviBh389EQ+d8fP4wDsBrJBf08oZfoiz1/uenKK9M78yAP4PoR7SsVoTjsw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/logger": "^5.6.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.6.0.tgz", + "integrity": "sha512-qMlSdOSTyp0MBeE+r7SUhr1jjDlC1zAXB8VD84hCnpijPQiSNbxr6GdiLXxpUs8UKzkDiNYYC5DRI3MZr+n+tg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/hdnode": "^5.6.0", + "@ethersproject/json-wallets": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/random": "^5.6.0", + "@ethersproject/signing-key": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/wordlists": "^5.6.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.6.0.tgz", + "integrity": "sha512-G/XHj0hV1FxI2teHRfCGvfBUHFmU+YOSbCxlAMqJklxSa7QMiHFQfAxvwY2PFqgvdkxEKwRNr/eCjfAPEm2Ctg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/base64": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.6.0.tgz", + "integrity": "sha512-q0bxNBfIX3fUuAo9OmjlEYxP40IB8ABgb7HjEZCL5IKubzV3j30CWi2rqQbjTS2HfoyQbfINoKcTVWP4ejwR7Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "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/ethers": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.6.3.tgz", + "integrity": "sha512-TkZAYGtbnzsGzEzqonLEMpOkmD5JIKCoWlAlj9ShAJlJszEkgKh3jVSeX1edLkYmqnpvJvGlBDK2y/HVe84LNg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "5.6.1", + "@ethersproject/abstract-provider": "5.6.0", + "@ethersproject/abstract-signer": "5.6.0", + "@ethersproject/address": "5.6.0", + "@ethersproject/base64": "5.6.0", + "@ethersproject/basex": "5.6.0", + "@ethersproject/bignumber": "5.6.0", + "@ethersproject/bytes": "5.6.1", + "@ethersproject/constants": "5.6.0", + "@ethersproject/contracts": "5.6.0", + "@ethersproject/hash": "5.6.0", + "@ethersproject/hdnode": "5.6.0", + "@ethersproject/json-wallets": "5.6.0", + "@ethersproject/keccak256": "5.6.0", + "@ethersproject/logger": "5.6.0", + "@ethersproject/networks": "5.6.2", + "@ethersproject/pbkdf2": "5.6.0", + "@ethersproject/properties": "5.6.0", + "@ethersproject/providers": "5.6.3", + "@ethersproject/random": "5.6.0", + "@ethersproject/rlp": "5.6.0", + "@ethersproject/sha2": "5.6.0", + "@ethersproject/signing-key": "5.6.0", + "@ethersproject/solidity": "5.6.0", + "@ethersproject/strings": "5.6.0", + "@ethersproject/transactions": "5.6.0", + "@ethersproject/units": "5.6.0", + "@ethersproject/wallet": "5.6.0", + "@ethersproject/web": "5.6.0", + "@ethersproject/wordlists": "5.6.0" + } + }, + "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==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "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==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "@ethersproject/abi": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.6.1.tgz", + "integrity": "sha512-0cqssYh6FXjlwKWBmLm3+zH2BNARoS5u/hxbz+LpQmcDB3w0W553h2btWui1/uZp2GBM/SI3KniTuMcYyHpA5w==", + "requires": { + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "@ethersproject/abstract-provider": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.6.0.tgz", + "integrity": "sha512-oPMFlKLN+g+y7a79cLK3WiLcjWFnZQtXWgnLAbHZcN3s7L4v90UHpTOrLk+m3yr0gt+/h9STTM6zrr7PM8uoRw==", + "requires": { + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/networks": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/web": "^5.6.0" + } + }, + "@ethersproject/abstract-signer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.6.0.tgz", + "integrity": "sha512-WOqnG0NJKtI8n0wWZPReHtaLkDByPL67tn4nBaDAhmVq8sjHTPbCdz4DRhVu/cfTOvfy9w3iq5QZ7BX7zw56BQ==", + "requires": { + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0" + } + }, + "@ethersproject/address": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.6.0.tgz", + "integrity": "sha512-6nvhYXjbXsHPS+30sHZ+U4VMagFC/9zAk6Gd/h3S21YW4+yfb0WfRtaAIZ4kfM4rrVwqiy284LP0GtL5HXGLxQ==", + "requires": { + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/rlp": "^5.6.0" + } + }, + "@ethersproject/base64": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.6.0.tgz", + "integrity": "sha512-2Neq8wxJ9xHxCF9TUgmKeSh9BXJ6OAxWfeGWvbauPh8FuHEjamgHilllx8KkSd5ErxyHIX7Xv3Fkcud2kY9ezw==", + "requires": { + "@ethersproject/bytes": "^5.6.0" + } + }, + "@ethersproject/basex": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.6.0.tgz", + "integrity": "sha512-qN4T+hQd/Md32MoJpc69rOwLYRUXwjTlhHDIeUkUmiN/JyWkkLLMoG0TqvSQKNqZOMgN5stbUYN6ILC+eD7MEQ==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/properties": "^5.6.0" + } + }, + "@ethersproject/bignumber": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.6.0.tgz", + "integrity": "sha512-VziMaXIUHQlHJmkv1dlcd6GY2PmT0khtAqaMctCIDogxkrarMzA9L94KN1NeXqqOfFD6r0sJT3vCTOFSmZ07DA==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "bn.js": "^4.11.9" + } + }, + "@ethersproject/bytes": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.6.1.tgz", + "integrity": "sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g==", + "requires": { + "@ethersproject/logger": "^5.6.0" + } + }, + "@ethersproject/constants": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.6.0.tgz", + "integrity": "sha512-SrdaJx2bK0WQl23nSpV/b1aq293Lh0sUaZT/yYKPDKn4tlAbkH96SPJwIhwSwTsoQQZxuh1jnqsKwyymoiBdWA==", + "requires": { + "@ethersproject/bignumber": "^5.6.0" + } + }, + "@ethersproject/contracts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.6.0.tgz", + "integrity": "sha512-74Ge7iqTDom0NX+mux8KbRUeJgu1eHZ3iv6utv++sLJG80FVuU9HnHeKVPfjd9s3woFhaFoQGf3B3iH/FrQmgw==", + "requires": { + "@ethersproject/abi": "^5.6.0", + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/transactions": "^5.6.0" + } + }, + "@ethersproject/hash": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.6.0.tgz", + "integrity": "sha512-fFd+k9gtczqlr0/BruWLAu7UAOas1uRRJvOR84uDf4lNZ+bTkGl366qvniUZHKtlqxBRU65MkOobkmvmpHU+jA==", + "requires": { + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "@ethersproject/hdnode": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.6.0.tgz", + "integrity": "sha512-61g3Jp3nwDqJcL/p4nugSyLrpl/+ChXIOtCEM8UDmWeB3JCAt5FoLdOMXQc3WWkc0oM2C0aAn6GFqqMcS/mHTw==", + "requires": { + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/basex": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/pbkdf2": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/sha2": "^5.6.0", + "@ethersproject/signing-key": "^5.6.0", + "@ethersproject/strings": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/wordlists": "^5.6.0" + } + }, + "@ethersproject/json-wallets": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.6.0.tgz", + "integrity": "sha512-fmh86jViB9r0ibWXTQipxpAGMiuxoqUf78oqJDlCAJXgnJF024hOOX7qVgqsjtbeoxmcLwpPsXNU0WEe/16qPQ==", + "requires": { + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/hdnode": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/pbkdf2": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/random": "^5.6.0", + "@ethersproject/strings": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "@ethersproject/keccak256": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.6.0.tgz", + "integrity": "sha512-tk56BJ96mdj/ksi7HWZVWGjCq0WVl/QvfhFQNeL8fxhBlGoP+L80uDCiQcpJPd+2XxkivS3lwRm3E0CXTfol0w==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "js-sha3": "0.8.0" + } + }, + "@ethersproject/logger": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.6.0.tgz", + "integrity": "sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg==" + }, + "@ethersproject/networks": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.6.2.tgz", + "integrity": "sha512-9uEzaJY7j5wpYGTojGp8U89mSsgQLc40PCMJLMCnFXTs7nhBveZ0t7dbqWUNrepWTszDbFkYD6WlL8DKx5huHA==", + "requires": { + "@ethersproject/logger": "^5.6.0" + } + }, + "@ethersproject/pbkdf2": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.6.0.tgz", + "integrity": "sha512-Wu1AxTgJo3T3H6MIu/eejLFok9TYoSdgwRr5oGY1LTLfmGesDoSx05pemsbrPT2gG4cQME+baTSCp5sEo2erZQ==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/sha2": "^5.6.0" + } + }, + "@ethersproject/properties": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.6.0.tgz", + "integrity": "sha512-szoOkHskajKePTJSZ46uHUWWkbv7TzP2ypdEK6jGMqJaEt2sb0jCgfBo0gH0m2HBpRixMuJ6TBRaQCF7a9DoCg==", + "requires": { + "@ethersproject/logger": "^5.6.0" + } + }, + "@ethersproject/providers": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.3.tgz", + "integrity": "sha512-PKTEZXjdAhGK/+hUwRLQqlGrwTRrK4D0FHEP2V9klUF8C6+AWfHkA3AoLI+a8oq5CUZoRAH0nMecYGkKxUjeSw==", + "requires": { + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/basex": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/networks": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/random": "^5.6.0", + "@ethersproject/rlp": "^5.6.0", + "@ethersproject/sha2": "^5.6.0", + "@ethersproject/strings": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/web": "^5.6.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "@ethersproject/random": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.6.0.tgz", + "integrity": "sha512-si0PLcLjq+NG/XHSZz90asNf+YfKEqJGVdxoEkSukzbnBgC8rydbgbUgBbBGLeHN4kAJwUFEKsu3sCXT93YMsw==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0" + } + }, + "@ethersproject/rlp": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.6.0.tgz", + "integrity": "sha512-dz9WR1xpcTL+9DtOT/aDO+YyxSSdO8YIS0jyZwHHSlAmnxA6cKU3TrTd4Xc/bHayctxTgGLYNuVVoiXE4tTq1g==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0" + } + }, + "@ethersproject/sha2": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.6.0.tgz", + "integrity": "sha512-1tNWCPFLu1n3JM9t4/kytz35DkuF9MxqkGGEHNauEbaARdm2fafnOyw1s0tIQDPKF/7bkP1u3dbrmjpn5CelyA==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "hash.js": "1.1.7" + } + }, + "@ethersproject/signing-key": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.6.0.tgz", + "integrity": "sha512-S+njkhowmLeUu/r7ir8n78OUKx63kBdMCPssePS89So1TH4hZqnWFsThEd/GiXYp9qMxVrydf7KdM9MTGPFukA==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "bn.js": "^4.11.9", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "@ethersproject/solidity": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.6.0.tgz", + "integrity": "sha512-YwF52vTNd50kjDzqKaoNNbC/r9kMDPq3YzDWmsjFTRBcIF1y4JCQJ8gB30wsTfHbaxgxelI5BfxQSxD/PbJOww==", + "requires": { + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/sha2": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "@ethersproject/strings": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.6.0.tgz", + "integrity": "sha512-uv10vTtLTZqrJuqBZR862ZQjTIa724wGPWQqZrofaPI/kUsf53TBG0I0D+hQ1qyNtllbNzaW+PDPHHUI6/65Mg==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/logger": "^5.6.0" + } + }, + "@ethersproject/transactions": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.6.0.tgz", + "integrity": "sha512-4HX+VOhNjXHZyGzER6E/LVI2i6lf9ejYeWD6l4g50AdmimyuStKc39kvKf1bXWQMg7QNVh+uC7dYwtaZ02IXeg==", + "requires": { + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/rlp": "^5.6.0", + "@ethersproject/signing-key": "^5.6.0" + } + }, + "@ethersproject/units": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.6.0.tgz", + "integrity": "sha512-tig9x0Qmh8qbo1w8/6tmtyrm/QQRviBh389EQ+d8fP4wDsBrJBf08oZfoiz1/uenKK9M78yAP4PoR7SsVoTjsw==", + "requires": { + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/logger": "^5.6.0" + } + }, + "@ethersproject/wallet": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.6.0.tgz", + "integrity": "sha512-qMlSdOSTyp0MBeE+r7SUhr1jjDlC1zAXB8VD84hCnpijPQiSNbxr6GdiLXxpUs8UKzkDiNYYC5DRI3MZr+n+tg==", + "requires": { + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/hdnode": "^5.6.0", + "@ethersproject/json-wallets": "^5.6.0", + "@ethersproject/keccak256": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/random": "^5.6.0", + "@ethersproject/signing-key": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/wordlists": "^5.6.0" + } + }, + "@ethersproject/web": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.6.0.tgz", + "integrity": "sha512-G/XHj0hV1FxI2teHRfCGvfBUHFmU+YOSbCxlAMqJklxSa7QMiHFQfAxvwY2PFqgvdkxEKwRNr/eCjfAPEm2Ctg==", + "requires": { + "@ethersproject/base64": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "@ethersproject/wordlists": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.6.0.tgz", + "integrity": "sha512-q0bxNBfIX3fUuAo9OmjlEYxP40IB8ABgb7HjEZCL5IKubzV3j30CWi2rqQbjTS2HfoyQbfINoKcTVWP4ejwR7Q==", + "requires": { + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/strings": "^5.6.0" + } + }, + "aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" + }, + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "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" + } + }, + "ethers": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.6.3.tgz", + "integrity": "sha512-TkZAYGtbnzsGzEzqonLEMpOkmD5JIKCoWlAlj9ShAJlJszEkgKh3jVSeX1edLkYmqnpvJvGlBDK2y/HVe84LNg==", + "requires": { + "@ethersproject/abi": "5.6.1", + "@ethersproject/abstract-provider": "5.6.0", + "@ethersproject/abstract-signer": "5.6.0", + "@ethersproject/address": "5.6.0", + "@ethersproject/base64": "5.6.0", + "@ethersproject/basex": "5.6.0", + "@ethersproject/bignumber": "5.6.0", + "@ethersproject/bytes": "5.6.1", + "@ethersproject/constants": "5.6.0", + "@ethersproject/contracts": "5.6.0", + "@ethersproject/hash": "5.6.0", + "@ethersproject/hdnode": "5.6.0", + "@ethersproject/json-wallets": "5.6.0", + "@ethersproject/keccak256": "5.6.0", + "@ethersproject/logger": "5.6.0", + "@ethersproject/networks": "5.6.2", + "@ethersproject/pbkdf2": "5.6.0", + "@ethersproject/properties": "5.6.0", + "@ethersproject/providers": "5.6.3", + "@ethersproject/random": "5.6.0", + "@ethersproject/rlp": "5.6.0", + "@ethersproject/sha2": "5.6.0", + "@ethersproject/signing-key": "5.6.0", + "@ethersproject/solidity": "5.6.0", + "@ethersproject/strings": "5.6.0", + "@ethersproject/transactions": "5.6.0", + "@ethersproject/units": "5.6.0", + "@ethersproject/wallet": "5.6.0", + "@ethersproject/web": "5.6.0", + "@ethersproject/wordlists": "5.6.0" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "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==" + }, + "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==" + }, + "scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} + } + } +} diff --git a/poly_merger/package.json b/poly_merger/package.json new file mode 100644 index 0000000..3aacd3d --- /dev/null +++ b/poly_merger/package.json @@ -0,0 +1,15 @@ +{ + "name": "poly-merger", + "version": "1.0.0", + "description": "Position merging utility for Polymarket", + "main": "merge.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "dotenv": "^16.4.5", + "ethers": "^5.6.3" + } +} diff --git a/poly_merger/safe-helpers.js b/poly_merger/safe-helpers.js new file mode 100644 index 0000000..bc4e023 --- /dev/null +++ b/poly_merger/safe-helpers.js @@ -0,0 +1,128 @@ +const { BigNumber, ethers } = require('ethers'); + +function joinHexData(hexData) { + return `0x${hexData + .map(hex => { + const stripped = hex.replace(/^0x/, ""); + return stripped.length % 2 === 0 ? stripped : "0" + stripped; + }) + .join("")}`; +} + +function abiEncodePacked(...params) { + return joinHexData( + params.map(({ type, value }) => { + const encoded = ethers.utils.defaultAbiCoder.encode([type], [value]); + + if (type === "bytes" || type === "string") { + const bytesLength = parseInt(encoded.slice(66, 130), 16); + return encoded.slice(130, 130 + 2 * bytesLength); + } + + let typeMatch = type.match(/^(?:u?int\d*|bytes\d+|address)\[\]$/); + if (typeMatch) { + return encoded.slice(130); + } + + if (type.startsWith("bytes")) { + const bytesLength = parseInt(type.slice(5)); + return encoded.slice(2, 2 + 2 * bytesLength); + } + + typeMatch = type.match(/^u?int(\d*)$/); + if (typeMatch) { + if (typeMatch[1] !== "") { + const bytesLength = parseInt(typeMatch[1]) / 8; + return encoded.slice(-2 * bytesLength); + } + return encoded.slice(-64); + } + + if (type === "address") { + return encoded.slice(-40); + } + + throw new Error(`unsupported type ${type}`); + }) + ); +} + +async function signTransactionHash(signer, message) { + const messageArray = ethers.utils.arrayify(message); + let sig = await signer.signMessage(messageArray); + let sigV = parseInt(sig.slice(-2), 16); + + switch (sigV) { + case 0: + case 1: + sigV += 31; + break; + case 27: + case 28: + sigV += 4; + break; + default: + throw new Error("Invalid signature"); + } + + sig = sig.slice(0, -2) + sigV.toString(16); + + return { + r: BigNumber.from("0x" + sig.slice(2, 66)).toString(), + s: BigNumber.from("0x" + sig.slice(66, 130)).toString(), + v: BigNumber.from("0x" + sig.slice(130, 132)).toString(), + }; +} + +async function signAndExecuteSafeTransaction(signer, safe, to, data, overrides = {}) { + const nonce = await safe.nonce(); + console.log("Nonce for safe: ", nonce); + const value = "0"; + const safeTxGas = "0"; + const baseGas = "0"; + const gasPrice = "0"; + const gasToken = ethers.constants.AddressZero; + const refundReceiver = ethers.constants.AddressZero; + const operation = 0; + + const txHash = await safe.getTransactionHash( + to, + value, + data, + operation, + safeTxGas, + baseGas, + gasPrice, + gasToken, + refundReceiver, + nonce + ); + console.log("Transaction hash: ", txHash); + + const rsvSignature = await signTransactionHash(signer, txHash); + const packedSig = abiEncodePacked( + { type: "uint256", value: rsvSignature.r }, + { type: "uint256", value: rsvSignature.s }, + { type: "uint8", value: rsvSignature.v } + ); + + console.log("Executing transaction"); + + return safe.execTransaction( + to, + value, + data, + operation, + safeTxGas, + baseGas, + gasPrice, + gasToken, + refundReceiver, + packedSig, + overrides + ); +} + +module.exports = { + signAndExecuteSafeTransaction, +}; \ No newline at end of file diff --git a/poly_merger/safeAbi.js b/poly_merger/safeAbi.js new file mode 100644 index 0000000..29d785a --- /dev/null +++ b/poly_merger/safeAbi.js @@ -0,0 +1,1140 @@ +const safeAbi = [ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "AddedOwner", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "approvedHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ApproveHash", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "handler", + "type": "address" + } + ], + "name": "ChangedFallbackHandler", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "guard", + "type": "address" + } + ], + "name": "ChangedGuard", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + } + ], + "name": "ChangedThreshold", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "DisabledModule", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "EnabledModule", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "txHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "payment", + "type": "uint256" + } + ], + "name": "ExecutionFailure", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "ExecutionFromModuleFailure", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "ExecutionFromModuleSuccess", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "txHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "payment", + "type": "uint256" + } + ], + "name": "ExecutionSuccess", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "RemovedOwner", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "module", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + } + ], + "name": "SafeModuleTransaction", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "safeTxGas", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "baseGas", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "gasPrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "gasToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "address payable", + "name": "refundReceiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "signatures", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "additionalInfo", + "type": "bytes" + } + ], + "name": "SafeMultiSigTransaction", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "SafeReceived", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "initiator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "owners", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "initializer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "fallbackHandler", + "type": "address" + } + ], + "name": "SafeSetup", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "msgHash", + "type": "bytes32" + } + ], + "name": "SignMsg", + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "fallback" + }, + { + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_threshold", + "type": "uint256" + } + ], + "name": "addOwnerWithThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "hashToApprove", + "type": "bytes32" + } + ], + "name": "approveHash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "approvedHashes", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_threshold", + "type": "uint256" + } + ], + "name": "changeThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "dataHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "signatures", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "requiredSignatures", + "type": "uint256" + } + ], + "name": "checkNSignatures", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "dataHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "signatures", + "type": "bytes" + } + ], + "name": "checkSignatures", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "prevModule", + "type": "address" + }, + { + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "disableModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "domainSeparator", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "enableModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "safeTxGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasPrice", + "type": "uint256" + }, + { + "internalType": "address", + "name": "gasToken", + "type": "address" + }, + { + "internalType": "address", + "name": "refundReceiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_nonce", + "type": "uint256" + } + ], + "name": "encodeTransactionData", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "safeTxGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasPrice", + "type": "uint256" + }, + { + "internalType": "address", + "name": "gasToken", + "type": "address" + }, + { + "internalType": "address payable", + "name": "refundReceiver", + "type": "address" + }, + { + "internalType": "bytes", + "name": "signatures", + "type": "bytes" + } + ], + "name": "execTransaction", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + } + ], + "name": "execTransactionFromModule", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + } + ], + "name": "execTransactionFromModuleReturnData", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "start", + "type": "address" + }, + { + "internalType": "uint256", + "name": "pageSize", + "type": "uint256" + } + ], + "name": "getModulesPaginated", + "outputs": [ + { + "internalType": "address[]", + "name": "array", + "type": "address[]" + }, + { + "internalType": "address", + "name": "next", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOwners", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "offset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "getStorageAt", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "safeTxGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasPrice", + "type": "uint256" + }, + { + "internalType": "address", + "name": "gasToken", + "type": "address" + }, + { + "internalType": "address", + "name": "refundReceiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_nonce", + "type": "uint256" + } + ], + "name": "getTransactionHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "module", + "type": "address" + } + ], + "name": "isModuleEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "isOwner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "prevOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_threshold", + "type": "uint256" + } + ], + "name": "removeOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "operation", + "type": "uint8" + } + ], + "name": "requiredTxGas", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "handler", + "type": "address" + } + ], + "name": "setFallbackHandler", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "guard", + "type": "address" + } + ], + "name": "setGuard", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_owners", + "type": "address[]" + }, + { + "internalType": "uint256", + "name": "_threshold", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "address", + "name": "fallbackHandler", + "type": "address" + }, + { + "internalType": "address", + "name": "paymentToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "payment", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "paymentReceiver", + "type": "address" + } + ], + "name": "setup", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "signedMessages", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "targetContract", + "type": "address" + }, + { + "internalType": "bytes", + "name": "calldataPayload", + "type": "bytes" + } + ], + "name": "simulateAndRevert", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "prevOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "oldOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "swapOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ]; + + module.exports = { safeAbi }; \ No newline at end of file diff --git a/poly_stats/__init__.py b/poly_stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/poly_stats/account_stats.py b/poly_stats/account_stats.py new file mode 100644 index 0000000..40b76da --- /dev/null +++ b/poly_stats/account_stats.py @@ -0,0 +1,137 @@ +import pandas as pd +from py_clob_client.headers.headers import create_level_2_headers +from py_clob_client.clob_types import RequestArgs + +from poly_utils.google_utils import get_spreadsheet +from gspread_dataframe import set_with_dataframe +import requests +import json +import os + +from dotenv import load_dotenv +load_dotenv() + +spreadsheet = get_spreadsheet() + +def get_markets_df(wk_full): + markets_df = pd.DataFrame(wk_full.get_all_records()) + markets_df = markets_df[['question', 'answer1', 'answer2', 'token1', 'token2']] + markets_df['token1'] = markets_df['token1'].astype(str) + markets_df['token2'] = markets_df['token2'].astype(str) + return markets_df + +def get_all_orders(client): + orders = client.client.get_orders() + orders_df = pd.DataFrame(orders) + + if len(orders_df) > 0: + orders_df['order_size'] = orders_df['original_size'].astype('float') - orders_df['size_matched'].astype('float') + orders_df = orders_df[['asset_id', 'order_size', 'side', 'price']] + + orders_df = orders_df.rename(columns={'side': 'order_side', 'price': 'order_price'}) + return orders_df + else: + return pd.DataFrame() + +def get_all_positions(client): + try: + positions = client.get_all_positions() + positions = positions[['asset', 'size', 'avgPrice', 'curPrice', 'percentPnl']] + positions = positions.rename(columns={'size': 'position_size'}) + return positions + except: + return pd.DataFrame() + +def combine_dfs(orders_df, positions, markets_df, selected_df): + merged_df = orders_df.merge(positions, left_on=['asset_id'], right_on=['asset'], how='outer') + merged_df['asset_id'] = merged_df['asset_id'].combine_first(merged_df['asset']) + merged_df = merged_df.drop(columns='asset', axis=1) + + merge_token1 = merged_df.merge(markets_df, left_on='asset_id', right_on='token1', how='inner') + merge_token1['merged_with'] = 'token1' + + # Merge with token2 + merge_token2 = merged_df.merge(markets_df, left_on='asset_id', right_on='token2', how='inner') + merge_token2['merged_with'] = 'token2' + + # Combine the results + combined_df = pd.concat([merge_token1, merge_token2]) + + assert len(merged_df) == len(combined_df) + + combined_df['answer'] = combined_df.apply( + lambda row: row['answer1'] if row['merged_with'] == 'token1' else row['answer2'], axis=1 + ) + + combined_df = combined_df[['question', 'answer', 'order_size', 'order_side', 'order_price', 'position_size', 'avgPrice', 'curPrice']] + combined_df['order_side'] = combined_df['order_side'].fillna('') + combined_df = combined_df.fillna(0) + + combined_df['marketInSelected'] = combined_df['question'].isin(selected_df['question']) + combined_df = combined_df.sort_values('question') + combined_df = combined_df.sort_values('marketInSelected') + return combined_df + +def get_earnings(client): + args = RequestArgs(method='GET', request_path='/rewards/user/markets') + l2Headers = create_level_2_headers(client.signer, client.creds, args) + url = "https://polymarket.com/api/rewards/markets" + + cursor = '' + markets = [] + + params = { + "l2Headers": json.dumps(l2Headers), + "orderBy": "earnings", + "position": "DESC", + "makerAddress": os.getenv('BROWSER_WALLET'), + "authenticationType": "eoa", + "nextCursor": cursor, + "requestPath": "/rewards/user/markets" + } + + r = requests.get(url, params=params) + results = r.json() + + data = pd.DataFrame(results['data']) + data['earnings'] = data['earnings'].apply(lambda x: x[0]['earnings']) + + data = data[data['earnings'] > 0].reset_index(drop=True) + data = data[['question', 'earnings', 'earning_percentage']] + return data + + + +def update_stats_once(client): + spreadsheet = get_spreadsheet() + wk_full = spreadsheet.worksheet('Full Markets') + wk_summary = spreadsheet.worksheet('Summary') + + + wk_sel = spreadsheet.worksheet('Selected Markets') + selected_df = pd.DataFrame(wk_sel.get_all_records()) + + markets_df = get_markets_df(wk_full) + print("Got spreadsheet...") + + orders_df = get_all_orders(client) + print("Got Orders...") + positions = get_all_positions(client) + print("Got Positions...") + + if len(positions) > 0 or len(orders_df) > 0: + combined_df = combine_dfs(orders_df, positions, markets_df, selected_df) + earnings = get_earnings(client.client) + print("Got Earnings...") + combined_df = combined_df.merge(earnings, on='question', how='left') + + combined_df = combined_df.fillna(0) + combined_df = combined_df.round(2) + + combined_df = combined_df.sort_values('earnings', ascending=False) + combined_df = combined_df[['question', 'answer', 'order_size', 'position_size', 'marketInSelected', 'earnings', 'earning_percentage']] + wk_summary.clear() + + set_with_dataframe(wk_summary, combined_df, include_index=False, include_column_header=True, resize=True) + else: + print("Position or order is empty") \ No newline at end of file diff --git a/poly_utils/__init__.py b/poly_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/poly_utils/google_utils.py b/poly_utils/google_utils.py new file mode 100644 index 0000000..7b63f18 --- /dev/null +++ b/poly_utils/google_utils.py @@ -0,0 +1,16 @@ +from google.oauth2.service_account import Credentials +import gspread +import os + + +def get_spreadsheet(): + scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"] + + creds_file = 'credentials.json' if os.path.exists('credentials.json') else '../credentials.json' + credentials = Credentials.from_service_account_file(creds_file, scopes=scope) + client = gspread.authorize(credentials) + spreadsheet_url = os.getenv("SPREADSHEET_URL") + if not spreadsheet_url: + raise ValueError("SPREADSHEET_URL environment variable is not set") + spreadsheet = client.open_by_url(spreadsheet_url) + return spreadsheet diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..51c2b0d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +py-clob-client +python-dotenv +pandas +gspread +gspread-dataframe +sortedcontainers +websockets==12.0.0 +cryptography==42.0.8 +web3==5.3.1 +asyncio +requests +google-auth \ No newline at end of file diff --git a/trading.py b/trading.py new file mode 100644 index 0000000..638a18a --- /dev/null +++ b/trading.py @@ -0,0 +1,422 @@ +import gc # Garbage collection +import os # Operating system interface +import json # JSON handling +import asyncio # Asynchronous I/O +import traceback # Exception handling +import pandas as pd # Data analysis library +import math # Mathematical functions + +import poly_data.global_state as global_state +import poly_data.CONSTANTS as CONSTANTS + +# Import utility functions for trading +from poly_data.trading_utils import get_best_bid_ask_deets, get_order_prices, get_buy_sell_amount, round_down, round_up +from poly_data.data_utils import get_position, get_order, set_position + +# Create directory for storing position risk information +if not os.path.exists('positions/'): + os.makedirs('positions/') + +def send_buy_order(order): + """ + Create a BUY order for a specific token. + + This function: + 1. Cancels any existing orders for the token + 2. Checks if the order price is within acceptable range + 3. Creates a new buy order if conditions are met + + Args: + order (dict): Order details including token, price, size, and market parameters + """ + client = global_state.client + + # Cancel existing orders for this token to avoid conflicts + if order['orders']['buy']['size'] > 0 or order['orders']['sell']['size'] > 0: + client.cancel_all_asset(order['token']) + + # Calculate minimum acceptable price based on market spread + incentive_start = order['mid_price'] - order['max_spread']/100 + + trade = True + + # Don't place orders that are below incentive threshold + if order['price'] < incentive_start: + trade = False + + if trade: + # Only place orders with prices between 0.1 and 0.9 to avoid extreme positions + if order['price'] >= 0.1 and order['price'] < 0.9: + print(f'Creating new order for {order["size"]} at {order["price"]}') + print(order['token'], 'BUY', order['price'], order['size']) + client.create_order( + order['token'], + 'BUY', + order['price'], + order['size'], + True if order['neg_risk'] == 'TRUE' else False + ) + else: + print("Not creating buy order because its outside acceptable price range (0.1-0.9)") + else: + print(f'Not creating new order because order price of {order["price"]} is less than incentive start price of {incentive_start}. Mid price is {order["mid_price"]}') + + +def send_sell_order(order): + """ + Create a SELL order for a specific token. + + This function: + 1. Cancels any existing orders for the token + 2. Creates a new sell order with the specified parameters + + Args: + order (dict): Order details including token, price, size, and market parameters + """ + client = global_state.client + + # Cancel existing orders for this token to avoid conflicts + if order['orders']['buy']['size'] > 0 or order['orders']['sell']['size'] > 0: + client.cancel_all_asset(order['token']) + + print(f'Creating new order for {order["size"]} at {order["price"]}') + client.create_order( + order['token'], + 'SELL', + order['price'], + order['size'], + True if order['neg_risk'] == 'TRUE' else False + ) + +# Dictionary to store locks for each market to prevent concurrent trading on the same market +market_locks = {} + +async def perform_trade(market): + """ + Main trading function that handles market making for a specific market. + + This function: + 1. Merges positions when possible to free up capital + 2. Analyzes the market to determine optimal bid/ask prices + 3. Manages buy and sell orders based on position size and market conditions + 4. Implements risk management with stop-loss and take-profit logic + + Args: + market (str): The market ID to trade on + """ + # Create a lock for this market if it doesn't exist + if market not in market_locks: + market_locks[market] = asyncio.Lock() + + # Use lock to prevent concurrent trading on the same market + async with market_locks[market]: + try: + client = global_state.client + # Get market details from the configuration + row = global_state.df[global_state.df['condition_id'] == market].iloc[0] + # Determine decimal precision from tick size + round_length = len(str(row['tick_size']).split(".")[1]) + + # Get trading parameters for this market type + params = global_state.params[row['param_type']] + + # Create a list with both outcomes for the market + deets = [ + {'name': 'token1', 'token': row['token1'], 'answer': row['answer1']}, + {'name': 'token2', 'token': row['token2'], 'answer': row['answer2']} + ] + print(f"\n\n{pd.Timestamp.utcnow().tz_localize(None)}: {row['question']}") + + # Get current positions for both outcomes + pos_1 = get_position(row['token1'])['size'] + pos_2 = get_position(row['token2'])['size'] + + # ------- POSITION MERGING LOGIC ------- + # Calculate if we have opposing positions that can be merged + amount_to_merge = min(pos_1, pos_2) + + # Only merge if positions are above minimum threshold + if float(amount_to_merge) > CONSTANTS.MIN_MERGE_SIZE: + # Get exact position sizes from blockchain for merging + pos_1 = client.get_position(row['token1'])[0] + pos_2 = client.get_position(row['token2'])[0] + amount_to_merge = min(pos_1, pos_2) + scaled_amt = amount_to_merge / 10**6 + + if scaled_amt > CONSTANTS.MIN_MERGE_SIZE: + print(f"Position 1 is of size {pos_1} and Position 2 is of size {pos_2}. Merging positions") + # Execute the merge operation + client.merge_positions(amount_to_merge, market, row['neg_risk'] == 'TRUE') + # Update our local position tracking + set_position(row['token1'], 'SELL', scaled_amt, 0, 'merge') + set_position(row['token2'], 'SELL', scaled_amt, 0, 'merge') + + # ------- TRADING LOGIC FOR EACH OUTCOME ------- + # Loop through both outcomes in the market (YES and NO) + for detail in deets: + token = int(detail['token']) + + # Get current orders for this token + orders = get_order(token) + + # Get market depth and price information + deets = get_best_bid_ask_deets(market, detail['name'], 100, 0.1) + + # Extract all order book details + best_bid = deets['best_bid'] + best_bid_size = deets['best_bid_size'] + second_best_bid = deets['second_best_bid'] + second_best_bid_size = deets['second_best_bid_size'] + top_bid = deets['top_bid'] + best_ask = deets['best_ask'] + best_ask_size = deets['best_ask_size'] + second_best_ask = deets['second_best_ask'] + second_best_ask_size = deets['second_best_ask_size'] + top_ask = deets['top_ask'] + + # Round prices to appropriate precision + best_bid = round(best_bid, round_length) + best_ask = round(best_ask, round_length) + + # Calculate ratio of buy vs sell liquidity in the market + try: + overall_ratio = (deets['bid_sum_within_n_percent']) / (deets['ask_sum_within_n_percent']) + except: + overall_ratio = 0 + + try: + second_best_bid = round(second_best_bid, round_length) + second_best_ask = round(second_best_ask, round_length) + except: + pass + + top_bid = round(top_bid, round_length) + top_ask = round(top_ask, round_length) + + # Get our current position and average price + pos = get_position(token) + position = pos['size'] + avgPrice = pos['avgPrice'] + + position = round_down(position, 2) + + # Calculate optimal bid and ask prices based on market conditions + bid_price, ask_price = get_order_prices( + best_bid, best_bid_size, top_bid, best_ask, + best_ask_size, top_ask, avgPrice, row + ) + + bid_price = round(bid_price, round_length) + ask_price = round(ask_price, round_length) + + # Calculate mid price for reference + mid_price = (top_bid + top_ask) / 2 + + # Log market conditions for this outcome + print(f"\nFor {detail['answer']}. Orders: {orders} Position: {position}, " + f"avgPrice: {avgPrice}, Best Bid: {best_bid}, Best Ask: {best_ask}, " + f"Bid Price: {bid_price}, Ask Price: {ask_price}, Mid Price: {mid_price}") + + # Calculate how much to buy or sell based on our position + buy_amount, sell_amount = get_buy_sell_amount(position, bid_price, row) + + # Prepare order object with all necessary information + order = { + "token": token, + "mid_price": mid_price, + "neg_risk": row['neg_risk'], + "max_spread": row['max_spread'], + 'orders': orders, + 'token_name': detail['name'], + 'row': row + } + + print(f"Position: {position}, Trade Size: {row['trade_size']}, " + f"buy_amount: {buy_amount}, sell_amount: {sell_amount}") + + # File to store risk management information for this market + fname = 'positions/' + str(market) + '.json' + + # ------- SELL ORDER LOGIC ------- + if sell_amount > 0: + # Skip if we have no average price (no real position) + if avgPrice == 0: + print("Avg Price is 0. Skipping") + continue + + order['size'] = sell_amount + order['price'] = ask_price + + # Get fresh market data for risk assessment + n_deets = get_best_bid_ask_deets(market, detail['name'], 100, 0.1) + + # Calculate current market price and spread + mid_price = round_up((n_deets['best_bid'] + n_deets['best_ask']) / 2, round_length) + spread = round(n_deets['best_ask'] - n_deets['best_bid'], 2) + + # Calculate current profit/loss on position + pnl = (mid_price - avgPrice) / avgPrice * 100 + + print(f"Mid Price: {mid_price}, Spread: {spread}, PnL: {pnl}") + + # Prepare risk details for tracking + risk_details = { + 'time': str(pd.Timestamp.utcnow().tz_localize(None)), + 'question': row['question'] + } + + try: + ratio = (n_deets['bid_sum_within_n_percent']) / (n_deets['ask_sum_within_n_percent']) + except: + ratio = 0 + + pos_to_sell = sell_amount # Amount to sell in risk-off scenario + + # ------- STOP-LOSS LOGIC ------- + # Trigger stop-loss if either: + # 1. PnL is below threshold and spread is tight enough to exit + # 2. Volatility is too high + if (pnl < params['stop_loss_threshold'] and spread <= params['spread_threshold']) or row['3_hour'] > params['volatility_threshold']: + risk_details['msg'] = (f"Selling {pos_to_sell} because spread is {spread} and pnl is {pnl} " + f"and ratio is {ratio} and 3 hour volatility is {row['3_hour']}") + print("Stop loss Triggered: ", risk_details['msg']) + + # Sell at market best bid to ensure execution + order['size'] = pos_to_sell + order['price'] = n_deets['best_bid'] + + # Set period to avoid trading after stop-loss + risk_details['sleep_till'] = str(pd.Timestamp.utcnow().tz_localize(None) + + pd.Timedelta(hours=params['sleep_period'])) + + print("Risking off") + send_sell_order(order) + client.cancel_all_market(market) + + # Save risk details to file + open(fname, 'w').write(json.dumps(risk_details)) + continue + + # ------- BUY ORDER LOGIC ------- + # Only buy if: + # 1. Position is less than 90% of target size + # 2. Position is less than absolute cap (250) + # 3. Buy amount is above minimum size + if position < 0.9 * row['trade_size'] and position < 250 and buy_amount > 0 and buy_amount >= row['min_size']: + # Get reference price from market data + sheet_value = row['best_bid'] + + if detail['name'] == 'token2': + sheet_value = 1 - row['best_ask'] + + sheet_value = round(sheet_value, round_length) + order['size'] = buy_amount + order['price'] = bid_price + + # Check if price is far from reference + price_change = abs(order['price'] - sheet_value) + + send_buy = True + + # ------- RISK-OFF PERIOD CHECK ------- + # If we're in a risk-off period (after stop-loss), don't buy + if os.path.isfile(fname): + risk_details = json.load(open(fname)) + + start_trading_at = pd.to_datetime(risk_details['sleep_till']) + current_time = pd.Timestamp.utcnow().tz_localize(None) + + print(risk_details, current_time, start_trading_at) + if current_time < start_trading_at: + send_buy = False + print(f"Not sending a buy order because recently risked off. " + f"Risked off at {risk_details['time']}") + + # Only proceed if we're not in risk-off period + if send_buy: + # Don't buy if volatility is high or price is far from reference + if row['3_hour'] > params['volatility_threshold'] or price_change >= 0.05: + print(f'3 Hour Volatility of {row["3_hour"]} is greater than max volatility of ' + f'{params["volatility_threshold"]} or price of {order["price"]} is outside ' + f'0.05 of {sheet_value}. Cancelling all orders') + client.cancel_all_asset(order['token']) + else: + # Check for reverse position (holding opposite outcome) + rev_token = global_state.REVERSE_TOKENS[str(token)] + rev_pos = get_position(rev_token) + + # If we have significant opposing position, don't buy more + if rev_pos['size'] > row['min_size']: + print("Bypassing creation of new buy order because there is a reverse position") + if orders['buy']['size'] > CONSTANTS.MIN_MERGE_SIZE: + print("Cancelling buy orders because there is a reverse position") + client.cancel_all_asset(order['token']) + + continue + + # Check market buy/sell volume ratio + if overall_ratio < 0: + send_buy = False + print(f"Not sending a buy order because overall ratio is {overall_ratio}") + client.cancel_all_asset(order['token']) + else: + # Place new buy order if any of these conditions are met: + # 1. We can get a better price than current order + if best_bid > orders['buy']['price']: + print(f"Sending Buy Order for {token} because better price. " + f"Orders look like this: {orders['buy']}. Best Bid: {best_bid}") + send_buy_order(order) + # 2. Current position + orders is not enough to reach target + elif position + orders['buy']['size'] < 0.95 * row['trade_size']: + print(f"Sending Buy Order for {token} because not enough position + size") + send_buy_order(order) + # 3. Our current order is too large and needs to be resized + elif orders['buy']['size'] > order['size'] * 1.01: + print(f"Resending buy orders because open orders are too large") + send_buy_order(order) + # Commented out logic for cancelling orders when market conditions change + # elif best_bid_size < orders['buy']['size'] * 0.98 and abs(best_bid - second_best_bid) > 0.03: + # print(f"Cancelling buy orders because best size is less than 90% of open orders and spread is too large") + # global_state.client.cancel_all_asset(order['token']) + + # ------- TAKE PROFIT / SELL ORDER MANAGEMENT ------- + elif sell_amount > 0: + order['size'] = sell_amount + + # Calculate take-profit price based on average cost + tp_price = round_up(avgPrice + (avgPrice * params['take_profit_threshold']/100), round_length) + order['price'] = round_up(tp_price if ask_price < tp_price else ask_price, round_length) + + tp_price = float(tp_price) + order_price = float(orders['sell']['price']) + + # Calculate % difference between current order and ideal price + diff = abs(order_price - tp_price)/tp_price * 100 + + # Update sell order if: + # 1. Current order price is significantly different from target + if diff > 2: + print(f"Sending Sell Order for {token} because better current order price of " + f"{order_price} is deviant from the tp_price of {tp_price} and diff is {diff}") + send_sell_order(order) + # 2. Current order size is too small for our position + elif orders['sell']['size'] < position * 0.97: + print(f"Sending Sell Order for {token} because not enough sell size. " + f"Position: {position}, Sell Size: {orders['sell']['size']}") + send_sell_order(order) + + # Commented out additional conditions for updating sell orders + # elif orders['sell']['price'] < ask_price: + # print(f"Updating Sell Order for {token} because its not at the right price") + # send_sell_order(order) + # elif best_ask_size < orders['sell']['size'] * 0.98 and abs(best_ask - second_best_ask) > 0.03...: + # print(f"Cancelling sell orders because best size is less than 90% of open orders...") + # send_sell_order(order) + + except Exception as ex: + print(f"Error performing trade for {market}") + traceback.print_exc() + + # Clean up memory and introduce a small delay + gc.collect() + await asyncio.sleep(2) \ No newline at end of file diff --git a/update_stats.py b/update_stats.py new file mode 100644 index 0000000..92c8a05 --- /dev/null +++ b/update_stats.py @@ -0,0 +1,18 @@ +from poly_data.polymarket_client import PolymarketClient +from poly_stats.account_stats import update_stats_once + +import pandas as pd +import time +import traceback + +client = PolymarketClient() + +if __name__ == '__main__': + while True: + try: + update_stats_once(client) + except Exception as e: + traceback.print_exc() + + print("Now sleeping\n") + time.sleep(60 * 60 * 3) #3 hours \ No newline at end of file