price constraints working
This commit is contained in:
@@ -44,6 +44,6 @@ 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', 3, batch_size=1000) # todo configure batch size... does it depend on log count? :(
|
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')
|
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_by_chain[_chain_id][_native.symbol] = _native
|
||||||
|
|
||||||
tokens = ByBlockchainDict[Token](_tokens_by_chain)
|
tokens = ByBlockchainDict[Token](_tokens_by_chain)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
|
|
||||||
from dexorder import db, config, Blockchain, blockchain
|
from dexorder import db, blockchain
|
||||||
from dexorder.base.chain import current_chain
|
|
||||||
from dexorder.bin.executable import execute
|
from dexorder.bin.executable import execute
|
||||||
from dexorder.blockstate.blockdata import BlockData
|
from dexorder.blockstate.blockdata import BlockData
|
||||||
from dexorder.blockstate.db_state import DbState
|
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 eth_abi.registry import registry as default_registry
|
||||||
|
|
||||||
from .. import current_w3 as _current_w3
|
from .. import current_w3 as _current_w3
|
||||||
|
from .abi import abis
|
||||||
|
from .contract_proxy import ContractProxy
|
||||||
|
|
||||||
abi_decoder = ABIDecoder(default_registry)
|
abi_decoder = ABIDecoder(default_registry)
|
||||||
abi_encoder = ABIEncoder(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):
|
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:
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import json
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import eth_account
|
import eth_account
|
||||||
from eth_utils import keccak
|
|
||||||
from web3.types import TxReceipt
|
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.base.account import current_account
|
||||||
from dexorder.database.model.block import current_block
|
from dexorder.database.model.block import current_block
|
||||||
from dexorder.base import TransactionDict
|
|
||||||
from dexorder.util import hexstr
|
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 import dec
|
||||||
from dexorder.base.chain import current_chain
|
from dexorder.base.chain import current_chain
|
||||||
from dexorder.blockstate import BlockDict
|
from dexorder.blockstate import BlockDict
|
||||||
|
from dexorder.blockstate.blockdata import K, V
|
||||||
from dexorder.util import json, defaultdictk
|
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)
|
# 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()},
|
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()})))
|
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,
|
|
||||||
|
|
||||||
|
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))))
|
pub=lambda k, v: (f'{current_chain.get().chain_id}|{k}', 'p', (k, str(v))))
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from sqlalchemy.orm import mapped_column, Mapped
|
from sqlalchemy.orm import mapped_column, Mapped
|
||||||
|
|
||||||
from dexorder.database.column import Json
|
|
||||||
from dexorder.database.model import Base
|
from dexorder.database.model import Base
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ from typing import Optional
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import ForeignKey
|
from sqlalchemy import ForeignKey
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
|
||||||
from dexorder.base.order import TransactionRequest as TransactionRequestDict, deserialize_transaction_request
|
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 import Dict, Bytes, UUID_PK, Blockchain, UUID
|
||||||
from dexorder.database.column_types import DataclassDict
|
from dexorder.database.column_types import DataclassDict
|
||||||
from dexorder.database.model import Base
|
from dexorder.database.model import Base
|
||||||
|
|||||||
@@ -3,20 +3,21 @@ from uuid import UUID
|
|||||||
|
|
||||||
from web3.types import EventData
|
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.chain import current_chain
|
||||||
from dexorder.base.order import TrancheExecutionRequest, TrancheKey, ExecutionRequest, new_tranche_execution_request
|
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.transaction import create_transactions, submit_transaction_request, handle_transaction_receipts, send_transactions
|
||||||
from dexorder.blockchain.uniswap import uniswap_price
|
from dexorder.uniswap import UniswapV3Pool, uniswap_price
|
||||||
from dexorder.contract.dexorder import get_factory_contract, vault_address, VaultContract, get_dexorder_contract
|
from dexorder.contract.dexorder import get_factory_contract, vault_address, VaultContract, get_dexorder_contract
|
||||||
from dexorder.contract import UniswapV3Pool, get_contract_event
|
from dexorder.contract import get_contract_event
|
||||||
from dexorder.data import pool_prices, vault_owners, vault_balances
|
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.block import current_block
|
||||||
from dexorder.database.model.transaction import TransactionJob
|
from dexorder.database.model.transaction import TransactionJob
|
||||||
from dexorder.order.orderlib import SwapOrderState, SwapOrderStatus
|
from dexorder.order.orderlib import SwapOrderState, SwapOrderStatus
|
||||||
from dexorder.order.orderstate import Order
|
from dexorder.order.orderstate import Order
|
||||||
from dexorder.order.triggers import OrderTriggers, close_order_and_disable_triggers, price_triggers, time_triggers, \
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ def setup_logevent_triggers(runner):
|
|||||||
# THIS IS BASICALLY THE MAIN RUN LOOP FOR EVERY BLOCK
|
# 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_vault_created, vault_created)
|
||||||
runner.add_event_trigger(handle_order_placed, get_contract_event('OrderLib', 'DexorderSwapPlaced'))
|
runner.add_event_trigger(handle_order_placed, get_contract_event('OrderLib', 'DexorderSwapPlaced'))
|
||||||
runner.add_event_trigger(handle_transfer, get_contract_event('ERC20', 'Transfer'))
|
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(handle_dexorderexecutions, executions)
|
||||||
runner.add_event_trigger(activate_time_triggers)
|
runner.add_event_trigger(activate_time_triggers)
|
||||||
runner.add_event_trigger(activate_price_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(process_execution_requests)
|
||||||
runner.add_event_trigger(handle_create_transactions)
|
runner.add_event_trigger(create_transactions)
|
||||||
runner.add_event_trigger(handle_send_transactions)
|
runner.add_event_trigger(send_transactions)
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
new_pool_prices.clear()
|
||||||
|
new_price_triggers.clear()
|
||||||
|
|
||||||
|
|
||||||
async def handle_order_placed(event: EventData):
|
async def handle_order_placed(event: EventData):
|
||||||
@@ -169,17 +177,16 @@ def handle_transfer(transfer: EventData):
|
|||||||
log.debug(f'vaults: {list(vaults)}')
|
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:
|
try:
|
||||||
sqrt_price = swap['args']['sqrtPriceX96']
|
sqrt_price = swap['args']['sqrtPriceX96']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return
|
return
|
||||||
addr = swap['address']
|
addr = swap['address']
|
||||||
price = uniswap_price(sqrt_price)
|
price: dec = await uniswap_price(addr, sqrt_price)
|
||||||
log.debug(f'pool {addr} {price}')
|
log.debug(f'pool {addr} {price}')
|
||||||
new_pool_prices[addr] = price
|
pool_prices[addr] = price
|
||||||
|
|
||||||
|
|
||||||
def handle_vault_created(created: EventData):
|
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)
|
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
|
now = current_block.get().timestamp
|
||||||
log.debug(f'activating time triggers')
|
log.debug(f'activating time triggers')
|
||||||
# time triggers
|
# time triggers
|
||||||
for tt in time_triggers:
|
for tt in time_triggers:
|
||||||
tt(now)
|
await maywait(tt(now))
|
||||||
|
|
||||||
def activate_price_triggers():
|
async def activate_price_triggers():
|
||||||
log.debug('activating price triggers')
|
log.debug(f'activating price triggers')
|
||||||
|
pools_triggered = set()
|
||||||
for pool, price in new_pool_prices.items():
|
for pool, price in new_pool_prices.items():
|
||||||
|
pools_triggered.add(pool)
|
||||||
for pt in price_triggers[pool]:
|
for pt in price_triggers[pool]:
|
||||||
pt(price)
|
await maywait(pt(price))
|
||||||
new_pool_prices.clear()
|
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:
|
for t in unconstrained_price_triggers:
|
||||||
# noinspection PyTypeChecker
|
# 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():
|
async def process_execution_requests():
|
||||||
height = current_block.get().height
|
height = current_block.get().height
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ from enum import Enum
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from dexorder import dec
|
from dexorder import dec
|
||||||
from dexorder.blockchain.uniswap import uniswap_price
|
from dexorder.uniswap import uniswapV3_pool_address, uniswap_price
|
||||||
from dexorder.contract.uniswap_contracts import uniswapV3_pool_address
|
|
||||||
from dexorder.contract import abi_decoder, abi_encoder
|
from dexorder.contract import abi_decoder, abi_encoder
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -106,10 +105,8 @@ NO_OCO = 18446744073709551615 # max uint64
|
|||||||
|
|
||||||
class ConstraintMode (Enum):
|
class ConstraintMode (Enum):
|
||||||
Time = 0
|
Time = 0
|
||||||
Limit = 1
|
Line = 1
|
||||||
Trailing = 2
|
Barrier = 2
|
||||||
Barrier = 3
|
|
||||||
Line = 4
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Constraint (ABC):
|
class Constraint (ABC):
|
||||||
@@ -120,51 +117,16 @@ class Constraint (ABC):
|
|||||||
mode = ConstraintMode(obj[0])
|
mode = ConstraintMode(obj[0])
|
||||||
if mode == ConstraintMode.Time:
|
if mode == ConstraintMode.Time:
|
||||||
return TimeConstraint.load(obj[1])
|
return TimeConstraint.load(obj[1])
|
||||||
|
elif mode == ConstraintMode.Line:
|
||||||
|
return LineConstraint.load(obj[1])
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise ValueError(f'Unknown constraint mode {mode}')
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def dump(self): ...
|
def dump(self): ...
|
||||||
|
|
||||||
def _dump(self, types, values):
|
def _dump(self, types, values):
|
||||||
return self.mode, abi_encoder.encode(types, values)
|
return self.mode.value, 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
|
|
||||||
|
|
||||||
|
|
||||||
class TimeMode (Enum):
|
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))
|
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
|
@dataclass
|
||||||
class Tranche:
|
class Tranche:
|
||||||
fraction: int # 18-decimal fraction of the order amount which is available to this tranche. must be <= 1
|
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
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Callable
|
from typing import Callable, Optional, Union, Coroutine, Awaitable
|
||||||
|
|
||||||
from dexorder.blockstate import BlockSet, BlockDict
|
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 dexorder.util import defaultdictk
|
||||||
from .orderstate import Order
|
from .orderstate import Order
|
||||||
|
from .. import dec
|
||||||
from ..base.order import OrderKey, TrancheKey, ExecutionRequest
|
from ..base.order import OrderKey, TrancheKey, ExecutionRequest
|
||||||
|
from ..data import pool_prices
|
||||||
from ..database.model.block import current_block
|
from ..database.model.block import current_block
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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)
|
TimeTrigger = Callable[[int], None] # func(timestamp)
|
||||||
time_triggers:BlockSet[TimeTrigger] = BlockSet('tt')
|
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
|
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
|
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?
|
# todo should this really be blockdata?
|
||||||
inflight_execution_requests:BlockDict[TrancheKey, int] = BlockDict('ei') # value is block height when the request was sent
|
inflight_execution_requests:BlockDict[TrancheKey, int] = BlockDict('ei') # value is block height when the request was sent
|
||||||
@@ -53,16 +59,16 @@ class TrancheTrigger:
|
|||||||
return
|
return
|
||||||
|
|
||||||
time_constraint = None # stored as a tuple of two ints for earliest and latest absolute timestamps
|
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:
|
for c in tranche.constraints:
|
||||||
if c.mode == ConstraintMode.Time:
|
if c.mode == ConstraintMode.Time:
|
||||||
c: TimeConstraint
|
c: TimeConstraint
|
||||||
earliest = c.earliest.timestamp(start)
|
earliest = c.earliest.timestamp(start)
|
||||||
latest = c.latest.timestamp(start)
|
latest = c.latest.timestamp(start)
|
||||||
time_constraint = (earliest, latest) if time_constraint is None else intersect_ranges(*time_constraint, earliest, latest)
|
time_constraint = (earliest, latest) if time_constraint is None else intersect_ranges(*time_constraint, earliest, latest)
|
||||||
elif c.mode == ConstraintMode.Limit:
|
elif c.mode == ConstraintMode.Line:
|
||||||
c: LimitConstraint
|
c: LineConstraint
|
||||||
raise NotImplementedError
|
self.line_constraints.append(c)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
self.time_constraint = time_constraint
|
self.time_constraint = time_constraint
|
||||||
@@ -92,8 +98,8 @@ class TrancheTrigger:
|
|||||||
return
|
return
|
||||||
if now >= self.time_constraint[1]:
|
if now >= self.time_constraint[1]:
|
||||||
log.debug(f'tranche expired {self.tk}')
|
log.debug(f'tranche expired {self.tk}')
|
||||||
self.disable()
|
|
||||||
self.status = TrancheStatus.Expired
|
self.status = TrancheStatus.Expired
|
||||||
|
self.disable()
|
||||||
# check for all tranches expired
|
# check for all tranches expired
|
||||||
OrderTriggers.instances[self.order.key].check_complete()
|
OrderTriggers.instances[self.order.key].check_complete()
|
||||||
elif self.status == TrancheStatus.Early and now >= self.time_constraint[0]:
|
elif self.status == TrancheStatus.Early and now >= self.time_constraint[0]:
|
||||||
@@ -102,30 +108,25 @@ class TrancheTrigger:
|
|||||||
self.enable_price_trigger()
|
self.enable_price_trigger()
|
||||||
|
|
||||||
def enable_price_trigger(self):
|
def enable_price_trigger(self):
|
||||||
if self.price_constraints:
|
if self.line_constraints:
|
||||||
price_triggers[self.order.pool_address].add(self.price_trigger)
|
price_triggers[self.order.pool_address].add(self.price_trigger)
|
||||||
|
new_price_triggers[self.order.pool_address].add(self.price_trigger)
|
||||||
else:
|
else:
|
||||||
unconstrained_price_triggers.add(self.price_trigger)
|
unconstrained_price_triggers.add(self.price_trigger)
|
||||||
|
|
||||||
def disable_price_trigger(self):
|
def disable_price_trigger(self):
|
||||||
if self.price_constraints:
|
if self.line_constraints:
|
||||||
price_triggers[self.order.pool_address].remove(self.price_trigger)
|
price_triggers[self.order.pool_address].remove(self.price_trigger)
|
||||||
else:
|
else:
|
||||||
unconstrained_price_triggers.remove(self.price_trigger)
|
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:
|
if self.closed:
|
||||||
log.debug(f'price trigger ignored because trigger status is {self.status}')
|
log.debug(f'price trigger ignored because trigger status is {self.status}')
|
||||||
return
|
return
|
||||||
if not self.price_constraints or all(pc.passes(cur) for pc in self.price_constraints):
|
if not self.line_constraints or all(await asyncio.gather(*[pc.passes(self.order.pool_address, cur) for pc in self.line_constraints])):
|
||||||
self.execute()
|
active_tranches[self.tk] = None # or PriceProof(...)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def fill(self, _amount_in, _amount_out ):
|
def fill(self, _amount_in, _amount_out ):
|
||||||
remaining = self.order.tranche_remaining(self.tk)
|
remaining = self.order.tranche_remaining(self.tk)
|
||||||
@@ -137,6 +138,10 @@ class TrancheTrigger:
|
|||||||
return filled
|
return filled
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
|
try:
|
||||||
|
del active_tranches[self.tk]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
self.disable_time_trigger()
|
self.disable_time_trigger()
|
||||||
self.disable_price_trigger()
|
self.disable_price_trigger()
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ def submit_transaction_request(tr: TransactionRequest):
|
|||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
async def handle_create_transactions():
|
async def create_transactions():
|
||||||
for job in db.session.query(TransactionJob).filter(
|
for job in db.session.query(TransactionJob).filter(
|
||||||
TransactionJob.chain == current_chain.get(),
|
TransactionJob.chain == current_chain.get(),
|
||||||
TransactionJob.state == TransactionJobState.Requested
|
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}')
|
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()
|
w3 = current_w3.get()
|
||||||
for job in db.session.query(TransactionJob).filter(
|
for job in db.session.query(TransactionJob).filter(
|
||||||
TransactionJob.chain == current_chain.get(),
|
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