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..c2d6cab --- /dev/null +++ b/alembic/versions/3c54832385e3_create_nft_trades_table.py @@ -0,0 +1,40 @@ +"""Create NFT Trades table + +Revision ID: 3c54832385e3 +Revises: 4b9d289f2d74 +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 = "4b9d289f2d74" +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_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), + sa.PrimaryKeyConstraint("transaction_hash", "trace_address"), + ) + + +def downgrade(): + op.drop_table("nft_trades") 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/helpers.py b/mev_inspect/classifiers/helpers.py index bb87661..ced108f 100644 --- a/mev_inspect/classifiers/helpers.py +++ b/mev_inspect/classifiers/helpers.py @@ -1,10 +1,66 @@ from typing import List, Optional, Sequence +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], + collection_address: str, + seller_address: str, + buyer_address: 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) + + 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_address = transfers_to_seller[0].token_address + payment_amount = transfers_to_seller[0].amount + token_id = transfers_to_buyer[0].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, + 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_address=payment_token_address, + payment_amount=payment_amount, + collection_address=collection_address, + 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 7ce3eb0..8473964 100644 --- a/mev_inspect/classifiers/specs/__init__.py +++ b/mev_inspect/classifiers/specs/__init__.py @@ -10,6 +10,7 @@ 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 @@ -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..10d4aab --- /dev/null +++ b/mev_inspect/classifiers/specs/opensea.py @@ -0,0 +1,42 @@ +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 +from mev_inspect.schemas.traces import DecodedCallTrace, Protocol +from mev_inspect.schemas.transfers import Transfer + +OPENSEA_WALLET_ADDRESS = "0x5b3256965e7c3cf26e11fcaf296dfc8807c01073" + + +class OpenseaClassifier(NftTradeClassifier): + @staticmethod + def parse_trade( + trace: DecodedCallTrace, + child_transfers: List[Transfer], + ) -> Optional[NftTrade]: + addresses = trace.inputs["addrs"] + buy_maker = addresses[1] + sell_maker = addresses[8] + target = addresses[4] + + return create_nft_trade_from_transfers( + trace, + child_transfers, + collection_address=target, + seller_address=sell_maker, + buyer_address=buy_maker, + exchange_wallet_address=OPENSEA_WALLET_ADDRESS, + ) + + +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, + }, +) + +OPENSEA_CLASSIFIER_SPECS = [OPENSEA_SPEC] diff --git a/mev_inspect/crud/nft_trades.py b/mev_inspect/crud/nft_trades.py new file mode 100644 index 0000000..f51f0a8 --- /dev/null +++ b/mev_inspect/crud/nft_trades.py @@ -0,0 +1,30 @@ +import json +from typing import List + +from mev_inspect.crud.shared import delete_by_block_range +from mev_inspect.models.nft_trades import NftTradeModel +from mev_inspect.schemas.nft_trades import NftTrade + + +def delete_nft_trades_for_blocks( + db_session, + after_block_number: int, + before_block_number: int, +) -> None: + delete_by_block_range( + db_session, + NftTradeModel, + after_block_number, + before_block_number, + ) + 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 0d33a4a..7e763dd 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_blocks, write_miner_payments, ) +from mev_inspect.crud.nft_trades import delete_nft_trades_for_blocks, write_nft_trades from mev_inspect.crud.punks import ( delete_punk_bid_acceptances_for_blocks, delete_punk_bids_for_blocks, @@ -34,12 +35,14 @@ from mev_inspect.crud.traces import ( from mev_inspect.crud.transfers import delete_transfers_for_blocks, write_transfers 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.schemas.arbitrages import Arbitrage from mev_inspect.schemas.blocks import Block from mev_inspect.schemas.liquidations import Liquidation from mev_inspect.schemas.miner_payments import MinerPayment +from mev_inspect.schemas.nft_trades import NftTrade from mev_inspect.schemas.punk_accept_bid import PunkBidAcceptance from mev_inspect.schemas.punk_bid import PunkBid from mev_inspect.schemas.punk_snipe import PunkSnipe @@ -98,6 +101,8 @@ async def inspect_many_blocks( all_miner_payments: List[MinerPayment] = [] + all_nft_trades: List[NftTrade] = [] + for block_number in range(after_block_number, before_block_number): block = await create_from_block_number( base_provider, @@ -144,6 +149,9 @@ async def inspect_many_blocks( punk_snipes = get_punk_snipes(punk_bids, punk_bid_acceptances) logger.info(f"Block: {block_number} -- Found {len(punk_snipes)} 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 ) @@ -160,6 +168,8 @@ async def inspect_many_blocks( all_punk_bid_acceptances.extend(punk_bid_acceptances) all_punk_snipes.extend(punk_snipes) + all_nft_trades.extend(nft_trades) + all_miner_payments.extend(miner_payments) delete_blocks(inspect_db_session, after_block_number, before_block_number) @@ -209,6 +219,11 @@ async def inspect_many_blocks( ) write_punk_snipes(inspect_db_session, punk_snipes) + delete_nft_trades_for_blocks( + inspect_db_session, after_block_number, before_block_number + ) + write_nft_trades(inspect_db_session, all_nft_trades) + delete_miner_payments_for_blocks( inspect_db_session, after_block_number, before_block_number ) diff --git a/mev_inspect/models/nft_trades.py b/mev_inspect/models/nft_trades.py new file mode 100644 index 0000000..c8c1643 --- /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_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/nft_trades.py b/mev_inspect/nft_trades.py new file mode 100644 index 0000000..67a0a0a --- /dev/null +++ b/mev_inspect/nft_trades.py @@ -0,0 +1,61 @@ +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 +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 = [] + + 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: + child_transfers = get_child_transfers( + trace.transaction_hash, + trace.trace_address, + traces, + ) + nft_trade = _parse_trade( + 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], +) -> Optional[NftTrade]: + classifier = get_classifier(trace) + + if classifier is not None and issubclass(classifier, NftTradeClassifier): + return classifier.parse_trade(trace, child_transfers) + + return None diff --git a/mev_inspect/schemas/classifiers.py b/mev_inspect/schemas/classifiers.py index c4d48e8..043f1ff 100644 --- a/mev_inspect/schemas/classifiers.py +++ b/mev_inspect/schemas/classifiers.py @@ -3,6 +3,7 @@ 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 @@ -53,6 +54,20 @@ class SeizeClassifier(Classifier): return Classification.seize +class NftTradeClassifier(Classifier): + @staticmethod + def get_classification() -> Classification: + return Classification.nft_trade + + @staticmethod + @abstractmethod + def parse_trade( + trace: DecodedCallTrace, + child_transfers: List[Transfer], + ) -> Optional[NftTrade]: + raise NotImplementedError() + + class ClassifierSpec(BaseModel): abi_name: str protocol: Optional[Protocol] = None diff --git a/mev_inspect/schemas/nft_trades.py b/mev_inspect/schemas/nft_trades.py new file mode 100644 index 0000000..bf5b8cd --- /dev/null +++ b/mev_inspect/schemas/nft_trades.py @@ -0,0 +1,21 @@ +from typing import List, Optional + +from pydantic import BaseModel + +from mev_inspect.schemas.traces import Protocol + + +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_address: str + payment_amount: int + collection_address: str + token_id: 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):