├── requirements.txt ├── module └── helpers.py ├── configs.py ├── readme.md ├── sample.py ├── .gitignore └── oneinch.py /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp>=3.7.4 2 | requests==2.26.0 3 | urllib3==1.26.7 4 | web3==5.23.1 5 | -------------------------------------------------------------------------------- /module/helpers.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from urllib3.util.retry import Retry 3 | from requests.adapters import HTTPAdapter 4 | 5 | def get_with_retry(url, headers=None): 6 | """ 7 | WARNING: 1inch ALSO returns 500 if your args are wrong 8 | """ 9 | 10 | retries = Retry(total=5, 11 | backoff_factor=0.3, 12 | status_forcelist=[i for i in range(500, 600)]) 13 | s = requests.Session() 14 | s.mount('https://', HTTPAdapter(max_retries=retries)) 15 | r = s.get(url, headers=headers) 16 | 17 | return r 18 | -------------------------------------------------------------------------------- /configs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Please ensure that RPCs are correct 3 | """ 4 | 5 | class Network: 6 | RPC = { 7 | # '1': '', 8 | # '3': '', 9 | '10': 'https://mainnet.optimism.io', 10 | '56': 'https://bsc-dataseed.binance.org', 11 | '137': 'https://polygon-rpc.com', 12 | # '42161': '', # https://medium.com/stakingbits/guide-to-arbitrum-and-setting-up-metamask-for-arbitrum-543e513cdd8b 13 | # '43114': '', # https://support.avax.network/en/articles/4626956-how-do-i-set-up-metamask-on-avalanche 14 | } 15 | 16 | EXPLORER = { 17 | # '1': 18 | # '3': 19 | '10': 'https://optimistic.etherscan.io', 20 | '56': 'https://bscscan.com', 21 | '137': 'https://polygonscan.com', 22 | # '42161': 23 | # '43114': 24 | } 25 | 26 | def __init__(self): 27 | pass 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | I find this kind of resource quite limited, so I decided to release parts of the code I used to do limit order with 1inch.io 2 | I hope this is useful to you. It support all networks. Currently in configs.py there are: 3 | 4 | Optimism '10': 'https://mainnet.optimism.io', 5 | Binance Smart Chain '56': 'https://bsc-dataseed.binance.org', 6 | Polygon Matic '137': 'https://polygon-rpc.com', 7 | 8 | For the first swap, you will need to approve spending first. Do that using https://app.1inch.io 's UI 9 | WARNING: NEVER use your main account for automatic trade 10 | 11 | More info and tutorials TBA 12 | 13 | If you find this useful send coffee beans here! 14 | 0x82A7C4C451EE04b93Bb36de492B171909003Fc13 15 | 16 | This piece of code comes with no warranty. What you do with it is ultimately your own decision. (auto-swap limit-swap *wink* *wink*) 17 | Goodluck! 18 | -------------------------------------------------------------------------------- /sample.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | import oneinch as inch 3 | import configs 4 | from pprint import pprint 5 | 6 | PRIVATE_KEY = 'YOUR PRIVATE KEY HERE' # Don't be lazy. Store your private key in a .env in a flash drive, and load it with python-dotenv 7 | MY_ADDRESS = 'YOUR ADDRESS HERE' 8 | RPC = configs.Network.RPC 9 | EXPLORER = configs.Network.EXPLORER 10 | 11 | swap_profile = { # This will swap 1 BSUD to 1 DAI 12 | 'chain_no': '56', 13 | 'fromTokenAddress': '0xe9e7cea3dedca5984780bafc599bd69add087d56', # BUSD 14 | 'toTokenAddress': '0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3', # DAI 15 | 'decimals': 18, # Not actually needed at the moment 1inch does give you decimals 16 | 17 | 'quote_amount': int(1*10**18), 18 | 'swap_amount': int(1*10**18), 19 | 'fromAddress': MY_ADDRESS, 20 | 'slippage': '1.0', # Percent 21 | 22 | 'opt_params_quote': {}, 23 | 'opt_params_swap': { 24 | 'referrerAddress': '0x82A7C4C451EE04b93Bb36de492B171909003Fc13', #! Keep these lines if you want to buy me a cup of coffee 25 | 'fee': '0.01', #! If you find this useful 0.01% will be sent to my address 26 | }, 27 | } 28 | 29 | busd_to_dai = inch.OneInch(**swap_profile) 30 | quote = busd_to_dai.get_quote() 31 | pprint(quote) 32 | 33 | swap = busd_to_dai.get_swap() 34 | pprint(swap) 35 | 36 | tx = swap['tx'] 37 | tx = parse_inch_swap_data(tx) 38 | swap_rpc = RPC[busd_to_dai.chain_no] 39 | 40 | print(swap_rpc) 41 | pprint(tx) 42 | 43 | input(f'Will execute swap now. Double check everything then,\nENTER to continue. CTRL-C to CANCEL.\n>') 44 | 45 | tx_hash = send_tx(swap_rpc, PRIVATE_KEY, tx) 46 | tx_stats = check_tx(swap_rpc, tx_hash) 47 | 48 | if tx_stats: 49 | print('SUCCESS!') 50 | print(f'{EXPLORER[busd_to_dai.chain_no]}/tx/{tx_hash}') 51 | else: 52 | print('FAILED!') 53 | print(f'{EXPLORER[busd_to_dai.chain_no]}/tx/{tx_hash}') 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,python 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Python ### 35 | # Byte-compiled / optimized / DLL files 36 | __pycache__/ 37 | *.py[cod] 38 | *$py.class 39 | 40 | # C extensions 41 | *.so 42 | 43 | # Distribution / packaging 44 | .Python 45 | build/ 46 | develop-eggs/ 47 | dist/ 48 | downloads/ 49 | eggs/ 50 | .eggs/ 51 | lib/ 52 | lib64/ 53 | parts/ 54 | sdist/ 55 | var/ 56 | wheels/ 57 | share/python-wheels/ 58 | *.egg-info/ 59 | .installed.cfg 60 | *.egg 61 | MANIFEST 62 | 63 | # PyInstaller 64 | # Usually these files are written by a python script from a template 65 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 66 | *.manifest 67 | *.spec 68 | 69 | # Installer logs 70 | pip-log.txt 71 | pip-delete-this-directory.txt 72 | 73 | # Unit test / coverage reports 74 | htmlcov/ 75 | .tox/ 76 | .nox/ 77 | .coverage 78 | .coverage.* 79 | .cache 80 | nosetests.xml 81 | coverage.xml 82 | *.cover 83 | *.py,cover 84 | .hypothesis/ 85 | .pytest_cache/ 86 | cover/ 87 | 88 | # Translations 89 | *.mo 90 | *.pot 91 | 92 | # Django stuff: 93 | *.log 94 | local_settings.py 95 | db.sqlite3 96 | db.sqlite3-journal 97 | 98 | # Flask stuff: 99 | instance/ 100 | .webassets-cache 101 | 102 | # Scrapy stuff: 103 | .scrapy 104 | 105 | # Sphinx documentation 106 | docs/_build/ 107 | 108 | # PyBuilder 109 | .pybuilder/ 110 | target/ 111 | 112 | # Jupyter Notebook 113 | .ipynb_checkpoints 114 | 115 | # IPython 116 | profile_default/ 117 | ipython_config.py 118 | 119 | # pyenv 120 | # For a library or package, you might want to ignore these files since the code is 121 | # intended to run in multiple environments; otherwise, check them in: 122 | # .python-version 123 | 124 | # pipenv 125 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 126 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 127 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 128 | # install all needed dependencies. 129 | #Pipfile.lock 130 | 131 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 132 | __pypackages__/ 133 | 134 | # Celery stuff 135 | celerybeat-schedule 136 | celerybeat.pid 137 | 138 | # SageMath parsed files 139 | *.sage.py 140 | 141 | # Environments 142 | .env 143 | .venv 144 | env/ 145 | venv/ 146 | ENV/ 147 | env.bak/ 148 | venv.bak/ 149 | 150 | # Spyder project settings 151 | .spyderproject 152 | .spyproject 153 | 154 | # Rope project settings 155 | .ropeproject 156 | 157 | # mkdocs documentation 158 | /site 159 | 160 | # mypy 161 | .mypy_cache/ 162 | .dmypy.json 163 | dmypy.json 164 | 165 | # Pyre type checker 166 | .pyre/ 167 | 168 | # pytype static type analyzer 169 | .pytype/ 170 | 171 | # Cython debug symbols 172 | cython_debug/ 173 | 174 | # End of https://www.toptal.com/developers/gitignore/api/macos,python 175 | -------------------------------------------------------------------------------- /oneinch.py: -------------------------------------------------------------------------------- 1 | from module.helpers import ( 2 | get_with_retry 3 | ) 4 | # import ssl 5 | # import nest_asyncio 6 | # nest_asyncio.apply() 7 | import aiohttp 8 | import asyncio 9 | import urllib 10 | import requests 11 | from web3 import Web3 12 | 13 | class OneInch: 14 | """ 15 | Optional params see: https://docs.1inch.io/api/quote-swap 16 | """ 17 | 18 | BASE_URL = 'https://api.1inch.exchange/v3.0' 19 | 20 | def __init__( 21 | self, 22 | chain_no, 23 | fromTokenAddress, 24 | toTokenAddress, 25 | decimals, 26 | quote_amount, 27 | swap_amount, 28 | fromAddress=None, 29 | slippage=None, 30 | opt_params_quote={}, 31 | opt_params_swap={} 32 | ): 33 | 34 | # Input 35 | self.chain_no = chain_no 36 | self.fromTokenAddress = fromTokenAddress 37 | self.toTokenAddress = toTokenAddress 38 | self.decimals = decimals 39 | self.quote_amount = quote_amount 40 | self.swap_amount = swap_amount 41 | self.fromAddress = fromAddress 42 | self.slippage = slippage 43 | self.opt_params_quote = opt_params_quote 44 | self.opt_params_swap = opt_params_swap 45 | 46 | # Generated 47 | self.quote_raw = None 48 | self.swap_raw = None 49 | 50 | def url_factory(self, chain_no, endpoint, **kwparams) -> str: 51 | params = urllib.parse.urlencode(kwparams) 52 | return f'{self.BASE_URL}/{chain_no}/{endpoint}?{params}' 53 | 54 | @classmethod 55 | def healthcheck(cls, chain_no) -> bool: 56 | url = cls.url_factory(cls, chain_no, 'healthcheck') 57 | r = get_with_retry(url) 58 | if r.json()['status'] == 'OK': 59 | return True 60 | else: 61 | return False 62 | 63 | def get_quote_url(self) -> str: 64 | url = self.url_factory( 65 | self.chain_no, 66 | 'quote', 67 | fromTokenAddress = self.fromTokenAddress, 68 | toTokenAddress = self.toTokenAddress, 69 | amount = self.quote_amount, 70 | **self.opt_params_quote, 71 | ) 72 | 73 | return url 74 | 75 | def get_quote(self) -> dict: 76 | url = self.get_quote_url() 77 | r = get_with_retry(url) 78 | self.quote_raw = r.json() 79 | 80 | return self.quote_raw 81 | 82 | async def async_get_quote(self) -> dict: 83 | """ 84 | Run this function with 85 | lst_get_quotes = [obj1.async_get_quote, obj2.async_get_quote, ...] 86 | loop = asyncio.get_event_loop() 87 | res = loop.run_until_complete(asyncio.gather(*lst_get_quotes)) 88 | """ 89 | url = self.get_quote_url() 90 | async with aiohttp.ClientSession() as session: 91 | async with session.get(url) as response: 92 | self.quote_raw = await response.json() 93 | 94 | return self.quote_raw 95 | 96 | def get_swap(self) -> dict: 97 | url = self.url_factory( 98 | self.chain_no, 99 | 'swap', 100 | fromTokenAddress = self.fromTokenAddress, 101 | toTokenAddress = self.toTokenAddress, 102 | amount = self.swap_amount, 103 | fromAddress = self.fromAddress, 104 | slippage = self.slippage, 105 | **self.opt_params_swap, 106 | ) 107 | # r = requests.get(url) 108 | r = get_with_retry(url) 109 | self.swap_raw = r.json() 110 | 111 | return self.swap_raw 112 | 113 | def __repr__(self): 114 | return f'{self.fromTokenAddress}>{self.toTokenAddress}' 115 | 116 | def __str__(self): 117 | return self.__repr__() 118 | 119 | 120 | def check_oneinch_health(lst_chain_nos) -> bool: 121 | health = [] 122 | print('Checking OneInch API health...') 123 | for chain_no in lst_chain_nos: 124 | is_healthy = OneInch.healthcheck(chain_no) 125 | if is_healthy: 126 | print(f'Chain: {chain_no} is healthy.') 127 | if not is_healthy: 128 | print(f'Chain: {chain_no} is BAD.') 129 | health.append(is_healthy) 130 | if all(health): 131 | # print(f'Chain: {lst_chain_nos} are healthy.') 132 | return True 133 | if not all(health): 134 | # print(f'At least one of the chains is/are BAD.') 135 | return False 136 | 137 | 138 | def parse_inch_swap_data(tx): 139 | tx['from'] = web3.toChecksumAddress(tx['from']) 140 | tx['to'] = web3.toChecksumAddress(tx['to']) 141 | tx['value'] = int(tx['value']) 142 | tx['gas'] = int(tx['gas']) 143 | tx['gasPrice'] = int(tx['gasPrice']) 144 | 145 | return tx 146 | 147 | 148 | def send_tx(RPC, PKEY, tx): 149 | web3 = Web3(Web3.HTTPProvider(RPC)) 150 | 151 | if not web3.isConnected(): 152 | raise ConnectionError(f'Couldn\'t connect to {RPC}') 153 | 154 | nonce = web3.eth.getTransactionCount(tx['from']) 155 | tx['nonce'] = nonce 156 | signed_tx = web3.eth.account.sign_transaction(tx, private_key=PKEY) 157 | tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction) 158 | 159 | return tx_hash 160 | 161 | 162 | def check_tx(RPC, tx_hash): 163 | web3 = Web3(Web3.HTTPProvider(RPC)) 164 | max_tries = 20 165 | tx_status = -1 166 | 167 | last_ts = time.time() 168 | i = 0 169 | while True: 170 | 171 | if i >= max_tries: 172 | return False 173 | 174 | if last_ts + 3 > time.time(): 175 | sleep(0.01) 176 | continue 177 | 178 | try: 179 | tx_status = web3.eth.getTransactionReceipt(tx_hash).status 180 | if tx_status: 181 | # log('Success!') 182 | return True 183 | except Exception as e: 184 | log('Waiting for tx status...') 185 | i += 1 186 | last_ts = time.time() 187 | continue 188 | 189 | i += 1 190 | last_ts = time.time() 191 | --------------------------------------------------------------------------------