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
|
Polygon = Blockchain(137, 'Polygon') # POS not zkEVM
|
||||||
Mumbai = Blockchain(80001, 'Mumbai')
|
Mumbai = Blockchain(80001, 'Mumbai')
|
||||||
BSC = Blockchain(56, 'BSC')
|
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')
|
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.contract import VaultContract
|
||||||
from dexorder.data import pool_prices, vault_addresses, vault_tokens, underfunded_vaults
|
from dexorder.data import pool_prices, vault_addresses, vault_tokens, underfunded_vaults
|
||||||
from dexorder.database.model.block import current_block
|
from dexorder.database.model.block import current_block
|
||||||
from dexorder.orderlib.orders import SwapOrderStatus
|
from dexorder.orderlib.orderlib import SwapOrderStatus
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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]
|
ocoGroup: Optional[int]
|
||||||
filledIn: int
|
filledIn: int
|
||||||
filledOut: int
|
filledOut: int
|
||||||
|
trancheFilledIn: list[int]
|
||||||
|
trancheFilledOut: list[int]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(obj):
|
def load(obj):
|
||||||
@@ -71,10 +73,12 @@ class SwapOrderStatus:
|
|||||||
ocoGroup = None if obj[3] == NO_OCO else obj[3]
|
ocoGroup = None if obj[3] == NO_OCO else obj[3]
|
||||||
filledIn = obj[4]
|
filledIn = obj[4]
|
||||||
filledOut = obj[5]
|
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):
|
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
|
NO_OCO = 18446744073709551615 # max uint64
|
||||||
@@ -88,6 +92,8 @@ class ConstraintMode (Enum):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Constraint (ABC):
|
class Constraint (ABC):
|
||||||
|
mode: ConstraintMode
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(obj):
|
def load(obj):
|
||||||
mode = ConstraintMode(obj[0])
|
mode = ConstraintMode(obj[0])
|
||||||
@@ -99,9 +105,8 @@ class Constraint (ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def dump(self): ...
|
def dump(self): ...
|
||||||
|
|
||||||
@staticmethod
|
def _dump(self, types, values):
|
||||||
def _dump(mode, types, values):
|
return self.mode, abi_encoder.encode(types, values)
|
||||||
return mode, abi_encoder.encode(types, values)
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PriceConstraint (Constraint):
|
class PriceConstraint (Constraint):
|
||||||
@@ -109,8 +114,14 @@ class PriceConstraint (Constraint):
|
|||||||
isRatio: bool
|
isRatio: bool
|
||||||
valueSqrtX96: int
|
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):
|
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
|
@dataclass
|
||||||
@@ -147,7 +158,7 @@ class TimeConstraint (Constraint):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def load(obj: bytes):
|
def load(obj: bytes):
|
||||||
earliest_mode, earliest_time, latest_mode, latest_time = abi_decoder.decode(TimeConstraint.TYPES, obj)
|
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):
|
def dump(self):
|
||||||
return Constraint._dump(ConstraintMode.Time, TimeConstraint.TYPES,
|
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
|
import re
|
||||||
|
from typing import Callable, TypeVar, Generic
|
||||||
|
|
||||||
from eth_utils import keccak
|
from eth_utils import keccak
|
||||||
from hexbytes import HexBytes
|
from hexbytes import HexBytes
|
||||||
@@ -23,6 +24,7 @@ def hexstr(value: bytes):
|
|||||||
elif type(value) is bytes:
|
elif type(value) is bytes:
|
||||||
return '0x' + value.hex()
|
return '0x' + value.hex()
|
||||||
elif type(value) is str:
|
elif type(value) is str:
|
||||||
|
# noinspection PyTypeChecker
|
||||||
return value if value.startswith('0x') else '0x' + value
|
return value if value.startswith('0x') else '0x' + value
|
||||||
else:
|
else:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
@@ -32,14 +34,14 @@ def hexbytes(value: str):
|
|||||||
""" converts an optionally 0x-prefixed hex string into bytes """
|
""" converts an optionally 0x-prefixed hex string into bytes """
|
||||||
return bytes.fromhex(value[2:] if value.startswith('0x') else value)
|
return bytes.fromhex(value[2:] if value.startswith('0x') else value)
|
||||||
|
|
||||||
def keystr(value):
|
def keystr(*keys):
|
||||||
if type(value) is str:
|
return '|'.join(
|
||||||
return value
|
value if type(value) is str else
|
||||||
if type(value) is HexBytes:
|
value.hex() if type(value) is HexBytes else
|
||||||
return value.hex()
|
'0x' + value.hex() if type(value) is bytes else
|
||||||
if type(value) is bytes:
|
str(value)
|
||||||
return '0x' + value.hex()
|
for value in keys
|
||||||
return str(value)
|
)
|
||||||
|
|
||||||
def strkey(s):
|
def strkey(s):
|
||||||
if s.startswith('0x'):
|
if s.startswith('0x'):
|
||||||
@@ -51,3 +53,18 @@ def topic(event_abi):
|
|||||||
result = '0x' + keccak(text=event_name).hex()
|
result = '0x' + keccak(text=event_name).hex()
|
||||||
print(event_name, result)
|
print(event_name, result)
|
||||||
return 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