diff --git a/mev_inspect/abis/opensea/WyvernExchange.json b/mev_inspect/abis/opensea/WyvernExchange.json new file mode 100644 index 0000000..ff79ac3 --- /dev/null +++ b/mev_inspect/abis/opensea/WyvernExchange.json @@ -0,0 +1 @@ +[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tokenTransferProxy","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"target","type":"address"},{"name":"calldata","type":"bytes"},{"name":"extradata","type":"bytes"}],"name":"staticCall","outputs":[{"name":"result","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newMinimumMakerProtocolFee","type":"uint256"}],"name":"changeMinimumMakerProtocolFee","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"newMinimumTakerProtocolFee","type":"uint256"}],"name":"changeMinimumTakerProtocolFee","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"array","type":"bytes"},{"name":"desired","type":"bytes"},{"name":"mask","type":"bytes"}],"name":"guardedArrayReplace","outputs":[{"name":"","type":"bytes"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"minimumTakerProtocolFee","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"codename","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"addr","type":"address"}],"name":"testCopyAddress","outputs":[{"name":"","type":"bytes"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"name":"arrToCopy","type":"bytes"}],"name":"testCopy","outputs":[{"name":"","type":"bytes"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"name":"addrs","type":"address[7]"},{"name":"uints","type":"uint256[9]"},{"name":"feeMethod","type":"uint8"},{"name":"side","type":"uint8"},{"name":"saleKind","type":"uint8"},{"name":"howToCall","type":"uint8"},{"name":"calldata","type":"bytes"},{"name":"replacementPattern","type":"bytes"},{"name":"staticExtradata","type":"bytes"}],"name":"calculateCurrentPrice_","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newProtocolFeeRecipient","type":"address"}],"name":"changeProtocolFeeRecipient","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"buyCalldata","type":"bytes"},{"name":"buyReplacementPattern","type":"bytes"},{"name":"sellCalldata","type":"bytes"},{"name":"sellReplacementPattern","type":"bytes"}],"name":"orderCalldataCanMatch","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"name":"addrs","type":"address[7]"},{"name":"uints","type":"uint256[9]"},{"name":"feeMethod","type":"uint8"},{"name":"side","type":"uint8"},{"name":"saleKind","type":"uint8"},{"name":"howToCall","type":"uint8"},{"name":"calldata","type":"bytes"},{"name":"replacementPattern","type":"bytes"},{"name":"staticExtradata","type":"bytes"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"validateOrder_","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"side","type":"uint8"},{"name":"saleKind","type":"uint8"},{"name":"basePrice","type":"uint256"},{"name":"extra","type":"uint256"},{"name":"listingTime","type":"uint256"},{"name":"expirationTime","type":"uint256"}],"name":"calculateFinalPrice","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"protocolFeeRecipient","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"addrs","type":"address[7]"},{"name":"uints","type":"uint256[9]"},{"name":"feeMethod","type":"uint8"},{"name":"side","type":"uint8"},{"name":"saleKind","type":"uint8"},{"name":"howToCall","type":"uint8"},{"name":"calldata","type":"bytes"},{"name":"replacementPattern","type":"bytes"},{"name":"staticExtradata","type":"bytes"}],"name":"hashOrder_","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"name":"addrs","type":"address[14]"},{"name":"uints","type":"uint256[18]"},{"name":"feeMethodsSidesKindsHowToCalls","type":"uint8[8]"},{"name":"calldataBuy","type":"bytes"},{"name":"calldataSell","type":"bytes"},{"name":"replacementPatternBuy","type":"bytes"},{"name":"replacementPatternSell","type":"bytes"},{"name":"staticExtradataBuy","type":"bytes"},{"name":"staticExtradataSell","type":"bytes"}],"name":"ordersCanMatch_","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"addrs","type":"address[7]"},{"name":"uints","type":"uint256[9]"},{"name":"feeMethod","type":"uint8"},{"name":"side","type":"uint8"},{"name":"saleKind","type":"uint8"},{"name":"howToCall","type":"uint8"},{"name":"calldata","type":"bytes"},{"name":"replacementPattern","type":"bytes"},{"name":"staticExtradata","type":"bytes"},{"name":"orderbookInclusionDesired","type":"bool"}],"name":"approveOrder_","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"registry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"minimumMakerProtocolFee","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"addrs","type":"address[7]"},{"name":"uints","type":"uint256[9]"},{"name":"feeMethod","type":"uint8"},{"name":"side","type":"uint8"},{"name":"saleKind","type":"uint8"},{"name":"howToCall","type":"uint8"},{"name":"calldata","type":"bytes"},{"name":"replacementPattern","type":"bytes"},{"name":"staticExtradata","type":"bytes"}],"name":"hashToSign_","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"cancelledOrFinalized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"exchangeToken","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"addrs","type":"address[7]"},{"name":"uints","type":"uint256[9]"},{"name":"feeMethod","type":"uint8"},{"name":"side","type":"uint8"},{"name":"saleKind","type":"uint8"},{"name":"howToCall","type":"uint8"},{"name":"calldata","type":"bytes"},{"name":"replacementPattern","type":"bytes"},{"name":"staticExtradata","type":"bytes"},{"name":"v","type":"uint8"},{"name":"r","type":"bytes32"},{"name":"s","type":"bytes32"}],"name":"cancelOrder_","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"addrs","type":"address[14]"},{"name":"uints","type":"uint256[18]"},{"name":"feeMethodsSidesKindsHowToCalls","type":"uint8[8]"},{"name":"calldataBuy","type":"bytes"},{"name":"calldataSell","type":"bytes"},{"name":"replacementPatternBuy","type":"bytes"},{"name":"replacementPatternSell","type":"bytes"},{"name":"staticExtradataBuy","type":"bytes"},{"name":"staticExtradataSell","type":"bytes"},{"name":"vs","type":"uint8[2]"},{"name":"rssMetadata","type":"bytes32[5]"}],"name":"atomicMatch_","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"addrs","type":"address[7]"},{"name":"uints","type":"uint256[9]"},{"name":"feeMethod","type":"uint8"},{"name":"side","type":"uint8"},{"name":"saleKind","type":"uint8"},{"name":"howToCall","type":"uint8"},{"name":"calldata","type":"bytes"},{"name":"replacementPattern","type":"bytes"},{"name":"staticExtradata","type":"bytes"}],"name":"validateOrderParameters_","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"INVERSE_BASIS_POINT","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"addrs","type":"address[14]"},{"name":"uints","type":"uint256[18]"},{"name":"feeMethodsSidesKindsHowToCalls","type":"uint8[8]"},{"name":"calldataBuy","type":"bytes"},{"name":"calldataSell","type":"bytes"},{"name":"replacementPatternBuy","type":"bytes"},{"name":"replacementPatternSell","type":"bytes"},{"name":"staticExtradataBuy","type":"bytes"},{"name":"staticExtradataSell","type":"bytes"}],"name":"calculateMatchPrice_","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"approvedOrders","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"registryAddress","type":"address"},{"name":"tokenTransferProxyAddress","type":"address"},{"name":"tokenAddress","type":"address"},{"name":"protocolFeeAddress","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"hash","type":"bytes32"},{"indexed":false,"name":"exchange","type":"address"},{"indexed":true,"name":"maker","type":"address"},{"indexed":false,"name":"taker","type":"address"},{"indexed":false,"name":"makerRelayerFee","type":"uint256"},{"indexed":false,"name":"takerRelayerFee","type":"uint256"},{"indexed":false,"name":"makerProtocolFee","type":"uint256"},{"indexed":false,"name":"takerProtocolFee","type":"uint256"},{"indexed":true,"name":"feeRecipient","type":"address"},{"indexed":false,"name":"feeMethod","type":"uint8"},{"indexed":false,"name":"side","type":"uint8"},{"indexed":false,"name":"saleKind","type":"uint8"},{"indexed":false,"name":"target","type":"address"}],"name":"OrderApprovedPartOne","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"hash","type":"bytes32"},{"indexed":false,"name":"howToCall","type":"uint8"},{"indexed":false,"name":"calldata","type":"bytes"},{"indexed":false,"name":"replacementPattern","type":"bytes"},{"indexed":false,"name":"staticTarget","type":"address"},{"indexed":false,"name":"staticExtradata","type":"bytes"},{"indexed":false,"name":"paymentToken","type":"address"},{"indexed":false,"name":"basePrice","type":"uint256"},{"indexed":false,"name":"extra","type":"uint256"},{"indexed":false,"name":"listingTime","type":"uint256"},{"indexed":false,"name":"expirationTime","type":"uint256"},{"indexed":false,"name":"salt","type":"uint256"},{"indexed":false,"name":"orderbookInclusionDesired","type":"bool"}],"name":"OrderApprovedPartTwo","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"hash","type":"bytes32"}],"name":"OrderCancelled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"buyHash","type":"bytes32"},{"indexed":false,"name":"sellHash","type":"bytes32"},{"indexed":true,"name":"maker","type":"address"},{"indexed":true,"name":"taker","type":"address"},{"indexed":false,"name":"price","type":"uint256"},{"indexed":true,"name":"metadata","type":"bytes32"}],"name":"OrdersMatched","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previousOwner","type":"address"}],"name":"OwnershipRenounced","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previousOwner","type":"address"},{"indexed":true,"name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"}] \ No newline at end of file diff --git a/mev_inspect/classifiers/specs/__init__.py b/mev_inspect/classifiers/specs/__init__.py index 7ce3eb0..c6d02d8 100644 --- a/mev_inspect/classifiers/specs/__init__.py +++ b/mev_inspect/classifiers/specs/__init__.py @@ -13,6 +13,7 @@ from .erc20 import ERC20_CLASSIFIER_SPECS from .uniswap import UNISWAP_CLASSIFIER_SPECS from .weth import WETH_ADDRESS, WETH_CLASSIFIER_SPECS from .zero_ex import ZEROX_CLASSIFIER_SPECS +from .opensea import OPENSEA_CLASSIFIER_SPECS ALL_CLASSIFIER_SPECS = ( ERC20_CLASSIFIER_SPECS @@ -24,6 +25,7 @@ ALL_CLASSIFIER_SPECS = ( + BALANCER_CLASSIFIER_SPECS + COMPOUND_CLASSIFIER_SPECS + CRYPTOPUNKS_CLASSIFIER_SPECS + + OPENSEA_CLASSIFIER_SPECS + BANCOR_CLASSIFIER_SPECS ) diff --git a/mev_inspect/classifiers/specs/opensea.py b/mev_inspect/classifiers/specs/opensea.py new file mode 100644 index 0000000..e76bbe0 --- /dev/null +++ b/mev_inspect/classifiers/specs/opensea.py @@ -0,0 +1,52 @@ +from typing import List +from mev_inspect.classifiers.helpers import _filter_transfers +from mev_inspect.schemas.classifiers import ClassifierSpec, NftTradeClassifier +from mev_inspect.schemas.nft_trade import NftTrade +from mev_inspect.schemas.traces import DecodedCallTrace, Protocol +from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS, Transfer + +OPENSEA_ETH_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000" + +class OpenseaClassifier(NftTradeClassifier): + @staticmethod + def parse_trade(trace: DecodedCallTrace) -> NftTrade: + uints = trace.inputs.get("uints") + addresses = trace.inputs.get("addrs") + buy_maker = addresses[1] + sell_maker = addresses[8] + base_price = uints[4] + payment_token = addresses[6] + target = addresses[4] + + if payment_token == OPENSEA_ETH_TOKEN_ADDRESS: + # Opensea uses the zero-address as a sentinel value for Ether. Convert this + # to the normal eth token address. + payment_token = ETH_TOKEN_ADDRESS + + return NftTrade( + abi_name=trace.abi_name, + transaction_hash=trace.transaction_hash, + transaction_position=trace.transaction_position, + block_number=trace.block_number, + trace_address=trace.trace_address, + protocol=trace.protocol, + error=trace.error, + seller_address=sell_maker, + buyer_address=buy_maker, + payment_token=payment_token, + payment_amount=base_price, + collection_address=target, + token_uri=0 # Todo + ) + + +OPENSEA_SPEC= ClassifierSpec( + abi_name="WyvernExchange", + protocol=Protocol.opensea, + valid_contract_addresses=["0x7be8076f4ea4a4ad08075c2508e481d6c946d12b"], + classifiers={ + "atomicMatch_(address[14],uint256[18],uint8[8],bytes,bytes,bytes,bytes,bytes,bytes,uint8[2],bytes32[5])": OpenseaClassifier, # TODO actual types + }, +) + +OPENSEA_CLASSIFIER_SPECS = [OPENSEA_SPEC] diff --git a/mev_inspect/inspect_block.py b/mev_inspect/inspect_block.py index d801315..e633b24 100644 --- a/mev_inspect/inspect_block.py +++ b/mev_inspect/inspect_block.py @@ -26,6 +26,7 @@ from mev_inspect.crud.punks import ( write_punk_snipes, ) from mev_inspect.crud.sandwiches import delete_sandwiches_for_block, write_sandwiches +from mev_inspect.nft_trades import get_nft_trades from mev_inspect.crud.swaps import delete_swaps_for_block, write_swaps from mev_inspect.crud.traces import ( delete_classified_traces_for_block, @@ -121,6 +122,9 @@ async def inspect_block( delete_punk_snipes_for_block(inspect_db_session, block_number) write_punk_snipes(inspect_db_session, punk_snipes) + nft_trades = get_nft_trades(classified_traces) + logger.info(f"Block: {block_number} -- Found {len(nft_trades)} nft trades") + miner_payments = get_miner_payments( block.miner, block.base_fee_per_gas, classified_traces, block.receipts ) diff --git a/mev_inspect/nft_trades.py b/mev_inspect/nft_trades.py new file mode 100644 index 0000000..1315280 --- /dev/null +++ b/mev_inspect/nft_trades.py @@ -0,0 +1,44 @@ +from typing import List, Optional +from mev_inspect.classifiers.specs import get_classifier +from mev_inspect.schemas.classifiers import NftTradeClassifier +from mev_inspect.schemas.nft_trade import NftTrade +from mev_inspect.schemas.traces import Classification, ClassifiedTrace, DecodedCallTrace +from mev_inspect.schemas.transfers import Transfer +from mev_inspect.traces import get_traces_by_transaction_hash + +def get_nft_trades(traces: List[ClassifiedTrace]) -> List[NftTrade]: + nft_trades = [] + + for _, transaction_traces in get_traces_by_transaction_hash(traces).items(): + nft_trades += _get_nft_trades_for_transaction( + list(transaction_traces) + ) + + return nft_trades + + +def _get_nft_trades_for_transaction( + traces: List[ClassifiedTrace], +) -> List[NftTrade]: + ordered_traces = list(sorted(traces, key=lambda t: t.trace_address)) + + nft_trades: List[NftTrade] = [] + + for trace in ordered_traces: + if not isinstance(trace, DecodedCallTrace): + continue + + elif trace.classification == Classification.nft_trade: + nft_transfer = _parse_trade(trace) + + nft_trades.append(nft_transfer) + + return nft_trades + +def _parse_trade(trace: DecodedCallTrace) -> Optional[NftTrade]: + classifier = get_classifier(trace) + + if classifier is not None and issubclass(classifier, NftTradeClassifier): + return classifier.parse_trade(trace) + + return None diff --git a/mev_inspect/schemas/classifiers.py b/mev_inspect/schemas/classifiers.py index c4d48e8..d741829 100644 --- a/mev_inspect/schemas/classifiers.py +++ b/mev_inspect/schemas/classifiers.py @@ -6,6 +6,7 @@ from pydantic import BaseModel from .swaps import Swap from .traces import Classification, DecodedCallTrace, Protocol from .transfers import Transfer +from .nft_trade import NftTrade class Classifier(ABC): @@ -52,6 +53,16 @@ class SeizeClassifier(Classifier): def get_classification() -> Classification: return Classification.seize +class NftTradeClassifier(Classifier): + @staticmethod + def get_classification() -> Classification: + return Classification.nft_trade + + @staticmethod + @abstractmethod + def parse_trade(trace: DecodedCallTrace) -> NftTrade: + return NotImplementedError() + class ClassifierSpec(BaseModel): abi_name: str diff --git a/mev_inspect/schemas/nft_trade.py b/mev_inspect/schemas/nft_trade.py new file mode 100644 index 0000000..13ebe10 --- /dev/null +++ b/mev_inspect/schemas/nft_trade.py @@ -0,0 +1,20 @@ +from typing import List, Optional +from mev_inspect.schemas.traces import Protocol + +from pydantic import BaseModel + + +class NftTrade(BaseModel): + abi_name: str + transaction_hash: str + transaction_position: int + block_number: int + trace_address: List[int] + protocol: Optional[Protocol] + error: Optional[str] + seller_address: str + buyer_address: str + payment_token: str + payment_amount: int + collection_address: str + token_uri: int diff --git a/mev_inspect/schemas/traces.py b/mev_inspect/schemas/traces.py index aa6451d..68c1592 100644 --- a/mev_inspect/schemas/traces.py +++ b/mev_inspect/schemas/traces.py @@ -33,6 +33,7 @@ class Classification(Enum): seize = "seize" punk_bid = "punk_bid" punk_accept_bid = "punk_accept_bid" + nft_trade = "nft_trade" class Protocol(Enum): @@ -48,6 +49,7 @@ class Protocol(Enum): cream = "cream" cryptopunks = "cryptopunks" bancor = "bancor" + opensea = "opensea" class ClassifiedTrace(Trace):