Merge pull request #173 from sketsdever/opensea

Opensea NFT Trade classifier
This commit is contained in:
Luke Van Seters 2021-12-25 16:56:29 -05:00 committed by GitHub
commit 6b8d66b976
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 306 additions and 0 deletions

View File

@ -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")

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,66 @@
from typing import List, Optional, Sequence 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.swaps import Swap
from mev_inspect.schemas.traces import ClassifiedTrace, DecodedCallTrace from mev_inspect.schemas.traces import ClassifiedTrace, DecodedCallTrace
from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS, Transfer 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( def create_swap_from_pool_transfers(
trace: DecodedCallTrace, trace: DecodedCallTrace,
recipient_address: str, recipient_address: str,

View File

@ -10,6 +10,7 @@ from .compound import COMPOUND_CLASSIFIER_SPECS
from .cryptopunks import CRYPTOPUNKS_CLASSIFIER_SPECS from .cryptopunks import CRYPTOPUNKS_CLASSIFIER_SPECS
from .curve import CURVE_CLASSIFIER_SPECS from .curve import CURVE_CLASSIFIER_SPECS
from .erc20 import ERC20_CLASSIFIER_SPECS from .erc20 import ERC20_CLASSIFIER_SPECS
from .opensea import OPENSEA_CLASSIFIER_SPECS
from .uniswap import UNISWAP_CLASSIFIER_SPECS from .uniswap import UNISWAP_CLASSIFIER_SPECS
from .weth import WETH_ADDRESS, WETH_CLASSIFIER_SPECS from .weth import WETH_ADDRESS, WETH_CLASSIFIER_SPECS
from .zero_ex import ZEROX_CLASSIFIER_SPECS from .zero_ex import ZEROX_CLASSIFIER_SPECS
@ -24,6 +25,7 @@ ALL_CLASSIFIER_SPECS = (
+ BALANCER_CLASSIFIER_SPECS + BALANCER_CLASSIFIER_SPECS
+ COMPOUND_CLASSIFIER_SPECS + COMPOUND_CLASSIFIER_SPECS
+ CRYPTOPUNKS_CLASSIFIER_SPECS + CRYPTOPUNKS_CLASSIFIER_SPECS
+ OPENSEA_CLASSIFIER_SPECS
+ BANCOR_CLASSIFIER_SPECS + BANCOR_CLASSIFIER_SPECS
) )

View File

@ -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]

View File

@ -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()

View File

@ -17,6 +17,7 @@ from mev_inspect.crud.miner_payments import (
delete_miner_payments_for_blocks, delete_miner_payments_for_blocks,
write_miner_payments, write_miner_payments,
) )
from mev_inspect.crud.nft_trades import delete_nft_trades_for_blocks, write_nft_trades
from mev_inspect.crud.punks import ( from mev_inspect.crud.punks import (
delete_punk_bid_acceptances_for_blocks, delete_punk_bid_acceptances_for_blocks,
delete_punk_bids_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.crud.transfers import delete_transfers_for_blocks, write_transfers
from mev_inspect.liquidations import get_liquidations from mev_inspect.liquidations import get_liquidations
from mev_inspect.miner_payments import get_miner_payments 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.punks import get_punk_bid_acceptances, get_punk_bids, get_punk_snipes
from mev_inspect.sandwiches import get_sandwiches from mev_inspect.sandwiches import get_sandwiches
from mev_inspect.schemas.arbitrages import Arbitrage from mev_inspect.schemas.arbitrages import Arbitrage
from mev_inspect.schemas.blocks import Block from mev_inspect.schemas.blocks import Block
from mev_inspect.schemas.liquidations import Liquidation from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.miner_payments import MinerPayment 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_accept_bid import PunkBidAcceptance
from mev_inspect.schemas.punk_bid import PunkBid from mev_inspect.schemas.punk_bid import PunkBid
from mev_inspect.schemas.punk_snipe import PunkSnipe from mev_inspect.schemas.punk_snipe import PunkSnipe
@ -98,6 +101,8 @@ async def inspect_many_blocks(
all_miner_payments: List[MinerPayment] = [] all_miner_payments: List[MinerPayment] = []
all_nft_trades: List[NftTrade] = []
for block_number in range(after_block_number, before_block_number): for block_number in range(after_block_number, before_block_number):
block = await create_from_block_number( block = await create_from_block_number(
base_provider, base_provider,
@ -144,6 +149,9 @@ async def inspect_many_blocks(
punk_snipes = get_punk_snipes(punk_bids, punk_bid_acceptances) punk_snipes = get_punk_snipes(punk_bids, punk_bid_acceptances)
logger.info(f"Block: {block_number} -- Found {len(punk_snipes)} punk snipes") 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( miner_payments = get_miner_payments(
block.miner, block.base_fee_per_gas, classified_traces, block.receipts 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_bid_acceptances.extend(punk_bid_acceptances)
all_punk_snipes.extend(punk_snipes) all_punk_snipes.extend(punk_snipes)
all_nft_trades.extend(nft_trades)
all_miner_payments.extend(miner_payments) all_miner_payments.extend(miner_payments)
delete_blocks(inspect_db_session, after_block_number, before_block_number) 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) 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( delete_miner_payments_for_blocks(
inspect_db_session, after_block_number, before_block_number inspect_db_session, after_block_number, before_block_number
) )

View File

@ -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)

61
mev_inspect/nft_trades.py Normal file
View File

@ -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

View File

@ -3,6 +3,7 @@ from typing import Dict, List, Optional, Type
from pydantic import BaseModel from pydantic import BaseModel
from .nft_trades import NftTrade
from .swaps import Swap from .swaps import Swap
from .traces import Classification, DecodedCallTrace, Protocol from .traces import Classification, DecodedCallTrace, Protocol
from .transfers import Transfer from .transfers import Transfer
@ -53,6 +54,20 @@ class SeizeClassifier(Classifier):
return Classification.seize 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): class ClassifierSpec(BaseModel):
abi_name: str abi_name: str
protocol: Optional[Protocol] = None protocol: Optional[Protocol] = None

View File

@ -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

View File

@ -33,6 +33,7 @@ class Classification(Enum):
seize = "seize" seize = "seize"
punk_bid = "punk_bid" punk_bid = "punk_bid"
punk_accept_bid = "punk_accept_bid" punk_accept_bid = "punk_accept_bid"
nft_trade = "nft_trade"
class Protocol(Enum): class Protocol(Enum):
@ -48,6 +49,7 @@ class Protocol(Enum):
cream = "cream" cream = "cream"
cryptopunks = "cryptopunks" cryptopunks = "cryptopunks"
bancor = "bancor" bancor = "bancor"
opensea = "opensea"
class ClassifiedTrace(Trace): class ClassifiedTrace(Trace):