Files
backend/src/dexorder/bin/mirror.py
2025-02-27 17:51:07 -04:00

220 lines
7.9 KiB
Python

"""
python -m dexorder.bin.mirror creates new pools on the rpc_url chain that mimic the tokens and prices of original
pools on a different mirror_source_rpc_url chain.
[config]
rpc_url=<destination for mock pools and tokens>
mirror_source_rpc_url = <source for original pools and tokens to copy>
mirror_pools = <required list of pool addresses to copy from mirror_source_rpc_url>
mirror_env = [address on rpc_url chain where MirrorEnv contract is deployed. If None, then discovered from deployment]
metadata = [if set, then a metadata.json file for the mock chain is written to this location]
polling = [seconds between price updates. If zero or negative, prices are updated once and the process exits]
"""
import asyncio
import logging
import os
from datetime import timedelta
from dexorder import config, blockchain, current_w3, now, ADDRESS_0
from dexorder.bin.executable import execute
from dexorder.blockchain.connection import create_w3
from dexorder.contract import get_deployment_address, ContractProxy, ERC20
from dexorder.metadata import generate_metadata, init_generating_metadata
from dexorder.pools import get_pool
from dexorder.tokens import get_token
from dexorder.uniswap import UniswapV3Pool
from dexorder.util.async_util import maywait
log = logging.getLogger('dexorder')
_token_infos = {}
source_w3 = None
def use_source(func):
async def wrapper(*args, **kwargs):
old_w3 = current_w3.get()
current_w3.set(source_w3)
result = await func(*args, **kwargs)
current_w3.set(old_w3)
return result
return wrapper
@use_source
async def get_token_info( token ):
# struct TokenInfo {
# IERC20Metadata addr;
# string name;
# string symbol;
# uint8 decimals;
# }
token = await maywait(token)
if token not in _token_infos:
t = ERC20(token)
name, symbol, decimals = await asyncio.gather(t.name(), t.symbol(), t.decimals())
log.debug(f'original token: {name} {symbol} .{decimals} {token}')
_token_infos[token] = [token, name, symbol, decimals]
return _token_infos[token]
@use_source
async def get_pool_price(pool):
slot0 = await UniswapV3Pool(pool).slot0()
return slot0[0]
@use_source
async def get_pool_info( pool ):
# struct PoolInfo {
# IUniswapV3Pool pool;
# TokenInfo token0;
# TokenInfo token1;
# uint24 fee;
# uint160 sqrtPriceX96;
# uint256 amount0;
# uint256 amount1;
# }
p = UniswapV3Pool(pool)
t0, t1 = await asyncio.gather(p.token0(), p.token1())
amount0, amount1, (price,*_), fee = \
await asyncio.gather(ERC20(t0).balanceOf(pool), ERC20(t1).balanceOf(pool), p.slot0(), p.fee())
token0 = await get_token(t0)
token1 = await get_token(t1)
log.debug(f'pool has {amount0/10**token0["decimals"]} {token0["name"]} and {amount1/10**token1["decimals"]} {token1["name"]}')
return [pool, t0, t1, fee, price, amount0, amount1]
async def write_metadata( pools, mirror_pools ):
filename = config.metadata
if filename is None:
return
pool_dicts = [get_pool(addr) for (addr,_inverted) in mirror_pools]
pool_dicts = await asyncio.gather(*pool_dicts)
for data, addr, (_,inverted) in zip(pool_dicts, pools, mirror_pools):
data['x'] = dict(data=dict(uri=f'https://app.dexorder.trade/ohlc/', chain=42161, symbol=addr, inverted=inverted))
tokens = set(p['base'] for p in pool_dicts)
tokens.update(p['quote'] for p in pool_dicts)
tokens = await asyncio.gather(*[get_token(t) for t in tokens])
for token in tokens:
token['x'] = {'mock':True}
with open(filename, 'w') as f:
generate_metadata(tokens, pool_dicts, file=f, include_unapproved=True)
log.info(f'wrote {filename}')
last_prices = {}
async def main():
init_generating_metadata()
if config.metadata is None:
log.error('Must configure metadata (filename to write)')
return
if config.mirror_source_rpc_url is None:
log.error('Must configure mirror_source_rpc_url')
return
delay = max(0.010, config.polling)
update_once = config.polling <= 0
global source_w3
source_w3 = await create_w3(config.mirror_source_rpc_url, name='source', autosign=False, archive_url=[])
pools = (config.mirror_pools or [])
if not pools:
log.error('must configure mirror_pools')
return
if not config.accounts:
# Dev Account #6 0x976EA74026E726554dB657fA54763abd0C3a0aa9
config.accounts = ['0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e']
await blockchain.connect(name='target')
mirror_addr = config.mirror_env
if mirror_addr is None:
mirror_addr = os.environ.get('MIRRORENV')
if mirror_addr is None:
mirror_addr = get_deployment_address('DeployMirror', 'MirrorEnv')
if mirror_addr is None:
log.error('must configure mirror_env or set envioronment MIRRORENV')
return
log.debug(f'Using MirrorEnv at {mirror_addr}')
mirrorenv = ContractProxy(mirror_addr, 'MirrorEnv')
pool_infos = [await get_pool_info(pool) for pool in pools]
tokens = set(i[1] for i in pool_infos).union(i[2] for i in pool_infos)
log.debug(f'Mirroring tokens')
for t in tokens:
# noinspection PyBroadException
try:
info = await get_token_info(t)
# anvil had trouble estimating the gas, so we hardcode it.
tx = await mirrorenv.transact.mirrorToken(info, gas=1_000_000)
await tx.wait()
except Exception:
log.exception(f'Failed to mirror token {t}')
exit(1)
log.info(f'Tokens deployed')
log.debug(f'Mirroring pools {", ".join(pools)}')
for pool, info in zip(pools, pool_infos):
# noinspection PyBroadException
try:
# anvil had trouble estimating the gas, so we hardcode it.
tx = await mirrorenv.transact.mirrorPool(info, gas=5_500_000)
await tx.wait()
except Exception:
log.exception(f'Failed to mirror pool {pool}')
exit(1)
log.info('Pools deployed')
mirror_pool_list = []
# log.debug(f'Getting pool info {" ".join(pools)}')
for pool in pools:
mirror_addr, mirror_inverted = await mirrorenv.pools(pool)
if mirror_addr == ADDRESS_0:
raise ValueError(f'Pool {pool} was not successfully mirrored')
log.debug(f'\t{pool} => {mirror_addr} inverted={mirror_inverted}')
mirror_pool_list.append((mirror_addr, mirror_inverted))
await write_metadata(pools, mirror_pool_list)
mirror_pools = dict(zip(pools, mirror_pool_list))
if update_once:
log.info(f'Updating pools once')
else:
log.info(f'Updating a pool every {delay} seconds')
delay = timedelta(seconds=delay)
to_update = pools
pool_iter = iter(to_update)
pool = next(pool_iter)
while True:
wake_up = now() + delay
# log.debug(f'querying {pool}')
try:
price = await get_pool_price(pool)
if price != last_prices.get(pool):
# anvil had trouble estimating the gas, so we hardcode it.
tx = await mirrorenv.transact.updatePool(pool, price, gas=1_000_000) # this is a B.S. gas number
await tx.wait()
last_prices[pool] = price
addr, inverted = mirror_pools[pool]
log.debug(f'Mirrored {addr} {price}')
except Exception as x:
log.debug(f'Could not update {pool}: {x}')
continue
try:
pool = next(pool_iter)
except StopIteration:
if update_once:
log.info('mirror completed')
break
pool_iter = iter(to_update)
pool = next(pool_iter)
sleep = (wake_up - now()).total_seconds()
if sleep > 0:
await asyncio.sleep(sleep)
if __name__ == '__main__':
execute(main)