price constraints working
This commit is contained in:
@@ -44,6 +44,6 @@ Polygon = Blockchain(137, 'Polygon') # POS not zkEVM
|
||||
Mumbai = Blockchain(80001, 'Mumbai')
|
||||
BSC = Blockchain(56, 'BSC')
|
||||
Arbitrum = Blockchain(42161, 'Arbitrum', 3, batch_size=1000) # todo configure batch size... does it depend on log count? :(
|
||||
Mock = Blockchain(31337, 'Mock', 3)
|
||||
Mock = Blockchain(31337, 'Mock', 3, batch_size=10000)
|
||||
|
||||
current_chain = ContextVar[Blockchain]('current_chain')
|
||||
|
||||
@@ -119,4 +119,3 @@ for _chain_id, _native in _native_tokens.items():
|
||||
_tokens_by_chain[_chain_id][_native.symbol] = _native
|
||||
|
||||
tokens = ByBlockchainDict[Token](_tokens_by_chain)
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ import logging
|
||||
import sys
|
||||
from asyncio import CancelledError
|
||||
|
||||
from dexorder import db, config, Blockchain, blockchain
|
||||
from dexorder.base.chain import current_chain
|
||||
from dexorder import db, blockchain
|
||||
from dexorder.bin.executable import execute
|
||||
from dexorder.blockstate.blockdata import BlockData
|
||||
from dexorder.blockstate.db_state import DbState
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
from charset_normalizer.md import getLogger
|
||||
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 abi_encoder
|
||||
from dexorder.util import hexbytes
|
||||
|
||||
UNISWAPV3_POOL_INIT_CODE_HASH = hexbytes('0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54')
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
class Fee:
|
||||
LOWEST = 100
|
||||
LOW = 500
|
||||
MEDIUM = 3000
|
||||
HIGH = 10000
|
||||
|
||||
|
||||
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 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(abi_encoder.encode(['address','address','uint24'],[token0, token1, fee]))
|
||||
contract_address = keccak(
|
||||
b"\xff"
|
||||
+ to_bytes(hexstr=factory_addr)
|
||||
+ salt
|
||||
+ UNISWAPV3_POOL_INIT_CODE_HASH
|
||||
).hex()[-40:]
|
||||
result = to_checksum_address(contract_address)
|
||||
# log.debug(f'uniswap pool address {factory_addr} {addr_a} {addr_b} {fee} => {result}')
|
||||
return result
|
||||
|
||||
def uniswap_price(sqrt_price):
|
||||
d = dec(sqrt_price)
|
||||
price = d * d / dec(2 ** (96 * 2))
|
||||
return price
|
||||
@@ -4,15 +4,12 @@ from eth_abi.codec import ABIDecoder, ABIEncoder
|
||||
from eth_abi.registry import registry as default_registry
|
||||
|
||||
from .. import current_w3 as _current_w3
|
||||
from .abi import abis
|
||||
from .contract_proxy import ContractProxy
|
||||
|
||||
abi_decoder = ABIDecoder(default_registry)
|
||||
abi_encoder = ABIEncoder(default_registry)
|
||||
|
||||
from .abi import abis
|
||||
from .contract_proxy import ContractProxy
|
||||
from .pool_contract import UniswapV3Pool
|
||||
from .uniswap_contracts import uniswapV3
|
||||
|
||||
|
||||
def get_contract_data(name):
|
||||
with open(f'../contract/out/{name}.sol/{name}.json', 'rt') as file:
|
||||
|
||||
@@ -2,13 +2,11 @@ import json
|
||||
from typing import Optional
|
||||
|
||||
import eth_account
|
||||
from eth_utils import keccak
|
||||
from web3.types import TxReceipt
|
||||
|
||||
from dexorder import current_w3, Account
|
||||
from dexorder import current_w3
|
||||
from dexorder.base.account import current_account
|
||||
from dexorder.database.model.block import current_block
|
||||
from dexorder.base import TransactionDict
|
||||
from dexorder.util import hexstr
|
||||
|
||||
|
||||
|
||||
16
src/dexorder/contract/decimals.py
Normal file
16
src/dexorder/contract/decimals.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import logging
|
||||
|
||||
from dexorder import db
|
||||
from dexorder.contract import ContractProxy
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def token_decimals(addr):
|
||||
key = f'td|{addr}'
|
||||
try:
|
||||
return db.kv[key]
|
||||
except KeyError:
|
||||
decimals = await ContractProxy(addr, 'ERC20').decimals()
|
||||
db.kv[key] = decimals
|
||||
return decimals
|
||||
@@ -1,10 +0,0 @@
|
||||
from .contract_proxy import ContractProxy
|
||||
from ..blockchain.uniswap import uniswap_price
|
||||
|
||||
|
||||
class UniswapV3Pool (ContractProxy):
|
||||
def __init__(self, address: str = None):
|
||||
super().__init__(address, 'IUniswapV3Pool')
|
||||
|
||||
async def price(self):
|
||||
return uniswap_price((await self.slot0())[0])
|
||||
@@ -1,23 +0,0 @@
|
||||
from dexorder.base.chain import Ethereum, Goerli, Polygon, Mumbai, Arbitrum, Mock
|
||||
from dexorder.blockchain.uniswap import uniswap_pool_address
|
||||
from dexorder.contract.contract_proxy import ContractProxy
|
||||
from dexorder.blockchain import ByBlockchainDict
|
||||
|
||||
|
||||
class _UniswapContracts (ByBlockchainDict[ContractProxy]):
|
||||
|
||||
def __init__(self):
|
||||
std = {
|
||||
'factory': ContractProxy('0x1F98431c8aD98523631AE4a59f267346ea31F984', 'IUniswapV3Factory'),
|
||||
'nfpm': ContractProxy('0xC36442b4a4522E871399CD717aBDD847Ab11FE88', 'INonfungiblePositionManager'),
|
||||
'quoter': ContractProxy('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', 'IQuoter'),
|
||||
'swap_router': ContractProxy('0xE592427A0AEce92De3Edee1F18E0157C05861564', 'ISwapRouter'),
|
||||
}
|
||||
super().__init__({chain.chain_id:std for chain in (Ethereum, Polygon, Goerli, Mumbai, Arbitrum, Mock)})
|
||||
|
||||
|
||||
uniswapV3 = _UniswapContracts()
|
||||
|
||||
|
||||
def uniswapV3_pool_address( addr_a: str, addr_b: str, fee: int):
|
||||
return uniswap_pool_address(uniswapV3['factory'].address, addr_a, addr_b, fee)
|
||||
@@ -1,6 +1,7 @@
|
||||
from dexorder import dec
|
||||
from dexorder.base.chain import current_chain
|
||||
from dexorder.blockstate import BlockDict
|
||||
from dexorder.blockstate.blockdata import K, V
|
||||
from dexorder.util import json, defaultdictk
|
||||
|
||||
# pub=... publishes to a channel for web clients to consume. argument is (key,value) and return must be (event,room,args)
|
||||
@@ -14,5 +15,15 @@ vault_balances: BlockDict[str, dict[str, int]] = BlockDict(
|
||||
str2value=lambda s: {k: int(v) for k, v in json.loads(s).items()},
|
||||
pub=lambda k, v: (f'{current_chain.get().chain_id}|{vault_owners[k]}', 'vb', (k,json.dumps({k2: str(v2) for k2, v2 in v.items()})))
|
||||
)
|
||||
pool_prices: BlockDict[str, dec] = BlockDict('p', db=True, redis=True, value2str=lambda d: f'{d:f}', str2value=dec,
|
||||
pub=lambda k, v: (f'{current_chain.get().chain_id}|{k}', 'p', (k, str(v))))
|
||||
|
||||
|
||||
class PoolPrices (BlockDict[str, dec]):
|
||||
def __setitem__(self, item: K, value: V) -> None:
|
||||
super().__setitem__(item, value)
|
||||
new_pool_prices[item] = value
|
||||
|
||||
|
||||
new_pool_prices: dict[str, dec] = {} # tracks which prices were set during the current block. cleared every block.
|
||||
pool_prices: PoolPrices = PoolPrices('p', db=True, redis=True,
|
||||
value2str=lambda d: f'{d:f}', str2value=dec,
|
||||
pub=lambda k, v: (f'{current_chain.get().chain_id}|{k}', 'p', (k, str(v))))
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import mapped_column, Mapped
|
||||
|
||||
from dexorder.database.column import Json
|
||||
from dexorder.database.model import Base
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -4,11 +4,9 @@ from typing import Optional
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||
|
||||
from dexorder.base.order import TransactionRequest as TransactionRequestDict, deserialize_transaction_request
|
||||
from dexorder.base import TransactionDict
|
||||
from dexorder.database.column import Dict, Bytes, UUID_PK, Blockchain, UUID
|
||||
from dexorder.database.column_types import DataclassDict
|
||||
from dexorder.database.model import Base
|
||||
|
||||
@@ -3,20 +3,21 @@ from uuid import UUID
|
||||
|
||||
from web3.types import EventData
|
||||
|
||||
from dexorder import current_pub, db
|
||||
from dexorder import current_pub, db, dec
|
||||
from dexorder.base.chain import current_chain
|
||||
from dexorder.base.order import TrancheExecutionRequest, TrancheKey, ExecutionRequest, new_tranche_execution_request
|
||||
from dexorder.transaction import handle_create_transactions, submit_transaction_request, handle_transaction_receipts, handle_send_transactions
|
||||
from dexorder.blockchain.uniswap import uniswap_price
|
||||
from dexorder.transaction import create_transactions, submit_transaction_request, handle_transaction_receipts, send_transactions
|
||||
from dexorder.uniswap import UniswapV3Pool, uniswap_price
|
||||
from dexorder.contract.dexorder import get_factory_contract, vault_address, VaultContract, get_dexorder_contract
|
||||
from dexorder.contract import UniswapV3Pool, get_contract_event
|
||||
from dexorder.data import pool_prices, vault_owners, vault_balances
|
||||
from dexorder.contract import get_contract_event
|
||||
from dexorder.data import pool_prices, vault_owners, vault_balances, new_pool_prices
|
||||
from dexorder.database.model.block import current_block
|
||||
from dexorder.database.model.transaction import TransactionJob
|
||||
from dexorder.order.orderlib import SwapOrderState, SwapOrderStatus
|
||||
from dexorder.order.orderstate import Order
|
||||
from dexorder.order.triggers import OrderTriggers, close_order_and_disable_triggers, price_triggers, time_triggers, \
|
||||
unconstrained_price_triggers, execution_requests, inflight_execution_requests, TrancheStatus
|
||||
unconstrained_price_triggers, execution_requests, inflight_execution_requests, TrancheStatus, active_tranches, new_price_triggers
|
||||
from dexorder.util.async_util import maywait
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -60,6 +61,7 @@ def setup_logevent_triggers(runner):
|
||||
# THIS IS BASICALLY THE MAIN RUN LOOP FOR EVERY BLOCK
|
||||
#
|
||||
|
||||
runner.add_event_trigger(init)
|
||||
runner.add_event_trigger(handle_vault_created, vault_created)
|
||||
runner.add_event_trigger(handle_order_placed, get_contract_event('OrderLib', 'DexorderSwapPlaced'))
|
||||
runner.add_event_trigger(handle_transfer, get_contract_event('ERC20', 'Transfer'))
|
||||
@@ -71,9 +73,15 @@ def setup_logevent_triggers(runner):
|
||||
runner.add_event_trigger(handle_dexorderexecutions, executions)
|
||||
runner.add_event_trigger(activate_time_triggers)
|
||||
runner.add_event_trigger(activate_price_triggers)
|
||||
runner.add_event_trigger(process_active_tranches)
|
||||
runner.add_event_trigger(process_execution_requests)
|
||||
runner.add_event_trigger(handle_create_transactions)
|
||||
runner.add_event_trigger(handle_send_transactions)
|
||||
runner.add_event_trigger(create_transactions)
|
||||
runner.add_event_trigger(send_transactions)
|
||||
|
||||
|
||||
def init():
|
||||
new_pool_prices.clear()
|
||||
new_price_triggers.clear()
|
||||
|
||||
|
||||
async def handle_order_placed(event: EventData):
|
||||
@@ -169,17 +177,16 @@ def handle_transfer(transfer: EventData):
|
||||
log.debug(f'vaults: {list(vaults)}')
|
||||
|
||||
|
||||
new_pool_prices: dict[str, int] = {}
|
||||
|
||||
def handle_uniswap_swap(swap: EventData):
|
||||
async def handle_uniswap_swap(swap: EventData):
|
||||
try:
|
||||
sqrt_price = swap['args']['sqrtPriceX96']
|
||||
except KeyError:
|
||||
return
|
||||
addr = swap['address']
|
||||
price = uniswap_price(sqrt_price)
|
||||
price: dec = await uniswap_price(addr, sqrt_price)
|
||||
log.debug(f'pool {addr} {price}')
|
||||
new_pool_prices[addr] = price
|
||||
pool_prices[addr] = price
|
||||
|
||||
|
||||
def handle_vault_created(created: EventData):
|
||||
@@ -204,22 +211,38 @@ def handle_vault_created(created: EventData):
|
||||
current_pub.get()(f'{current_chain.get().chain_id}|{owner}', 'vaults', vaults)
|
||||
|
||||
|
||||
def activate_time_triggers():
|
||||
async def activate_time_triggers():
|
||||
now = current_block.get().timestamp
|
||||
log.debug(f'activating time triggers')
|
||||
# time triggers
|
||||
for tt in time_triggers:
|
||||
tt(now)
|
||||
await maywait(tt(now))
|
||||
|
||||
def activate_price_triggers():
|
||||
log.debug('activating price triggers')
|
||||
async def activate_price_triggers():
|
||||
log.debug(f'activating price triggers')
|
||||
pools_triggered = set()
|
||||
for pool, price in new_pool_prices.items():
|
||||
pools_triggered.add(pool)
|
||||
for pt in price_triggers[pool]:
|
||||
pt(price)
|
||||
new_pool_prices.clear()
|
||||
await maywait(pt(price))
|
||||
for pool, triggers in new_price_triggers.items():
|
||||
if pool not in pools_triggered:
|
||||
price = pool_prices[pool]
|
||||
for pt in triggers:
|
||||
await maywait(pt(price))
|
||||
for t in unconstrained_price_triggers:
|
||||
# noinspection PyTypeChecker
|
||||
t(None)
|
||||
await maywait(t(None))
|
||||
|
||||
|
||||
def process_active_tranches():
|
||||
for tk, proof in active_tranches.items():
|
||||
old_req = execution_requests.get(tk)
|
||||
height = current_block.get().height
|
||||
if old_req is None or old_req.height <= height:
|
||||
log.info(f'execution request for {tk}')
|
||||
execution_requests[tk] = ExecutionRequest(height, proof)
|
||||
|
||||
|
||||
async def process_execution_requests():
|
||||
height = current_block.get().height
|
||||
|
||||
@@ -5,8 +5,7 @@ from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from dexorder import dec
|
||||
from dexorder.blockchain.uniswap import uniswap_price
|
||||
from dexorder.contract.uniswap_contracts import uniswapV3_pool_address
|
||||
from dexorder.uniswap import uniswapV3_pool_address, uniswap_price
|
||||
from dexorder.contract import abi_decoder, abi_encoder
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -106,10 +105,8 @@ NO_OCO = 18446744073709551615 # max uint64
|
||||
|
||||
class ConstraintMode (Enum):
|
||||
Time = 0
|
||||
Limit = 1
|
||||
Trailing = 2
|
||||
Barrier = 3
|
||||
Line = 4
|
||||
Line = 1
|
||||
Barrier = 2
|
||||
|
||||
@dataclass
|
||||
class Constraint (ABC):
|
||||
@@ -120,51 +117,16 @@ class Constraint (ABC):
|
||||
mode = ConstraintMode(obj[0])
|
||||
if mode == ConstraintMode.Time:
|
||||
return TimeConstraint.load(obj[1])
|
||||
elif mode == ConstraintMode.Line:
|
||||
return LineConstraint.load(obj[1])
|
||||
else:
|
||||
raise NotImplementedError
|
||||
raise ValueError(f'Unknown constraint mode {mode}')
|
||||
|
||||
@abstractmethod
|
||||
def dump(self): ...
|
||||
|
||||
def _dump(self, types, values):
|
||||
return self.mode, abi_encoder.encode(types, values)
|
||||
|
||||
|
||||
class PriceConstraint (Constraint, ABC):
|
||||
@abstractmethod
|
||||
def passes(self, price: dec) -> bool:...
|
||||
|
||||
|
||||
@dataclass
|
||||
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(LimitConstraint.TYPES, obj)
|
||||
return LimitConstraint(ConstraintMode.Limit, isAbove, isRatio, valueSqrtX96)
|
||||
|
||||
def dump(self):
|
||||
return self._dump(LimitConstraint.TYPES, (self.isAbove, self.isRatio, self.valueSqrtX96))
|
||||
|
||||
def passes(self, price: dec) -> bool:
|
||||
return self.isAbove and price >= self.limit or not self.isAbove and price <= self.limit
|
||||
|
||||
|
||||
@dataclass
|
||||
class LineConstraint (Constraint):
|
||||
isAbove: bool
|
||||
isRatio: bool
|
||||
time: int
|
||||
valueSqrtX96: int
|
||||
slopeSqrtX96: int
|
||||
return self.mode.value, abi_encoder.encode(types, values)
|
||||
|
||||
|
||||
class TimeMode (Enum):
|
||||
@@ -201,6 +163,31 @@ class TimeConstraint (Constraint):
|
||||
return self._dump(TimeConstraint.TYPES, (self.earliest.mode.value, self.earliest.time, self.latest.mode.value, self.latest.time))
|
||||
|
||||
|
||||
@dataclass
|
||||
class LineConstraint (Constraint):
|
||||
isAbove: bool
|
||||
isRatio: bool
|
||||
time: int
|
||||
valueSqrtX96: int
|
||||
slopeSqrtX96: int
|
||||
|
||||
TYPES = 'bool','bool','uint32','uint160','int160'
|
||||
|
||||
@staticmethod
|
||||
def load(obj):
|
||||
return LineConstraint(ConstraintMode.Line, *abi_decoder.decode(LineConstraint.TYPES, obj))
|
||||
|
||||
def dump(self):
|
||||
return self._dump(LineConstraint.TYPES, (self.isAbove, self.isRatio, self.time, self.valueSqrtX96, self.slopeSqrtX96))
|
||||
|
||||
async def passes(self, pool_addr: str, price: dec) -> bool:
|
||||
limit = await uniswap_price(pool_addr, self.valueSqrtX96)
|
||||
# todo slopes
|
||||
# todo ratios
|
||||
# prices AT the limit get zero volume, so we only trigger on >, not >=
|
||||
return self.isAbove and price > limit or not self.isAbove and price < limit
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tranche:
|
||||
fraction: int # 18-decimal fraction of the order amount which is available to this tranche. must be <= 1
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from enum import Enum, auto
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional, Union, Coroutine, Awaitable
|
||||
|
||||
from dexorder.blockstate import BlockSet, BlockDict
|
||||
from .orderlib import TimeConstraint, LimitConstraint, ConstraintMode, SwapOrderState, PriceProof
|
||||
from .orderlib import TimeConstraint, LineConstraint, ConstraintMode, SwapOrderState, PriceProof
|
||||
from dexorder.util import defaultdictk
|
||||
from .orderstate import Order
|
||||
from .. import dec
|
||||
from ..base.order import OrderKey, TrancheKey, ExecutionRequest
|
||||
from ..data import pool_prices
|
||||
from ..database.model.block import current_block
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# todo time and price triggers should be BlockSortedSets that support range queries, for efficient lookup of triggers
|
||||
# todo time and price triggers should be BlockSortedSets that support range queries for efficient lookup of triggers
|
||||
TimeTrigger = Callable[[int], None] # func(timestamp)
|
||||
time_triggers:BlockSet[TimeTrigger] = BlockSet('tt')
|
||||
|
||||
PriceTrigger = Callable[[int], None] # func(pool_price)
|
||||
PriceTrigger = Callable[[dec], Union[Awaitable[None],None]] # [async] func(pool_price)
|
||||
price_triggers:dict[str, BlockSet[PriceTrigger]] = defaultdictk(lambda addr:BlockSet(f'pt|{addr}')) # different BlockSet per pool address
|
||||
new_price_triggers:dict[str, set[PriceTrigger]] = defaultdict(set) # when price triggers are first set, they must be tested against the current price even if it didnt change this block
|
||||
unconstrained_price_triggers: BlockSet[PriceTrigger] = BlockSet('upt') # tranches with no price constraints, whose time constraint is fulfilled
|
||||
execution_requests:BlockDict[TrancheKey, ExecutionRequest] = BlockDict('e')
|
||||
active_tranches: BlockDict[TrancheKey, Optional[PriceProof]] = BlockDict('at') # tranches which have passed all constraints and should be executed
|
||||
execution_requests:BlockDict[TrancheKey, ExecutionRequest] = BlockDict('e') # generated by the active tranches
|
||||
|
||||
# todo should this really be blockdata?
|
||||
inflight_execution_requests:BlockDict[TrancheKey, int] = BlockDict('ei') # value is block height when the request was sent
|
||||
@@ -53,16 +59,16 @@ class TrancheTrigger:
|
||||
return
|
||||
|
||||
time_constraint = None # stored as a tuple of two ints for earliest and latest absolute timestamps
|
||||
self.price_constraints = []
|
||||
self.line_constraints: list[LineConstraint] = []
|
||||
for c in tranche.constraints:
|
||||
if c.mode == ConstraintMode.Time:
|
||||
c: TimeConstraint
|
||||
earliest = c.earliest.timestamp(start)
|
||||
latest = c.latest.timestamp(start)
|
||||
time_constraint = (earliest, latest) if time_constraint is None else intersect_ranges(*time_constraint, earliest, latest)
|
||||
elif c.mode == ConstraintMode.Limit:
|
||||
c: LimitConstraint
|
||||
raise NotImplementedError
|
||||
elif c.mode == ConstraintMode.Line:
|
||||
c: LineConstraint
|
||||
self.line_constraints.append(c)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
self.time_constraint = time_constraint
|
||||
@@ -92,8 +98,8 @@ class TrancheTrigger:
|
||||
return
|
||||
if now >= self.time_constraint[1]:
|
||||
log.debug(f'tranche expired {self.tk}')
|
||||
self.disable()
|
||||
self.status = TrancheStatus.Expired
|
||||
self.disable()
|
||||
# check for all tranches expired
|
||||
OrderTriggers.instances[self.order.key].check_complete()
|
||||
elif self.status == TrancheStatus.Early and now >= self.time_constraint[0]:
|
||||
@@ -102,30 +108,25 @@ class TrancheTrigger:
|
||||
self.enable_price_trigger()
|
||||
|
||||
def enable_price_trigger(self):
|
||||
if self.price_constraints:
|
||||
if self.line_constraints:
|
||||
price_triggers[self.order.pool_address].add(self.price_trigger)
|
||||
new_price_triggers[self.order.pool_address].add(self.price_trigger)
|
||||
else:
|
||||
unconstrained_price_triggers.add(self.price_trigger)
|
||||
|
||||
def disable_price_trigger(self):
|
||||
if self.price_constraints:
|
||||
if self.line_constraints:
|
||||
price_triggers[self.order.pool_address].remove(self.price_trigger)
|
||||
else:
|
||||
unconstrained_price_triggers.remove(self.price_trigger)
|
||||
|
||||
def price_trigger(self, cur):
|
||||
async def price_trigger(self, cur: dec):
|
||||
# must be idempotent. could be called twice when first activated: once for the initial price lookup then once again if that price was changed in the current block
|
||||
if self.closed:
|
||||
log.debug(f'price trigger ignored because trigger status is {self.status}')
|
||||
return
|
||||
if not self.price_constraints or all(pc.passes(cur) for pc in self.price_constraints):
|
||||
self.execute()
|
||||
|
||||
def execute(self, proof: PriceProof = None):
|
||||
old_req = execution_requests.get(self.tk)
|
||||
height = current_block.get().height
|
||||
if old_req is None or old_req.height <= height:
|
||||
log.info(f'execution request for {self.tk}')
|
||||
execution_requests[self.tk] = ExecutionRequest(height, proof)
|
||||
if not self.line_constraints or all(await asyncio.gather(*[pc.passes(self.order.pool_address, cur) for pc in self.line_constraints])):
|
||||
active_tranches[self.tk] = None # or PriceProof(...)
|
||||
|
||||
def fill(self, _amount_in, _amount_out ):
|
||||
remaining = self.order.tranche_remaining(self.tk)
|
||||
@@ -137,6 +138,10 @@ class TrancheTrigger:
|
||||
return filled
|
||||
|
||||
def disable(self):
|
||||
try:
|
||||
del active_tranches[self.tk]
|
||||
except KeyError:
|
||||
pass
|
||||
self.disable_time_trigger()
|
||||
self.disable_price_trigger()
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ def submit_transaction_request(tr: TransactionRequest):
|
||||
return job
|
||||
|
||||
|
||||
async def handle_create_transactions():
|
||||
async def create_transactions():
|
||||
for job in db.session.query(TransactionJob).filter(
|
||||
TransactionJob.chain == current_chain.get(),
|
||||
TransactionJob.state == TransactionJobState.Requested
|
||||
@@ -75,7 +75,7 @@ async def create_transaction(job: TransactionJob):
|
||||
log.info(f'servicing transaction request {job.request.__class__.__name__} {job.id} with tx {ctx.id}')
|
||||
|
||||
|
||||
async def handle_send_transactions():
|
||||
async def send_transactions():
|
||||
w3 = current_w3.get()
|
||||
for job in db.session.query(TransactionJob).filter(
|
||||
TransactionJob.chain == current_chain.get(),
|
||||
|
||||
87
src/dexorder/uniswap.py
Normal file
87
src/dexorder/uniswap.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from charset_normalizer.md import getLogger
|
||||
from eth_utils import keccak, to_bytes, to_checksum_address
|
||||
|
||||
from dexorder import dec, db
|
||||
from dexorder.base.chain import Ethereum, Polygon, Goerli, Mumbai, Arbitrum, Mock
|
||||
from dexorder.blockchain import ByBlockchainDict
|
||||
from dexorder.contract import abi_encoder, ContractProxy
|
||||
from dexorder.contract.decimals import token_decimals
|
||||
from dexorder.util import hexbytes
|
||||
|
||||
UNISWAPV3_POOL_INIT_CODE_HASH = hexbytes('0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54')
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
class Fee:
|
||||
LOWEST = 100
|
||||
LOW = 500
|
||||
MEDIUM = 3000
|
||||
HIGH = 10000
|
||||
|
||||
|
||||
class UniswapV3Pool (ContractProxy):
|
||||
def __init__(self, address: str = None):
|
||||
super().__init__(address, 'IUniswapV3Pool')
|
||||
|
||||
async def price(self):
|
||||
if not self.address:
|
||||
raise ValueError
|
||||
return await uniswap_price(self.address, (await self.slot0())[0])
|
||||
|
||||
|
||||
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 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(abi_encoder.encode(['address','address','uint24'],[token0, token1, fee]))
|
||||
contract_address = keccak(
|
||||
b"\xff"
|
||||
+ to_bytes(hexstr=factory_addr)
|
||||
+ salt
|
||||
+ UNISWAPV3_POOL_INIT_CODE_HASH
|
||||
).hex()[-40:]
|
||||
result = to_checksum_address(contract_address)
|
||||
# log.debug(f'uniswap pool address {factory_addr} {addr_a} {addr_b} {fee} => {result}')
|
||||
return result
|
||||
|
||||
|
||||
async def uniswap_price(addr, sqrt_price) -> dec:
|
||||
price = dec(sqrt_price*sqrt_price) / 2 ** (96 * 2)
|
||||
decimals = await pool_decimals(addr)
|
||||
return price * dec(10) ** dec(decimals)
|
||||
|
||||
|
||||
async def pool_decimals(addr):
|
||||
key = f'pd|{addr}'
|
||||
try:
|
||||
return db.kv[key]
|
||||
except KeyError:
|
||||
pool = UniswapV3Pool(addr)
|
||||
token0 = await pool.token0()
|
||||
token1 = await pool.token1()
|
||||
decimals0 = await token_decimals(token0)
|
||||
decimals1 = await token_decimals(token1)
|
||||
decimals = decimals0 - decimals1
|
||||
db.kv[key] = decimals
|
||||
return decimals
|
||||
|
||||
|
||||
class _UniswapContracts (ByBlockchainDict[ContractProxy]):
|
||||
|
||||
def __init__(self):
|
||||
std = {
|
||||
'factory': ContractProxy('0x1F98431c8aD98523631AE4a59f267346ea31F984', 'IUniswapV3Factory'),
|
||||
'nfpm': ContractProxy('0xC36442b4a4522E871399CD717aBDD847Ab11FE88', 'INonfungiblePositionManager'),
|
||||
'quoter': ContractProxy('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', 'IQuoter'),
|
||||
'swap_router': ContractProxy('0xE592427A0AEce92De3Edee1F18E0157C05861564', 'ISwapRouter'),
|
||||
}
|
||||
super().__init__({chain.chain_id:std for chain in (Ethereum, Polygon, Goerli, Mumbai, Arbitrum, Mock)})
|
||||
|
||||
|
||||
uniswapV3 = _UniswapContracts()
|
||||
|
||||
|
||||
def uniswapV3_pool_address( addr_a: str, addr_b: str, fee: int):
|
||||
return uniswap_pool_address(uniswapV3['factory'].address, addr_a, addr_b, fee)
|
||||
Reference in New Issue
Block a user