From f92737b00c18855e0e993a6ca5f13d27177b3b8a Mon Sep 17 00:00:00 2001 From: Shea Ketsdever Date: Sun, 19 Dec 2021 12:16:49 -0800 Subject: [PATCH 1/8] Classify opensea nft trades --- mev_inspect/abis/opensea/WyvernExchange.json | 1 + mev_inspect/classifiers/specs/__init__.py | 2 + mev_inspect/classifiers/specs/opensea.py | 52 ++++++++++++++++++++ mev_inspect/inspect_block.py | 4 ++ mev_inspect/nft_trades.py | 44 +++++++++++++++++ mev_inspect/schemas/classifiers.py | 11 +++++ mev_inspect/schemas/nft_trade.py | 20 ++++++++ mev_inspect/schemas/traces.py | 2 + 8 files changed, 136 insertions(+) create mode 100644 mev_inspect/abis/opensea/WyvernExchange.json create mode 100644 mev_inspect/classifiers/specs/opensea.py create mode 100644 mev_inspect/nft_trades.py create mode 100644 mev_inspect/schemas/nft_trade.py 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): From b75ee98018dd3dc7b463a08f31094a6f47cc0331 Mon Sep 17 00:00:00 2001 From: Shea Ketsdever Date: Sun, 19 Dec 2021 14:31:49 -0800 Subject: [PATCH 2/8] Create nft trade from transfers --- mev_inspect/classifiers/helpers.py | 52 ++++++++++++++++++++++++ mev_inspect/classifiers/specs/opensea.py | 36 ++++++---------- mev_inspect/nft_trades.py | 26 +++++++++--- mev_inspect/schemas/classifiers.py | 5 ++- mev_inspect/schemas/nft_trade.py | 2 +- 5 files changed, 90 insertions(+), 31 deletions(-) diff --git a/mev_inspect/classifiers/helpers.py b/mev_inspect/classifiers/helpers.py index bb87661..de99e18 100644 --- a/mev_inspect/classifiers/helpers.py +++ b/mev_inspect/classifiers/helpers.py @@ -1,9 +1,61 @@ from typing import List, Optional, Sequence +from mev_inspect.schemas.nft_trade import NftTrade from mev_inspect.schemas.swaps import Swap from mev_inspect.schemas.traces import ClassifiedTrace, DecodedCallTrace from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS, Transfer +def create_nft_trade_from_transfers( + trace: DecodedCallTrace, + child_transfers: List[Transfer], + collection_address: str, + seller_address: str, + buyer_address: str, + exchange_wallet_address: Optional[str], +) -> NftTrade: + transfers_to_buyer = _filter_transfers( + child_transfers, to_address=buyer_address + ) + transfers_to_seller = _filter_transfers( + child_transfers, to_address=seller_address + ) + + if len(transfers_to_buyer) != 1 or len(transfers_to_seller) != 1: + return None + + if transfers_to_buyer[0].token_address != collection_address: + return None + + payment_token = transfers_to_seller[0].token_address + payment_amount = transfers_to_seller[0].amount + token_id = transfers_to_buyer[0].amount + + if exchange_wallet_address is not None: + transfers_from_seller_to_exchange = _filter_transfers( + child_transfers, from_address=seller_address, to_address=exchange_wallet_address + ) + transfers_from_buyer_to_exchange = _filter_transfers( + child_transfers, from_address=buyer_address, to_address=exchange_wallet_address + ) + for fee in [*transfers_from_seller_to_exchange, *transfers_from_buyer_to_exchange]: + # Assumes that exchange fees are paid with the same token as the sale + payment_amount -= fee.amount + + 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=seller_address, + buyer_address=buyer_address, + payment_token=payment_token, + payment_amount=payment_amount, + collection_address=collection_address, + token_id=token_id + ) def create_swap_from_pool_transfers( trace: DecodedCallTrace, diff --git a/mev_inspect/classifiers/specs/opensea.py b/mev_inspect/classifiers/specs/opensea.py index e76bbe0..2507134 100644 --- a/mev_inspect/classifiers/specs/opensea.py +++ b/mev_inspect/classifiers/specs/opensea.py @@ -1,42 +1,30 @@ -from typing import List -from mev_inspect.classifiers.helpers import _filter_transfers +from typing import List, Optional +from mev_inspect.classifiers.helpers import _filter_transfers, create_nft_trade_from_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" +OPENSEA_WALLET_ADDRESS = "0x5b3256965e7c3cf26e11fcaf296dfc8807c01073" class OpenseaClassifier(NftTradeClassifier): @staticmethod - def parse_trade(trace: DecodedCallTrace) -> NftTrade: - uints = trace.inputs.get("uints") + def parse_trade( + trace: DecodedCallTrace, + child_transfers: List[Transfer], + ) -> Optional[NftTrade]: 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, + return create_nft_trade_from_transfers( + trace, + child_transfers, + collection_address=target, seller_address=sell_maker, buyer_address=buy_maker, - payment_token=payment_token, - payment_amount=base_price, - collection_address=target, - token_uri=0 # Todo + exchange_wallet_address=OPENSEA_WALLET_ADDRESS, ) diff --git a/mev_inspect/nft_trades.py b/mev_inspect/nft_trades.py index 1315280..710a087 100644 --- a/mev_inspect/nft_trades.py +++ b/mev_inspect/nft_trades.py @@ -5,6 +5,10 @@ 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 +from mev_inspect.transfers import ( + get_child_transfers, + remove_child_transfers_of_transfers, +) def get_nft_trades(traces: List[ClassifiedTrace]) -> List[NftTrade]: nft_trades = [] @@ -29,16 +33,28 @@ def _get_nft_trades_for_transaction( continue elif trace.classification == Classification.nft_trade: - nft_transfer = _parse_trade(trace) - - nft_trades.append(nft_transfer) + child_transfers = get_child_transfers( + trace.transaction_hash, + trace.trace_address, + traces, + ) + nft_transfer = _parse_trade( + trace, + remove_child_transfers_of_transfers(child_transfers), + ) + + if nft_transfer is not None: + nft_trades.append(nft_transfer) return nft_trades -def _parse_trade(trace: DecodedCallTrace) -> Optional[NftTrade]: +def _parse_trade( + trace: DecodedCallTrace, + child_transfers: List[Transfer], +) -> Optional[NftTrade]: classifier = get_classifier(trace) if classifier is not None and issubclass(classifier, NftTradeClassifier): - return classifier.parse_trade(trace) + return classifier.parse_trade(trace, child_transfers) return None diff --git a/mev_inspect/schemas/classifiers.py b/mev_inspect/schemas/classifiers.py index d741829..06597f4 100644 --- a/mev_inspect/schemas/classifiers.py +++ b/mev_inspect/schemas/classifiers.py @@ -60,7 +60,10 @@ class NftTradeClassifier(Classifier): @staticmethod @abstractmethod - def parse_trade(trace: DecodedCallTrace) -> NftTrade: + def parse_trade( + trace: DecodedCallTrace, + child_transfers: List[Transfer], + ) -> Optional[NftTrade]: return NotImplementedError() diff --git a/mev_inspect/schemas/nft_trade.py b/mev_inspect/schemas/nft_trade.py index 13ebe10..75603ca 100644 --- a/mev_inspect/schemas/nft_trade.py +++ b/mev_inspect/schemas/nft_trade.py @@ -17,4 +17,4 @@ class NftTrade(BaseModel): payment_token: str payment_amount: int collection_address: str - token_uri: int + token_id: int From 97e6c156ab57e1d18a1cd8cb022964c6fe0b674e Mon Sep 17 00:00:00 2001 From: Shea Ketsdever Date: Sun, 19 Dec 2021 15:13:01 -0800 Subject: [PATCH 3/8] Add nft_trades table to db --- .../3c54832385e3_create_nft_trades_table.py | 41 +++++++++++++++++++ mev_inspect/classifiers/helpers.py | 2 +- mev_inspect/classifiers/specs/opensea.py | 2 +- mev_inspect/crud/nft_trades.py | 28 +++++++++++++ mev_inspect/inspect_block.py | 7 +++- mev_inspect/models/nft_trades.py | 21 ++++++++++ mev_inspect/nft_trades.py | 8 ++-- mev_inspect/schemas/classifiers.py | 2 +- .../schemas/{nft_trade.py => nft_trades.py} | 0 9 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 alembic/versions/3c54832385e3_create_nft_trades_table.py create mode 100644 mev_inspect/crud/nft_trades.py create mode 100644 mev_inspect/models/nft_trades.py rename mev_inspect/schemas/{nft_trade.py => nft_trades.py} (100%) diff --git a/alembic/versions/3c54832385e3_create_nft_trades_table.py b/alembic/versions/3c54832385e3_create_nft_trades_table.py new file mode 100644 index 0000000..9807db5 --- /dev/null +++ b/alembic/versions/3c54832385e3_create_nft_trades_table.py @@ -0,0 +1,41 @@ + + +"""Create NFT Trades table + +Revision ID: 3c54832385e3 +Revises: 15ba9c27ee8a +Create Date: 2021-12-19 22:50:28.936516 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "3c54832385e3" +down_revision = "15ba9c27ee8a" +branch_labels = None +depends_on = None + +def upgrade(): + op.create_table( + "nft_trades", + sa.Column("created_at", sa.TIMESTAMP, server_default=sa.func.now()), + sa.Column("abi_name", sa.String(1024), nullable=False), + sa.Column("transaction_hash", sa.String(66), nullable=False), + sa.Column("transaction_position", sa.Numeric, nullable=False), + sa.Column("block_number", sa.Numeric, nullable=False), + sa.Column("trace_address", sa.String(256), nullable=False), + sa.Column("protocol", sa.String(256), nullable=False), + sa.Column("error", sa.String(256), nullable=True), + sa.Column("seller_address", sa.String(256), nullable=False), + sa.Column("buyer_address", sa.String(256), nullable=False), + sa.Column("payment_token", sa.String(256), nullable=False), + sa.Column("payment_amount", sa.Numeric, nullable=False), + sa.Column("collection_address", sa.String(256), nullable=False), + sa.Column("token_id", sa.Numeric, nullable=False), + sa.PrimaryKeyConstraint("transaction_hash", "trace_address"), + ) + + +def downgrade(): + op.drop_table("nft_trades") diff --git a/mev_inspect/classifiers/helpers.py b/mev_inspect/classifiers/helpers.py index de99e18..c242371 100644 --- a/mev_inspect/classifiers/helpers.py +++ b/mev_inspect/classifiers/helpers.py @@ -1,5 +1,5 @@ from typing import List, Optional, Sequence -from mev_inspect.schemas.nft_trade import NftTrade +from mev_inspect.schemas.nft_trades import NftTrade from mev_inspect.schemas.swaps import Swap from mev_inspect.schemas.traces import ClassifiedTrace, DecodedCallTrace diff --git a/mev_inspect/classifiers/specs/opensea.py b/mev_inspect/classifiers/specs/opensea.py index 2507134..630bfa2 100644 --- a/mev_inspect/classifiers/specs/opensea.py +++ b/mev_inspect/classifiers/specs/opensea.py @@ -1,7 +1,7 @@ from typing import List, Optional from mev_inspect.classifiers.helpers import _filter_transfers, create_nft_trade_from_transfers from mev_inspect.schemas.classifiers import ClassifierSpec, NftTradeClassifier -from mev_inspect.schemas.nft_trade import NftTrade +from mev_inspect.schemas.nft_trades import NftTrade from mev_inspect.schemas.traces import DecodedCallTrace, Protocol from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS, Transfer diff --git a/mev_inspect/crud/nft_trades.py b/mev_inspect/crud/nft_trades.py new file mode 100644 index 0000000..1317b03 --- /dev/null +++ b/mev_inspect/crud/nft_trades.py @@ -0,0 +1,28 @@ +import json +from typing import List + +from mev_inspect.models.nft_trades import NftTradeModel +from mev_inspect.schemas.nft_trades import NftTrade + + +def delete_nft_trades_for_block( + db_session, + block_number: int, +) -> None: + ( + db_session.query(NftTradeModel) + .filter(NftTradeModel.block_number == block_number) + .delete() + ) + + db_session.commit() + + +def write_nft_trades( + db_session, + nft_trades: List[NftTrade], +) -> None: + models = [NftTradeModel(**json.loads(nft_trade.json())) for nft_trade in nft_trades] + + db_session.bulk_save_objects(models) + db_session.commit() diff --git a/mev_inspect/inspect_block.py b/mev_inspect/inspect_block.py index e633b24..f581c55 100644 --- a/mev_inspect/inspect_block.py +++ b/mev_inspect/inspect_block.py @@ -26,19 +26,21 @@ 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, write_classified_traces, ) from mev_inspect.crud.transfers import delete_transfers_for_block, write_transfers +from mev_inspect.crud.nft_trades import delete_nft_trades_for_block, write_nft_trades + from mev_inspect.liquidations import get_liquidations from mev_inspect.miner_payments import get_miner_payments from mev_inspect.punks import get_punk_bid_acceptances, get_punk_bids, get_punk_snipes from mev_inspect.sandwiches import get_sandwiches from mev_inspect.swaps import get_swaps from mev_inspect.transfers import get_transfers +from mev_inspect.nft_trades import get_nft_trades logger = logging.getLogger(__name__) @@ -125,6 +127,9 @@ async def inspect_block( nft_trades = get_nft_trades(classified_traces) logger.info(f"Block: {block_number} -- Found {len(nft_trades)} nft trades") + delete_nft_trades_for_block(inspect_db_session, block_number) + write_nft_trades(inspect_db_session, nft_trades) + miner_payments = get_miner_payments( block.miner, block.base_fee_per_gas, classified_traces, block.receipts ) diff --git a/mev_inspect/models/nft_trades.py b/mev_inspect/models/nft_trades.py new file mode 100644 index 0000000..db55084 --- /dev/null +++ b/mev_inspect/models/nft_trades.py @@ -0,0 +1,21 @@ +from sqlalchemy import ARRAY, Column, Integer, Numeric, String + +from .base import Base + + +class NftTradeModel(Base): + __tablename__ = "nft_trades" + + abi_name = Column(String, nullable=False) + transaction_hash = Column(String, primary_key=True) + transaction_position = Column(Numeric, nullable=True) + block_number = Column(Numeric, nullable=False) + trace_address = Column(ARRAY(Integer), primary_key=True) + protocol = Column(String, nullable=True) + error = Column(String, nullable=True) + seller_address = Column(String, nullable=False) + buyer_address = Column(String, nullable=False) + payment_token = Column(String, nullable=False) + payment_amount = Column(Numeric, nullable=False) + collection_address = Column(String, nullable=False) + token_id = Column(Numeric, nullable=False) diff --git a/mev_inspect/nft_trades.py b/mev_inspect/nft_trades.py index 710a087..eb35c62 100644 --- a/mev_inspect/nft_trades.py +++ b/mev_inspect/nft_trades.py @@ -1,7 +1,7 @@ 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.nft_trades 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 @@ -38,13 +38,13 @@ def _get_nft_trades_for_transaction( trace.trace_address, traces, ) - nft_transfer = _parse_trade( + nft_trade = _parse_trade( trace, remove_child_transfers_of_transfers(child_transfers), ) - if nft_transfer is not None: - nft_trades.append(nft_transfer) + if nft_trade is not None: + nft_trades.append(nft_trade) return nft_trades diff --git a/mev_inspect/schemas/classifiers.py b/mev_inspect/schemas/classifiers.py index 06597f4..03eb548 100644 --- a/mev_inspect/schemas/classifiers.py +++ b/mev_inspect/schemas/classifiers.py @@ -6,7 +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 +from .nft_trades import NftTrade class Classifier(ABC): diff --git a/mev_inspect/schemas/nft_trade.py b/mev_inspect/schemas/nft_trades.py similarity index 100% rename from mev_inspect/schemas/nft_trade.py rename to mev_inspect/schemas/nft_trades.py From bf85025b84e2ef0603227bd8fdcad2138bb4a7a4 Mon Sep 17 00:00:00 2001 From: Shea Ketsdever Date: Mon, 20 Dec 2021 09:05:21 -0800 Subject: [PATCH 4/8] Fix lint issue --- mev_inspect/classifiers/specs/opensea.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mev_inspect/classifiers/specs/opensea.py b/mev_inspect/classifiers/specs/opensea.py index 630bfa2..e3a602a 100644 --- a/mev_inspect/classifiers/specs/opensea.py +++ b/mev_inspect/classifiers/specs/opensea.py @@ -1,9 +1,9 @@ from typing import List, Optional -from mev_inspect.classifiers.helpers import _filter_transfers, create_nft_trade_from_transfers +from mev_inspect.classifiers.helpers import create_nft_trade_from_transfers from mev_inspect.schemas.classifiers import ClassifierSpec, NftTradeClassifier from mev_inspect.schemas.nft_trades import NftTrade from mev_inspect.schemas.traces import DecodedCallTrace, Protocol -from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS, Transfer +from mev_inspect.schemas.transfers import Transfer OPENSEA_WALLET_ADDRESS = "0x5b3256965e7c3cf26e11fcaf296dfc8807c01073" @@ -33,7 +33,7 @@ OPENSEA_SPEC= ClassifierSpec( 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 + "atomicMatch_(address[14],uint256[18],uint8[8],bytes,bytes,bytes,bytes,bytes,bytes,uint8[2],bytes32[5])": OpenseaClassifier, }, ) From 66e1e64675d19fe56084a45f871da71cf6f7dc72 Mon Sep 17 00:00:00 2001 From: Shea Ketsdever Date: Mon, 20 Dec 2021 11:05:05 -0800 Subject: [PATCH 5/8] Actually fix lint issues --- .../3c54832385e3_create_nft_trades_table.py | 3 +- mev_inspect/classifiers/helpers.py | 33 +++++++++++-------- mev_inspect/classifiers/specs/__init__.py | 2 +- mev_inspect/classifiers/specs/opensea.py | 8 +++-- mev_inspect/inspect_block.py | 5 ++- mev_inspect/nft_trades.py | 9 ++--- mev_inspect/schemas/classifiers.py | 5 +-- mev_inspect/schemas/nft_trades.py | 3 +- 8 files changed, 39 insertions(+), 29 deletions(-) diff --git a/alembic/versions/3c54832385e3_create_nft_trades_table.py b/alembic/versions/3c54832385e3_create_nft_trades_table.py index 9807db5..6baebd6 100644 --- a/alembic/versions/3c54832385e3_create_nft_trades_table.py +++ b/alembic/versions/3c54832385e3_create_nft_trades_table.py @@ -1,5 +1,3 @@ - - """Create NFT Trades table Revision ID: 3c54832385e3 @@ -16,6 +14,7 @@ down_revision = "15ba9c27ee8a" branch_labels = None depends_on = None + def upgrade(): op.create_table( "nft_trades", diff --git a/mev_inspect/classifiers/helpers.py b/mev_inspect/classifiers/helpers.py index c242371..0094172 100644 --- a/mev_inspect/classifiers/helpers.py +++ b/mev_inspect/classifiers/helpers.py @@ -1,10 +1,11 @@ from typing import List, Optional, Sequence -from mev_inspect.schemas.nft_trades import NftTrade +from mev_inspect.schemas.nft_trades import NftTrade from mev_inspect.schemas.swaps import Swap from mev_inspect.schemas.traces import ClassifiedTrace, DecodedCallTrace from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS, Transfer + def create_nft_trade_from_transfers( trace: DecodedCallTrace, child_transfers: List[Transfer], @@ -12,32 +13,35 @@ def create_nft_trade_from_transfers( seller_address: str, buyer_address: str, exchange_wallet_address: Optional[str], -) -> NftTrade: - transfers_to_buyer = _filter_transfers( - child_transfers, to_address=buyer_address - ) - transfers_to_seller = _filter_transfers( - child_transfers, to_address=seller_address - ) +) -> Optional[NftTrade]: + transfers_to_buyer = _filter_transfers(child_transfers, to_address=buyer_address) + transfers_to_seller = _filter_transfers(child_transfers, to_address=seller_address) if len(transfers_to_buyer) != 1 or len(transfers_to_seller) != 1: return None - + if transfers_to_buyer[0].token_address != collection_address: return None - + payment_token = transfers_to_seller[0].token_address payment_amount = transfers_to_seller[0].amount token_id = transfers_to_buyer[0].amount if exchange_wallet_address is not None: transfers_from_seller_to_exchange = _filter_transfers( - child_transfers, from_address=seller_address, to_address=exchange_wallet_address + child_transfers, + from_address=seller_address, + to_address=exchange_wallet_address, ) transfers_from_buyer_to_exchange = _filter_transfers( - child_transfers, from_address=buyer_address, to_address=exchange_wallet_address + child_transfers, + from_address=buyer_address, + to_address=exchange_wallet_address, ) - for fee in [*transfers_from_seller_to_exchange, *transfers_from_buyer_to_exchange]: + for fee in [ + *transfers_from_seller_to_exchange, + *transfers_from_buyer_to_exchange, + ]: # Assumes that exchange fees are paid with the same token as the sale payment_amount -= fee.amount @@ -54,9 +58,10 @@ def create_nft_trade_from_transfers( payment_token=payment_token, payment_amount=payment_amount, collection_address=collection_address, - token_id=token_id + token_id=token_id, ) + def create_swap_from_pool_transfers( trace: DecodedCallTrace, recipient_address: str, diff --git a/mev_inspect/classifiers/specs/__init__.py b/mev_inspect/classifiers/specs/__init__.py index c6d02d8..8473964 100644 --- a/mev_inspect/classifiers/specs/__init__.py +++ b/mev_inspect/classifiers/specs/__init__.py @@ -10,10 +10,10 @@ from .compound import COMPOUND_CLASSIFIER_SPECS from .cryptopunks import CRYPTOPUNKS_CLASSIFIER_SPECS from .curve import CURVE_CLASSIFIER_SPECS from .erc20 import ERC20_CLASSIFIER_SPECS +from .opensea import OPENSEA_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 diff --git a/mev_inspect/classifiers/specs/opensea.py b/mev_inspect/classifiers/specs/opensea.py index e3a602a..72db312 100644 --- a/mev_inspect/classifiers/specs/opensea.py +++ b/mev_inspect/classifiers/specs/opensea.py @@ -1,4 +1,5 @@ from typing import List, Optional + from mev_inspect.classifiers.helpers import create_nft_trade_from_transfers from mev_inspect.schemas.classifiers import ClassifierSpec, NftTradeClassifier from mev_inspect.schemas.nft_trades import NftTrade @@ -7,13 +8,16 @@ from mev_inspect.schemas.transfers import Transfer OPENSEA_WALLET_ADDRESS = "0x5b3256965e7c3cf26e11fcaf296dfc8807c01073" -class OpenseaClassifier(NftTradeClassifier): + +class OpenseaClassifier(NftTradeClassifier): @staticmethod def parse_trade( trace: DecodedCallTrace, child_transfers: List[Transfer], ) -> Optional[NftTrade]: addresses = trace.inputs.get("addrs") + if addresses is None: + return None buy_maker = addresses[1] sell_maker = addresses[8] target = addresses[4] @@ -28,7 +32,7 @@ class OpenseaClassifier(NftTradeClassifier): ) -OPENSEA_SPEC= ClassifierSpec( +OPENSEA_SPEC = ClassifierSpec( abi_name="WyvernExchange", protocol=Protocol.opensea, valid_contract_addresses=["0x7be8076f4ea4a4ad08075c2508e481d6c946d12b"], diff --git a/mev_inspect/inspect_block.py b/mev_inspect/inspect_block.py index f581c55..0bdf958 100644 --- a/mev_inspect/inspect_block.py +++ b/mev_inspect/inspect_block.py @@ -17,6 +17,7 @@ from mev_inspect.crud.miner_payments import ( delete_miner_payments_for_block, write_miner_payments, ) +from mev_inspect.crud.nft_trades import delete_nft_trades_for_block, write_nft_trades from mev_inspect.crud.punks import ( delete_punk_bid_acceptances_for_block, delete_punk_bids_for_block, @@ -32,15 +33,13 @@ from mev_inspect.crud.traces import ( write_classified_traces, ) from mev_inspect.crud.transfers import delete_transfers_for_block, write_transfers -from mev_inspect.crud.nft_trades import delete_nft_trades_for_block, write_nft_trades - from mev_inspect.liquidations import get_liquidations from mev_inspect.miner_payments import get_miner_payments +from mev_inspect.nft_trades import get_nft_trades from mev_inspect.punks import get_punk_bid_acceptances, get_punk_bids, get_punk_snipes from mev_inspect.sandwiches import get_sandwiches from mev_inspect.swaps import get_swaps from mev_inspect.transfers import get_transfers -from mev_inspect.nft_trades import get_nft_trades logger = logging.getLogger(__name__) diff --git a/mev_inspect/nft_trades.py b/mev_inspect/nft_trades.py index eb35c62..67a0a0a 100644 --- a/mev_inspect/nft_trades.py +++ b/mev_inspect/nft_trades.py @@ -1,4 +1,5 @@ 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_trades import NftTrade @@ -10,13 +11,12 @@ from mev_inspect.transfers import ( remove_child_transfers_of_transfers, ) + 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) - ) + nft_trades += _get_nft_trades_for_transaction(list(transaction_traces)) return nft_trades @@ -42,12 +42,13 @@ def _get_nft_trades_for_transaction( trace, remove_child_transfers_of_transfers(child_transfers), ) - + if nft_trade is not None: nft_trades.append(nft_trade) return nft_trades + def _parse_trade( trace: DecodedCallTrace, child_transfers: List[Transfer], diff --git a/mev_inspect/schemas/classifiers.py b/mev_inspect/schemas/classifiers.py index 03eb548..043f1ff 100644 --- a/mev_inspect/schemas/classifiers.py +++ b/mev_inspect/schemas/classifiers.py @@ -3,10 +3,10 @@ from typing import Dict, List, Optional, Type from pydantic import BaseModel +from .nft_trades import NftTrade from .swaps import Swap from .traces import Classification, DecodedCallTrace, Protocol from .transfers import Transfer -from .nft_trades import NftTrade class Classifier(ABC): @@ -53,6 +53,7 @@ class SeizeClassifier(Classifier): def get_classification() -> Classification: return Classification.seize + class NftTradeClassifier(Classifier): @staticmethod def get_classification() -> Classification: @@ -64,7 +65,7 @@ class NftTradeClassifier(Classifier): trace: DecodedCallTrace, child_transfers: List[Transfer], ) -> Optional[NftTrade]: - return NotImplementedError() + raise NotImplementedError() class ClassifierSpec(BaseModel): diff --git a/mev_inspect/schemas/nft_trades.py b/mev_inspect/schemas/nft_trades.py index 75603ca..9448cdd 100644 --- a/mev_inspect/schemas/nft_trades.py +++ b/mev_inspect/schemas/nft_trades.py @@ -1,8 +1,9 @@ from typing import List, Optional -from mev_inspect.schemas.traces import Protocol from pydantic import BaseModel +from mev_inspect.schemas.traces import Protocol + class NftTrade(BaseModel): abi_name: str From 1f84f95fffe6af77262f3b0cf08df684d3f5eaf0 Mon Sep 17 00:00:00 2001 From: Shea Ketsdever Date: Thu, 23 Dec 2021 18:57:11 -0600 Subject: [PATCH 6/8] Require exchange_wallet_address and rename payment_token -> payment_token_address --- .../3c54832385e3_create_nft_trades_table.py | 2 +- mev_inspect/classifiers/helpers.py | 39 +++++++++---------- mev_inspect/models/nft_trades.py | 2 +- mev_inspect/schemas/nft_trades.py | 2 +- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/alembic/versions/3c54832385e3_create_nft_trades_table.py b/alembic/versions/3c54832385e3_create_nft_trades_table.py index 6baebd6..0eb55b5 100644 --- a/alembic/versions/3c54832385e3_create_nft_trades_table.py +++ b/alembic/versions/3c54832385e3_create_nft_trades_table.py @@ -28,7 +28,7 @@ def upgrade(): sa.Column("error", sa.String(256), nullable=True), sa.Column("seller_address", sa.String(256), nullable=False), sa.Column("buyer_address", sa.String(256), nullable=False), - sa.Column("payment_token", sa.String(256), nullable=False), + sa.Column("payment_token_address", sa.String(256), nullable=False), sa.Column("payment_amount", sa.Numeric, nullable=False), sa.Column("collection_address", sa.String(256), nullable=False), sa.Column("token_id", sa.Numeric, nullable=False), diff --git a/mev_inspect/classifiers/helpers.py b/mev_inspect/classifiers/helpers.py index 0094172..ced108f 100644 --- a/mev_inspect/classifiers/helpers.py +++ b/mev_inspect/classifiers/helpers.py @@ -12,7 +12,7 @@ def create_nft_trade_from_transfers( collection_address: str, seller_address: str, buyer_address: str, - exchange_wallet_address: Optional[str], + exchange_wallet_address: str, ) -> Optional[NftTrade]: transfers_to_buyer = _filter_transfers(child_transfers, to_address=buyer_address) transfers_to_seller = _filter_transfers(child_transfers, to_address=seller_address) @@ -23,27 +23,26 @@ def create_nft_trade_from_transfers( if transfers_to_buyer[0].token_address != collection_address: return None - payment_token = transfers_to_seller[0].token_address + payment_token_address = transfers_to_seller[0].token_address payment_amount = transfers_to_seller[0].amount token_id = transfers_to_buyer[0].amount - if exchange_wallet_address is not None: - transfers_from_seller_to_exchange = _filter_transfers( - child_transfers, - from_address=seller_address, - to_address=exchange_wallet_address, - ) - transfers_from_buyer_to_exchange = _filter_transfers( - child_transfers, - from_address=buyer_address, - to_address=exchange_wallet_address, - ) - for fee in [ - *transfers_from_seller_to_exchange, - *transfers_from_buyer_to_exchange, - ]: - # Assumes that exchange fees are paid with the same token as the sale - payment_amount -= fee.amount + transfers_from_seller_to_exchange = _filter_transfers( + child_transfers, + from_address=seller_address, + to_address=exchange_wallet_address, + ) + transfers_from_buyer_to_exchange = _filter_transfers( + child_transfers, + from_address=buyer_address, + to_address=exchange_wallet_address, + ) + for fee in [ + *transfers_from_seller_to_exchange, + *transfers_from_buyer_to_exchange, + ]: + # Assumes that exchange fees are paid with the same token as the sale + payment_amount -= fee.amount return NftTrade( abi_name=trace.abi_name, @@ -55,7 +54,7 @@ def create_nft_trade_from_transfers( error=trace.error, seller_address=seller_address, buyer_address=buyer_address, - payment_token=payment_token, + payment_token_address=payment_token_address, payment_amount=payment_amount, collection_address=collection_address, token_id=token_id, diff --git a/mev_inspect/models/nft_trades.py b/mev_inspect/models/nft_trades.py index db55084..c8c1643 100644 --- a/mev_inspect/models/nft_trades.py +++ b/mev_inspect/models/nft_trades.py @@ -15,7 +15,7 @@ class NftTradeModel(Base): error = Column(String, nullable=True) seller_address = Column(String, nullable=False) buyer_address = Column(String, nullable=False) - payment_token = Column(String, nullable=False) + payment_token_address = Column(String, nullable=False) payment_amount = Column(Numeric, nullable=False) collection_address = Column(String, nullable=False) token_id = Column(Numeric, nullable=False) diff --git a/mev_inspect/schemas/nft_trades.py b/mev_inspect/schemas/nft_trades.py index 9448cdd..bf5b8cd 100644 --- a/mev_inspect/schemas/nft_trades.py +++ b/mev_inspect/schemas/nft_trades.py @@ -15,7 +15,7 @@ class NftTrade(BaseModel): error: Optional[str] seller_address: str buyer_address: str - payment_token: str + payment_token_address: str payment_amount: int collection_address: str token_id: int From ce7585e0b35517f2673f34806157de3962c35a26 Mon Sep 17 00:00:00 2001 From: Shea Ketsdever Date: Thu, 23 Dec 2021 19:41:26 -0600 Subject: [PATCH 7/8] Fix getting addr --- mev_inspect/classifiers/specs/opensea.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mev_inspect/classifiers/specs/opensea.py b/mev_inspect/classifiers/specs/opensea.py index 72db312..10d4aab 100644 --- a/mev_inspect/classifiers/specs/opensea.py +++ b/mev_inspect/classifiers/specs/opensea.py @@ -15,9 +15,7 @@ class OpenseaClassifier(NftTradeClassifier): trace: DecodedCallTrace, child_transfers: List[Transfer], ) -> Optional[NftTrade]: - addresses = trace.inputs.get("addrs") - if addresses is None: - return None + addresses = trace.inputs["addrs"] buy_maker = addresses[1] sell_maker = addresses[8] target = addresses[4] From 59908386035b4f46645453d968ab0119c0f37f00 Mon Sep 17 00:00:00 2001 From: Shea Ketsdever Date: Sat, 25 Dec 2021 15:53:13 -0600 Subject: [PATCH 8/8] Last nits --- alembic/versions/3c54832385e3_create_nft_trades_table.py | 4 ++-- mev_inspect/inspect_block.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/alembic/versions/3c54832385e3_create_nft_trades_table.py b/alembic/versions/3c54832385e3_create_nft_trades_table.py index 0eb55b5..c2d6cab 100644 --- a/alembic/versions/3c54832385e3_create_nft_trades_table.py +++ b/alembic/versions/3c54832385e3_create_nft_trades_table.py @@ -1,7 +1,7 @@ """Create NFT Trades table Revision ID: 3c54832385e3 -Revises: 15ba9c27ee8a +Revises: 4b9d289f2d74 Create Date: 2021-12-19 22:50:28.936516 """ @@ -10,7 +10,7 @@ from alembic import op # revision identifiers, used by Alembic. revision = "3c54832385e3" -down_revision = "15ba9c27ee8a" +down_revision = "4b9d289f2d74" branch_labels = None depends_on = None diff --git a/mev_inspect/inspect_block.py b/mev_inspect/inspect_block.py index 97785dc..7e763dd 100644 --- a/mev_inspect/inspect_block.py +++ b/mev_inspect/inspect_block.py @@ -222,7 +222,7 @@ async def inspect_many_blocks( delete_nft_trades_for_blocks( inspect_db_session, after_block_number, before_block_number ) - write_nft_trades(inspect_db_session, nft_trades) + write_nft_trades(inspect_db_session, all_nft_trades) delete_miner_payments_for_blocks( inspect_db_session, after_block_number, before_block_number