From 8127a6c900e7cbb613897ff1b60c3f7fd2592cfb Mon Sep 17 00:00:00 2001 From: tim Date: Tue, 19 Aug 2025 17:43:10 -0400 Subject: [PATCH] basic gmx impl running --- alembic/versions/87dcd5929323_gmx.py | 32 -- src/dexorder/__init__.py | 2 +- src/dexorder/accounting/accounting.py | 4 +- src/dexorder/addrmeta.py | 11 +- src/dexorder/base/__init__.py | 4 +- src/dexorder/base/metadecl.py | 65 ++++ src/dexorder/base/orderlib.py | 27 +- src/dexorder/bin/backfill_ohlc.py | 4 +- src/dexorder/bin/executable.py | 2 + src/dexorder/bin/finaldata.py | 11 +- src/dexorder/bin/main.py | 10 +- src/dexorder/bin/refill.py | 2 +- src/dexorder/blockstate/blockdata.py | 6 +- src/dexorder/blockstate/state.py | 3 +- src/dexorder/configuration/load.py | 6 +- src/dexorder/configuration/resolve.py | 2 +- src/dexorder/configuration/schema.py | 1 + src/dexorder/contract/__init__.py | 33 +- src/dexorder/contract/abi.py | 2 +- src/dexorder/contract/contract_proxy.py | 12 +- src/dexorder/contract/dexorder.py | 40 ++- src/dexorder/database/model/pool.py | 12 +- src/dexorder/database/model/token.py | 49 +-- src/dexorder/event_handler.py | 32 +- src/dexorder/feemanager.py | 2 +- src/dexorder/gmx/__init__.py | 2 +- src/dexorder/gmx/_abi.py | 34 +-- src/dexorder/gmx/_base.py | 104 ++++--- src/dexorder/gmx/_chaininfo.py | 3 +- src/dexorder/gmx/_contract.py | 15 +- src/dexorder/gmx/_datastore.py | 14 +- src/dexorder/gmx/_error.py | 292 ++++++++++++++++++ src/dexorder/gmx/_handle.py | 390 ++++++++++++++++++------ src/dexorder/gmx/_metadata.py | 127 ++++---- src/dexorder/memcache/__init__.py | 1 - src/dexorder/metadata.py | 5 +- src/dexorder/order/executionhandler.py | 22 +- src/dexorder/order/orderstate.py | 7 + src/dexorder/order/triggers.py | 15 +- src/dexorder/pools.py | 11 +- src/dexorder/tokens.py | 24 +- src/dexorder/transactions.py | 4 +- src/dexorder/util/json.py | 2 +- 43 files changed, 1027 insertions(+), 419 deletions(-) delete mode 100644 alembic/versions/87dcd5929323_gmx.py create mode 100644 src/dexorder/base/metadecl.py create mode 100644 src/dexorder/gmx/_error.py diff --git a/alembic/versions/87dcd5929323_gmx.py b/alembic/versions/87dcd5929323_gmx.py deleted file mode 100644 index af376e3..0000000 --- a/alembic/versions/87dcd5929323_gmx.py +++ /dev/null @@ -1,32 +0,0 @@ -"""GMX - -Revision ID: 87dcd5929323 -Revises: e47d1bca4b3d -Create Date: 2025-06-16 16:48:11.177904 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import dexorder.database -import dexorder.database.column_types - - -# revision identifiers, used by Alembic. -revision: str = '87dcd5929323' -down_revision: Union[str, None] = 'e47d1bca4b3d' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('token', sa.Column('gmx_synthetic', sa.Boolean(), server_default=sa.text('false'), nullable=False)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('token', 'gmx_synthetic') - # ### end Alembic commands ### diff --git a/src/dexorder/__init__.py b/src/dexorder/__init__.py index e253e2d..b5af337 100644 --- a/src/dexorder/__init__.py +++ b/src/dexorder/__init__.py @@ -3,7 +3,7 @@ import logging from contextvars import ContextVar from datetime import datetime, timezone from decimal import Decimal -from typing import Callable, Any, Union, Optional +from typing import Callable, Any from web3 import AsyncWeb3 diff --git a/src/dexorder/accounting/accounting.py b/src/dexorder/accounting/accounting.py index d934a54..92172d4 100644 --- a/src/dexorder/accounting/accounting.py +++ b/src/dexorder/accounting/accounting.py @@ -2,12 +2,12 @@ import asyncio import logging from typing import Union -from sqlalchemy import select, func, text +from sqlalchemy import select, func from typing_extensions import Optional from web3.exceptions import ContractLogicError from web3.types import EventData -from dexorder import db, dec, NATIVE_TOKEN, from_timestamp, config, ADDRESS_0, now, Account, metric +from dexorder import db, dec, NATIVE_TOKEN, from_timestamp, config, ADDRESS_0, now, Account from dexorder.base import TransactionReceiptDict from dexorder.base.chain import current_chain from dexorder.blocks import get_block_timestamp, get_block, current_block diff --git a/src/dexorder/addrmeta.py b/src/dexorder/addrmeta.py index 67743c0..9060bff 100644 --- a/src/dexorder/addrmeta.py +++ b/src/dexorder/addrmeta.py @@ -2,16 +2,15 @@ import logging from typing import TypedDict from dexorder import db +from dexorder.base import OldPoolDict, OldGMXDict, OldTokenDict from dexorder.base.chain import current_chain from dexorder.blockstate import BlockDict -from dexorder.database.model import Pool -from dexorder.database.model.pool import OldPoolDict -from dexorder.database.model.token import Token, OldTokenDict +from dexorder.database.model import Pool, Token log = logging.getLogger(__name__) # address_metadata is a polymorphic BlockDict which maps address keys to a dict of metadata describing the address -# used for Tokens and Pools +# used for Tokens and Pools and GMX Markets class AddressMetadata (TypedDict): @@ -45,8 +44,10 @@ def save_addrmeta(address: str, meta: AddressMetadata): pool.quote = updated.quote pool.fee = updated.fee pool.decimals = updated.decimals + elif meta['type'] == 'GMX': + pass else: log.warning(f'Address {address} had unknown metadata type {meta["type"]}') -address_metadata: BlockDict[str,dict] = BlockDict('a', redis=True, db=True, finalize_cb=save_addrmeta) +address_metadata: BlockDict[str,OldPoolDict|OldTokenDict|OldGMXDict] = BlockDict('a', redis=True, db=True, finalize_cb=save_addrmeta) diff --git a/src/dexorder/base/__init__.py b/src/dexorder/base/__init__.py index 74a0e17..287dee2 100644 --- a/src/dexorder/base/__init__.py +++ b/src/dexorder/base/__init__.py @@ -1,6 +1,6 @@ -from abc import abstractmethod from dataclasses import dataclass -from typing import TypedDict, Union, Type, Any, Callable +from typing import TypedDict, Union, Any, Callable +from dexorder.base.metadecl import OldTokenDict, OldPoolDict, OldGMXDict Address = str Quantity = Union[str,int] diff --git a/src/dexorder/base/metadecl.py b/src/dexorder/base/metadecl.py new file mode 100644 index 0000000..64c6722 --- /dev/null +++ b/src/dexorder/base/metadecl.py @@ -0,0 +1,65 @@ +import logging +from typing import TypedDict, NotRequired + +log = logging.getLogger(__name__) + + +class TokenDict (TypedDict): + """ + Token metadata dictionary + + Fields: + a: The address of the token. + n: The name of the token. + s: The symbol of the token. + d: Number of decimals. + l: Indicates if approved ("listed"). + g: gmx synthetic flag + x: Optional extra data. + """ + + a: str + n: str + s: str + d: int + l: NotRequired[bool] + g: NotRequired[bool] + x: NotRequired[dict] + + +# OldTokenDict is the primary dict we use in-memory, with basic JSON-able types + +class OldTokenDict (TypedDict): + type: str + chain: int + address: str + name: str + symbol: str + decimals: int + approved: bool # whether this token is in the whitelist or not + x: NotRequired[dict] # extra data + + +class OldPoolDict (TypedDict): + type: str + chain: int + address: str + exchange: int + base: str + quote: str + fee: int + decimals: int + + + +class OldGMXDict (TypedDict): + type: str + chain: int + address: str + exchange: int + index: str + long: str + short: str + leverage: int + decimals: int + diff --git a/src/dexorder/base/orderlib.py b/src/dexorder/base/orderlib.py index 0994178..fa48c37 100644 --- a/src/dexorder/base/orderlib.py +++ b/src/dexorder/base/orderlib.py @@ -37,9 +37,10 @@ class SwapOrderState (Enum): class Exchange (Enum): - Unknown = -1 - UniswapV2 = 0 - UniswapV3 = 1 + Unknown = -1 + OTC = 0 + UniswapV3 = 1 + GMX = 2 @dataclass class Route: @@ -75,6 +76,20 @@ class Line: return self.intercept, self.slope +@dataclass +class GMXOrder: + reserve_amount: int # todo + is_long: bool + is_increase: bool + + @staticmethod + def load(obj: Optional[tuple[int,bool,bool]]): + return GMXOrder(*obj) if obj is not None else None + + def dump(self): + return self.reserve_amount, self.is_long, self.is_increase + + @dataclass class SwapOrder: tokenIn: str @@ -87,6 +102,7 @@ class SwapOrder: inverted: bool conditionalOrder: int tranches: list['Tranche'] + gmx: Optional[GMXOrder] = None @property def min_input_amount(self): @@ -95,7 +111,7 @@ class SwapOrder: @staticmethod def load(obj): return SwapOrder(obj[0], obj[1], Route.load(obj[2]), int(obj[3]), int(obj[4]), obj[5], obj[6], obj[7], obj[8], - [Tranche.load(t) for t in obj[9]]) + [Tranche.load(t) for t in obj[9]], GMXOrder.load(obj[10]) if len(obj) > 10 else None) @staticmethod def load_from_chain(obj): @@ -106,7 +122,8 @@ class SwapOrder: return (self.tokenIn, self.tokenOut, self.route.dump(), str(self.amount), str(self.minFillAmount), self.amountIsInput, self.outputDirectlyToOwner, self.inverted, self.conditionalOrder, - [t.dump() for t in self.tranches]) + [t.dump() for t in self.tranches], + self.gmx.dump() if self.gmx is not None else None) def __str__(self): msg = f''' diff --git a/src/dexorder/bin/backfill_ohlc.py b/src/dexorder/bin/backfill_ohlc.py index d58e93f..a85ccef 100644 --- a/src/dexorder/bin/backfill_ohlc.py +++ b/src/dexorder/bin/backfill_ohlc.py @@ -14,7 +14,7 @@ from dexorder.blockstate.fork import Fork from dexorder.configuration import parse_args from dexorder.contract import get_contract_event from dexorder.database import db -from dexorder.event_handler import check_ohlc_rollover, handle_uniswap_swaps +from dexorder.event_handler import handle_uniswap_swaps from dexorder.memcache import memcache from dexorder.memcache.memcache_state import RedisState, publish_all from dexorder.ohlc import recent_ohlcs, ohlc_save, ohlcs @@ -58,7 +58,7 @@ async def main(): runner = BlockStateRunner(state, publish_all=publish_all if redis_state else None, timer_period=0) runner.add_event_trigger(handle_uniswap_swaps, get_contract_event('IUniswapV3PoolEvents', 'Swap'), multi=True) - runner.add_callback(check_ohlc_rollover) + # runner.add_callback(check_ohlc_rollover) runner.on_promotion.append(finalize_callback) if db: # noinspection PyUnboundLocalVariable diff --git a/src/dexorder/bin/executable.py b/src/dexorder/bin/executable.py index a85fdfa..476d91c 100644 --- a/src/dexorder/bin/executable.py +++ b/src/dexorder/bin/executable.py @@ -13,6 +13,7 @@ from omegaconf import OmegaConf from dexorder import configuration, config from dexorder.alert import init_alerts +from dexorder.configuration.load import config_file from dexorder.configuration.schema import Config from dexorder.metric.metric_startup import start_metrics_server @@ -65,6 +66,7 @@ def execute(main:Callable[...,Coroutine[Any,Any,Any]], shutdown=None, *, parse_l logging.basicConfig(level=logging.INFO, stream=sys.stdout) log.setLevel(logging.DEBUG) log.info('Logging configured to default') + log.info(f'Loaded main config from {config_file}') xconf = None if parse_args: # NOTE: there is special command-line argument handling in config/load.py to get a config filename. diff --git a/src/dexorder/bin/finaldata.py b/src/dexorder/bin/finaldata.py index 3a59b67..b52e7df 100644 --- a/src/dexorder/bin/finaldata.py +++ b/src/dexorder/bin/finaldata.py @@ -11,7 +11,7 @@ from dexorder.bin.executable import execute from dexorder.blocks import get_block_timestamp, get_block from dexorder.blockstate.fork import current_fork from dexorder.configuration import parse_args -from dexorder.contract import get_contract_event +from dexorder.event_handler import wire_dexorder_debug from dexorder.final_ohlc import FinalOHLCRepository from dexorder.gmx import gmx_wire_runner_late, gmx_wire_runner_early from dexorder.pools import get_uniswap_data @@ -57,9 +57,12 @@ async def main(): ohlcs = FinalOHLCRepository() await blockchain.connect() walker = BlockWalker(flush_callback, timedelta(seconds=config.walker_flush_interval)) - gmx_wire_runner_early(walker, backfill=ohlcs) - walker.add_event_trigger(handle_backfill_uniswap_swaps, - get_contract_event('IUniswapV3PoolEvents', 'Swap'), multi=True) + # gmx_wire_runner_early(walker, backfill=ohlcs) + gmx_wire_runner_early(walker) # todo re-enable backfill + wire_dexorder_debug(walker) + # todo re-enable uniswap + # walker.add_event_trigger(handle_backfill_uniswap_swaps, + # get_contract_event('IUniswapV3PoolEvents', 'Swap'), multi=True) gmx_wire_runner_late(walker) await walker.run() diff --git a/src/dexorder/bin/main.py b/src/dexorder/bin/main.py index d757109..a7c6ed0 100644 --- a/src/dexorder/bin/main.py +++ b/src/dexorder/bin/main.py @@ -14,7 +14,10 @@ from dexorder.contract import get_contract_event from dexorder.contract.dexorder import get_dexorder_contract from dexorder.event_handler import (init, dump_log, handle_vault_created, handle_order_placed, handle_transfer, handle_swap_filled, handle_order_canceled, handle_order_cancel_all, - handle_uniswap_swaps, handle_vault_impl_changed, update_metrics) + handle_uniswap_swaps, handle_vault_impl_changed, update_metrics, + activate_new_price_triggers) +from dexorder.gmx import gmx_wire_runner_early, gmx_wire_runner_late +from dexorder.gmx._handle import gmx_wire_runner_init from dexorder.marks import publish_marks from dexorder.memcache import memcache from dexorder.memcache.memcache_state import RedisState, publish_all @@ -61,20 +64,23 @@ def setup_logevent_triggers(runner): runner.add_callback(check_activate_orders) runner.add_callback(init) + gmx_wire_runner_init(runner) runner.add_event_trigger(handle_transaction_receipts) runner.add_event_trigger(handle_vault_created, get_contract_event('Vault', 'VaultCreated')) runner.add_event_trigger(handle_vault_impl_changed, get_contract_event('Vault', 'VaultImplChanged')) runner.add_event_trigger(handle_order_placed, get_contract_event('VaultImpl', 'DexorderSwapPlaced')) + gmx_wire_runner_early(runner) # must come after DexorderSwapPlaced so the GMXOrder event can add data to the existing order runner.add_event_trigger(handle_transfer, get_contract_event('ERC20', 'Transfer')) runner.add_event_trigger(handle_uniswap_swaps, get_contract_event('IUniswapV3PoolEvents', 'Swap'), multi=True) runner.add_event_trigger(handle_swap_filled, get_contract_event('VaultImpl', 'DexorderSwapFilled')) runner.add_event_trigger(handle_order_canceled, get_contract_event('VaultImpl', 'DexorderSwapCanceled')) runner.add_event_trigger(handle_order_cancel_all, get_contract_event('VaultImpl', 'DexorderCancelAll')) - + gmx_wire_runner_late(runner) runner.add_event_trigger(handle_dexorderexecutions, executions) runner.add_event_trigger(handle_vault_creation_requests) + runner.add_event_trigger(activate_new_price_triggers) runner.add_callback(end_trigger_updates) runner.add_callback(execute_tranches) diff --git a/src/dexorder/bin/refill.py b/src/dexorder/bin/refill.py index f63d10e..6cc991b 100644 --- a/src/dexorder/bin/refill.py +++ b/src/dexorder/bin/refill.py @@ -1,7 +1,7 @@ import logging from dataclasses import dataclass -from dexorder import blockchain, db, dec +from dexorder import dec from dexorder.bin.executable import execute log = logging.getLogger(__name__) diff --git a/src/dexorder/blockstate/blockdata.py b/src/dexorder/blockstate/blockdata.py index 5de892a..f9df2c0 100644 --- a/src/dexorder/blockstate/blockdata.py +++ b/src/dexorder/blockstate/blockdata.py @@ -52,7 +52,7 @@ class BlockData (Generic[T]): def setitem(self, item, value: T, overwrite=True): state = current_blockstate.get() fork = current_fork.get() - state.set(fork, self.series, item, value, overwrite) + return state.set(fork, self.series, item, value, overwrite) def getitem(self, item, default=NARG) -> T: state = current_blockstate.get() @@ -63,9 +63,11 @@ class BlockData (Generic[T]): result = default if self.lazy_getitem: lazy = self.lazy_getitem(self, item) - if lazy is not NARG: + if lazy is not NARG and lazy is not DELETE: state.set(state.root_fork, self.series, item, lazy, readonly_override=True) result = lazy + if result is DELETE: + result = default if result is NARG: raise KeyError return result diff --git a/src/dexorder/blockstate/state.py b/src/dexorder/blockstate/state.py index 625d89c..841a685 100644 --- a/src/dexorder/blockstate/state.py +++ b/src/dexorder/blockstate/state.py @@ -232,8 +232,9 @@ class BlockState: for diff in diffs: if diff.branch_id == branch.id: # if there's an existing value for this branch, we replace it + old_value = diff.value diff.value = value - return + return old_value elif self._fork_has_diff(fork, diff): # if there's an existing value on this fork, remember it old_value = diff.value diff --git a/src/dexorder/configuration/load.py b/src/dexorder/configuration/load.py index 34b21cd..e0f63ad 100644 --- a/src/dexorder/configuration/load.py +++ b/src/dexorder/configuration/load.py @@ -10,7 +10,7 @@ from .schema import Config schema = OmegaConf.structured(Config(), flags={'struct': False}) -_config_file = 'dexorder.toml' +config_file = 'dexorder.toml' class ConfigException (Exception): pass @@ -21,7 +21,7 @@ def load_config(): result:ConfigDict = OmegaConf.merge( schema, from_toml('.secret.toml'), - from_toml(_config_file), + from_toml(config_file), from_toml('config.toml'), from_env() ) @@ -73,7 +73,7 @@ if len(sys.argv) > 1 and (sys.argv[1] == '-c' or sys.argv[1] == '--config'): if len(sys.argv) < 3: raise ConfigException('Missing config file argument') else: - _config_file = sys.argv[2] + config_file = sys.argv[2] sys.argv = [sys.argv[0], *sys.argv[3:]] config = load_config() diff --git a/src/dexorder/configuration/resolve.py b/src/dexorder/configuration/resolve.py index e57a115..b28b04f 100644 --- a/src/dexorder/configuration/resolve.py +++ b/src/dexorder/configuration/resolve.py @@ -1,3 +1,3 @@ -from .load import config + diff --git a/src/dexorder/configuration/schema.py b/src/dexorder/configuration/schema.py index b40d4cf..25f4a5c 100644 --- a/src/dexorder/configuration/schema.py +++ b/src/dexorder/configuration/schema.py @@ -9,6 +9,7 @@ from typing import Optional @dataclass class Config: + contract_version: Optional[str] = None # version tag of the contract deployment to use. if None then confirms: Optional[int] = None # number of blocks before data is considered finalized. if None then the chain's default setting is used batch_size: Optional[int] = None # max number of blocks to query in a single backfill rpc request rpc_url: str = 'http://localhost:8545' # may be a comma-separated list. may include names of entries in rpc_urls. diff --git a/src/dexorder/contract/__init__.py b/src/dexorder/contract/__init__.py index b8e98b6..6e880b8 100644 --- a/src/dexorder/contract/__init__.py +++ b/src/dexorder/contract/__init__.py @@ -1,5 +1,6 @@ import glob import json +import logging import os from eth_abi.exceptions import InsufficientDataBytes @@ -9,7 +10,7 @@ from web3.exceptions import BadFunctionCallOutput, ContractLogicError from .abi import abis from .contract_proxy import ContractProxy -from .. import current_w3 +from .. import current_w3, config from ..base.chain import current_chain CONTRACT_ERRORS = (InsufficientDataBytes, ContractLogicError, BadFunctionCallOutput) @@ -18,10 +19,28 @@ CONTRACT_ERRORS = (InsufficientDataBytes, ContractLogicError, BadFunctionCallOut # set initially to the string filename, then loaded on demand and set to the parsed JSON result _contract_data: dict[str,Union[str,dict]] = {} -# finds all .sol files and sets _contract_data with their pathname -for _file in glob.glob('../contract/out/**/*.sol/*.json', recursive=True): - if os.path.isfile(_file): - _contract_data[os.path.basename(_file)[:-5]] = _file +initialized = False +_contract_path = '' + +def get_contract_path(): + init_contract_data() + return _contract_path + +log = logging.getLogger(__name__) + +def init_contract_data(): + global initialized, _contract_path + if initialized: + return + subpath = '' if config.contract_version is None else f'/deployment/{config.contract_version}' + _contract_path = f'../contract{subpath}' + + # finds all .json files in the out path and sets _contract_data with their pathname + for _file in glob.glob(f'{_contract_path}/out/**/*.sol/*.json', recursive=True): + if os.path.isfile(_file): + _contract_data[os.path.basename(_file)[:-5]] = _file + initialized = True + log.info(f'Configured contracts from {_contract_path}') def get_abi(name): @@ -29,6 +48,7 @@ def get_abi(name): def get_contract_data(name): + init_contract_data() try: return {'abi':abis[name]} except KeyError: @@ -43,9 +63,10 @@ def get_contract_data(name): def get_deployment_address(deployment_name, contract_name, *, chain_id=None): + init_contract_data() if chain_id is None: chain_id = current_chain.get().id - with open(f'../contract/broadcast/{deployment_name}.sol/{chain_id}/run-latest.json', 'rt') as file: + with open(f'{_contract_path}/broadcast/{deployment_name}.sol/{chain_id}/run-latest.json', 'rt') as file: data = json.load(file) for tx in data.get('transactions',[]): if tx.get('contractName') == contract_name: diff --git a/src/dexorder/contract/abi.py b/src/dexorder/contract/abi.py index 68e0b0a..cff455b 100644 --- a/src/dexorder/contract/abi.py +++ b/src/dexorder/contract/abi.py @@ -1,5 +1,5 @@ abis = { - # ERC20 where symbol() returns a bytes32 instead of a string + # Special ERC20 definition where symbol() returns a bytes32 instead of a string 'ERC20.sb': '''[{"type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"name","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"}]''', } diff --git a/src/dexorder/contract/contract_proxy.py b/src/dexorder/contract/contract_proxy.py index ceed163..2339313 100644 --- a/src/dexorder/contract/contract_proxy.py +++ b/src/dexorder/contract/contract_proxy.py @@ -60,14 +60,14 @@ class DeployTransaction (ContractTransaction): def call_wrapper(addr, name, func): - async def f(*args, block_identifier=None, **kwargs): + async def f(*args, block_identifier=None, kwargs=None): if block_identifier is None: try: block_identifier = current_block.get().height except (LookupError, AttributeError): block_identifier = 'latest' try: - return await func(*args).call(block_identifier=block_identifier, **kwargs) + return await func(*args).call(block_identifier=block_identifier, **(kwargs or {})) except Web3Exception as e: e.args += addr, name raise e @@ -75,8 +75,8 @@ def call_wrapper(addr, name, func): def transact_wrapper(addr, name, func): - async def f(*args, **kwargs): - tx = await func(*args).build_transaction(kwargs) + async def f(*args, kwargs=None): + tx = await func(*args).build_transaction(kwargs or {}) ct = ContractTransaction(tx) account = await Account.acquire() if account is None: @@ -96,8 +96,8 @@ def transact_wrapper(addr, name, func): def build_wrapper(_addr, _name, func): - async def f(*args, **kwargs): - tx = await func(*args).build_transaction(kwargs) + async def f(*args, kwargs=None): + tx = await func(*args).build_transaction(kwargs or {}) return ContractTransaction(tx) return f diff --git a/src/dexorder/contract/dexorder.py b/src/dexorder/contract/dexorder.py index 1a1582e..523d04c 100644 --- a/src/dexorder/contract/dexorder.py +++ b/src/dexorder/contract/dexorder.py @@ -6,26 +6,37 @@ from eth_utils import keccak, to_bytes, to_checksum_address from typing_extensions import Optional from dexorder.base.chain import current_chain -from dexorder.contract import ContractProxy +from dexorder.contract import ContractProxy, get_contract_path log = logging.getLogger(__name__) +version = None +chain_info = None + _factory = {} _dexorder = {} _vault_init_code_hash = {} +_initialized = False + +def _ensure_init(): + global version, chain_info + with open(f'{get_contract_path()}/version.json') as version_file: + version = json.load(version_file) + log.info(f'Version: {version}') + chain_info = version['chainInfo'] + for _chain_id, info in chain_info.items(): + _chain_id = int(_chain_id) + _factory[_chain_id] = ContractProxy(info['factory'], 'VaultFactory') + _dexorder[_chain_id] = ContractProxy(info['dexorder'], 'DexorderGMX') + _vault_init_code_hash[_chain_id] = to_bytes(hexstr=info['vaultInitCodeHash']) -with open('../contract/version.json') as version_file: - version = json.load(version_file) -log.info(f'Version: {version}') - -chain_info = version['chainInfo'] - -for _chain_id, info in chain_info.items(): - _chain_id = int(_chain_id) - _factory[_chain_id] = ContractProxy(info['factory'], 'VaultFactory') - _dexorder[_chain_id] = ContractProxy(info['dexorder'], 'Dexorder') - _vault_init_code_hash[_chain_id] = to_bytes(hexstr=info['vaultInitCodeHash']) +def __getattr__(name): + global _initialized + if not _initialized: + _ensure_init() + _initialized = True + raise AttributeError() def get_by_chain(d): return d[current_chain.get().id] @@ -40,11 +51,12 @@ def get_vault_init_code_hash() -> bytes: return get_by_chain(_vault_init_code_hash) def get_mockenv() -> Optional[ContractProxy]: - addr = chain_info.get(str(current_chain.get().id),{}).get('mockenv') + addr = globals()['chain_info'].get(str(current_chain.get().id), {}).get('mockenv') return ContractProxy(addr, 'MockEnv') if addr is not None else None + def get_mirrorenv() -> Optional[ContractProxy]: - addr = chain_info.get(str(current_chain.get().id),{}).get('mirrorenv') + addr = globals()['chain_info'].get(str(current_chain.get().id), {}).get('mirrorenv') return ContractProxy(addr, 'MirrorEnv') if addr is not None else None def vault_address(owner, num): diff --git a/src/dexorder/database/model/pool.py b/src/dexorder/database/model/pool.py index a3e1a5d..3ef6ae8 100644 --- a/src/dexorder/database/model/pool.py +++ b/src/dexorder/database/model/pool.py @@ -3,6 +3,7 @@ from typing import TypedDict, Optional from sqlalchemy.orm import Mapped, mapped_column +from dexorder.base import OldPoolDict from dexorder.base.orderlib import Exchange from dexorder.database.column import Address, Blockchain from dexorder.database.model import Base @@ -20,17 +21,6 @@ class PoolDict (TypedDict): x: Optional[dict] -class OldPoolDict (TypedDict): - type: str - chain: int - address: str - exchange: int - base: str - quote: str - fee: int - decimals: int - - class Pool (Base): __tablename__ = 'pool' diff --git a/src/dexorder/database/model/token.py b/src/dexorder/database/model/token.py index eb22289..3488611 100644 --- a/src/dexorder/database/model/token.py +++ b/src/dexorder/database/model/token.py @@ -1,52 +1,15 @@ import logging -from typing import TypedDict, Optional, NotRequired -from sqlalchemy import Index, text +from sqlalchemy import Index from sqlalchemy.orm import Mapped, mapped_column +from dexorder.base import OldTokenDict from dexorder.database.column import Address, Blockchain, Uint8 from dexorder.database.model import Base log = logging.getLogger(__name__) -class TokenDict (TypedDict): - """ - Token metadata dictionary - - Fields: - a: The address of the token. - n: The name of the token. - s: The symbol of the token. - d: Number of decimals. - l: Indicates if approved ("listed"). - g: gmx synthetic flag - x: Optional extra data. - """ - - a: str - n: str - s: str - d: int - l: NotRequired[bool] - g: NotRequired[bool] - x: NotRequired[dict] - - -# OldTokenDict is the primary dict we use in-memory, with basic JSON-able types - -class OldTokenDict (TypedDict): - type: str - chain: int - address: str - name: str - symbol: str - decimals: int - approved: bool # whether this token is in the whitelist or not - gmx_synthetic: NotRequired[bool] - x: NotRequired[dict] # extra data - - # the database object is primarily write-only so we are able to index queries for pools-by-token from the nodejs server class Token (Base): @@ -58,7 +21,6 @@ class Token (Base): symbol: Mapped[str] = mapped_column(index=True) decimals: Mapped[Uint8] approved: Mapped[bool] = mapped_column(index=True) - gmx_synthetic: Mapped[bool] = mapped_column(default=False, server_default=text('false')) __table_args__ = ( Index('ix_token_name', 'name', postgresql_using='gist'), # full text search on name @@ -69,13 +31,10 @@ class Token (Base): def load(token_dict: OldTokenDict) -> 'Token': return Token(chain=Blockchain.get(token_dict['chain']), address=token_dict['address'], name=token_dict['name'], symbol=token_dict['symbol'], decimals=token_dict['decimals'], - approved=token_dict['approved'], gmx_synthetic=token_dict.get('gmx_synthetic', False)) + approved=token_dict['approved']) def dump(self): - token = OldTokenDict(type='Token', chain=self.chain.chain_id, address=self.address, + return OldTokenDict(type='Token', chain=self.chain.chain_id, address=self.address, name=self.name, symbol=self.symbol, decimals=self.decimals, approved=self.approved) - if self.gmx_synthetic: - token['gmx_synthetic'] = True - return token diff --git a/src/dexorder/event_handler.py b/src/dexorder/event_handler.py index 2bd9dad..d42e5f5 100644 --- a/src/dexorder/event_handler.py +++ b/src/dexorder/event_handler.py @@ -1,13 +1,14 @@ import asyncio import logging +from eth_utils import keccak from web3.types import EventData from dexorder import db, metric, current_w3, timestamp from dexorder.accounting import accounting_fill, accounting_placement from dexorder.base.chain import current_chain from dexorder.base.order import TrancheKey, OrderKey -from dexorder.base.orderlib import SwapOrderState +from dexorder.base.orderlib import SwapOrderState, Exchange, GMXOrder from dexorder.blocks import get_block_timestamp from dexorder.blockstate import current_blockstate from dexorder.contract.dexorder import VaultContract, get_factory_contract @@ -17,7 +18,8 @@ from dexorder.ohlc import ohlcs from dexorder.order.orderstate import Order from dexorder.order.triggers import (OrderTriggers, activate_order, update_balance_triggers, start_trigger_updates, update_price_triggers, TimeTrigger, PriceLineTrigger) -from dexorder.pools import new_pool_prices, pool_prices, get_uniswap_data +from dexorder.pools import new_pool_prices, pool_prices, get_uniswap_data, get_pool +from dexorder.progressor import BlockProgressor from dexorder.util import hexstr from dexorder.vault_blockdata import vault_owners, adjust_balance, verify_vault, publish_vaults @@ -33,6 +35,14 @@ def init(): start_trigger_updates() +def wire_dexorder_debug(runner: BlockProgressor): + runner.add_event_trigger(handle_dexorderdebug, None, {"topics":[keccak(text='DexorderDebug(string)')]}) + +def handle_dexorderdebug(events: list): + for event in events: + print(f'DexorderDebug {event}') + + async def handle_order_placed(event: EventData): # event DexorderSwapPlaced (uint64 startOrderIndex, uint8 numOrders, uint); addr = event['address'] @@ -57,6 +67,9 @@ async def handle_order_placed(event: EventData): obj = await contract.swapOrderStatus(index) log.debug(f'raw order status {obj}') order = Order.create(addr, index, event['transactionHash'], obj) + if order.order.route.exchange == Exchange.GMX: + gmxStatus = await contract.gmxOrderStatus(index) + order.order.gmx = GMXOrder.load(gmxStatus[0]) await activate_order(order) log.debug(f'new order {order.key} {await order.pprint()}') @@ -81,9 +94,10 @@ async def handle_swap_filled(event: EventData): except KeyError: log.warning(f'DexorderSwapFilled IGNORED due to missing order {vault} {order_index}') return - value = await accounting_fill(event, order.order.tokenOut) - if value is not None: - metric.volume.inc(float(value)) + usd_value = await accounting_fill(event, order.order.tokenOut) + # from here down is almost the same as a section of handle_gmxorderexecuted() + if usd_value is not None: + metric.volume.inc(float(usd_value)) order.status.trancheStatus[tranche_index].activationTime = next_execution_time # update rate limit try: triggers = OrderTriggers.instances[order.key] @@ -167,11 +181,17 @@ async def update_pool_price(addr, time, price, decimals): Price should be an adjusted price with decimals, not the raw price from the pool. The decimals are used to convert the price back to blockchain format for the triggers. """ - pool_prices[addr] = price + pool_prices[addr] = price # this will update new_pool_prices if necessary await ohlcs.update_all(addr, time, price) update_price_triggers(addr, price, decimals) +async def activate_new_price_triggers(): + for addr, price in new_pool_prices.items(): + pool = await get_pool(addr) + update_price_triggers(addr, price, pool['decimals']) + + async def handle_vault_created(created: EventData): try: owner = created['args']['owner'] diff --git a/src/dexorder/feemanager.py b/src/dexorder/feemanager.py index dbed88a..9ad1153 100644 --- a/src/dexorder/feemanager.py +++ b/src/dexorder/feemanager.py @@ -2,7 +2,7 @@ import asyncio import logging from dexorder.contract import ContractProxy -from dexorder.contract.dexorder import get_factory_contract, get_fee_manager_contract +from dexorder.contract.dexorder import get_fee_manager_contract log = logging.getLogger(__name__) diff --git a/src/dexorder/gmx/__init__.py b/src/dexorder/gmx/__init__.py index 4e098e1..3de292b 100644 --- a/src/dexorder/gmx/__init__.py +++ b/src/dexorder/gmx/__init__.py @@ -1,4 +1,4 @@ -from ._base import GMXPool, gmx_prices, gmx_tokens, gmx_pools +from ._base import gmx_prices, gmx_tk_in_flight, tk_gmx_in_flight from ._chaininfo import gmx_chain_info from ._handle import gmx_wire_runner_early, gmx_wire_runner_late from ._metadata import * diff --git a/src/dexorder/gmx/_abi.py b/src/dexorder/gmx/_abi.py index 72a8d88..7b9217e 100644 --- a/src/dexorder/gmx/_abi.py +++ b/src/dexorder/gmx/_abi.py @@ -1,16 +1,14 @@ -import itertools import logging import re -from datetime import datetime from eth_utils import keccak -from dexorder import dec, from_timestamp from dexorder.util import hexbytes, hexstr from dexorder.util.abiencode import abi_decoder log = logging.getLogger(__name__) + def no_ws(s): return re.sub(r"\s+", "", s) @@ -37,21 +35,13 @@ def topic_hash(signature): return hexstr(keccak(text=no_ws(signature))) -def parse_event_log_data(event_log_data): - """Parse GMX event log data into a structured dictionary. - - Args: - event_log_data: Raw event log data tuple containing address items, uint items, - int items, bool items, bytes32 items, bytes items and string items - - Returns: - dict: Parsed event data with typed values - """ +def parse_event_log_data(event_log): + event_log_data = event_log['data'] if type(event_log_data) is str: event_log_data = hexbytes(event_log_data) sender, event_name, event_log_data = abi_decoder.decode(('address', 'string', no_ws(EventLogDataType),), event_log_data) - result = {'sender': sender, 'event': event_name} + result = {'sender': sender, 'event': event_name, 'tx': hexstr(event_log['transactionHash'])} for items, array_items in event_log_data: for k, v in items: result[k] = v @@ -59,19 +49,3 @@ def parse_event_log_data(event_log_data): result[k] = v return result - -class OracleEvent: - - def __init__(self, event_log): - event_log_data = event_log['...'] - data = parse_event_log_data(event_log_data) - self.token: str = data['token'] - self.provider: str = data['provider'] - self.min_price: dec = dec(data['minPrice']) - self.max_price: dec = dec(data['maxPrice']) - self.time: datetime = from_timestamp(data['timestamp']) - -if __name__ == '__main__': - print(EventLogTopic) - print(EventLog1Topic) - print(EventLog2Topic) diff --git a/src/dexorder/gmx/_base.py b/src/dexorder/gmx/_base.py index 2a54bbe..faf597e 100644 --- a/src/dexorder/gmx/_base.py +++ b/src/dexorder/gmx/_base.py @@ -1,56 +1,79 @@ import logging from dataclasses import dataclass +from enum import Enum +from typing import NamedTuple import requests +from eth_utils import to_checksum_address from ._chaininfo import GMX_API_BASE_URLS from .. import dec -from ..addrmeta import address_metadata from ..base.chain import current_chain +from ..base.order import TrancheKey from ..blockstate import BlockDict -from ..database.model.token import OldTokenDict from ..util import json log = logging.getLogger(__name__) @dataclass -class GMXPool: - chain_id: int - address: str - index_token: str - long_token: str - short_token: str - min_collateral: dec - disabled: bool = False +class GMXPosition: + # compound key fields + market_token: str + collateral_token: str + is_long: bool + + # non-key attrs + size: dec = dec(0) + + class Key (NamedTuple): + market_token: str + collateral_token: str + is_long: bool + + def __str__(self): + return f'{self.market_token}|{self.collateral_token}|{"L" if self.is_long else "S"}' + + @staticmethod + def str2key(keystring: str): + market_token, collateral_token, is_long = keystring.split('|') + return GMXPosition.Key(market_token.lower(), collateral_token.lower(), is_long == 'L') @property - def max_leverage(self): - return round(1/self.min_collateral) - - @property - def is_enabled(self): - return not self.disabled - - def __bool__(self): - return self.is_enabled - - def __str__(self): - # noinspection PyTypedDict - name = address_metadata[self.index_token]['symbol'] if self.index_token in address_metadata else self.index_token - # return f'GMX:{name}' - def t(addr): - # noinspection PyTypedDict - return address_metadata[addr]['symbol'] if addr in address_metadata and address_metadata[addr] else addr - return f'GMX:{self.address} ({t(self.index_token)}) {t(self.long_token)}-{t(self.short_token)} {self.max_leverage}x' - - def __repr__(self): - return str(self) + def key(self): + return GMXPosition.Key(self.market_token, self.collateral_token, self.is_long) @staticmethod - def load(s: str): - d = json.loads(s) - return GMXPool(d['chain_id'], d['address'], d['index_token'], d['long_token'], d['short_token'], dec(d['min_collateral'])) + def load(d: dict): + return GMXPosition(to_checksum_address(d['m']), to_checksum_address(d['c']), d['l'], dec(d['s'])) + + + def dump(self): + return { + 'm': self.market_token, + 'c': self.collateral_token, + 'l': self.is_long, + 's': str(self.size), + } + + + def __hash__(self): + return hash(self.key) + + def __eq__(self, other): + return self.key == other.key + + +class GMXOrderType (Enum): + MarketSwap = 0 + LimitSwap = 1 + MarketIncrease = 2 + LimitIncrease = 3 + MarketDecrease = 4 + LimitDecrease = 5 + StopLossDecrease = 6 + Liquidation = 7 + StopIncrease = 8 GMX_API_BASE_URL = None @@ -59,11 +82,16 @@ def gmx_api(method, **params): global GMX_API_BASE_URL if GMX_API_BASE_URL is None: GMX_API_BASE_URL = GMX_API_BASE_URLS[current_chain.get().id] - return requests.get(GMX_API_BASE_URL+method, params=params).json() + return requests.get(GMX_API_BASE_URL+method, params=params, timeout=5).json() - -gmx_tokens: BlockDict[str, OldTokenDict] = BlockDict('gmx_t', redis=True) -gmx_pools: BlockDict[str, GMXPool] = BlockDict('gmx_m', redis=True, str2value=GMXPool.load) +gmx_markets_by_index_token: BlockDict[str, list[str]] = BlockDict('gmx_t_m', redis=True, db=True, value2str=lambda mks: json.dumps(mks), str2value=lambda s: json.loads(s)) gmx_prices: BlockDict[str, dec] = BlockDict('gmx_p', redis=True, str2value=dec) +# open positions by vault +gmx_positions: BlockDict[str, list[GMXPosition]] = BlockDict('gmx_pos', redis=True, db=True, + value2str=lambda positions: json.dumps([p.dump() for p in positions]), + str2value=lambda positions: [GMXPosition.load(p) for p in json.loads(positions)] ) +# dual mappings of our TrancheKey to a GMX Order key exist only when a GMX order has been placed but not yet handled +gmx_tk_in_flight: BlockDict[str, TrancheKey] = BlockDict('gmx_tif', db=True, str2value=TrancheKey.str2key) +tk_gmx_in_flight: BlockDict[TrancheKey, str] = BlockDict('tk2gmx', db=True, str2key=TrancheKey.str2key) diff --git a/src/dexorder/gmx/_chaininfo.py b/src/dexorder/gmx/_chaininfo.py index 0c820d4..35a0f67 100644 --- a/src/dexorder/gmx/_chaininfo.py +++ b/src/dexorder/gmx/_chaininfo.py @@ -11,5 +11,6 @@ gmx_chain_info = { } GMX_API_BASE_URLS={ - 42161: 'https://arbitrum-api.gmxinfra.io/' + 31337: 'https://arbitrum-api.gmxinfra.io/', + 42161: 'https://arbitrum-api.gmxinfra.io/', } diff --git a/src/dexorder/gmx/_contract.py b/src/dexorder/gmx/_contract.py index d4d06f7..ed2c451 100644 --- a/src/dexorder/gmx/_contract.py +++ b/src/dexorder/gmx/_contract.py @@ -2,14 +2,23 @@ import logging from functools import cache from dexorder.contract import ContractProxy +from dexorder.gmx._datastore import DataStore from dexorder.util import json log = logging.getLogger(__name__) -@cache -def get_gmx_contract(name: str): +def get_gmx_contract_info(name: str): with open(f'./resource/abi/42161/gmx/{name}.json') as file: info = json.load(file) - return ContractProxy(info['address'], abi=info['abi']) + return info + +@cache +def get_gmx_contract(name: str): + info = get_gmx_contract_info(name) + if name == 'DataStore': + clazz = DataStore + else: + clazz = ContractProxy + return clazz(info['address'], abi=info['abi']) diff --git a/src/dexorder/gmx/_datastore.py b/src/dexorder/gmx/_datastore.py index 83a464b..cd7f796 100644 --- a/src/dexorder/gmx/_datastore.py +++ b/src/dexorder/gmx/_datastore.py @@ -2,6 +2,8 @@ import logging from eth_utils import keccak +from dexorder import dec +from dexorder.contract import ContractProxy from dexorder.util.abiencode import abi_encoder log = logging.getLogger(__name__) @@ -14,5 +16,13 @@ IS_MARKET_DISABLED_KEY = 'IS_MARKET_DISABLED' MIN_COLLATERAL_FACTOR_KEY = 'MIN_COLLATERAL_FACTOR' -async def is_market_disabled(ds, market): - return await ds.getBool(combo_key(IS_MARKET_DISABLED_KEY, market.address)) +class DataStore (ContractProxy): + + async def is_market_disabled(self, market_addr: str): + return await self.getBool(combo_key(IS_MARKET_DISABLED_KEY, market_addr)) + + async def min_collateral_factor(self, market_addr: str): + result = await self.getUint(combo_key(MIN_COLLATERAL_FACTOR_KEY, market_addr)) + if result == 0: + log.warning(f'no min collateral factor for market {market_addr}') + return 2 * dec(result) / dec(1e30) diff --git a/src/dexorder/gmx/_error.py b/src/dexorder/gmx/_error.py new file mode 100644 index 0000000..47c5e1e --- /dev/null +++ b/src/dexorder/gmx/_error.py @@ -0,0 +1,292 @@ +import logging + +from dexorder.util.abiencode import abi_decoder + +log = logging.getLogger(__name__) +gmx_error_map = { + 'b244a107': 'ActionAlreadySignalled()', + '94fdaea2': 'ActionNotSignalled()', + '3285dc57': 'AdlNotEnabled()', + 'd06ed8be': 'AdlNotRequired(int256,uint256)', + '70657e04': 'ArrayOutOfBoundsBytes(bytes[],uint256,string)', + '9d18e63b': 'ArrayOutOfBoundsUint256(uint256[],uint256,string)', + '60c5e472': 'AvailableFeeAmountIsZero(address,address,uint256)', + '11aeaf6b': 'BlockNumbersNotSorted(uint256,uint256)', + 'ec775484': 'BuybackAndFeeTokenAreEqual(address,address)', + 'd6b52b60': 'ChainlinkPriceFeedNotUpdated(address,uint256,uint256)', + 'ec6d89c8': 'CollateralAlreadyClaimed(uint256,uint256)', + 'bdec9c0d': 'CompactedArrayOutOfBounds(uint256[],uint256,uint256,string)', + '5ebb87c9': 'ConfigValueExceedsAllowedRange(bytes32,uint256)', + '413f9a54': 'DataStreamIdAlreadyExistsForToken(address)', + '83f2ba20': 'DeadlinePassed(uint256,uint256)', + '43e30ca8': 'DepositNotFound(bytes32)', + 'dd70e0c9': 'DisabledFeature(bytes32)', + '09f8c937': 'DisabledMarket(address)', + 'd4064737': 'DuplicatedIndex(uint256,string)', + '91c78b78': 'DuplicatedMarketInSwapPath(address)', + 'dd7016a2': 'EmptyAccount()', + 'e474a425': 'EmptyAddressInMarketTokenBalanceValidation(address,address)', + '52dfddfd': 'EmptyChainlinkPaymentToken()', + '8db88ccf': 'EmptyChainlinkPriceFeed(address)', + 'b86fffef': 'EmptyChainlinkPriceFeedMultiplier(address)', + '616daf1f': 'EmptyClaimFeesMarket()', + '62e402cc': 'EmptyDataStreamFeedId(address)', + '088405c6': 'EmptyDataStreamMultiplier(address)', + '95b66fe9': 'EmptyDeposit()', + '01af8c24': 'EmptyDepositAmounts()', + 'd1c3d5bd': 'EmptyDepositAmountsAfterSwap()', + 'a14e1b3d': 'EmptyGlv(address)', + 'bd192971': 'EmptyGlvDeposit()', + '03251ce6': 'EmptyGlvDepositAmounts()', + '94409f52': 'EmptyGlvMarketAmount()', + '93856b1a': 'EmptyGlvTokenSupply()', + '0e5be78f': 'EmptyGlvWithdrawal()', + '402a866f': 'EmptyGlvWithdrawalAmount()', + 'e9b78bd4': 'EmptyHoldingAddress()', + '05fbc1ae': 'EmptyMarket()', + 'eb1947dd': 'EmptyMarketPrice(address)', + '2ee3d69c': 'EmptyMarketTokenSupply()', + '16307797': 'EmptyOrder()', + '4dfbbff3': 'EmptyPosition()', + 'cd64a025': 'EmptyPrimaryPrice(address)', + 'd551823d': 'EmptyReceiver()', + '6af5e96f': 'EmptyShift()', + '60d5e84a': 'EmptyShiftAmount()', + '3df42531': 'EmptySizeDeltaInTokens()', + '9fc297fa': 'EmptyTokenTranferGasLimit(address)', + '9231be69': 'EmptyValidatedPrices()', + '6d4bb5e9': 'EmptyWithdrawal()', + '01d6f7b1': 'EmptyWithdrawalAmount()', + '4e48dcda': 'EndOfOracleSimulation()', + '59afd6c6': 'ExternalCallFailed(bytes)', + '2df6dc23': 'FeeBatchNotFound(bytes32)', + 'e44992d0': 'GlvAlreadyExists(bytes32,address)', + '057058b6': 'GlvDepositNotFound(bytes32)', + '30b8a225': 'GlvDisabledMarket(address,address)', + '8da31161': 'GlvEnabledMarket(address,address)', + 'c8b70b2c': 'GlvInsufficientMarketTokenBalance(address,address,uint256,uint256)', + '80ad6831': 'GlvInvalidLongToken(address,address,address)', + '9673a10b': 'GlvInvalidShortToken(address,address,address)', + '3aa9fc91': 'GlvMarketAlreadyExists(address,address)', + 'af7d3787': 'GlvMaxMarketCountExceeded(address,uint256)', + 'd859f947': 'GlvMaxMarketTokenBalanceAmountExceeded(address,address,uint256,uint256)', + '66560e7d': 'GlvMaxMarketTokenBalanceUsdExceeded(address,address,uint256,uint256)', + '155712e1': 'GlvNameTooLong()', + '2e3780e5': 'GlvNegativeMarketPoolValue(address,address)', + '3afc5e65': 'GlvNonZeroMarketBalance(address,address)', + '6c00ed8a': 'GlvNotFound(address)', + '232d7165': 'GlvShiftIntervalNotYetPassed(uint256,uint256,uint256)', + 'c906a05a': 'GlvShiftMaxPriceImpactExceeded(uint256,uint256)', + 'de45e162': 'GlvShiftNotFound(bytes32)', + '9cb4f5c5': 'GlvSymbolTooLong()', + '07e9c4d5': 'GlvUnsupportedMarket(address,address)', + '20dcb068': 'GlvWithdrawalNotFound(bytes32)', + 'd90abe06': 'GmEmptySigner(uint256)', + 'ee6e8ecf': 'GmInvalidBlockNumber(uint256,uint256)', + 'b8aaa455': 'GmInvalidMinMaxBlockNumber(uint256,uint256)', + 'c7b44b28': 'GmMaxOracleSigners(uint256,uint256)', + '0f885e52': 'GmMaxPricesNotSorted(address,uint256,uint256)', + '5b1250e7': 'GmMaxSignerIndex(uint256,uint256)', + 'dc2a99e7': 'GmMinOracleSigners(uint256,uint256)', + 'cc7bbd5b': 'GmMinPricesNotSorted(address,uint256,uint256)', + 'a581f648': 'InsufficientBuybackOutputAmount(address,address,uint256,uint256)', + '74cc815b': 'InsufficientCollateralAmount(uint256,int256)', + '2159b161': 'InsufficientCollateralUsd(int256)', + '5dac504d': 'InsufficientExecutionFee(uint256,uint256)', + 'bb416f93': 'InsufficientExecutionGas(uint256,uint256,uint256)', + '79293964': 'InsufficientExecutionGasForErrorHandling(uint256,uint256)', + '19d50093': 'InsufficientFundsToPayForCosts(uint256,string)', + 'd3dacaac': 'InsufficientGasForCancellation(uint256,uint256)', + '79a2abad': 'InsufficientGasLeftForCallback(uint256,uint256)', + '3083b9e5': 'InsufficientHandleExecutionErrorGas(uint256,uint256)', + '82c8828a': 'InsufficientMarketTokens(uint256,uint256)', + 'd28d3eb5': 'InsufficientOutputAmount(uint256,uint256)', + '23090a31': 'InsufficientPoolAmount(uint256,uint256)', + '9cd76295': 'InsufficientRelayFee(uint256,uint256)', + '315276c9': 'InsufficientReserve(uint256,uint256)', + 'b98c6179': 'InsufficientReserveForOpenInterest(uint256,uint256)', + 'a7aebadc': 'InsufficientSwapOutputAmount(uint256,uint256)', + '041b3483': 'InsufficientWntAmount(uint256,uint256)', + '3a78cd7e': 'InsufficientWntAmountForExecutionFee(uint256,uint256)', + '1d4fc3c0': 'InvalidAdl(int256,int256)', + '8ac146e6': 'InvalidAmountInForFeeBatch(uint256,uint256)', + 'eb19d3f5': 'InvalidBaseKey(bytes32)', + '25e5dc07': 'InvalidBlockRangeSet(uint256,uint256)', + '752fdb63': 'InvalidBuybackToken(address)', + '89736584': 'InvalidCancellationReceiverForSubaccountOrder(address,address)', + '5b3043dd': 'InvalidClaimAffiliateRewardsInput(uint256,uint256)', + '42c0d1f2': 'InvalidClaimCollateralInput(uint256,uint256,uint256)', + '7363cfa5': 'InvalidClaimFundingFeesInput(uint256,uint256)', + '74cee48d': 'InvalidClaimUiFeesInput(uint256,uint256)', + '6c2738d3': 'InvalidClaimableFactor(uint256)', + '839c693e': 'InvalidCollateralTokenForMarket(address,address)', + '4a591309': 'InvalidContributorToken(address)', + '8d56bea1': 'InvalidDataStreamBidAsk(address,int192,int192)', + 'a4949e25': 'InvalidDataStreamFeedId(address,bytes32,bytes32)', + '2a74194d': 'InvalidDataStreamPrices(address,int192,int192)', + '6e0c29ed': 'InvalidDataStreamSpreadReductionFactor(address,uint256)', + '9fbe2cbc': 'InvalidDecreaseOrderSize(uint256,uint256)', + '751951f9': 'InvalidDecreasePositionSwapType(uint256)', + '9b867f31': 'InvalidExecutionFee(uint256,uint256,uint256)', + '99e26b44': 'InvalidExecutionFeeForMigration(uint256,uint256)', + '831e9f11': 'InvalidExternalCallInput(uint256,uint256)', + 'be55c895': 'InvalidExternalCallTarget(address)', + 'e15f2701': 'InvalidExternalReceiversInput(uint256,uint256)', + 'fa804399': 'InvalidFeeBatchTokenIndex(uint256,uint256)', + 'cb9339d5': 'InvalidFeeReceiver(address)', + 'be6514b6': 'InvalidFeedPrice(address,int256)', + 'fc90fcc3': 'InvalidGlpAmount(uint256,uint256)', + 'bf16cb0a': 'InvalidGlvDepositInitialLongToken(address)', + 'df0f9a23': 'InvalidGlvDepositInitialShortToken(address)', + '055ab8b9': 'InvalidGlvDepositSwapPath(uint256,uint256)', + '993417d5': 'InvalidGmMedianMinMaxPrice(uint256,uint256)', + 'a54d4339': 'InvalidGmOraclePrice(address)', + '8d648a7f': 'InvalidGmSignature(address,address)', + 'b21c863e': 'InvalidGmSignerMinMaxPrice(uint256,uint256)', + 'e5feddc0': 'InvalidKeeperForFrozenOrder(address)', + '33a1ea6b': 'InvalidMarketTokenBalance(address,address,uint256,uint256)', + '9dd026db': 'InvalidMarketTokenBalanceForClaimableFunding(address,address,uint256,uint256)', + '808c464f': 'InvalidMarketTokenBalanceForCollateralAmount(address,address,uint256,uint256)', + 'c08bb8a0': 'InvalidMinGlvTokensForFirstGlvDeposit(uint256,uint256)', + '3f9c06ab': 'InvalidMinMarketTokensForFirstDeposit(uint256,uint256)', + '1608d41a': 'InvalidMinMaxForPrice(address,uint256,uint256)', + 'e71a51be': 'InvalidNativeTokenSender(address)', + '05d102a2': 'InvalidOracleProvider(address)', + '68b49e6c': 'InvalidOracleProviderForToken(address,address)', + 'f9996e9f': 'InvalidOracleSetPricesDataParam(uint256,uint256)', + 'dd51dc73': 'InvalidOracleSetPricesProvidersParam(uint256,uint256)', + 'c1b14c91': 'InvalidOracleSigner(address)', + '0481a15a': 'InvalidOrderPrices(uint256,uint256,uint256,uint256)', + '253c8c02': 'InvalidOutputToken(address,address)', + '3c0ac199': 'InvalidPermitSpender(address,address)', + 'adaa688d': 'InvalidPoolValueForDeposit(int256)', + '90a6af3b': 'InvalidPoolValueForWithdrawal(int256)', + '182e30e3': 'InvalidPositionMarket(address)', + 'bff65b3f': 'InvalidPositionSizeValues(uint256,uint256)', + '663de023': 'InvalidPrimaryPricesForSimulation(uint256,uint256)', + '9cfea583': 'InvalidReceiver(address)', + '77e8e698': 'InvalidReceiverForFirstDeposit(address,address)', + '6eedac2f': 'InvalidReceiverForFirstGlvDeposit(address,address)', + '4baab816': 'InvalidReceiverForSubaccountOrder(address,address)', + '370abac2': 'InvalidRelayParams()', + '530b2590': 'InvalidSetContributorPaymentInput(uint256,uint256)', + '29a93dc4': 'InvalidSetMaxTotalContributorTokenAmountInput(uint256,uint256)', + '2a34f7fe': 'InvalidSignature(string)', + '720bb461': 'InvalidSizeDeltaForAdl(uint256,uint256)', + '3044992f': 'InvalidSubaccountApprovalNonce(uint256,uint256)', + '545e8f2b': 'InvalidSubaccountApprovalSubaccount()', + 'cb9bd134': 'InvalidSwapMarket(address)', + '6ba3dd8b': 'InvalidSwapOutputToken(address,address)', + '672e4fba': 'InvalidSwapPathForV1(address[],address)', + 'e6b0ddb6': 'InvalidTimelockDelay(uint256)', + '53f81711': 'InvalidTokenIn(address,address)', + '81468139': 'InvalidUiFeeFactor(uint256,uint256)', + 'f3d06236': 'InvalidUserNonce(uint256,uint256)', + '1de2bca4': 'InvalidVersion(uint256)', + 'bc121108': 'LiquidatablePosition(string,int256,int256,int256)', + 'a38dfb2a': 'LongTokensAreNotEqual(address,address)', + '25e34fa1': 'MarketAlreadyExists(bytes32,address)', + '6918f9bf': 'MarketNotFound(address)', + '143e2156': 'MaskIndexOutOfBounds(uint256,string)', + 'f0794a60': 'MaxAutoCancelOrdersExceeded(uint256,uint256)', + '4e3f62a8': 'MaxBuybackPriceAgeExceeded(uint256,uint256,uint256)', + '10aeb692': 'MaxCallbackGasLimitExceeded(uint256,uint256)', + '4f82a998': 'MaxFundingFactorPerSecondLimitExceeded(uint256,uint256)', + '2bf127cf': 'MaxOpenInterestExceeded(uint256,uint256)', + 'dd9c6b9a': 'MaxOracleTimestampRangeExceeded(uint256,uint256)', + '6429ff3f': 'MaxPoolAmountExceeded(uint256,uint256)', + '46169f04': 'MaxPoolUsdForDepositExceeded(uint256,uint256)', + '2b6e7c3f': 'MaxPriceAgeExceeded(uint256,uint256)', + '3d1986f7': 'MaxRefPriceDeviationExceeded(address,uint256,uint256,uint256)', + '519ba753': 'MaxSubaccountActionCountExceeded(address,address,uint256,uint256)', + '9da36043': 'MaxSwapPathLengthExceeded(uint256,uint256)', + 'faf66f0c': 'MaxTimelockDelayExceeded(uint256)', + 'c10ceac7': 'MaxTotalCallbackGasLimitForAutoCancelOrdersExceeded(uint256,uint256)', + '043038f0': 'MaxTotalContributorTokenAmountExceeded(address,uint256,uint256)', + '961b4025': 'MinContributorPaymentIntervalBelowAllowedRange(uint256)', + 'b9dc7b9d': 'MinContributorPaymentIntervalNotYetPassed(uint256)', + '966fea10': 'MinGlvTokens(uint256,uint256)', + 'f442c0bc': 'MinLongTokens(uint256,uint256)', + '6ce23460': 'MinMarketTokens(uint256,uint256)', + '85efb31a': 'MinPositionSize(uint256,uint256)', + 'b4a196af': 'MinShortTokens(uint256,uint256)', + 'cc32db99': 'NegativeExecutionPrice(int256,uint256,uint256,int256,uint256)', + '53410c43': 'NonAtomicOracleProvider(address)', + '28f773e9': 'NonEmptyExternalCallsForSubaccountOrder()', + 'ef2df9b5': 'NonEmptyTokensWithPrices(uint256)', + '730293fd': 'OpenInterestCannotBeUpdatedForSwapOnlyMarket(address)', + '8cf95e58': 'OracleProviderAlreadyExistsForToken(address)', + 'd84b8ee8': 'OracleTimestampsAreLargerThanRequestExpirationTime(uint256,uint256,uint256)', + '7d677abf': 'OracleTimestampsAreSmallerThanRequired(uint256,uint256)', + '730d44b1': 'OrderAlreadyFrozen()', + '59485ed9': 'OrderNotFound(bytes32)', + 'e09ad0e9': 'OrderNotFulfillableAtAcceptablePrice(uint256,uint256)', + '9aba92cb': 'OrderNotUpdatable(uint256)', + '8a4bd513': 'OrderTypeCannotBeCreated(uint256)', + 'cf9319d6': 'OrderValidFromTimeNotReached(uint256,uint256)', + 'b92fb250': 'PnlFactorExceededForLongs(int256,uint256)', + 'b0010694': 'PnlFactorExceededForShorts(int256,uint256)', + '9f0bc7de': 'PnlOvercorrected(int256,uint256)', + '426cfff0': 'PositionNotFound(bytes32)', + 'ee919dd9': 'PositionShouldNotBeLiquidated(string,int256,int256,int256)', + 'ded099de': 'PriceAlreadySet(address,uint256,uint256)', + 'd4141298': 'PriceFeedAlreadyExistsForToken(address)', + 'f0641c92': 'PriceImpactLargerThanOrderSize(int256,uint256)', + 'e8266438': 'RequestNotYetCancellable(uint256,uint256,string)', + 'e70f9152': 'SelfTransferNotSupported(address)', + '032b3d00': 'SequencerDown()', + '113cfc03': 'SequencerGraceDurationNotYetPassed(uint256,uint256)', + '950227bb': 'ShiftFromAndToMarketAreEqual(address)', + 'b611f297': 'ShiftNotFound(bytes32)', + 'f54d8776': 'ShortTokensAreNotEqual(address,address)', + '20b23584': 'SignalTimeNotYetPassed(uint256)', + '26025b4e': 'SubaccountApprovalDeadlinePassed(uint256,uint256)', + '9b539f07': 'SubaccountApprovalExpired(address,address,uint256,uint256)', + '9be0a43c': 'SubaccountNotAuthorized(address,address)', + '75885d69': 'SwapPriceImpactExceedsAmountIn(uint256,int256)', + 'd2e229e6': 'SwapsNotAllowedForAtomicWithdrawal(uint256,uint256)', + '7bf8d2b3': 'SyncConfigInvalidInputLengths(uint256,uint256)', + '624b5b13': 'SyncConfigInvalidMarketFromData(address,address)', + '8b3d4655': 'SyncConfigUpdatesDisabledForMarket(address)', + '0798d283': 'SyncConfigUpdatesDisabledForMarketParameter(address,string)', + '8ea7eb18': 'SyncConfigUpdatesDisabledForParameter(string)', + 'b783c88a': 'ThereMustBeAtLeastOneRoleAdmin()', + '282b5b70': 'ThereMustBeAtLeastOneTimelockMultiSig()', + '979dc780': 'TokenTransferError(address,address,uint256)', + '0e92b837': 'Uint256AsBytesLengthExceeds32Bytes(uint256)', + '6afad778': 'UnableToGetBorrowingFactorEmptyPoolUsd()', + 'be4729a2': 'UnableToGetCachedTokenPrice(address,address)', + '11423d95': 'UnableToGetFundingFactorEmptyOpenInterest()', + '7a0ca681': 'UnableToGetOppositeToken(address,address)', + '3a61a4a9': 'UnableToWithdrawCollateral(int256)', + 'a35b150b': 'Unauthorized(address,string)', + '99b2d582': 'UnexpectedBorrowingFactor(uint256,uint256)', + 'cc3459ff': 'UnexpectedMarket()', + '3b42e952': 'UnexpectedPoolValue(int256)', + '814991c3': 'UnexpectedPositionState()', + 'e949114e': 'UnexpectedRelayFeeToken(address,address)', + 'a9721241': 'UnexpectedRelayFeeTokenAfterSwap(address,address)', + '785ee469': 'UnexpectedTokenForVirtualInventory(address,address)', + '3af14617': 'UnexpectedValidFromTime(uint256)', + '3784f834': 'UnsupportedOrderType(uint256)', + '0d0fcc0b': 'UnsupportedRelayFeeToken(address,address)', + 'eadaf93a': 'UsdDeltaExceedsLongOpenInterest(int256,uint256)', + '2e949409': 'UsdDeltaExceedsPoolValue(int256,uint256)', + '8af0d140': 'UsdDeltaExceedsShortOpenInterest(int256,uint256)', + '60737bc0': 'WithdrawalNotFound(bytes32)', +} +gmx_error_map = {bytes.fromhex(k):v for k,v in gmx_error_map.items()} + + +def gmx_parse_reason_bytes(e: bytes) -> str: + sig_bytes = e[:4] + sig = gmx_error_map.get(e) + if sig is None: + return f'Unknown GMX error {e.hex()}' + name, types = sig.split('(',1) + types = types[:-1] + if len(e) > 4: + data = e[4:] + values = abi_decoder.decode(types.split(','), data) + return f'{name}({",".join(map(str, values))})' + return name diff --git a/src/dexorder/gmx/_handle.py b/src/dexorder/gmx/_handle.py index 2b15e18..ba3b5b7 100644 --- a/src/dexorder/gmx/_handle.py +++ b/src/dexorder/gmx/_handle.py @@ -1,118 +1,295 @@ import asyncio import logging +from copy import copy from datetime import timedelta +from eth_utils import to_checksum_address +from web3.types import EventData + from ._abi import parse_event_log_data -from ._base import gmx_api +from ._base import GMXPosition, gmx_positions, GMXOrderType, gmx_tk_in_flight, tk_gmx_in_flight, gmx_api, \ + gmx_markets_by_index_token from ._chaininfo import gmx_chain_info -from ._metadata import gmx_update_metadata, gmx_token_symbol_map +from ._error import gmx_parse_reason_bytes +from ._metadata import gmx_update_metadata from .. import dec, from_timestamp +from ..addrmeta import address_metadata +from ..base import OldTokenDict, OldGMXDict +from ..base.chain import current_chain +from ..base.order import TrancheKey +from ..contract import get_contract_event +from ..contract.dexorder import get_dexorder_contract from ..event_handler import update_pool_price from ..final_ohlc import FinalOHLCRepository from ..ohlc import period_name from ..periodic import periodic from ..progressor import BlockProgressor from ..tokens import get_token +from ..util import hexstr +from ..util.async_util import maywait log = logging.getLogger(__name__) +def gmx_wire_runner_init(runner: BlockProgressor): + pass -def gmx_wire_runner_early(runner: BlockProgressor, backfill: FinalOHLCRepository=None): - runner.add_event_trigger(gmx_update_metadata) - runner.add_event_trigger(create_backfill_handler(backfill) if backfill else gmx_handle_price_update) - runner.add_event_trigger(handle_gmx_events, log_filter={'address':gmx_chain_info[42161]['EventEmitter'], }) +def gmx_wire_runner_early(runner: BlockProgressor, backfill: FinalOHLCRepository = None): + runner.add_event_trigger(handle_gmxcallbackerror_event, get_contract_event('GMXCallbackHandler', 'GMXCallbackError')) + runner.add_callback(gmx_handle_metadata_update) + if backfill is not None: + runner.add_callback(create_backfill_handler(backfill) if backfill else gmx_update_prices) + runner.add_event_trigger(handle_gmx_events, log_filter={'address':gmx_chain_info[current_chain.get().id]['EventEmitter'], }) + runner.add_event_trigger(handle_gmxorderplaced, get_contract_event('GMX', 'GMXOrderPlaced')) def gmx_wire_runner_late(runner: BlockProgressor): pass +def handle_gmxcallbackerror_event(event: EventData): + log.error(f'GMX callback error {event["args"]["reason"]}') -def handle_marketpoolvalueupdated_event(event: dict): - # log.info(f'marketpoolvalueupdated: {event}') - # { - # 'sender': '0x3f6df0c3a7221ba1375e87e7097885a601b41afc', - # 'event': 'MarketPoolValueUpdated', - # 'market': '0x3680d7bfe9260d3c5de81aeb2194c119a59a99d1', - # 'longTokenAmount': 19476307269091870091, - # 'shortTokenAmount': 50700920349, - # 'longTokenUsd': 53158489854101648408924993481922790, - # 'shortTokenUsd': 50693714261673382360507024950000000, - # 'totalBorrowingFees': 41601639378800206440170070581268, - # 'borrowingFeePoolFactor': 630000000000000000000000000000, - # 'impactPoolAmount': 3152510497, - # 'marketTokensSupply': 93026785041268146396614, - # 'poolValue': 102896287811364604212637168767403111, - # 'longPnl': 142313435006510688238425736783147, - # 'shortPnl': -19431172516799270439150252797270, - # 'netPnl': 122882262489711417799275483985877, - # 'actionType': b'`y\x91\xfcYc\xe2d\xf1\xa9O\xaa\x12lcH/\xdcZ\xf1Jeo\x08u\x1d\xc8\xb0\xc5\xd4v0', - # 'tradeKey': b'Z]Qws\xaf\x115\x89\x85\xdd\xce)\x93t\xc4C\xccx{\x85N\xa0\x17B\x99r#\xd9~\x94\xd1' - # } +# GMX orders wait on-chain a few blocks before the GMX Handlers execute or cancel them. Also, liquidation orders can +# occur without any associated vault order. Therefore, we take the following approach: +# +# When orders are placed, a GMXOrderPlaced event is emitted alongside the DexorderSwapPlaced event, providing a mapping +# between vault tranche keys and GMX order keys, as well as an in-flight locking mechanism in both the vault and +# backend. In a few blocks' time, the GMX Handlers will deal with the order and emit an OrderCreated or OrderCancelled +# event in addition to invoking the corresponding callback method on the vault, which unlocks the tranche, adjusts +# rate limits, and emits the regular DexorderSwapFilled event, using amountOut as the USD amount filled and amountIn +# as the "price," a virtual amount calculated to make the execution price equal amountOut/amountIn, matching the format +# for non-inverted swaps. +# +# Therefore, the regular backend triggers and fill records act normally on GMX orders without modification. +# +# The backend in-flight lock and tranche-key to gmx-order-key mapping is maintained in gmx_in_flight using a vault event +# to open and a GMX order event to close. +# +# The Position object is maintained by watching GMX PositionIncrease and PositionDecrease events, which capture +# liquidations as well as vault-initiated orders to accurately maintain the Position state. - pass + +def invalid_vault(vault): + # return vault not in vault_owners + return False # todo debug + + +# +# GMXOrderPlaced along with OrderCancelled and OrderExecuted maintain the gmx_in_flight lock and mapping to a tranche key +# + +def handle_gmxorderplaced(event: EventData): + # This is emitted alongside the DexorderSwapPlaced event in order to provide additional information for GMX. + # event GMXOrderPlaced(uint64 orderIndex, uint8 trancheIndex, bytes32 gmxOrderKey); + log.info(f'GMXOrderPlaced {event}') + vault = event['address'] + if invalid_vault(vault): + return + order_index = event['args']['orderIndex'] + tranche_index = event['args']['trancheIndex'] + gmx_order_key = event['args']['gmxOrderKey'] + # register the gmx order key as in-flight + keystr = hexstr(gmx_order_key) + tk = TrancheKey(vault, order_index, tranche_index) + # start gmx in flight. see end_gmx_in_flight() + gmx_tk_in_flight[keystr] = tk + tk_gmx_in_flight[tk] = keystr + + +def handle_ordercancelled_event(event: dict, data: dict): + log.info(f'GMX order cancelled {data}') + vault = data['account'] + if invalid_vault(vault): + return + reason = gmx_parse_reason_bytes(data['reasonBytes']) + gmx_order_key = data['key'] + if gmx_order_key not in gmx_tk_in_flight: + log.warning(f'GMX order cancelled but not in flight: {gmx_order_key}') + return + end_gmx_in_flight(gmx_order_key) + log.info(f'GMX order cancelled due to {reason} in tx {data['tx']}') + + +def handle_orderexecuted_event(event: dict, data: dict): + log.info(f'GMX order executed {data}') + vault = data['account'] + if invalid_vault(vault): + return + gmx_order_key = data['key'] + if gmx_order_key not in gmx_tk_in_flight: + # todo handle liquidation either here or with PositionDecrease events + log.warning(f'GMX order executed but not in flight: {gmx_order_key}') + return + end_gmx_in_flight(gmx_order_key) + + +def end_gmx_in_flight(gmx_order_key): + gmx_order_key = hexstr(gmx_order_key) + tk = gmx_tk_in_flight[gmx_order_key] + del gmx_tk_in_flight[gmx_order_key] + del tk_gmx_in_flight[tk] + +# +# GMXPositionIncrease and GMXPositionDecrease events maintain our Position records +# + +def handle_position_event(event: dict, data: dict, is_increase: bool): + log.info(f'GMX position {"increase" if is_increase else "decrease"} {event}') + # {'account': '0xdfc16a4247677d723d897aa4fe865a02f5d78746', + # 'borrowingFactor': 250545812647447573795593810338, + # 'collateralAmount': 1019200, + # 'collateralDeltaAmount': 1019200, + # 'collateralToken': '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + # 'collateralTokenPrice.max': 999856563986601850000000, + # 'collateralTokenPrice.min': 999856563986601850000000, + # 'event': 'PositionIncrease', + # 'executionPrice': 3816407734365198, + # 'fundingFeeAmountPerSize': 430546959972637644839, + # 'increasedAtTime': 1753748680, + # 'indexTokenPrice.max': 3817347116613155, + # 'indexTokenPrice.min': 3817347116613155, + # 'isLong': True, + # 'longTokenClaimableFundingAmountPerSize': 4117446384759965489999004204, + # 'market': '0x70d95587d40a2caf56bd97485ab3eec10bee6336', + # 'orderKey': b'2\xe6\x8a\x07\xe9x\x839\x8f\xdd\xd5j\x16\x88\x80\xff[HY\xadk\x0f\xb4n3\xfe\xa2.\xd6\x97\x90\x9b', + # 'orderType': 2, + # 'positionKey': b"\xa8r\xc6\xcf^\x89\xf8k\xfa='\xe9\x19\x12\x11\xb8|;k3Df8\xee^\x9a\x9f)\xef8\x8c\x86", + # 'priceImpactAmount': 128960267235, + # 'priceImpactUsd': 492286104290598018742093888, + # 'sender': '0xe68caaacdf6439628dfd2fe624847602991a31eb', + # 'shortTokenClaimableFundingAmountPerSize': 7250294981528901831, + # 'sizeDeltaInTokens': 524053020328728, + # 'sizeDeltaUsd': 2000000000000000000000000000000, + # 'sizeInTokens': 524053020328728, + # 'sizeInUsd': 2000000000000000000000000000000, + # 'tx': '0x74e3aee1e4a92d3fe4e05d8050197c080c51dc0170ac12e8e90dbbe9fb3cc4b5'} + + vault = to_checksum_address(data['account']) + if invalid_vault(vault): + return + order_type = GMXOrderType(data['orderType']) + gmx_order_key = data['orderKey'] + is_long = data['isLong'] + size_delta = data['sizeDeltaUsd'] + size = data['sizeInUsd'] + market = data['market'] + collateral_token = data['collateralToken'] + collateral_amount = data['collateralAmount'] + collateral_delta = data['collateralDeltaAmount'] + price = data['executionPrice'] + + key = GMXPosition.Key(market, collateral_token, is_long) + positions = gmx_positions.get(vault) + pos = GMXPosition(key.market_token, key.collateral_token, key.is_long) + if positions is None: + positions = [pos] + else: + positions = list(positions) + if pos in positions: + old = [p for p in positions if p==pos][0] + positions.remove(old) + pos = copy(old) + positions.append(pos) + buy = is_long == is_increase + if buy: + if -size_delta < pos.size < 0: + log.error(f'GMX short position becoming positive: {pos} + {size_delta}') + pos.size += size_delta + else: + if 0 < pos.size < size_delta: + log.error(f'GMX long position becoming negative: {pos} - {size_delta}') + pos.size -= size_delta + if pos.size != size: + log.error(f'GMX position size mismatch: {pos} != {size}') + if not pos.size: + positions.remove(pos) + if not positions: + del gmx_positions[vault] + else: + gmx_positions[vault] = positions + + + # todo DANNY: if a position is liquidated, should I cancel pending orders in that market? + + +def handle_positionincrease_event(event: dict, data: dict): + handle_position_event(event, data, True) + +def handle_positiondecrease_event(event: dict, data: dict): + handle_position_event(event, data, False) + +# def handle_depositcreated_event(event: dict, data: dict): +# log.info(f'GMX deposit created {event}') +# +# def handle_depositexecuted_event(event: dict, data: dict): +# log.info(f'GMX deposit executed {event}') +# +# def handle_withdrawalcreated_event(event: dict, data: dict): +# log.info(f'GMX withdrawal created {event}') +# +# def handle_withdrawalexecuted_event(event: dict, data: dict): +# log.info(f'GMX withdrawal executed {event}') event_handlers = { 'OraclePriceUpdate': None, 'MarketPoolValueInfo': None, - 'MarketPoolValueUpdated': handle_marketpoolvalueupdated_event, - - 'OrderCreated': None, - 'OrderUpdated': None, - 'OrderCancelled': None, - 'OrderExecuted': None, - 'OrderCollateralDeltaAmountAutoUpdated': None, - - 'PositionIncrease': None, - 'PositionDecrease': None, - 'PositionFeesCollected': None, - - 'OpenInterestInTokensUpdated': None, - 'OpenInterestUpdated': None, - - 'CollateralSumUpdated': None, - - 'ClaimableFeeAmountUpdated': None, - 'ClaimableFundingUpdated': None, - 'ClaimableFundingAmountPerSizeUpdated': None, - 'FundingFeesClaimed': None, - - 'FeesClaimed': None, - 'SetAvailableFeeAmount': None, - 'BuybackFees': None, - 'OrderSizeDeltaAutoUpdated': None, - 'SubaccountAutoTopUp': None, - 'SetSubaccountAutoTopUpAmount': None, - 'SetMaxAllowedSubaccountActionCount': None, - 'AffiliateRewardClaimed': None, - 'CollateralClaimed': None, - 'ExecutionFeeRefundCallback': None, - - 'PoolAmountUpdated': None, - - 'VirtualSwapInventoryUpdated': None, - 'SwapInfo': None, - 'SwapFeesCollected': None, - - 'SwapImpactPoolAmountUpdated': None, - 'PositionImpactPoolAmountUpdated': None, - 'VirtualPositionInventoryUpdated': None, - - 'CumulativeBorrowingFactorUpdated': None, - - 'KeeperExecutionFee': None, - 'ExecutionFeeRefund': None, - 'FundingFeeAmountPerSizeUpdated': None, - - 'SetUint': None, - # SetBytes32 presumably and others... - 'SyncConfig': None, + 'MarketPoolValueUpdated': None, 'DepositCreated': None, 'DepositExecuted': None, 'WithdrawalCreated': None, 'WithdrawalExecuted': None, + + 'OrderCreated': None, + 'OrderUpdated': None, + 'OrderCancelled': handle_ordercancelled_event, + 'OrderExecuted': handle_orderexecuted_event, + 'OrderSizeDeltaAutoUpdated': None, # ADL? + 'OrderCollateralDeltaAmountAutoUpdated': None, + + 'PositionIncrease': handle_positionincrease_event, + 'PositionDecrease': handle_positiondecrease_event, + 'PositionFeesCollected': None, + + 'PositionImpactPoolAmountUpdated': None, + 'PositionImpactPoolDistributed': None, + 'VirtualPositionInventoryUpdated': None, + + 'ClaimableFeeAmountUpdated': None, + 'ClaimableFundingUpdated': None, + 'ClaimableFundingAmountPerSizeUpdated': None, + 'FundingFeeAmountPerSizeUpdated': None, + 'FundingFeesClaimed': None, + + 'CollateralSumUpdated': None, + 'CollateralClaimed': None, + + 'OpenInterestInTokensUpdated': None, + 'OpenInterestUpdated': None, + + 'SetAvailableFeeAmount': None, + 'BuybackFees': None, + 'FeesClaimed': None, + + 'ExecutionFeeRefundCallback': None, + + 'PoolAmountUpdated': None, + + 'SwapInfo': None, + 'SwapFeesCollected': None, + 'SwapImpactPoolAmountUpdated': None, + 'VirtualSwapInventoryUpdated': None, + + 'CumulativeBorrowingFactorUpdated': None, + + 'KeeperExecutionFee': None, + 'ExecutionFeeRefund': None, + + 'SetUint': None, + # SetBytes32 presumably and others... + 'SyncConfig': None, + 'ShiftCreated': None, 'ShiftExecuted': None, @@ -125,23 +302,35 @@ event_handlers = { 'GlvShiftExecuted': None, 'AffiliateRewardUpdated': None, + 'AffiliateRewardClaimed': None, + + 'SetMaxAllowedSubaccountActionCount': None, 'IncrementSubaccountActionCount': None, + 'SetSubaccountAutoTopUpAmount': None, + 'SubaccountAutoTopUp': None, } async def handle_gmx_events(events: list[dict]): for event in events: - data = parse_event_log_data(event['data']) + data = parse_event_log_data(event) + log.info(f'GMX Event {data}') event_name = data['event'] try: - event_handlers[event_name](data) + func = event_handlers[event_name] except KeyError: log.debug(f'Unknown event {event_name}') - except TypeError: - pass + else: + if func: + await maywait(func(event, data)) +# +# Metadata update triggers +# todo These are here because they used to be blockchain event handlers and should be once again... +# + initialized = False @periodic(timedelta(hours=1)) @@ -157,11 +346,11 @@ async def gmx_handle_metadata_update(): log.exception('Exception in gmx_handle_metadata_update()') -@periodic(timedelta(seconds=1)) -async def gmx_handle_price_update(): - updates = await fetch_price_updates() - # ticker updates have only one price per addr so we can parallelize setting prices - await asyncio.gather(*[update_pool_price(addr, time, price, 30) for addr, time, price in updates]) +# @periodic(timedelta(seconds=1)) +# async def gmx_handle_price_update(): +# updates = await fetch_price_updates() +# # ticker updates have only one price per addr so we can parallelize setting prices +# await asyncio.gather(*[update_pool_price(addr, time, price, 30) for addr, time, price in updates]) def create_backfill_handler(ohlcs: FinalOHLCRepository): @@ -202,7 +391,7 @@ async def backfill_token(ohlcs: FinalOHLCRepository, addr: str): addr = token['address'] for period in GMX_OHLC_PERIODS: # Polling a large window is the only history method GMX provides :( It's also how their web client works! - symbol = gmx_token_symbol_map[addr] + symbol = token['symbol'] interval = period_name(period).lower() response = gmx_api('prices/candles', tokenSymbol=symbol, period=interval, limit=10_000) if 'error' in response: @@ -214,8 +403,27 @@ async def backfill_token(ohlcs: FinalOHLCRepository, addr: str): log.info(f'Backfilled new GMX token {token["symbol"]}') +@periodic(timedelta(seconds=1)) +async def gmx_update_prices(): + for token, time, price in await fetch_price_updates(): + for market in gmx_markets_by_index_token.get(token, []): + info: OldGMXDict = address_metadata[market]['index'] + decimals = info['decimals'] + await update_pool_price(market, time, price*dec(10)**decimals, decimals) + + async def fetch_price_updates(): + tokens = list(gmx_markets_by_index_token.keys()) + prices = await get_dexorder_contract().getGMXPrices(tokens) + factor = dec(10)**-30 + return [ + (addr, from_timestamp(timestamp), (dec(bid) + dec(ask)) / 2 * factor) + for addr, (timestamp, bid, ask) in zip(tokens, prices) + ] + +async def fetch_price_updates_using_gmx_api(): updates = [] + # todo use on-chain oracle events for t in gmx_api('prices/tickers'): """ { @@ -228,7 +436,7 @@ async def fetch_price_updates(): }, """ addr = t['tokenAddress'] - if addr not in gmx_token_symbol_map: + if addr not in address_metadata: continue # GMX prices use 30 decimal places price = (dec(t['minPrice']) + dec(t['maxPrice'])) / 2 * dec(10) ** dec(-30) diff --git a/src/dexorder/gmx/_metadata.py b/src/dexorder/gmx/_metadata.py index 8023b21..b49ef57 100644 --- a/src/dexorder/gmx/_metadata.py +++ b/src/dexorder/gmx/_metadata.py @@ -1,94 +1,93 @@ -__all__ = ['gmx_update_metadata', 'gmx_token_symbol_map'] +__all__ = ['gmx_update_metadata'] import asyncio import logging import re -from copy import copy -from pprint import pprint +from typing import Optional -from dexorder import dec, ADDRESS_0 +from dexorder import ADDRESS_0 from dexorder.addrmeta import address_metadata +from dexorder.base import OldTokenDict, OldGMXDict from dexorder.base.chain import current_chain -from dexorder.database.model.token import OldTokenDict -from dexorder.gmx._base import GMXPool, gmx_tokens, gmx_pools, gmx_api +from dexorder.base.orderlib import Exchange +from dexorder.gmx._base import gmx_api, gmx_markets_by_index_token from dexorder.gmx._contract import get_gmx_contract -from dexorder.gmx._datastore import combo_key, MIN_COLLATERAL_FACTOR_KEY, is_market_disabled from dexorder.tokens import get_token log = logging.getLogger(__name__) async def gmx_update_metadata(): - await gmx_detect_tokens() - await gmx_detect_pools() + log.info('Updating GMX metadata') + await gmx_detect_markets() -async def gmx_detect_tokens(): - chain_id = current_chain.get().id - response = gmx_api('tokens') - if 'tokens' not in response: - raise ValueError('No tokens in GMX response') +token_response: Optional[dict] = None - tokens = [] - for info in response['tokens']: - synthetic = info.get('synthetic',False) - name = f'GMX {info["symbol"]}' - if synthetic: - name += ' Synthetic' - token = OldTokenDict(type='Token', chain=chain_id, address=info['address'], name=name, - symbol=info['symbol'], decimals=info['decimals'], approved=True, gmx_synthetic=synthetic) - if re.search(r'deprecated', info['symbol'], re.IGNORECASE): - continue - gmx_token_symbol_map[token['address']] = token['symbol'] - if synthetic and token['address'] not in address_metadata: - # noinspection PyTypeChecker +async def gmx_get_token(addr: str): + # The GMX API appears to be the only way to obtain the index token metadata, since there is no corresponding ERC20 + # on-chain at the synthetic address. + found = await get_token(addr, squelch=True) # use our normal lookup first + if found is not None: + return found + global token_response + if token_response is None or addr not in token_response['tokens']: + token_response = gmx_api('tokens') + for info in token_response['tokens']: + if info['address'] == addr: + synthetic = info.get('synthetic',False) + if not synthetic: + log.warning('loading non-synthetic token via GMX API') + name = f'GMX {info["symbol"]}' + if synthetic: + name += ' Synthetic' + chain_id = current_chain.get().id + approved = not re.search(r'deprecated', info['symbol'], re.IGNORECASE) + token = OldTokenDict(type='Token', chain=chain_id, address=info['address'], name=name, + symbol=info['symbol'], decimals=info['decimals'], + approved=approved) address_metadata[info['address']] = token - gmx_tokens[token['address']] = token - tokens.append(token) - - # Delete any tokens that are no longer listed - valid_addrs = set(t['address'] for t in tokens) - invalid_addrs = set(md['address'] for md in gmx_tokens.values() if md['address'] not in valid_addrs) - for addr in invalid_addrs: - log.info(f'Deleting invalid GMX token {gmx_tokens[addr]}') - del gmx_tokens[addr] + return token + log.error(f'Could not find index token {addr} in GMX tokens API') + return None -async def gmx_detect_pools(): +async def gmx_detect_markets(): ds = get_gmx_contract('DataStore') reader = get_gmx_contract('Reader') market_info = await reader.getMarkets(ds.address, 0, 1000) markets = [ - GMXPool(42161, market_token, index_token, long_token, short_token, dec('nan')) - for market_token, long_token, short_token, index_token in market_info + OldGMXDict(type='GMX', chain=current_chain.get().id, exchange=Exchange.GMX.value, address=market_token, + index=index_token, long=long_token, short=short_token, decimals=0, leverage=0) + for market_token, index_token, long_token, short_token in market_info + # discard spot-only markets that do not have an index token + # todo support single-asset markets + if market_token != ADDRESS_0 and index_token != ADDRESS_0 and + long_token != ADDRESS_0 and short_token != ADDRESS_0 and market_token not in address_metadata ] - # some pools have ADDRESS_0 as the long token. wat? - markets = [m for m in markets if m.long_token != ADDRESS_0 and m.short_token != ADDRESS_0 and m.index_token != ADDRESS_0 and m.address != ADDRESS_0] - market_disabled = await asyncio.gather(*[is_market_disabled(ds, m) for m in markets]) - markets = [m for m,d in zip(markets, market_disabled) if not d] - valid_addrs = set(m.address for m in markets) - invalid_addrs = set(p.address for p in gmx_pools.values() if p.address not in valid_addrs) - new_markets = [m for m in markets if m.address not in gmx_pools] + market_disabled = await asyncio.gather(*[ds.is_market_disabled(m['address']) for m in markets]) + new_markets = [m for m,d in zip(markets, market_disabled) if not d and m['address'] not in address_metadata] - async def init_market(m): - result = await ds.getUint(combo_key(MIN_COLLATERAL_FACTOR_KEY, m.address)) - if result == 0: - # raise ValueError(f'no min collateral factor for market {m.address}') - log.warning(f'no min collateral factor for market {m.address}') - m.min_collateral = 2 * dec(result) / dec(1e30) - gmx_pools[m.address] = m + async def init_market(m: OldGMXDict): + min_collateral_factor, token = await asyncio.gather( + ds.min_collateral_factor(m['address']), gmx_get_token(m['index'])) + m['decimals'] = token['decimals'] + m['leverage'] = round(1 / min_collateral_factor) + address_metadata[m['address']] = m + cur = gmx_markets_by_index_token.get(m['index']) + if cur is None: + gmx_markets_by_index_token[m['index']] = [m['address']] + else: + if m['address'] not in cur: + gmx_markets_by_index_token[m['index']] = cur + [m['address']] await asyncio.gather(*[init_market(m) for m in new_markets]) - token_addrs = set(t for m in new_markets for t in (m.address, m.index_token, m.long_token, m.short_token)) + token_addrs = set(t for m in new_markets for t in (m['address'], m['long'], m['short'])) await asyncio.gather(*[get_token(t) for t in token_addrs]) - # Disable any markets that are not longer valid - for addr in invalid_addrs: - log.info(f'Disabling GMX market {gmx_pools[addr]}') - updated = copy(gmx_pools[addr]) - updated.disabled = True - gmx_pools[addr] = updated - pprint(new_markets) - return markets - -gmx_token_symbol_map: dict[str, str] = {} # maps addresses to GMX token symbols + # Log the markets + def t(addr): + # noinspection PyTypedDict + return address_metadata[addr]['symbol'] if addr in address_metadata and address_metadata[addr] else addr + for m in new_markets: + log.info(f'GMX:{m["address"]} {t(m["index"])}/USD [{t(m["long"])}-{t(m["short"])}] {m["leverage"]}x') diff --git a/src/dexorder/memcache/__init__.py b/src/dexorder/memcache/__init__.py index 8d8df71..0665175 100644 --- a/src/dexorder/memcache/__init__.py +++ b/src/dexorder/memcache/__init__.py @@ -1,4 +1,3 @@ -import itertools import logging from contextlib import asynccontextmanager from contextvars import ContextVar diff --git a/src/dexorder/metadata.py b/src/dexorder/metadata.py index 0e9c19b..900b141 100644 --- a/src/dexorder/metadata.py +++ b/src/dexorder/metadata.py @@ -26,10 +26,10 @@ import sys from typing import Union, Iterable, Optional from dexorder import config, NARG +from dexorder.base import OldPoolDict, OldTokenDict from dexorder.base.chain import current_chain from dexorder.database.model import Token, Pool -from dexorder.database.model.pool import OldPoolDict, PoolDict -from dexorder.database.model.token import OldTokenDict, TokenDict +from dexorder.database.model.pool import PoolDict from dexorder.util import json log = logging.getLogger(__name__) @@ -50,7 +50,6 @@ def dump_tokens(out, tokens, include_unapproved=False): approved_addrs = set() had_output = False for token in tokens: - token: Token if isinstance(token, Token): token: Token a = token.address diff --git a/src/dexorder/order/executionhandler.py b/src/dexorder/order/executionhandler.py index 1ad8e4c..cb87993 100644 --- a/src/dexorder/order/executionhandler.py +++ b/src/dexorder/order/executionhandler.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from typing import Optional, Union, Any from uuid import UUID -from triton.profiler import deactivate from web3.exceptions import ContractPanicError, ContractLogicError from web3.types import EventData @@ -11,10 +10,13 @@ from dexorder import db, metric, config from dexorder.accounting import accounting_transaction_gas from dexorder.base import TransactionReceiptDict, TransactionRequest, transaction_request_deserializers from dexorder.base.order import TrancheKey, OrderKey -from dexorder.base.orderlib import PriceProof +from dexorder.base.orderlib import PriceProof, Exchange +from dexorder.contract import ContractProxy +from dexorder.contract.contract_proxy import ContractTransaction from dexorder.contract.dexorder import get_dexorder_contract from dexorder.database.model.accounting import AccountingSubcategory from dexorder.database.model.transaction import TransactionJob +from dexorder.gmx import tk_gmx_in_flight from dexorder.order.orderstate import Order from dexorder.order.triggers import (OrderTriggers, TrancheState, active_tranches, order_error) @@ -69,10 +71,18 @@ class TrancheExecutionHandler (TransactionHandler): def __init__(self): super().__init__('te') - async def build_transaction(self, job_id: UUID, req: TrancheExecutionRequest) -> dict: + async def build_transaction(self, job_id: UUID, req: TrancheExecutionRequest) -> Optional[ContractTransaction]: tk = req.tranche_key try: - return await get_dexorder_contract().build.execute(job_id.bytes, (req.vault, req.order_index, req.tranche_index, req.price_proof)) + kwargs = {} + if Order.of(tk).order.route.exchange == Exchange.GMX: + if tk_gmx_in_flight.get(tk): + return None # a GMX order is already in flight + fee = await ContractProxy(req.vault, 'IVaultGMX').gmxExecutionFee(False) + kwargs['value'] = round(fee * 1.1) # extra 10% because gas prices can change quickly + return await get_dexorder_contract().build.execute( + job_id.bytes, (req.vault, req.order_index, req.tranche_index, req.price_proof), + kwargs=kwargs) except ContractPanicError as x: exception = x errcode = '' @@ -234,6 +244,10 @@ async def handle_dexorderexecutions(event: EventData): if job is None: log.warning(f'Job {exe_id} not found!') return + # verify that the transaction hash of the event is the same as that of our request + if job.tx_id != event['transactionHash']: + log.warning(f'Ignoring rogue DexorderExecutions {exe_id} with wrong txid {job.tx_id} != {event["transactionHash"]}') + return # noinspection PyTypeChecker req: TrancheExecutionRequest = job.request tk = TrancheKey(req.vault, req.order_index, req.tranche_index) diff --git a/src/dexorder/order/orderstate.py b/src/dexorder/order/orderstate.py index 1931394..7b9b1db 100644 --- a/src/dexorder/order/orderstate.py +++ b/src/dexorder/order/orderstate.py @@ -304,6 +304,13 @@ SwapOrder {self.key} amount: {"input" if self.order.amountIsInput else "output"} {await adjust_decimals(amount_token, self.filled):f}/{await adjust_decimals(amount_token, self.amount):f}{" to owner" if self.order.outputDirectlyToOwner else ""} minFill: {await adjust_decimals(amount_token, self.min_fill_amount):f} inverted: {self.order.inverted} +''' + if self.order.gmx: + msg += f''' + gmx order: {"increase" if self.order.gmx.is_increase else "decrease"} {"long" if self.order.gmx.is_long else "short"} + collateral: {self.order.gmx.reserve_amount} +''' + msg += ''' tranches: ''' for i in range(len(self.order.tranches)): diff --git a/src/dexorder/order/triggers.py b/src/dexorder/order/triggers.py index c501381..d3c9a47 100644 --- a/src/dexorder/order/triggers.py +++ b/src/dexorder/order/triggers.py @@ -1,6 +1,5 @@ import asyncio import logging -from abc import abstractmethod from collections import defaultdict from datetime import timedelta from enum import Enum, auto @@ -13,11 +12,11 @@ from dexorder.base.orderlib import SwapOrderState, PriceProof, DISTANT_FUTURE, D MIN_SLIPPAGE_EPSILON from dexorder.blockstate import BlockDict from .orderstate import Order -from .. import dec, order_log, timestamp, from_timestamp, config +from .. import dec, order_log, timestamp, config +from ..base import OldPoolDict from ..base.chain import current_clock from ..base.order import OrderKey, TrancheKey from ..contract import ERC20 -from ..database.model.pool import OldPoolDict from ..pools import ensure_pool_price, pool_prices, get_pool from ..routing import pool_address from ..vault_blockdata import vault_balances, adjust_balance @@ -38,7 +37,7 @@ execution should be attempted on the tranche. """ -# tranches which have passed all constraints and should be executed +# tranches which have passed all constraints and should be executed. This set gets checked against already in- active_tranches: BlockDict[TrancheKey, Optional[PriceProof]] = BlockDict('at') @@ -178,6 +177,7 @@ class Trigger: Expiration = 2 MinLine = 3 MaxLine = 4 + GMXInFlight = 5 def __init__(self, trigger_type: TriggerType, tk: TrancheKey, value: bool): """ @@ -211,9 +211,7 @@ class Trigger: def _value_changed(self): pass - - @abstractmethod - def remove(self): ... + def remove(self): pass async def has_funds(tk: TrancheKey): @@ -233,6 +231,7 @@ async def has_funds(tk: TrancheKey): async def input_amount_is_sufficient(order, token_balance): + # todo modify for GMX # log.debug(f'input is sufficient? {order.min_fill_amount}') if order.amount_is_input: # log.debug(f'amount is input: {token_balance} >= {order.min_fill_amount}') @@ -593,7 +592,7 @@ class TrancheTrigger: def fill(self, _amount_in, _amount_out, _next_activation_time ): - if _next_activation_time != DISTANT_PAST: + if _next_activation_time != 0: # rate limit if self.activation_trigger is None: self.activation_trigger = TimeTrigger(True, self.tk, _next_activation_time, timestamp()) diff --git a/src/dexorder/pools.py b/src/dexorder/pools.py index 719ffea..c42292e 100644 --- a/src/dexorder/pools.py +++ b/src/dexorder/pools.py @@ -4,18 +4,18 @@ from dataclasses import dataclass from datetime import datetime from typing import Optional -from web3.exceptions import ContractLogicError +from web3.exceptions import ContractLogicError, BadFunctionCallOutput from web3.types import EventData from dexorder import dec, ADDRESS_0, from_timestamp, db, config, NATIVE_TOKEN from dexorder.addrmeta import address_metadata +from dexorder.base import OldPoolDict from dexorder.base.chain import current_chain from dexorder.base.orderlib import Exchange from dexorder.blocks import get_block_timestamp from dexorder.blockstate import BlockDict from dexorder.blockstate.blockdata import K, V from dexorder.database.model import Pool -from dexorder.database.model.pool import OldPoolDict from dexorder.tokens import get_token, adjust_decimals as adj_dec from dexorder.uniswap import UniswapV3Pool, uniswapV3_pool_address @@ -64,7 +64,7 @@ async def load_pool(address: str, *, use_db=True) -> OldPoolDict: log.debug(f'new UniswapV3 pool {token0["symbol"]}/{token1["symbol"]} {fee/1_000_000:.2%} ' f'{("."+str(decimals)) if decimals >= 0 else (str(-decimals)+".")} {address}') add_mark_pool(address, t0, t1, fee) - except ContractLogicError: + except (ContractLogicError, BadFunctionCallOutput): pass except ValueError as v: try: @@ -85,8 +85,9 @@ async def load_pool(address: str, *, use_db=True) -> OldPoolDict: class PoolPrices (BlockDict[str, dec]): def __setitem__(self, item: K, value: V) -> None: - super().__setitem__(item, value) - new_pool_prices[item] = value + old = self.setitem(item, value) + if value != old: + new_pool_prices[item] = value def pub_pool_price(_s,k,v): diff --git a/src/dexorder/tokens.py b/src/dexorder/tokens.py index 29fa333..8d036f9 100644 --- a/src/dexorder/tokens.py +++ b/src/dexorder/tokens.py @@ -1,3 +1,4 @@ +import asyncio import logging from typing import Optional @@ -6,11 +7,11 @@ from web3.exceptions import BadFunctionCallOutput from dexorder import ADDRESS_0, db, NATIVE_TOKEN, dec, current_w3 from dexorder.addrmeta import address_metadata +from dexorder.base import OldTokenDict from dexorder.base.chain import current_chain from dexorder.blocks import current_block from dexorder.contract import ERC20, ContractProxy, CONTRACT_ERRORS from dexorder.database.model import Token -from dexorder.database.model.token import OldTokenDict from dexorder.metadata import get_metadata log = logging.getLogger(__name__) @@ -45,7 +46,7 @@ async def get_native_balance(addr, *, adjust_decimals=True) -> dec: return value -async def get_token(address) -> Optional[OldTokenDict]: +async def get_token(address, *, squelch=False) -> Optional[OldTokenDict]: if address == ADDRESS_0: raise ValueError('No token at address 0') try: @@ -53,11 +54,11 @@ async def get_token(address) -> Optional[OldTokenDict]: return address_metadata[address] except KeyError: # noinspection PyTypeChecker - result = address_metadata[address] = await load_token(address) + result = address_metadata[address] = await load_token(address, squelch=squelch) return result -async def load_token(address: str) -> Optional[OldTokenDict]: +async def load_token(address: str, *, squelch=False) -> Optional[OldTokenDict]: contract = ERC20(address) chain_id = current_chain.get().id if db: @@ -75,7 +76,8 @@ async def load_token(address: str) -> Optional[OldTokenDict]: try: rb: bytes = await ContractProxy(address, 'ERC20.sb').symbol() except CONTRACT_ERRORS: - log.warning(f'token {address} has broken {func_name}()') + if not squelch: + log.warning(f'token {address} has broken {func_name}()') return None end = rb.find(b'\x00') if end == -1: @@ -83,22 +85,20 @@ async def load_token(address: str) -> Optional[OldTokenDict]: try: return rb[:end].decode('utf8') except UnicodeDecodeError: - log.warning(f'token {address} has an invalid {func_name}() {rb}') + if not squelch: + log.warning(f'token {address} has an invalid {func_name}() {rb}') return None dec_prom = contract.decimals() - symbol_prom = get_string_or_bytes32('symbol') - name_prom = get_string_or_bytes32('name') try: decimals = await dec_prom except CONTRACT_ERRORS: - log.info(f'token {address} has no decimals()') - decimals = 0 + if not squelch: + log.info(f'token {address} has no decimals()') return None # we do not support coins that don't specify decimals. approved = False # never approve new coins chain_id = current_chain.get().id - symbol = await symbol_prom - name = await name_prom + name, symbol = await asyncio.gather(get_string_or_bytes32('name'), get_string_or_bytes32('symbol')) td = OldTokenDict(type='Token', chain=chain_id, address=address, name=name, symbol=symbol, decimals=decimals, approved=approved) md = get_metadata(address, chain_id=chain_id) diff --git a/src/dexorder/transactions.py b/src/dexorder/transactions.py index 93566e3..7f07700 100644 --- a/src/dexorder/transactions.py +++ b/src/dexorder/transactions.py @@ -1,6 +1,6 @@ import asyncio import logging -from abc import abstractmethod +from abc import abstractmethod, ABC from typing import Optional from uuid import uuid4 @@ -18,7 +18,7 @@ from dexorder.util import hexstr log = logging.getLogger(__name__) -class TransactionHandler: +class TransactionHandler (ABC): instances: dict[str,'TransactionHandler'] = {} @staticmethod diff --git a/src/dexorder/util/json.py b/src/dexorder/util/json.py index 2876dc4..d36de0e 100644 --- a/src/dexorder/util/json.py +++ b/src/dexorder/util/json.py @@ -37,7 +37,7 @@ def dumps(obj): return dumpb(obj).decode('utf8') def dumpb(obj): - opts = orjson.OPT_PASSTHROUGH_SUBCLASS + opts = orjson.OPT_PASSTHROUGH_SUBCLASS | orjson.OPT_SERIALIZE_DATACLASS return orjson.dumps(obj, default=_serialize, option=opts)