triggers impl, written but not connected or tested
This commit is contained in:
@@ -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 '' "$@"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user