triggers impl, written but not connected or tested

This commit is contained in:
Tim Olson
2023-10-13 01:47:25 -04:00
parent 393d4d4019
commit 1ed6b759bc
17 changed files with 360 additions and 107 deletions

View File

@@ -1,2 +1,2 @@
#!/bin/bash
docker run --network=host --ulimit memlock=-1 docker.dragonflydb.io/dragonflydb/dragonfly --maxmemory 1G "$@"
docker run --network=host --ulimit memlock=-1 docker.dragonflydb.io/dragonflydb/dragonfly --maxmemory 1G --dbfilename '' "$@"

View File

@@ -11,6 +11,7 @@ class _NARG:
def __bool__(self): return False
NARG = _NARG()
DELETE = object() # used as a value token to indicate removal of the key
UNLOAD = object() # used as a value token to indicate the key is no longer needed in memory
ADDRESS_0 = '0x0000000000000000000000000000000000000000'
WEI = 1
GWEI = 1_000_000_000

View File

@@ -44,5 +44,6 @@ Polygon = Blockchain(137, 'Polygon') # POS not zkEVM
Mumbai = Blockchain(80001, 'Mumbai')
BSC = Blockchain(56, 'BSC')
Arbitrum = Blockchain(42161, 'Arbitrum', 10, batch_size=1000) # todo configure batch size... does it depend on log count? :(
Mock = Blockchain(1338, 'Mock', 10)
current_chain = ContextVar[Blockchain]('current_chain')

View File

@@ -1,3 +1,13 @@
from eth_abi.packed import encode_packed
from eth_utils import keccak, to_bytes, to_checksum_address
from dexorder import dec
from dexorder.contract import uniswapV3
from dexorder.util import hexbytes
UNISWAPV3_POOL_INIT_CODE_HASH = hexbytes('0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54')
class Fee:
LOWEST = 100
LOW = 500
@@ -8,21 +18,21 @@ class Fee:
def ordered_addresses(addr_a:str, addr_b:str):
return (addr_a, addr_b) if addr_a.lower() <= addr_b.lower() else (addr_b, addr_a)
def uniswapV3_pool_address( addr_a: str, addr_b: str, fee: int):
return uniswap_pool_address(uniswapV3['factory'], addr_a, addr_b, fee)
def pool_address(_factory_addr:str, _addr_a:str, _addr_b:str):
# todo compute pool address
raise NotImplementedError
# addr0, addr1 = ordered_addresses(addr_a, addr_b)
# get_create2_address()
def uniswap_pool_address(factory_addr: str, addr_a: str, addr_b: str, fee: int) -> str:
token0, token1 = ordered_addresses(addr_a, addr_b)
salt = keccak(encode_packed(['address','address','uint24'],[token0, token1, fee]))
contract_address = keccak(
b"\xff"
+ to_bytes(hexstr=factory_addr)
+ salt
+ UNISWAPV3_POOL_INIT_CODE_HASH
).hex()[-40:]
return to_checksum_address(contract_address)
# use the util.liquidity or util.simple_liquidity package instead
# def liquidity_for_amount_0(lower: int, upper: int, amount_0: int):
# lower = convert.tick_to_price(lower)
# upper = convert.tick_to_price(upper)
# return amount_0 * upper * lower / (upper - lower)
#
# def liquidity_for_amount_1(lower: int, upper: int, amount_1: int ):
# lower = convert.tick_to_price(lower)
# upper = convert.tick_to_price(upper)
# return amount_1 / (upper - lower)
def uniswap_price(sqrt_price):
d = dec(sqrt_price)
price = d * d / dec(2 ** (96 * 2))
return price

View File

@@ -18,7 +18,7 @@ def get_factory() -> ContractProxy:
if found is None:
deployment_tag = config.deployments.get(str(chain_id), 'latest')
try:
with open(f'contract/broadcast/Deploy.sol/{chain_id}/run-{deployment_tag}.json', 'rt') as file:
with open(f'../contract/broadcast/Deploy.sol/{chain_id}/run-{deployment_tag}.json', 'rt') as file:
deployment = json.load(file)
for tx in deployment['transactions']:
if tx['contractName'] == 'Factory':
@@ -31,18 +31,21 @@ def get_factory() -> ContractProxy:
def get_contract_data(name):
with open(f'contract/out/{name}.sol/{name}.json', 'rt') as file:
with open(f'../contract/out/{name}.sol/{name}.json', 'rt') as file:
return json.load(file)
def get_contract_event(contract_name:str, event_name:str):
return getattr(current_w3.get().eth.contract(abi=get_contract_data(contract_name)['abi']).events, event_name)()
with open('contract/out/Vault.sol/Vault.json', 'rt') as _file:
_vault_info = json.load(_file)
VAULT_INIT_CODE_HASH = keccak(to_bytes(hexstr=_vault_info['bytecode']['object']))
VAULT_INIT_CODE_HASH = None
def vault_address(owner, num):
global VAULT_INIT_CODE_HASH
if VAULT_INIT_CODE_HASH is None:
with open('../contract/out/Vault.sol/Vault.json', 'rt') as _file:
vault_info = json.load(_file)
VAULT_INIT_CODE_HASH = keccak(to_bytes(hexstr=vault_info['bytecode']['object']))
salt = keccak(encode_packed(['address','uint8'],[owner,num]))
contract_address = keccak(
b"\xff"

View File

@@ -1,4 +1,4 @@
from .diff import DiffEntry, DiffItem, DELETE
from .diff import DiffEntry, DiffItem, DELETE, UNLOAD
from .state import BlockState, current_blockstate
from .blockdata import DataType, BlockDict, BlockSet

View File

@@ -1,15 +1,17 @@
import logging
from collections import defaultdict
from enum import Enum
from typing import TypeVar, Generic, Iterable, Union, Any
from typing import TypeVar, Generic, Iterable, Union, Any, Iterator
from dexorder import NARG, DELETE
from dexorder import NARG, DELETE, UNLOAD
from dexorder.base.fork import current_fork
from .state import current_blockstate
from dexorder.util import key2str as util_key2str, str2key as util_str2key
log = logging.getLogger(__name__)
T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')
class DataType(Enum):
@@ -61,6 +63,14 @@ class BlockData:
def delitem(self, item, overwrite=True):
self.setitem(item, DELETE, overwrite)
def unload(self, item):
"""
forgets the entry in memory when the current block is finalized, but does not delete the key from the collection. may only be
used when lazy_getitem is set
"""
assert self.lazy_getitem is not None
self.setitem(item, UNLOAD)
def contains(self, item):
try:
self.getitem(item)
@@ -89,41 +99,41 @@ class BlockSet(Generic[T], Iterable[T], BlockData):
super().__init__(DataType.SET, series, **tags)
self.series = series
def add(self, item):
def add(self, item: T):
""" set-like semantics. the item key is added with a value of None. """
self.setitem(item, None, overwrite=False)
def __delitem__(self, item):
def remove(self, item: T):
self.delitem(item, overwrite=False)
def __contains__(self, item):
def __contains__(self, item: T) -> bool:
return self.contains(item)
def __iter__(self):
def __iter__(self) -> Iterator[T]:
yield from (k for k,v in self.iter_items(self.series))
class BlockDict(Generic[T], BlockData):
class BlockDict(Generic[K,V], BlockData):
def __init__(self, series: Any, **tags):
super().__init__(DataType.DICT, series, **tags)
def __setitem__(self, item, value):
def __setitem__(self, item: K, value: V) -> None:
self.setitem(item, value)
def __getitem__(self, item):
def __getitem__(self, item: K) -> V:
return self.getitem(item)
def __delitem__(self, item):
def __delitem__(self, item: K) -> None:
self.delitem(item)
def __contains__(self, item):
def __contains__(self, item: K) -> bool:
return self.contains(item)
def items(self):
def items(self) -> Iterable[tuple[K,V]]:
return self.iter_items(self.series)
def get(self, item, default=None):
def get(self, item: K, default: V = None) -> V:
return self.getitem(item, default)

View File

@@ -4,7 +4,7 @@ from typing import Iterable, Optional, Union, Any
from . import DiffItem, BlockSet, BlockDict, DELETE, BlockState, current_blockstate, DataType
from .blockdata import BlockData, SeriesCollection
from .diff import DiffEntryItem
from .. import db
from .. import db, UNLOAD
from ..base.chain import current_chain
from ..base.fork import current_fork, Fork
from ..database.model import SeriesSet, SeriesDict, Block
@@ -52,6 +52,8 @@ class DbState(SeriesCollection):
if diff.value is DELETE:
Entity = SeriesSet if t == DataType.SET else SeriesDict if t == DataType.DICT else None
db.session.query(Entity).filter(Entity.chain==chain_id, Entity.series==diffseries, Entity.key==diffkey).delete()
elif diff.value is UNLOAD:
pass
else:
# upsert
if t == DataType.SET:

View File

@@ -1,12 +1,12 @@
from dataclasses import dataclass
from typing import Union, Any
from dexorder import DELETE
from dexorder import DELETE, UNLOAD
@dataclass
class DiffEntry:
value: Union[Any, DELETE]
value: Union[Any, DELETE, UNLOAD]
height: int
hash: bytes
@@ -18,7 +18,7 @@ class DiffItem:
value: Any
def __str__(self):
return f'{self.series}.{self.key}={"[DEL]" if self.value is DELETE else self.value}'
return f'{self.series}.{self.key}={"[DEL]" if self.value is DELETE else "[UNL]" if self.value is UNLOAD else self.value}'
@dataclass
class DiffEntryItem:
@@ -31,4 +31,5 @@ class DiffEntryItem:
return self.entry.value
def __str__(self):
return f'{self.entry.hash.hex()} {self.series}.{self.key}={"[DEL]" if self.entry.value is DELETE else self.entry.value}'
return (f'{self.entry.hash.hex()} {self.series}.{self.key}='
f'{"[DEL]" if self.entry.value is DELETE else "[UNL]" if self.value is UNLOAD else self.entry.value}')

View File

@@ -6,7 +6,7 @@ from typing import Any, Optional, Union, Sequence, Reversible
from sortedcontainers import SortedList
from dexorder import NARG
from dexorder import NARG, UNLOAD
from dexorder.base.fork import Fork, DisjointFork
from dexorder.database.model import Block
from dexorder.util import hexstr
@@ -125,7 +125,7 @@ class BlockState:
def _get_from_diffs(self, fork, diffs):
for diff in reversed(diffs):
if diff.height <= self.root_block.height or fork is not None and diff in fork:
if diff.height <= self.root_block.height or fork is not None and diff in fork and diff.value is not UNLOAD:
if diff.value is DELETE:
break
else:
@@ -134,7 +134,6 @@ class BlockState:
return diff.value
return DELETE
def set(self, fork: Optional[Fork], series, key, value, overwrite=True):
diffs = self.diffs_by_series[series][key]
if overwrite or self._get_from_diffs(fork, diffs) != value:
@@ -162,6 +161,7 @@ class BlockState:
# walk the by_height list to delete any aged-out block data
# in order to prune diffs_by_series, updated_keys remembers all the keys that were touched by any aged-out block
series_deletions = []
key_unloads: list[tuple[Any,Any]] = []
updated_keys = set()
while self.by_height and self.by_height[0].height <= block.height:
dead = self.by_height.pop(0)
@@ -175,6 +175,8 @@ class BlockState:
for d in block_diffs:
if d.key == BlockState._DELETE_SERIES_KEY and dead.hash in new_root_fork:
series_deletions.append(d.series)
elif d.value is UNLOAD and dead.hash in new_root_fork:
key_unloads.append((d.series, d.key))
else:
updated_keys.add((d.series, d.key))
del self.diffs_by_hash[dead.hash]
@@ -200,6 +202,11 @@ class BlockState:
# if only one diff remains, and it's old, and it's a delete, then we can actually delete the diff list
if not difflist or len(difflist) == 1 and difflist[0].value == DELETE and difflist[0].height <= new_root_fork.height:
del self.diffs_by_series[s][k]
for s,k in key_unloads:
try:
del self.diffs_by_series[s][k]
except KeyError:
pass
for s in series_deletions:
del self.diffs_by_series[s]
self.root_block = block

View File

@@ -1,7 +1,7 @@
from .abi import abis
from .contract_proxy import ContractProxy, Transaction
from .pool_contract import UniswapV3Pool
from .uniswap_contracts import uniswap
from .uniswap_contracts import uniswapV3
from eth_abi.codec import ABIDecoder, ABIEncoder
from eth_abi.registry import registry as default_registry

View File

@@ -66,7 +66,7 @@ class ContractProxy:
def __init__(self, address: str = None, name=None, *, _contracts=None, _wrapper=call_wrapper, abi=None):
"""
For regular contract use, either name or abi must be supplied. If abi is None, then name is used to find
the ABI in the project's contract/out/ directory. Otherwise, abi may be either a string or preparsed dict.
the ABI in the project's ../contract/out/ directory. Otherwise, abi may be either a string or preparsed dict.
If address is not supplied, this proxy may still be used to construct a new contract via deploy(). After
deploy() completes, the address member will be populated.
"""

View File

@@ -1,4 +1,4 @@
from dexorder.base.chain import Ethereum, Goerli, Polygon, Mumbai
from dexorder.base.chain import Ethereum, Goerli, Polygon, Mumbai, Arbitrum, Mock
from dexorder.contract.contract_proxy import ContractProxy
from dexorder.blockchain import ByBlockchainDict
@@ -12,8 +12,8 @@ class _UniswapContracts (ByBlockchainDict[ContractProxy]):
'quoter': ContractProxy('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', 'IQuoter'),
'swap_router': ContractProxy('0xE592427A0AEce92De3Edee1F18E0157C05861564', 'ISwapRouter'),
}
super().__init__({chain.chain_id:std for chain in (Ethereum, Polygon, Goerli, Mumbai)})
super().__init__({chain.chain_id:std for chain in (Ethereum, Polygon, Goerli, Mumbai, Arbitrum, Mock)})
uniswap = _UniswapContracts()
uniswapV3 = _UniswapContracts()

View File

@@ -2,8 +2,9 @@ import logging
from web3.types import EventData
from dexorder import dec, current_pub, current_w3
from dexorder import current_pub, current_w3
from dexorder.base.chain import current_chain
from dexorder.blockchain.uniswap import uniswap_price
from dexorder.blockchain.util import vault_address, get_contract_event, get_factory, get_contract_data
from dexorder.contract import VaultContract
from dexorder.data import pool_prices, vault_owners, vault_tokens, underfunded_vaults
@@ -81,8 +82,7 @@ def handle_swap(swap: EventData):
except KeyError:
return
addr = swap['address']
d = dec(sqrt_price)
price = d * d / dec(2 ** (96 * 2))
price = uniswap_price(sqrt_price)
log.debug(f'pool {addr} {price}')
pool_prices[addr] = price

View File

@@ -4,20 +4,22 @@ from dataclasses import dataclass
from enum import Enum
from typing import Optional
from dexorder.contract import abi_decoder, abi_encoder
from dexorder import dec
from dexorder.blockchain.uniswap import uniswap_pool_address, uniswap_price, uniswapV3_pool_address
from dexorder.contract import abi_decoder, abi_encoder, uniswapV3
log = logging.getLogger(__name__)
# enum SwapOrderState {
# Open, Canceled, Filled, Template
# Open, Canceled, Filled, Expired
# }
class SwapOrderState (Enum):
Open = 0
Canceled = 1
Filled = 2
Template = 3
Expired = 3
class Exchange (Enum):
UniswapV2 = 0
@@ -54,9 +56,16 @@ class SwapOrder:
return (self.tokenIn, self.tokenOut, self.route.dump(), self.amount, self.amountIsInput,
self.outputDirectlyToOwner, self.chainOrder, [t.dump() for t in self.tranches])
@property
def pool_address(self):
if self.route.exchange == Exchange.UniswapV3:
return uniswapV3_pool_address( self.tokenIn, self.tokenOut, self.route.fee )
else:
raise NotImplementedError
@dataclass
class SwapStatus:
state: SwapOrderState # todo refactor into canceled flag
state: SwapOrderState
start: int
ocoGroup: Optional[int]
filledIn: Optional[int] # if None then look in the order_filled blockstate
@@ -69,7 +78,7 @@ class SwapStatus:
class SwapOrderStatus (SwapStatus):
order: SwapOrder
def __init__(self, order, *swapstatus_args):
def __init__(self, order: SwapOrder, *swapstatus_args):
""" init with order object first follewed by the swap status args"""
super().__init__(*swapstatus_args)
self.order = order
@@ -89,6 +98,8 @@ class SwapOrderStatus (SwapStatus):
def dump(self):
return self.order.dump(), self.state.value, self.start, self.ocoGroup, self.filledIn, self.filledOut, self.trancheFilledIn, self.trancheFilledOut
def copy(self):
return SwapOrderStatus.load(self.dump())
NO_OCO = 18446744073709551615 # max uint64
@@ -117,20 +128,33 @@ class Constraint (ABC):
def _dump(self, types, values):
return self.mode, abi_encoder.encode(types, values)
class PriceConstraint (Constraint, ABC):
@abstractmethod
def passes(self, old_price: dec, new_price: dec) -> bool:...
@dataclass
class PriceConstraint (Constraint):
class LimitConstraint (PriceConstraint):
isAbove: bool
isRatio: bool
valueSqrtX96: int
TYPES = 'bool','bool','uint160'
def __init__(self, *args):
self.isAbove, self.isRatio, self.valueSqrtX96 = args
self.limit = uniswap_price(self.valueSqrtX96)
def load(self, obj):
isAbove, isRatio, valueSqrtX96 = abi_decoder.decode(PriceConstraint.TYPES, obj)
return PriceConstraint(ConstraintMode.Limit, isAbove, isRatio, valueSqrtX96)
isAbove, isRatio, valueSqrtX96 = abi_decoder.decode(LimitConstraint.TYPES, obj)
return LimitConstraint(ConstraintMode.Limit, isAbove, isRatio, valueSqrtX96)
def dump(self):
return self._dump(PriceConstraint.TYPES, (self.isAbove, self.isRatio, self.valueSqrtX96))
return self._dump(LimitConstraint.TYPES, (self.isAbove, self.isRatio, self.valueSqrtX96))
def passes(self, old_price: dec, new_price: dec) -> bool:
return self.isAbove and new_price >= self.limit or not self.isAbove and new_price <= self.limit
@dataclass

View File

@@ -1,8 +1,9 @@
import logging
from dataclasses import dataclass
from typing import overload
from dexorder.blockstate import BlockDict
from dexorder.order.orderlib import SwapStatus
from dexorder.blockstate import BlockDict, BlockSet
from dexorder.order.orderlib import SwapOrderStatus, SwapOrderState
log = logging.getLogger(__name__)
@@ -33,33 +34,167 @@ class TrancheKey (OrderKey):
@dataclass
class Remaining:
isInput: bool # True iff the remaining amount is in terms of the input token
remaining: int
class Filled:
filled_in: int
filled_out: int
@staticmethod
def basic2remaining(basic):
return Remaining(*basic)
return Filled(*basic)
def remaining2basic(self):
return self.isInput, self.remaining
return self.filled_in, self.filled_out
# ORDER STATE
# various blockstate fields hold different aspects of an order's state.
# all order and status data: writes to db but lazy-loads
orders = BlockDict[OrderKey,SwapStatus]('o', str2key=OrderKey.str2key, db='lazy') # todo lazy what's that about?
# the set of unfilled, not-canceled orders
active_orders = BlockDict[OrderKey]('ao', str2key=OrderKey.str2key, db=True, redis=True)
# total remaining amount per order, for all unfilled, not-canceled orders
order_remaining = BlockDict[OrderKey,Remaining](
'or', str2key=OrderKey.str2key, value2basic=Remaining.remaining2basic, basic2value=Remaining.basic2remaining, db=True, redis=True)
# total remaining amount per tranche
tranche_remaining = BlockDict[TrancheKey,Remaining](
'tr', str2key=TrancheKey.str2key, value2basic=Remaining.remaining2basic, basic2value=Remaining.basic2remaining, db=True, redis=True)
# todo oco groups
class Order:
"""
represents the canonical internal representation of an order. some members are immutable like the order spec, and some are
represented in various blockstate structures. this class hides that complexity to provide a clean interface to orders.
"""
instances: dict[OrderKey, 'Order'] = {}
@staticmethod
@overload
def of(key: OrderKey):...
@staticmethod
@overload
def of(vault: str, order_index: int):...
@staticmethod
def of(a, b=None):
return Order.instances[a if b is None else OrderKey(a,b)]
@staticmethod
def create(vault: str, order_index: int, status: SwapOrderStatus):
""" use when a brand new order is detected by the system """
key = OrderKey(vault, order_index)
Order._statuses[key] = status.copy() # always copy the struct when setting. values in BlockData must be immutable
order = Order(key)
if order.is_open:
Order._open_keys.add(key)
Order._order_filled[key] = Filled(status.filledIn, status.filledOut)
for i, tk in enumerate(order.tranche_keys):
Order._tranche_filled[tk] = Filled(status.trancheFilledIn[i], status.trancheFilledOut[i])
return order
@overload
def __init__(self, key: OrderKey): ...
@overload
def __init__(self, vault: str, order_index: int): ...
def __init__(self, a, b=None):
""" references an existing Order in the system. to create a new order, use create() """
key = a if b is None else OrderKey(a,b)
assert key not in Order.instances
self.key = key
self.status: SwapOrderStatus = Order._statuses[key].copy()
self.pool_address: str = self.status.order.pool_address
self.tranche_keys = [TrancheKey(key.vault, key.order_index, i) for i in range(len(self.status.trancheFilledIn))]
@property
def state(self):
return self.status.state
@property
def order(self):
return self.status.order
@property
def amount(self):
return self.order.amount
@property
def remaining(self):
return self.amount - self.filled
@property
def filled_in(self):
return Order._order_filled[self.key].filled_in if self.is_open else self.status.filledIn
@property
def filled_out(self):
return Order._order_filled[self.key].filled_out if self.is_open else self.status.filledOut
def tranche_filled_in(self, tk: TrancheKey):
return Order._tranche_filled[tk].filled_in if self.is_open else self.status.trancheFilledIn[tk.tranche_index]
def tranche_filled_out(self, tk: TrancheKey):
return Order._tranche_filled[tk].filled_out if self.is_open else self.status.trancheFilledIn[tk.tranche_index]
def tranche_filled(self, tk: TrancheKey):
return self.tranche_filled_in(tk) if self.amount_is_input else self.tranche_filled_out(tk)
@property
def filled(self):
return self.filled_in if self.amount_is_input else self.filled_out
@property
def amount_is_input(self):
return self.order.amountIsInput
@property
def is_open(self):
return self.state is SwapOrderState.Open
def add_fill(self, tranche_index: int, filled_in: int, filled_out: int):
# order fill
old = Order._order_filled[self.key]
fin = old.filled_in + filled_in
fout = old.filled_out + filled_out
Order._order_filled[self.key] = Filled(fin, fout)
# tranche fill
tk = self.tranche_keys[tranche_index]
old = Order._tranche_filled[tk]
fin = old.filled_in + filled_in
fout = old.filled_out + filled_out
Order._tranche_filled[tk] = Filled(fin, fout)
def complete(self, final_state: SwapOrderState):
""" updates the static order record with its final values, then deletes all its dynamic blockstate and removes the Order from the actives list """
assert final_state is not SwapOrderState.Open
status = self.status
status.state = final_state
if self.is_open:
del Order._open_keys[self.key]
filled = Order._order_filled[self.key]
del Order._order_filled[self.key]
status.filledIn = filled.filled_in
status.filledOut = filled.filled_out
for i, tk in enumerate(self.tranche_keys):
filled = Order._tranche_filled[tk]
del Order._tranche_filled[tk]
status.trancheFilledIn[i] = filled.filled_in
status.trancheFilledOut[i] = filled.filled_out
final_status = status.copy()
Order._statuses[self.key] = final_status # set the status in order to save it
Order._statuses.unload(self.key) # but then unload from memory after root promotion
# ORDER STATE
# various blockstate fields hold different aspects of an order's state.
# this series holds "everything" about an order in the canonical format specified by the contract orderlib, except
# the filled amount fields for active orders are maintained in the order_remainings and tranche_remainings series.
_statuses: BlockDict[OrderKey, SwapOrderStatus] = BlockDict('o', db='lazy', str2key=OrderKey.str2key)
# open orders = the set of unfilled, not-canceled orders
_open_keys: BlockSet[OrderKey] = BlockSet('oo', db=True, redis=True, str2key=OrderKey.str2key)
# total remaining amount per order, for all unfilled, not-canceled orders
_order_filled: BlockDict[OrderKey, Filled] = BlockDict(
'of', db=True, redis=True, str2key=OrderKey.str2key, value2basic=Filled.remaining2basic, basic2value=Filled.basic2remaining)
# total remaining amount per tranche
_tranche_filled: BlockDict[TrancheKey, Filled] = BlockDict(
'tf', db=True, redis=True, str2key=TrancheKey.str2key, value2basic=Filled.remaining2basic, basic2value=Filled.basic2remaining)
active_orders: dict[OrderKey,Order] = {}

View File

@@ -1,19 +1,23 @@
import logging
from enum import Enum
from typing import Callable
from dexorder.blockstate import BlockSet
from .orderlib import SwapOrderStatus, TimeConstraint, PriceConstraint, ConstraintMode
from dexorder.blockstate import BlockSet, BlockDict
from .orderlib import TimeConstraint, LimitConstraint, ConstraintMode
from dexorder.util import defaultdictk
from .orderstate import TrancheKey, Order
from ..database.model.block import current_block
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)
TimeTrigger = Callable[[int, int], None] # func(previous_timestamp, current_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
price_triggers:dict[str, BlockSet[PriceTrigger]] = defaultdictk(lambda addr:BlockSet(f'pt|{addr}')) # different BlockSet per pool address
execution_requests:BlockDict[TrancheKey,int] = BlockDict('te') # value is block height of the request
def intersect_ranges( a_low, a_high, b_low, b_high):
low, high = max(a_low,b_low), min(a_high,b_high)
@@ -21,35 +25,90 @@ def intersect_ranges( a_low, a_high, b_low, b_high):
low, high = None, None
return low, high
class TrancheStatus (Enum):
Early = 0 # first time trigger hasnt happened yet
Pricing = 1 # we are inside the time window and checking prices
Filled = 1 # tranche has no more available amount
Expired = 2 # time deadline has past and this tranche cannot be filled
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
def __init__(self, order: Order, tranche_key: TrancheKey):
assert order.key.vault == tranche_key.vault and order.key.order_index == tranche_key.order_index
self.order = order
self.tk = tranche_key
self.status = TrancheStatus.Early
# todo refactor so we have things like tranche amount filled as blockstate, order amount remaining
tranche = order.order.tranches[self.tk.tranche_index]
tranche_amount = order.amount * tranche.fraction // 10**18
tranche_filled = order.tranche_filled(self.tk)
tranche_remaining = tranche_amount - tranche_filled
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?
if tranche_remaining <= 0:
self.status = TrancheStatus.Filled
return
time_constraint = None
price_constraints = []
if status.filledOut:
...
self.time_constraint = time_constraint = None
self.price_constraints = []
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
c: LimitConstraint
raise NotImplementedError
else:
raise NotImplementedError
if time_constraint is None:
self.status = TrancheStatus.Pricing
else:
timestamp = current_block.get().timestamp
earliest, latest = time_constraint
self.status = TrancheStatus.Early if timestamp < earliest else TrancheStatus.Expired if timestamp > latest else TrancheStatus.Pricing
self.enable_time_trigger()
if self.status == TrancheStatus.Pricing:
self.enable_price_trigger()
def enable_time_trigger(self):
if self.time_constraint:
time_triggers.add(self.time_trigger)
def disable_time_trigger(self):
if self.time_constraint:
time_triggers.remove(self.time_trigger)
def time_trigger(self, _prev, now):
if now >= self.time_constraint[1]:
self.disable()
self.status = TrancheStatus.Expired
if self.status == TrancheStatus.Early and now >= self.time_constraint[0]:
self.status = TrancheStatus.Pricing
self.enable_price_trigger()
def enable_price_trigger(self):
price_triggers[self.order.pool_address].add(self.price_trigger)
def disable_price_trigger(self):
price_triggers[self.order.pool_address].remove(self.price_trigger)
def price_trigger(self, prev, cur):
if all(pc.passes(prev,cur) for pc in self.price_constraints):
self.execute()
def execute(self):
log.info(f'execution request for {self.tk}')
execution_requests[self.tk] = current_block.get().height
def disable(self):
self.disable_time_trigger()
self.disable_price_trigger()
class OrderTriggers:
def __init__(self, order: Order):
self.order = order
self.triggers = [TrancheTrigger(order, tk) for tk in self.order.tranche_keys]
def disable(self):
for t in self.triggers:
t.disable()