Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -43,6 +43,6 @@ Goerli = Blockchain(5, 'Goerli')
|
||||
Polygon = Blockchain(137, 'Polygon') # POS not zkEVM
|
||||
Mumbai = Blockchain(80001, 'Mumbai')
|
||||
BSC = Blockchain(56, 'BSC')
|
||||
Arbitrum = Blockchain(42161, 'Arbitrum', 10)
|
||||
Arbitrum = Blockchain(42161, 'Arbitrum', 10, batch_size=1000) # todo configure batch size... does it depend on log count? :(
|
||||
|
||||
current_chain = ContextVar[Blockchain]('current_chain')
|
||||
|
||||
@@ -8,7 +8,7 @@ from dexorder.blockchain.util import vault_address, get_contract_event, get_fact
|
||||
from dexorder.contract import VaultContract
|
||||
from dexorder.data import pool_prices, vault_addresses, vault_tokens, underfunded_vaults
|
||||
from dexorder.database.model.block import current_block
|
||||
from dexorder.orderlib.orders import SwapOrderStatus
|
||||
from dexorder.orderlib.orderlib import SwapOrderStatus
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
4
src/dexorder/order/__init__.py
Normal file
4
src/dexorder/order/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
def order_key(vault:str, ):
|
||||
return f'{vault}'
|
||||
|
||||
@@ -62,6 +62,8 @@ class SwapOrderStatus:
|
||||
ocoGroup: Optional[int]
|
||||
filledIn: int
|
||||
filledOut: int
|
||||
trancheFilledIn: list[int]
|
||||
trancheFilledOut: list[int]
|
||||
|
||||
@staticmethod
|
||||
def load(obj):
|
||||
@@ -71,10 +73,12 @@ class SwapOrderStatus:
|
||||
ocoGroup = None if obj[3] == NO_OCO else obj[3]
|
||||
filledIn = obj[4]
|
||||
filledOut = obj[5]
|
||||
return SwapOrderStatus(order, state, start, ocoGroup, filledIn, filledOut)
|
||||
trancheFilledIn = obj[6]
|
||||
trancheFilledOut = obj[7]
|
||||
return SwapOrderStatus(order, state, start, ocoGroup, filledIn, filledOut, trancheFilledIn, trancheFilledOut)
|
||||
|
||||
def dump(self):
|
||||
return self.order.dump(), self.state.value, self.start, self.ocoGroup, self.filledIn, self.filledOut
|
||||
return self.order.dump(), self.state.value, self.start, self.ocoGroup, self.filledIn, self.filledOut, self.trancheFilledIn, self.trancheFilledOut
|
||||
|
||||
|
||||
NO_OCO = 18446744073709551615 # max uint64
|
||||
@@ -88,6 +92,8 @@ class ConstraintMode (Enum):
|
||||
|
||||
@dataclass
|
||||
class Constraint (ABC):
|
||||
mode: ConstraintMode
|
||||
|
||||
@staticmethod
|
||||
def load(obj):
|
||||
mode = ConstraintMode(obj[0])
|
||||
@@ -99,9 +105,8 @@ class Constraint (ABC):
|
||||
@abstractmethod
|
||||
def dump(self): ...
|
||||
|
||||
@staticmethod
|
||||
def _dump(mode, types, values):
|
||||
return mode, abi_encoder.encode(types, values)
|
||||
def _dump(self, types, values):
|
||||
return self.mode, abi_encoder.encode(types, values)
|
||||
|
||||
@dataclass
|
||||
class PriceConstraint (Constraint):
|
||||
@@ -109,8 +114,14 @@ class PriceConstraint (Constraint):
|
||||
isRatio: bool
|
||||
valueSqrtX96: int
|
||||
|
||||
TYPES = 'bool','bool','uint160'
|
||||
|
||||
def load(self, obj):
|
||||
isAbove, isRatio, valueSqrtX96 = abi_decoder.decode(PriceConstraint.TYPES, obj)
|
||||
return PriceConstraint(ConstraintMode.Limit, isAbove, isRatio, valueSqrtX96)
|
||||
|
||||
def dump(self):
|
||||
return Constraint._dump(ConstraintMode.Limit, ('bool','bool','uint160'), (self.isAbove, self.isRatio, self.valueSqrtX96))
|
||||
return self._dump(PriceConstraint.TYPES, (self.isAbove, self.isRatio, self.valueSqrtX96))
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -147,7 +158,7 @@ class TimeConstraint (Constraint):
|
||||
@staticmethod
|
||||
def load(obj: bytes):
|
||||
earliest_mode, earliest_time, latest_mode, latest_time = abi_decoder.decode(TimeConstraint.TYPES, obj)
|
||||
return TimeConstraint(Time(TimeMode(earliest_mode),earliest_time), Time(TimeMode(latest_mode),latest_time))
|
||||
return TimeConstraint(ConstraintMode.Time, Time(TimeMode(earliest_mode),earliest_time), Time(TimeMode(latest_mode),latest_time))
|
||||
|
||||
def dump(self):
|
||||
return Constraint._dump(ConstraintMode.Time, TimeConstraint.TYPES,
|
||||
30
src/dexorder/order/orderstate.py
Normal file
30
src/dexorder/order/orderstate.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import logging
|
||||
|
||||
from dexorder.blockstate import BlockSet, BlockDict
|
||||
from dexorder.order.orderlib import SwapOrderStatus
|
||||
from dexorder.util import keystr
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def order_key( vault:str, order_index:int ):
|
||||
return keystr(vault, str(order_index))
|
||||
|
||||
def tranche_key( vault:str, order_index:int, tranche_index:int ):
|
||||
return keystr(vault,str(order_index),str(tranche_index))
|
||||
|
||||
active_orders = BlockSet('ao') # unfilled, not-canceled orders whose triggers have been loaded/set
|
||||
order_remaining = BlockDict('or') # by order key
|
||||
tranche_remaining = BlockDict('tr') # by tranche key
|
||||
|
||||
# todo forcibly dispose of entire series
|
||||
|
||||
class OrderState:
|
||||
def __init__(self, vault:str, order_index:int, status: SwapOrderStatus):
|
||||
self.vault = vault
|
||||
self.order_index = order_index
|
||||
self.key = f'{vault}|{order_index}'
|
||||
self.tranche_keys = [f'{self.key}|{i}' for i in range(len(status.trancheFilledIn))]
|
||||
|
||||
###### TODO TODO TODO vault state needs to be a dict pointing to owner addr
|
||||
|
||||
|
||||
55
src/dexorder/order/triggers.py
Normal file
55
src/dexorder/order/triggers.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from dexorder.blockstate import BlockSet
|
||||
from .orderlib import SwapOrderStatus, TimeConstraint, PriceConstraint, ConstraintMode
|
||||
from dexorder.util import defaultdictk
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# todo time and price triggers should be BlockSortedSets that support range queries
|
||||
TimeTrigger = Callable[[int, int], None] # func(start_timestamp, end_timestamp)
|
||||
time_triggers:BlockSet[TimeTrigger] = BlockSet('tt')
|
||||
|
||||
PriceTrigger = Callable[[int, int], None] # pool previous price, pool new price
|
||||
price_triggers:dict[str, BlockSet[PriceTrigger]] = defaultdictk(BlockSet) # different BlockSet per pool address
|
||||
|
||||
|
||||
def intersect_ranges( a_low, a_high, b_low, b_high):
|
||||
low, high = max(a_low,b_low), min(a_high,b_high)
|
||||
if high <= low:
|
||||
low, high = None, None
|
||||
return low, high
|
||||
|
||||
class TrancheTrigger:
|
||||
def __init__(self, vault: str, order_index:int, tranche_index: int):
|
||||
self.series = f'{vault}|{order_index}|{tranche_index}|'
|
||||
self.vault = vault
|
||||
self.order_index = order_index
|
||||
self.tranche_index = tranche_index
|
||||
|
||||
# todo refactor so we have things like tranche amount filled as blockstate, order amount remaining
|
||||
|
||||
def enable(self, status: SwapOrderStatus):
|
||||
tranche = status.order.tranches[self.tranche_index]
|
||||
tranche_amount = status.order.amount * tranche.fraction // 10**18
|
||||
tranche_filled = status.trancheFilledIn[self.tranche_index] if status.order.amountIsInput else status.trancheFilledOut[self.tranche_index]
|
||||
order_filled = status.filledIn if status.order.amountIsInput else status.filledOut
|
||||
remaining = min(tranche_amount - tranche_filled, status.order.amount - order_filled)
|
||||
if remaining <= 0: # todo dust?
|
||||
return
|
||||
|
||||
time_constraint = None
|
||||
price_constraints = []
|
||||
if status.filledOut:
|
||||
...
|
||||
for c in tranche.constraints:
|
||||
if c.mode == ConstraintMode.Time:
|
||||
c: TimeConstraint
|
||||
time_constraint = (c.earliest, c.latest) if time_constraint is None else intersect_ranges(*time_constraint, c.earliest, c.latest)
|
||||
elif c.mode == ConstraintMode.Limit:
|
||||
c: PriceConstraint
|
||||
raise NotImplementedError
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from typing import Callable, TypeVar, Generic
|
||||
|
||||
from eth_utils import keccak
|
||||
from hexbytes import HexBytes
|
||||
@@ -23,6 +24,7 @@ def hexstr(value: bytes):
|
||||
elif type(value) is bytes:
|
||||
return '0x' + value.hex()
|
||||
elif type(value) is str:
|
||||
# noinspection PyTypeChecker
|
||||
return value if value.startswith('0x') else '0x' + value
|
||||
else:
|
||||
raise ValueError
|
||||
@@ -32,14 +34,14 @@ def hexbytes(value: str):
|
||||
""" converts an optionally 0x-prefixed hex string into bytes """
|
||||
return bytes.fromhex(value[2:] if value.startswith('0x') else value)
|
||||
|
||||
def keystr(value):
|
||||
if type(value) is str:
|
||||
return value
|
||||
if type(value) is HexBytes:
|
||||
return value.hex()
|
||||
if type(value) is bytes:
|
||||
return '0x' + value.hex()
|
||||
return str(value)
|
||||
def keystr(*keys):
|
||||
return '|'.join(
|
||||
value if type(value) is str else
|
||||
value.hex() if type(value) is HexBytes else
|
||||
'0x' + value.hex() if type(value) is bytes else
|
||||
str(value)
|
||||
for value in keys
|
||||
)
|
||||
|
||||
def strkey(s):
|
||||
if s.startswith('0x'):
|
||||
@@ -51,3 +53,18 @@ def topic(event_abi):
|
||||
result = '0x' + keccak(text=event_name).hex()
|
||||
print(event_name, result)
|
||||
return result
|
||||
|
||||
|
||||
K = TypeVar('K')
|
||||
V = TypeVar('V')
|
||||
class defaultdictk (Generic[K,V], dict[K,V]):
|
||||
def __init__(self, default_factory: Callable[[K],V]):
|
||||
super().__init__()
|
||||
self.default_factory = default_factory
|
||||
|
||||
def __getitem__(self, item: K) -> V:
|
||||
try:
|
||||
return super().__getitem__(item)
|
||||
except KeyError:
|
||||
default = self[item] = self.default_factory(item)
|
||||
return default
|
||||
|
||||
Reference in New Issue
Block a user