backend metadata; approved tokens; logging.toml;
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@ venv
|
|||||||
*secret*
|
*secret*
|
||||||
!/.secret-mock.toml
|
!/.secret-mock.toml
|
||||||
dexorder.toml
|
dexorder.toml
|
||||||
|
logging.toml
|
||||||
/contract
|
/contract
|
||||||
__pycache__
|
__pycache__
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ def upgrade() -> None:
|
|||||||
sa.Column('name', sa.String(), nullable=False),
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
sa.Column('symbol', sa.String(), nullable=False),
|
sa.Column('symbol', sa.String(), nullable=False),
|
||||||
sa.Column('decimals', sa.SMALLINT(), nullable=False),
|
sa.Column('decimals', sa.SMALLINT(), nullable=False),
|
||||||
|
sa.Column('approved', sa.Boolean(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('chain', 'address')
|
sa.PrimaryKeyConstraint('chain', 'address')
|
||||||
)
|
)
|
||||||
op.create_table('pool',
|
op.create_table('pool',
|
||||||
|
|||||||
27
logging-default.toml
Normal file
27
logging-default.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
|
||||||
|
version=1
|
||||||
|
|
||||||
|
[loggers.'']
|
||||||
|
level='INFO'
|
||||||
|
handlers=['console',]
|
||||||
|
|
||||||
|
[loggers.dexorder]
|
||||||
|
level='DEBUG'
|
||||||
|
|
||||||
|
[handlers.console]
|
||||||
|
class='logging.StreamHandler'
|
||||||
|
formatter='default'
|
||||||
|
level='INFO'
|
||||||
|
stream='ext://sys.stdout'
|
||||||
|
|
||||||
|
[formatters.default]
|
||||||
|
# https://docs.python.org/3/library/logging.html#logrecord-attributes
|
||||||
|
format='%(asctime)s %(name)s %(message)s'
|
||||||
|
# https://docs.python.org/3/library/time.html#time.strftime
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
|
||||||
|
[formatters.notime]
|
||||||
|
# https://docs.python.org/3/library/logging.html#logrecord-attributes
|
||||||
|
format='%(name)s %(message)s'
|
||||||
|
# https://docs.python.org/3/library/time.html#time.strftime
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
92
metadata-finaldata.json
Normal file
92
metadata-finaldata.json
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"42161": {
|
||||||
|
"t": [
|
||||||
|
{
|
||||||
|
"a": "0x2653aB60a6fD19d3e1f4E729412C219257CA6e24",
|
||||||
|
"n": "USD Coin (Arbitrum Native)",
|
||||||
|
"s": "USDC",
|
||||||
|
"d": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x87b60276434d5cB5CE46d2316B596c7Bc5f87cCA",
|
||||||
|
"n": "Wrapped Ether",
|
||||||
|
"s": "WETH",
|
||||||
|
"d": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xEaD55ce1cC577C8020F5FcE39D4e5C643F591a0c",
|
||||||
|
"n": "USD Coin (Ethereum Bridged)",
|
||||||
|
"s": "USDC.e",
|
||||||
|
"d": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f",
|
||||||
|
"s": "WBTC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x912CE59144191C1204E64559FE8253a0e49E6548",
|
||||||
|
"s": "ARB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x9623063377AD1B27544C965cCd7342f7EA7e88C7",
|
||||||
|
"s": "GRT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
|
||||||
|
"s": "USDT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
|
||||||
|
"s": "DAI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4",
|
||||||
|
"s": "LINK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0",
|
||||||
|
"s": "UNI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xba5DdD1f9d7F570dc94a51479a000E3BCE967196",
|
||||||
|
"s": "AAVE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x371c7ec6D8039ff7933a2AA28EB827Ffe1F52f07",
|
||||||
|
"s": "JOE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a",
|
||||||
|
"s": "GMX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978",
|
||||||
|
"s": "CRV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x13Ad51ed4F1B7e9Dc168d8a00cB3f4dDD85EfA60",
|
||||||
|
"s": "LDO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xba0Dda8762C24dA9487f5FA026a9B64b695A07Ea",
|
||||||
|
"s": "OX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x561877b6b3DD7651313794e5F2894B2F18bE0766",
|
||||||
|
"s": "MATIC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x5979D7b546E38E414F7E9822514be443A4800529",
|
||||||
|
"s": "wstETH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x539bdE0d7Dbd336b79148AA742883198BBF60342",
|
||||||
|
"s": "MAGIC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x0c880f6761F1af8d9Aa9C466984b80DAb9a8c9e8",
|
||||||
|
"s": "PENDLE"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import tomllib
|
||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
from traceback import print_exception
|
from traceback import print_exception
|
||||||
import asyncio
|
import asyncio
|
||||||
from signal import Signals
|
from signal import Signals
|
||||||
from typing import Coroutine
|
from typing import Coroutine
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
from dexorder import configuration
|
from dexorder import configuration
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@@ -30,7 +34,22 @@ async def _shutdown_coro(_sig, loop, extra_shutdown):
|
|||||||
print_exception(x)
|
print_exception(x)
|
||||||
|
|
||||||
|
|
||||||
def execute(main:Coroutine, shutdown=None, parse_args=True):
|
def execute(main:Coroutine, shutdown=None, *, parse_logging=True, parse_args=True):
|
||||||
|
configured = False
|
||||||
|
if parse_logging:
|
||||||
|
try:
|
||||||
|
with open('logging.toml', 'rb') as file:
|
||||||
|
dictconf = tomllib.load(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
logging.config.dictConfig(dictconf)
|
||||||
|
log.info('Logging configured from logging.toml')
|
||||||
|
configured = True
|
||||||
|
if not configured:
|
||||||
|
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
log.info('Logging configured to default')
|
||||||
if parse_args:
|
if parse_args:
|
||||||
configuration.parse_args()
|
configuration.parse_args()
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from dexorder.blocktime import get_block_timestamp
|
|||||||
from dexorder.configuration import parse_args
|
from dexorder.configuration import parse_args
|
||||||
from dexorder.contract import get_contract_event
|
from dexorder.contract import get_contract_event
|
||||||
from dexorder.database.model.block import current_block, latest_block
|
from dexorder.database.model.block import current_block, latest_block
|
||||||
from dexorder.ohlc import LightOHLCRepository
|
from dexorder.ohlc import FinalOHLCRepository
|
||||||
from dexorder.pools import get_uniswap_data
|
from dexorder.pools import get_uniswap_data
|
||||||
from dexorder.util import hexstr
|
from dexorder.util import hexstr
|
||||||
from dexorder.util.shutdown import fatal
|
from dexorder.util.shutdown import fatal
|
||||||
@@ -22,7 +22,7 @@ from dexorder.walker import BlockWalker
|
|||||||
log = logging.getLogger('dexorder')
|
log = logging.getLogger('dexorder')
|
||||||
|
|
||||||
|
|
||||||
ohlcs = LightOHLCRepository()
|
ohlcs = FinalOHLCRepository()
|
||||||
|
|
||||||
|
|
||||||
async def handle_backfill_uniswap_swaps(swaps: list[EventData]):
|
async def handle_backfill_uniswap_swaps(swaps: list[EventData]):
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ def setup_logevent_triggers(runner):
|
|||||||
|
|
||||||
# noinspection DuplicatedCode
|
# noinspection DuplicatedCode
|
||||||
async def main():
|
async def main():
|
||||||
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
|
||||||
log.setLevel(logging.DEBUG)
|
|
||||||
parse_args()
|
|
||||||
await blockchain.connect()
|
await blockchain.connect()
|
||||||
redis_state = None
|
redis_state = None
|
||||||
state = None
|
state = None
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from dexorder import config, blockchain, current_w3
|
from dexorder import config, blockchain, current_w3
|
||||||
from dexorder.bin.executable import execute
|
from dexorder.bin.executable import execute
|
||||||
@@ -9,7 +8,7 @@ from dexorder.blockchain.connection import create_w3
|
|||||||
from dexorder.blockstate import current_blockstate
|
from dexorder.blockstate import current_blockstate
|
||||||
from dexorder.blockstate.state import FinalizedBlockState
|
from dexorder.blockstate.state import FinalizedBlockState
|
||||||
from dexorder.contract import get_deployment_address, ContractProxy, ERC20
|
from dexorder.contract import get_deployment_address, ContractProxy, ERC20
|
||||||
from dexorder.metadata import generate_metadata
|
from dexorder.metadata import generate_metadata, init_generating_metadata
|
||||||
from dexorder.pools import get_pool
|
from dexorder.pools import get_pool
|
||||||
from dexorder.tokens import get_token
|
from dexorder.tokens import get_token
|
||||||
from dexorder.uniswap import UniswapV3Pool
|
from dexorder.uniswap import UniswapV3Pool
|
||||||
@@ -71,7 +70,7 @@ async def get_pool_info( pool ):
|
|||||||
return [pool, t0, t1, fee, price, amount0, amount1]
|
return [pool, t0, t1, fee, price, amount0, amount1]
|
||||||
|
|
||||||
async def write_metadata( pools, mirror_pools ):
|
async def write_metadata( pools, mirror_pools ):
|
||||||
filename = config.mirror_metadata
|
filename = config.metadata
|
||||||
if filename is None:
|
if filename is None:
|
||||||
return
|
return
|
||||||
pool_dicts = [get_pool(addr) for (addr,_inverted) in mirror_pools]
|
pool_dicts = [get_pool(addr) for (addr,_inverted) in mirror_pools]
|
||||||
@@ -92,6 +91,10 @@ async def await_mirror(tx, pool_addr, mirror_addr, mirror_inverted ):
|
|||||||
|
|
||||||
|
|
||||||
async def main():
|
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:
|
if config.mirror_source_rpc_url is None:
|
||||||
log.error('Must configure mirror_source_rpc_url')
|
log.error('Must configure mirror_source_rpc_url')
|
||||||
return
|
return
|
||||||
@@ -139,6 +142,4 @@ async def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
|
||||||
log.setLevel(logging.DEBUG)
|
|
||||||
execute(main())
|
execute(main())
|
||||||
|
|||||||
29
src/dexorder/bin/tokenlist_metadata.py
Normal file
29
src/dexorder/bin/tokenlist_metadata.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Prints a JSON string to stdout containing metadata information for all the known tokens and pools
|
||||||
|
#
|
||||||
|
# see metadata.py
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from dexorder import db
|
||||||
|
from dexorder.configuration import parse_args
|
||||||
|
from dexorder.database.model import Pool, Token
|
||||||
|
from dexorder.metadata import generate_metadata
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
parse_args()
|
||||||
|
db.connect(migrate=False)
|
||||||
|
tokens = db.session.scalars(select(Token))
|
||||||
|
pools = db.session.scalars(select(Pool))
|
||||||
|
generate_metadata(tokens, pools)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -34,7 +34,6 @@ async def create_w3(rpc_url=None, account=NARG, autosign=False):
|
|||||||
# chain_id = self.w3s[0].eth.chain_id
|
# chain_id = self.w3s[0].eth.chain_id
|
||||||
# assert all(w3.eth.chain_id == chain_id for w3 in self.w3s) # all rpc urls must be the same blockchain
|
# assert all(w3.eth.chain_id == chain_id for w3 in self.w3s) # all rpc urls must be the same blockchain
|
||||||
# self.w3iter = itertools.cycle(self.w3s)
|
# self.w3iter = itertools.cycle(self.w3s)
|
||||||
|
|
||||||
url = resolve_rpc_url(rpc_url)
|
url = resolve_rpc_url(rpc_url)
|
||||||
w3 = AsyncWeb3(RetryHTTPProvider(url))
|
w3 = AsyncWeb3(RetryHTTPProvider(url))
|
||||||
# w3.middleware_onion.inject(geth_poa_middleware, layer=0) # todo is this line needed?
|
# w3.middleware_onion.inject(geth_poa_middleware, layer=0) # todo is this line needed?
|
||||||
@@ -114,13 +113,12 @@ def _make_contract(w3_eth):
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_CONCURRENCY = config.concurrent_rpc_connections
|
|
||||||
|
|
||||||
class RetryHTTPProvider (AsyncHTTPProvider):
|
class RetryHTTPProvider (AsyncHTTPProvider):
|
||||||
|
|
||||||
def __init__(self, endpoint_uri: Optional[Union[URI, str]] = None, request_kwargs: Optional[Any] = None) -> None:
|
def __init__(self, endpoint_uri: Optional[Union[URI, str]] = None, request_kwargs: Optional[Any] = None) -> None:
|
||||||
super().__init__(endpoint_uri, request_kwargs)
|
super().__init__(endpoint_uri, request_kwargs)
|
||||||
self.in_flight = asyncio.Semaphore(MAX_CONCURRENCY)
|
self.in_flight = asyncio.Semaphore(config.concurrent_rpc_connections)
|
||||||
self.rate_allowed = asyncio.Event()
|
self.rate_allowed = asyncio.Event()
|
||||||
self.rate_allowed.set()
|
self.rate_allowed.set()
|
||||||
|
|
||||||
|
|||||||
@@ -33,5 +33,5 @@ class Config:
|
|||||||
|
|
||||||
mirror_source_rpc_url: Optional[str] = None # source RPC for original pools
|
mirror_source_rpc_url: Optional[str] = None # source RPC for original pools
|
||||||
mirror_env: Optional[str] = None
|
mirror_env: Optional[str] = None
|
||||||
mirror_pools: Optional[list[str]] = field(default_factory=list)
|
mirror_pools: list[str] = field(default_factory=list)
|
||||||
mirror_metadata: str = field(default='metadata.json')
|
metadata: Optional[str] = field(default='../web/public/metadata.json')
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
abis = {
|
abis = {
|
||||||
|
# ERC20 where symbol() returns a bytes32 instead of a string
|
||||||
|
'ERC20.sb': '''[{"type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"}]'''
|
||||||
# 'WMATIC': '''[{"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"}]''',
|
# 'WMATIC': '''[{"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"}]''',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import eth_account
|
import eth_account
|
||||||
|
from web3.exceptions import BadFunctionCallOutput, Web3Exception
|
||||||
from web3.types import TxReceipt
|
from web3.types import TxReceipt
|
||||||
|
|
||||||
from dexorder import current_w3
|
from dexorder import current_w3
|
||||||
@@ -9,6 +11,7 @@ from dexorder.base.account import current_account
|
|||||||
from dexorder.database.model.block import current_block
|
from dexorder.database.model.block import current_block
|
||||||
from dexorder.util import hexstr
|
from dexorder.util import hexstr
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class ContractTransaction:
|
class ContractTransaction:
|
||||||
def __init__(self, id_bytes: bytes, rawtx: Optional[bytes] = None):
|
def __init__(self, id_bytes: bytes, rawtx: Optional[bytes] = None):
|
||||||
@@ -42,29 +45,37 @@ class DeployTransaction (ContractTransaction):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def call_wrapper(func):
|
def call_wrapper(addr, name, func):
|
||||||
async def f(*args, **kwargs):
|
async def f(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
blockhash = hexstr(current_block.get().hash)
|
blockhash = hexstr(current_block.get().hash)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
blockhash = 'latest'
|
blockhash = 'latest'
|
||||||
|
try:
|
||||||
return await func(*args, **kwargs).call(block_identifier=blockhash)
|
return await func(*args, **kwargs).call(block_identifier=blockhash)
|
||||||
|
except Web3Exception as e:
|
||||||
|
log.error(f"Exception calling {addr}.{name}()")
|
||||||
|
raise e
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
def transact_wrapper(func):
|
def transact_wrapper(addr, name, func):
|
||||||
async def f(*args, **kwargs):
|
async def f(*args, **kwargs):
|
||||||
|
try:
|
||||||
tx_id = await func(*args, **kwargs).transact()
|
tx_id = await func(*args, **kwargs).transact()
|
||||||
|
except Web3Exception as e:
|
||||||
|
log.error(f'Exception transacting {addr}.{name}()')
|
||||||
|
raise e
|
||||||
return ContractTransaction(tx_id)
|
return ContractTransaction(tx_id)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
def build_wrapper(func):
|
def build_wrapper(addr, name, func):
|
||||||
async def f(*args, **kwargs):
|
async def f(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
account = current_account.get()
|
account = current_account.get()
|
||||||
except LookupError:
|
except LookupError:
|
||||||
raise RuntimeError('Cannot invoke a transaction without setting an Account.')
|
raise RuntimeError(f'Cannot invoke transaction {addr}.{name}() without setting an Account.')
|
||||||
tx = await func(*args, **kwargs).build_transaction()
|
tx = await func(*args, **kwargs).build_transaction()
|
||||||
tx['from'] = account.address
|
tx['from'] = account.address
|
||||||
tx['nonce'] = await account.next_nonce()
|
tx['nonce'] = await account.next_nonce()
|
||||||
@@ -128,7 +139,7 @@ class ContractProxy:
|
|||||||
found = self.contract.functions[item]
|
found = self.contract.functions[item]
|
||||||
else:
|
else:
|
||||||
raise AttributeError(item)
|
raise AttributeError(item)
|
||||||
return self._wrapper(found)
|
return self._wrapper(self.address, item, found)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
addr = self.contract.address
|
addr = self.contract.address
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import TypedDict
|
from typing import TypedDict, Optional
|
||||||
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@ class PoolDict (TypedDict):
|
|||||||
quote: str
|
quote: str
|
||||||
fee: int
|
fee: int
|
||||||
decimals: int
|
decimals: int
|
||||||
|
approved: bool # whether this pool has only whitelisted tokens
|
||||||
|
liquidity: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class Pool (Base):
|
class Pool (Base):
|
||||||
@@ -42,7 +44,8 @@ class Pool (Base):
|
|||||||
|
|
||||||
|
|
||||||
def dump(self):
|
def dump(self):
|
||||||
return PoolDict(chain=self.chain.chain_id, address=self.address,
|
return PoolDict(type='Pool',
|
||||||
|
chain=self.chain.chain_id, address=self.address,
|
||||||
exchange=self.exchange.value,
|
exchange=self.exchange.value,
|
||||||
base=self.base.address, quote=self.quote,
|
base=self.base.address, quote=self.quote,
|
||||||
fee=self.fee, decimals=self.decimals)
|
fee=self.fee, decimals=self.decimals)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class TokenDict (TypedDict):
|
|||||||
name: str
|
name: str
|
||||||
symbol: str
|
symbol: str
|
||||||
decimals: int
|
decimals: int
|
||||||
|
approved: bool # whether this token is in the whitelist or not
|
||||||
|
|
||||||
|
|
||||||
# the database object is primarily write-only so we are able to index queries for pools-by-token from the nodejs server
|
# the database object is primarily write-only so we are able to index queries for pools-by-token from the nodejs server
|
||||||
@@ -29,15 +30,17 @@ class Token (Base):
|
|||||||
name: Mapped[str]
|
name: Mapped[str]
|
||||||
symbol: Mapped[str]
|
symbol: Mapped[str]
|
||||||
decimals: Mapped[Uint8]
|
decimals: Mapped[Uint8]
|
||||||
|
approved: Mapped[bool]
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(token_dict: TokenDict) -> 'Token':
|
def load(token_dict: TokenDict) -> 'Token':
|
||||||
return Token(chain=Blockchain.get(token_dict['chain']), address=token_dict['address'],
|
return Token(chain=Blockchain.get(token_dict['chain']), address=token_dict['address'],
|
||||||
name=token_dict['name'], symbol=token_dict['symbol'], decimals=token_dict['decimals'])
|
name=token_dict['name'], symbol=token_dict['symbol'], decimals=token_dict['decimals'],
|
||||||
|
approved=token_dict['approved'])
|
||||||
|
|
||||||
|
|
||||||
def dump(self):
|
def dump(self):
|
||||||
return TokenDict(type='Token', chain=self.chain.chain_id, address=self.address,
|
return TokenDict(type='Token', chain=self.chain.chain_id, address=self.address,
|
||||||
name=self.name, symbol=self.symbol, decimals=self.decimals)
|
name=self.name, symbol=self.symbol, decimals=self.decimals, approved=self.approved)
|
||||||
|
|
||||||
|
|||||||
@@ -21,18 +21,23 @@
|
|||||||
# ]
|
# ]
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import Union, Iterable
|
from typing import Union, Iterable
|
||||||
|
|
||||||
|
from dexorder import config, NARG
|
||||||
|
from dexorder.base.chain import current_chain
|
||||||
from dexorder.database.model import Token, Pool
|
from dexorder.database.model import Token, Pool
|
||||||
from dexorder.database.model.pool import PoolDict
|
from dexorder.database.model.pool import PoolDict
|
||||||
from dexorder.database.model.token import TokenDict
|
from dexorder.database.model.token import TokenDict
|
||||||
from dexorder.util import json
|
from dexorder.util import json
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
token_map: dict[str, Token] = {}
|
token_map: dict[str, Token] = {}
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyShadowingNames
|
||||||
def dump(file, *args):
|
def dump(file, *args):
|
||||||
print(*args, end='', file=file)
|
print(*args, end='', file=file)
|
||||||
|
|
||||||
@@ -96,10 +101,47 @@ def dump_pools(out, pools):
|
|||||||
json_dump(out,**data)
|
json_dump(out,**data)
|
||||||
|
|
||||||
|
|
||||||
|
generating_metadata = False
|
||||||
|
|
||||||
|
def init_generating_metadata():
|
||||||
|
"""
|
||||||
|
Calling this will prevent the metadata whitelist from squelching unknown pools
|
||||||
|
"""
|
||||||
|
global generating_metadata
|
||||||
|
generating_metadata = True
|
||||||
|
|
||||||
|
def is_generating_metadata():
|
||||||
|
return generating_metadata
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyShadowingNames
|
||||||
def generate_metadata(tokens: Iterable[Union[Token, TokenDict]], pools: Iterable[Union[Pool, PoolDict]],
|
def generate_metadata(tokens: Iterable[Union[Token, TokenDict]], pools: Iterable[Union[Pool, PoolDict]],
|
||||||
file=sys.stdout):
|
file=sys.stdout):
|
||||||
dump(file, '{"t":[')
|
dump(file, '{"'+str(current_chain.get().chain_id)+'":{"t":[')
|
||||||
dump_tokens(file, tokens)
|
dump_tokens(file, tokens)
|
||||||
dump(file, '],"p":[')
|
dump(file, '],"p":[')
|
||||||
dump_pools(file, pools)
|
dump_pools(file, pools)
|
||||||
dump(file, ']}')
|
dump(file, ']}}')
|
||||||
|
|
||||||
|
|
||||||
|
metadata = NARG
|
||||||
|
metadata_by_chainaddr = {}
|
||||||
|
|
||||||
|
def get_metadata(addr=None, *, chain_id=None):
|
||||||
|
if chain_id is None:
|
||||||
|
chain_id = current_chain.get().chain_id
|
||||||
|
global metadata
|
||||||
|
if metadata is NARG:
|
||||||
|
if config.metadata is None or generating_metadata:
|
||||||
|
metadata = None
|
||||||
|
else:
|
||||||
|
with open(config.metadata) as file:
|
||||||
|
metadata = json.load(file)
|
||||||
|
for chain_id, chain_info in metadata.items():
|
||||||
|
chain_id = int(chain_id)
|
||||||
|
for t in chain_info['t']:
|
||||||
|
metadata_by_chainaddr[chain_id,t['a']] = t
|
||||||
|
for p in chain_info['p']:
|
||||||
|
metadata_by_chainaddr[chain_id,p['a']] = p
|
||||||
|
log.info(f'Loaded metadata from {config.metadata}')
|
||||||
|
return metadata if addr is None else metadata_by_chainaddr.get((chain_id,addr))
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from decimal import InvalidOperation
|
from decimal import InvalidOperation
|
||||||
from typing import Optional, NamedTuple, Reversible, Union
|
from typing import Optional, NamedTuple, Reversible, Union, TypedDict
|
||||||
|
|
||||||
from cachetools import LFUCache
|
from cachetools import LFUCache
|
||||||
|
|
||||||
@@ -49,6 +50,9 @@ def dt(v):
|
|||||||
return v if isinstance(v, datetime) else from_timestamp(v)
|
return v if isinstance(v, datetime) else from_timestamp(v)
|
||||||
|
|
||||||
class NativeOHLC:
|
class NativeOHLC:
|
||||||
|
"""
|
||||||
|
in-memory version of OHLC data using native python types of datetime and decimal
|
||||||
|
"""
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_ohlc(ohlc: OHLC) -> 'NativeOHLC':
|
def from_ohlc(ohlc: OHLC) -> 'NativeOHLC':
|
||||||
return NativeOHLC(*[cast(value) for value, cast in zip(ohlc,(dt, opt_dec, opt_dec, opt_dec, try_dec))], ohlc=ohlc)
|
return NativeOHLC(*[cast(value) for value, cast in zip(ohlc,(dt, opt_dec, opt_dec, opt_dec, try_dec))], ohlc=ohlc)
|
||||||
@@ -175,6 +179,11 @@ def quotes_path(chain_id: int = None):
|
|||||||
chain_id = current_chain.get().chain_id
|
chain_id = current_chain.get().chain_id
|
||||||
return f'{chain_id}/quotes.json'
|
return f'{chain_id}/quotes.json'
|
||||||
|
|
||||||
|
def series_path(chain_id: int = None):
|
||||||
|
if chain_id is None:
|
||||||
|
chain_id = current_chain.get().chain_id
|
||||||
|
return f'{chain_id}/series.json'
|
||||||
|
|
||||||
|
|
||||||
def chunk_path(symbol: str, period: timedelta, time: datetime, *, chain_id: int = None) -> str:
|
def chunk_path(symbol: str, period: timedelta, time: datetime, *, chain_id: int = None) -> str:
|
||||||
if chain_id is None:
|
if chain_id is None:
|
||||||
@@ -189,7 +198,10 @@ def chunk_path(symbol: str, period: timedelta, time: datetime, *, chain_id: int
|
|||||||
|
|
||||||
|
|
||||||
class Chunk:
|
class Chunk:
|
||||||
""" Chunks map to files of OHLC's on disk """
|
"""
|
||||||
|
Chunks map to files of OHLC's on disk. If an OHLC contains 6 fields instead of just 5, the 6th field is a
|
||||||
|
timestamp pointing to the next
|
||||||
|
"""
|
||||||
def __init__(self, repo_dir: str, symbol: str, period: timedelta, time: datetime,
|
def __init__(self, repo_dir: str, symbol: str, period: timedelta, time: datetime,
|
||||||
*, bars: Optional[list[NativeOHLC]] = None, chain_id: int = None):
|
*, bars: Optional[list[NativeOHLC]] = None, chain_id: int = None):
|
||||||
self.repo_dir = repo_dir
|
self.repo_dir = repo_dir
|
||||||
@@ -251,14 +263,7 @@ class Chunk:
|
|||||||
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
for _ in range(2):
|
save_json([n.ohlc for n in self.bars], self.fullpath)
|
||||||
try:
|
|
||||||
with open(self.fullpath, 'w') as file:
|
|
||||||
json.dump([n.ohlc for n in self.bars], file)
|
|
||||||
return
|
|
||||||
except FileNotFoundError:
|
|
||||||
os.makedirs(os.path.dirname(self.fullpath), exist_ok=True)
|
|
||||||
raise IOError(f'Could not write chunk {self.fullpath}')
|
|
||||||
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
@@ -270,12 +275,23 @@ class Chunk:
|
|||||||
|
|
||||||
|
|
||||||
class OHLCRepository:
|
class OHLCRepository:
|
||||||
def __init__(self, base_dir: str = None):
|
"""
|
||||||
|
this is only used for the backend now and should not write to disk. the finaldata process runs LightOHLCRepository
|
||||||
|
instead to create json files.
|
||||||
|
OHLCRepository still functions for in-memory recent ohlc's
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_dir: str = None, *, chain_id: int = None):
|
||||||
""" can't actually make more than one of these because there's a global recent_ohlcs BlockDict """
|
""" can't actually make more than one of these because there's a global recent_ohlcs BlockDict """
|
||||||
self._dir = base_dir
|
self._dir = base_dir
|
||||||
|
self._chain_id = chain_id
|
||||||
self.cache = LFUCache(len(OHLC_PERIODS) * 1024)
|
self.cache = LFUCache(len(OHLC_PERIODS) * 1024)
|
||||||
self.dirty_chunks = set()
|
self.dirty_chunks = set()
|
||||||
self._quotes:Optional[dict[str,tuple[int,str]]] = None # todo tim
|
self._quotes = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chain_id(self):
|
||||||
|
return self._chain_id if self._chain_id is not None else current_chain.get().chain_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dir(self):
|
def dir(self):
|
||||||
@@ -291,7 +307,7 @@ class OHLCRepository:
|
|||||||
self._quotes = {}
|
self._quotes = {}
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(self.dir, quotes_path()), 'r') as f:
|
with open(os.path.join(self.dir, quotes_path(self.chain_id)), 'r') as f:
|
||||||
self._quotes = json.load(f)
|
self._quotes = json.load(f)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
self._quotes = {}
|
self._quotes = {}
|
||||||
@@ -318,7 +334,7 @@ class OHLCRepository:
|
|||||||
"""
|
"""
|
||||||
if price is None, then bars are advanced based on the time but no new price is added to the series.
|
if price is None, then bars are advanced based on the time but no new price is added to the series.
|
||||||
"""
|
"""
|
||||||
logname = f'{symbol} {period_name(period)}'
|
# logname = f'{symbol} {period_name(period)}'
|
||||||
# log.debug(f'Updating OHLC {logname} {minutely(time)} {price}')
|
# log.debug(f'Updating OHLC {logname} {minutely(time)} {price}')
|
||||||
if price is not None:
|
if price is not None:
|
||||||
self.quotes[symbol] = timestamp(time), str(price)
|
self.quotes[symbol] = timestamp(time), str(price)
|
||||||
@@ -338,6 +354,7 @@ class OHLCRepository:
|
|||||||
# drop any historical bars that are older than we need
|
# drop any historical bars that are older than we need
|
||||||
# oldest_needed = cover the root block time plus one period prior
|
# oldest_needed = cover the root block time plus one period prior
|
||||||
oldest_needed = from_timestamp(current_blockstate.get().root_block.timestamp) - period
|
oldest_needed = from_timestamp(current_blockstate.get().root_block.timestamp) - period
|
||||||
|
# noinspection PyTypeChecker
|
||||||
trim = (oldest_needed - historical[0].start) // period
|
trim = (oldest_needed - historical[0].start) // period
|
||||||
if trim > 0:
|
if trim > 0:
|
||||||
historical = historical[trim:]
|
historical = historical[trim:]
|
||||||
@@ -375,10 +392,9 @@ class OHLCRepository:
|
|||||||
chunk.update(native)
|
chunk.update(native)
|
||||||
self.dirty_chunks.add(chunk)
|
self.dirty_chunks.add(chunk)
|
||||||
|
|
||||||
def get_chunk(self, symbol: str, period: timedelta, time: datetime) -> Chunk:
|
def get_chunk(self, symbol: str, period: timedelta, start_time: datetime) -> Chunk:
|
||||||
start_time = ohlc_start_time(time, period)
|
|
||||||
chain_id = current_chain.get().chain_id
|
chain_id = current_chain.get().chain_id
|
||||||
key = chunk_path(symbol, period, time, chain_id=chain_id)
|
key = chunk_path(symbol, period, start_time, chain_id=chain_id)
|
||||||
found = self.cache.get(key)
|
found = self.cache.get(key)
|
||||||
if found is None:
|
if found is None:
|
||||||
found = self.cache[key] = Chunk(self.dir, symbol, period, start_time, chain_id=chain_id)
|
found = self.cache[key] = Chunk(self.dir, symbol, period, start_time, chain_id=chain_id)
|
||||||
@@ -397,14 +413,20 @@ class OHLCRepository:
|
|||||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
class LightOHLCRepository (OHLCRepository):
|
class SeriesDict (TypedDict):
|
||||||
|
start: int # timestamp of the start of the series
|
||||||
|
|
||||||
|
|
||||||
|
class FinalOHLCRepository (OHLCRepository):
|
||||||
"""
|
"""
|
||||||
Used for backfill when all data is known to be final. Keeps a simple table of current NativeOHLC candles.
|
Used for backfill when all data is known to be final. Keeps a simple table of current NativeOHLC candles.
|
||||||
"""
|
"""
|
||||||
def __init__(self, base_dir: str = None):
|
def __init__(self, base_dir: str = None, *, chain_id: int = None):
|
||||||
super().__init__(base_dir)
|
super().__init__(base_dir, chain_id=chain_id)
|
||||||
self.current:dict[tuple[str,timedelta],NativeOHLC] = {} # current bars keyed by symbol
|
self.current:dict[tuple[str,timedelta],NativeOHLC] = {} # current bars keyed by symbol
|
||||||
self.dirty_bars = set()
|
self.dirty_bars = set()
|
||||||
|
self.series:dict[int,dict[str,SeriesDict]] = defaultdict(dict) # keyed by [chain_id][symbol]
|
||||||
|
self._series_dirty = False
|
||||||
|
|
||||||
def light_update_all(self, symbol: str, time: datetime, price: Optional[dec]):
|
def light_update_all(self, symbol: str, time: datetime, price: Optional[dec]):
|
||||||
for period in OHLC_PERIODS:
|
for period in OHLC_PERIODS:
|
||||||
@@ -423,8 +445,11 @@ class LightOHLCRepository (OHLCRepository):
|
|||||||
# cache miss. load from chunk.
|
# cache miss. load from chunk.
|
||||||
prev = self.current[key] = chunk.bar_at(start)
|
prev = self.current[key] = chunk.bar_at(start)
|
||||||
# log.debug(f'loaded prev bar from chunk {prev}')
|
# log.debug(f'loaded prev bar from chunk {prev}')
|
||||||
|
if prev is None and symbol in self.quotes:
|
||||||
|
latest_bar_time = ohlc_start_time(self.quotes[symbol][0], period)
|
||||||
|
prev = self.current[key] = self.get_chunk(symbol, period, latest_bar_time).bar_at(latest_bar_time)
|
||||||
if prev is None:
|
if prev is None:
|
||||||
# not in cache or chunk. create new bar.
|
# never seen before. create new bar.
|
||||||
# log.debug(f'no prev bar')
|
# log.debug(f'no prev bar')
|
||||||
if price is not None:
|
if price is not None:
|
||||||
close = price
|
close = price
|
||||||
@@ -437,6 +462,8 @@ class LightOHLCRepository (OHLCRepository):
|
|||||||
bar = self.current[key] = NativeOHLC(start, price, price, price, close)
|
bar = self.current[key] = NativeOHLC(start, price, price, price, close)
|
||||||
chunk.update(bar, backfill=backfill)
|
chunk.update(bar, backfill=backfill)
|
||||||
self.dirty_chunks.add(chunk)
|
self.dirty_chunks.add(chunk)
|
||||||
|
self.series[current_chain.get().chain_id][f'{key[0]}|{key[1]}'] = {'start': timestamp(start)}
|
||||||
|
self._series_dirty = True
|
||||||
else:
|
else:
|
||||||
updated = update_ohlc(prev, period, time, price)
|
updated = update_ohlc(prev, period, time, price)
|
||||||
for bar in updated:
|
for bar in updated:
|
||||||
@@ -445,6 +472,25 @@ class LightOHLCRepository (OHLCRepository):
|
|||||||
self.dirty_chunks.add(chunk)
|
self.dirty_chunks.add(chunk)
|
||||||
self.current[key] = updated[-1]
|
self.current[key] = updated[-1]
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
# flush chunks
|
||||||
|
super().flush()
|
||||||
|
# flush series.json if needed
|
||||||
|
if self._series_dirty:
|
||||||
|
save_json(self.series, os.path.join(self.dir, series_path(self.chain_id)))
|
||||||
|
self._series_dirty = False
|
||||||
|
|
||||||
|
|
||||||
|
def save_json(obj, filename):
|
||||||
|
for _ in range(2):
|
||||||
|
try:
|
||||||
|
with open(filename, 'w') as file:
|
||||||
|
json.dump(obj, file)
|
||||||
|
return
|
||||||
|
except FileNotFoundError:
|
||||||
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||||
|
raise IOError(f'Could not write {filename}')
|
||||||
|
|
||||||
|
|
||||||
def pub_ohlc(_series:str, key: OHLCKey, bars: list[NativeOHLC]):
|
def pub_ohlc(_series:str, key: OHLCKey, bars: list[NativeOHLC]):
|
||||||
pool_addr, period = key
|
pool_addr, period = key
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from web3.exceptions import ContractLogicError
|
from web3.exceptions import ContractLogicError
|
||||||
from web3.types import EventData
|
from web3.types import EventData
|
||||||
@@ -12,6 +14,7 @@ from dexorder.blockstate import BlockDict
|
|||||||
from dexorder.blockstate.blockdata import K, V
|
from dexorder.blockstate.blockdata import K, V
|
||||||
from dexorder.blocktime import get_block_timestamp
|
from dexorder.blocktime import get_block_timestamp
|
||||||
from dexorder.database.model.pool import PoolDict
|
from dexorder.database.model.pool import PoolDict
|
||||||
|
from dexorder.metadata import generating_metadata, is_generating_metadata
|
||||||
from dexorder.tokens import get_token
|
from dexorder.tokens import get_token
|
||||||
from dexorder.uniswap import UniswapV3Pool, uniswapV3_pool_address
|
from dexorder.uniswap import UniswapV3Pool, uniswapV3_pool_address
|
||||||
|
|
||||||
@@ -32,27 +35,31 @@ async def load_pool(address: str) -> PoolDict:
|
|||||||
# todo other exchanges
|
# todo other exchanges
|
||||||
try:
|
try:
|
||||||
v3 = UniswapV3Pool(address)
|
v3 = UniswapV3Pool(address)
|
||||||
t0, t1, fee = await asyncio.gather(v3.token0(), v3.token1(), v3.fee())
|
t0, t1 = await asyncio.gather(v3.token0(), v3.token1())
|
||||||
if uniswapV3_pool_address(t0, t1, fee) == address: # VALIDATE don't just trust that it's a Uniswap pool
|
|
||||||
token0, token1 = await asyncio.gather(get_token(t0), get_token(t1))
|
token0, token1 = await asyncio.gather(get_token(t0), get_token(t1))
|
||||||
if token0 is None or token1 is None:
|
if (token0 is not None and token1 is not None
|
||||||
found = None
|
and (token0['approved'] and token1['approved'] or is_generating_metadata())):
|
||||||
else:
|
fee = await v3.fee()
|
||||||
|
if uniswapV3_pool_address(t0, t1, fee) == address: # VALIDATE don't just trust that it's a Uniswap pool
|
||||||
decimals = token0['decimals'] - token1['decimals']
|
decimals = token0['decimals'] - token1['decimals']
|
||||||
found = PoolDict(type='Pool', chain=chain_id, address=address, exchange=Exchange.UniswapV3.value,
|
found = PoolDict(type='Pool', chain=chain_id, address=address, exchange=Exchange.UniswapV3.value,
|
||||||
base=t0, quote=t1, fee=fee, decimals=decimals)
|
base=t0, quote=t1, fee=fee, decimals=decimals)
|
||||||
log.debug(f'new UniswapV3 pool {token0["symbol"]}\\{token1["symbol"]} '
|
log.debug(f'new UniswapV3 pool {token0["symbol"]}\\{token1["symbol"]} '
|
||||||
f'{("."+str(decimals)) if decimals >= 0 else (str(-decimals)+".")} {address}')
|
f'{("."+str(decimals)) if decimals >= 0 else (str(-decimals)+".")} {address}')
|
||||||
else: # NOT a genuine Uniswap V3 pool if the address test doesn't pass
|
|
||||||
log.debug(f'new Unknown pool at {address}')
|
|
||||||
except ContractLogicError:
|
except ContractLogicError:
|
||||||
log.debug(f'new Unknown pool at {address}')
|
pass
|
||||||
except ValueError as v:
|
except ValueError as v:
|
||||||
if v.args[0].get('code') == -32000:
|
try:
|
||||||
log.debug(f'new Unknown pool at {address}')
|
code = v.args[0].get('code')
|
||||||
|
except:
|
||||||
|
raise v
|
||||||
else:
|
else:
|
||||||
raise
|
if code == -32000:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise v
|
||||||
if found is None:
|
if found is None:
|
||||||
|
log.debug(f'new Unknown pool at {address}')
|
||||||
found = PoolDict(type='Pool', chain=chain_id, address=address, exchange=Exchange.Unknown.value,
|
found = PoolDict(type='Pool', chain=chain_id, address=address, exchange=Exchange.Unknown.value,
|
||||||
base=ADDRESS_0, quote=ADDRESS_0, fee=0, decimals=0)
|
base=ADDRESS_0, quote=ADDRESS_0, fee=0, decimals=0)
|
||||||
return found
|
return found
|
||||||
@@ -94,7 +101,7 @@ async def ensure_pool_price(pool: PoolDict):
|
|||||||
# todo other exchanges
|
# todo other exchanges
|
||||||
|
|
||||||
|
|
||||||
async def get_uniswap_data(swap: EventData):
|
async def get_uniswap_data(swap: EventData) -> Optional[tuple[PoolDict, datetime, dec]]:
|
||||||
try:
|
try:
|
||||||
sqrt_price = swap['args']['sqrtPriceX96']
|
sqrt_price = swap['args']['sqrtPriceX96']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -102,7 +109,6 @@ async def get_uniswap_data(swap: EventData):
|
|||||||
addr = swap['address']
|
addr = swap['address']
|
||||||
pool = await get_pool(addr)
|
pool = await get_pool(addr)
|
||||||
if pool['exchange'] != Exchange.UniswapV3.value:
|
if pool['exchange'] != Exchange.UniswapV3.value:
|
||||||
# log.debug(f'Ignoring {Exchange(pool["exchange"])} pool {addr}')
|
|
||||||
return None
|
return None
|
||||||
price: dec = await uniswap_price(pool, sqrt_price)
|
price: dec = await uniswap_price(pool, sqrt_price)
|
||||||
timestamp = await get_block_timestamp(swap['blockHash'])
|
timestamp = await get_block_timestamp(swap['blockHash'])
|
||||||
|
|||||||
@@ -5,15 +5,19 @@ from typing import Optional
|
|||||||
from eth_abi.exceptions import InsufficientDataBytes
|
from eth_abi.exceptions import InsufficientDataBytes
|
||||||
from web3.exceptions import ContractLogicError, BadFunctionCallOutput
|
from web3.exceptions import ContractLogicError, BadFunctionCallOutput
|
||||||
|
|
||||||
|
from dexorder import ADDRESS_0
|
||||||
from dexorder.addrmeta import address_metadata
|
from dexorder.addrmeta import address_metadata
|
||||||
from dexorder.base.chain import current_chain
|
from dexorder.base.chain import current_chain
|
||||||
from dexorder.contract import ERC20
|
from dexorder.contract import ERC20, ContractProxy
|
||||||
from dexorder.database.model.token import TokenDict
|
from dexorder.database.model.token import TokenDict
|
||||||
|
from dexorder.metadata import get_metadata
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def get_token(address) -> Optional[TokenDict]:
|
async def get_token(address) -> Optional[TokenDict]:
|
||||||
|
if address == ADDRESS_0:
|
||||||
|
raise ValueError('No token at address 0')
|
||||||
try:
|
try:
|
||||||
return address_metadata[address]
|
return address_metadata[address]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -23,17 +27,35 @@ async def get_token(address) -> Optional[TokenDict]:
|
|||||||
|
|
||||||
async def load_token(address: str) -> Optional[TokenDict]:
|
async def load_token(address: str) -> Optional[TokenDict]:
|
||||||
contract = ERC20(address)
|
contract = ERC20(address)
|
||||||
prom = asyncio.gather(contract.name(), contract.symbol())
|
name_prom = contract.name()
|
||||||
|
dec_prom = contract.decimals()
|
||||||
try:
|
try:
|
||||||
decimals = await contract.decimals()
|
symbol = await contract.symbol()
|
||||||
|
except (InsufficientDataBytes, BadFunctionCallOutput):
|
||||||
|
# this happens when the token returns bytes32 instead of a string
|
||||||
|
try:
|
||||||
|
symbol = await ContractProxy(address, 'ERC20.sb').symbol()
|
||||||
|
log.info(f'got bytes32 symbol {symbol}')
|
||||||
|
except (InsufficientDataBytes, ContractLogicError, BadFunctionCallOutput):
|
||||||
|
log.warning(f'token {address} has broken symbol()')
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
decimals = await dec_prom
|
||||||
except (InsufficientDataBytes, ContractLogicError, BadFunctionCallOutput):
|
except (InsufficientDataBytes, ContractLogicError, BadFunctionCallOutput):
|
||||||
log.warning(f'token {address} has no decimals()')
|
log.warning(f'token {address} has no decimals()')
|
||||||
decimals = 0
|
decimals = 0
|
||||||
try:
|
name = await name_prom
|
||||||
name, symbol = await prom
|
|
||||||
except OverflowError:
|
|
||||||
# this happens when the token returns bytes32 instead of a string
|
|
||||||
return None
|
|
||||||
log.debug(f'new token {name} {symbol} {address}')
|
log.debug(f'new token {name} {symbol} {address}')
|
||||||
return TokenDict(type='Token', chain=current_chain.get().chain_id, address=address,
|
chain_id = current_chain.get().chain_id
|
||||||
name=name, symbol=symbol, decimals=decimals)
|
td = TokenDict(type='Token', chain=chain_id, address=address,
|
||||||
|
name=name, symbol=symbol, decimals=decimals, approved=False)
|
||||||
|
md = get_metadata(address, chain_id=chain_id)
|
||||||
|
if md is not None:
|
||||||
|
td['approved'] = True
|
||||||
|
if 'n' in md:
|
||||||
|
td['name'] = md['n']
|
||||||
|
if 's' in md:
|
||||||
|
td['symbol'] = md['s']
|
||||||
|
if 'd' in md:
|
||||||
|
td['decimals'] = md['d']
|
||||||
|
return td
|
||||||
|
|||||||
Reference in New Issue
Block a user