mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
The indexer had the item id all along and threw it away at the edge.
formatTokenTransfer wrote every row in the ERC-20 shape — an amount and a
null decimals — so the three Lux Genesis mints, which moved ids 0, 1 and 2,
came back as three identical rows and no page could name what had moved.
The id now rides in `total`, whose shape follows the token type the way
luxfi/explore's Erc721TotalPayload / Erc1155TotalPayload already read it.
/tokens/{addr}/instances was a hardcoded empty list and /instances/{id} was
an unregistered route, so a collection had no items and an item had no page.
Both are real now. The item list comes from the transfer table rather than
the balance table: balances hold only what someone holds now, so a burned
item would vanish from a collection that still minted it, while its last
transfer always exists and names its current owner.
ERC-1155 was not indexed at all. It is now, single and batch — a batch log
yields one row per (id, amount) pair, so no id is folded away.
Underneath, one decoder. There were two, and they disagreed: the daemon's
path read ERC-20 and ERC-721 only, spelled types "ERC-721", and stored the
raw amount hex; the adapter's path also read ERC-1155 and ERC-404 but
spelled types "ERC721" and stored ids two different ways depending on which
event carried them. decodeTransfers is now the only place a log becomes a
transfer, with one spelling, decimal amounts, and one stored id shape.
applyERC20BalanceDelta and applyERC721BalanceDelta collapse into move(),
which is one function because the three token kinds differ only in what the
id column holds.
An item's tokenURI is read on chain and recorded; the document behind it is
not fetched. An indexer that waits on an IPFS gateway stops indexing, and
the document is not ours and can change. uri_state distinguishes the three
answers that were previously one blank: the chain gave a URI, the chain
names none, or we have not asked. image_url and metadata stay null rather
than assert media nobody read.
Verified against C-Chain 96369: Lux Genesis
(0x9e04fc57c20b2ee45627c4aa280eb471f2ca6ea5, totalSupply 3) indexed from the
live RPC now reports ids 0, 1, 2 on its transfers and lists three items with
their owners, matching eth_getLogs topics[3] and ownerOf.
migrations/004_nft_marketplace.sql stays unapplied and now says why: no
marketplace contract is deployed on any Lux chain, so its tables could only
ever be empty. LLM.md's EVM parity list claimed Seaport/LooksRare/Blur/
X2Y2/Rarible/Zora coverage for a package nothing imports; it now says what
runs.
Co-authored-by: Hanzo Dev <[email protected]>
344 lines
14 KiB
PL/PgSQL
344 lines
14 KiB
PL/PgSQL
-- NFT Marketplace Tables Migration
|
|
-- Description: Adds NFT marketplace indexing for OpenSea Seaport, LooksRare, Blur, Rarible, X2Y2, Zora
|
|
--
|
|
-- NOT APPLIED. No marketplace contract is deployed on any Lux chain, so there
|
|
-- are no sales to index and these tables would only ever be empty. The live
|
|
-- API reads the SQLite tables evm/indexer.go creates, not this Postgres
|
|
-- schema. Applying it is a decision to take once a venue exists.
|
|
--
|
|
-- NFT items themselves are indexed and served today; see evm_token_instances
|
|
-- and /tokens/{addr}/instances.
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- NFT Orders (Listings, Bids, Offers)
|
|
--------------------------------------------------------------------------------
|
|
|
|
CREATE TABLE nft_orders (
|
|
id BIGSERIAL,
|
|
chain_id BIGINT NOT NULL REFERENCES chains(chain_id),
|
|
order_hash BYTEA NOT NULL,
|
|
protocol VARCHAR(50) NOT NULL, -- seaport, looksrare, blur, rarible, x2y2, zora
|
|
order_type VARCHAR(20) NOT NULL, -- listing, bid, offer, auction
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, filled, cancelled, expired
|
|
maker BYTEA NOT NULL,
|
|
taker BYTEA,
|
|
collection BYTEA NOT NULL,
|
|
token_id NUMERIC(78), -- NULL for collection offers
|
|
token_standard VARCHAR(20) DEFAULT 'ERC721', -- ERC721, ERC1155
|
|
quantity BIGINT DEFAULT 1,
|
|
price NUMERIC(100) NOT NULL,
|
|
payment_token BYTEA NOT NULL, -- 0x0 for native currency
|
|
start_time TIMESTAMPTZ,
|
|
end_time TIMESTAMPTZ,
|
|
salt NUMERIC(100),
|
|
zone BYTEA, -- Seaport zone
|
|
conduit BYTEA, -- Seaport conduit
|
|
signature TEXT,
|
|
raw_order TEXT, -- Original order JSON
|
|
block_number BIGINT NOT NULL,
|
|
tx_hash BYTEA NOT NULL,
|
|
log_index INTEGER NOT NULL,
|
|
timestamp BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
PRIMARY KEY (chain_id, order_hash)
|
|
) PARTITION BY LIST (chain_id);
|
|
|
|
-- Create partitions
|
|
CREATE TABLE nft_orders_96369 PARTITION OF nft_orders FOR VALUES IN (96369);
|
|
CREATE TABLE nft_orders_96368 PARTITION OF nft_orders FOR VALUES IN (96368);
|
|
CREATE TABLE nft_orders_200200 PARTITION OF nft_orders FOR VALUES IN (200200);
|
|
CREATE TABLE nft_orders_200201 PARTITION OF nft_orders FOR VALUES IN (200201);
|
|
CREATE TABLE nft_orders_36963 PARTITION OF nft_orders FOR VALUES IN (36963);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_nft_orders_collection ON nft_orders(chain_id, collection);
|
|
CREATE INDEX idx_nft_orders_maker ON nft_orders(chain_id, maker);
|
|
CREATE INDEX idx_nft_orders_status ON nft_orders(chain_id, status) WHERE status = 'active';
|
|
CREATE INDEX idx_nft_orders_protocol ON nft_orders(chain_id, protocol);
|
|
CREATE INDEX idx_nft_orders_type ON nft_orders(chain_id, order_type);
|
|
CREATE INDEX idx_nft_orders_token ON nft_orders(chain_id, collection, token_id) WHERE token_id IS NOT NULL;
|
|
CREATE INDEX idx_nft_orders_timestamp ON nft_orders(chain_id, timestamp DESC);
|
|
CREATE INDEX idx_nft_orders_expiry ON nft_orders(chain_id, end_time) WHERE status = 'active';
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- NFT Order Fees
|
|
--------------------------------------------------------------------------------
|
|
|
|
CREATE TABLE nft_order_fees (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
chain_id BIGINT NOT NULL,
|
|
order_hash BYTEA NOT NULL,
|
|
recipient BYTEA NOT NULL,
|
|
amount NUMERIC(100) NOT NULL,
|
|
basis_points INTEGER,
|
|
fee_type VARCHAR(20) NOT NULL, -- protocol, royalty, platform
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
FOREIGN KEY (chain_id, order_hash) REFERENCES nft_orders(chain_id, order_hash) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX idx_nft_order_fees_order ON nft_order_fees(chain_id, order_hash);
|
|
CREATE INDEX idx_nft_order_fees_recipient ON nft_order_fees(chain_id, recipient);
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- NFT Sales
|
|
--------------------------------------------------------------------------------
|
|
|
|
CREATE TABLE nft_sales (
|
|
id BIGSERIAL,
|
|
chain_id BIGINT NOT NULL REFERENCES chains(chain_id),
|
|
order_hash BYTEA, -- May be null for some protocols
|
|
protocol VARCHAR(50) NOT NULL,
|
|
collection BYTEA NOT NULL,
|
|
token_id NUMERIC(78) NOT NULL,
|
|
token_standard VARCHAR(20) DEFAULT 'ERC721',
|
|
quantity BIGINT DEFAULT 1,
|
|
seller BYTEA NOT NULL,
|
|
buyer BYTEA NOT NULL,
|
|
price NUMERIC(100) NOT NULL,
|
|
payment_token BYTEA NOT NULL,
|
|
price_usd NUMERIC(24, 8), -- USD value at time of sale
|
|
fee_total NUMERIC(100) DEFAULT 0,
|
|
royalty_total NUMERIC(100) DEFAULT 0,
|
|
seller_received NUMERIC(100),
|
|
block_number BIGINT NOT NULL,
|
|
tx_hash BYTEA NOT NULL,
|
|
log_index INTEGER NOT NULL,
|
|
timestamp BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
PRIMARY KEY (chain_id, tx_hash, log_index)
|
|
) PARTITION BY LIST (chain_id);
|
|
|
|
-- Create partitions
|
|
CREATE TABLE nft_sales_96369 PARTITION OF nft_sales FOR VALUES IN (96369);
|
|
CREATE TABLE nft_sales_96368 PARTITION OF nft_sales FOR VALUES IN (96368);
|
|
CREATE TABLE nft_sales_200200 PARTITION OF nft_sales FOR VALUES IN (200200);
|
|
CREATE TABLE nft_sales_200201 PARTITION OF nft_sales FOR VALUES IN (200201);
|
|
CREATE TABLE nft_sales_36963 PARTITION OF nft_sales FOR VALUES IN (36963);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_nft_sales_collection ON nft_sales(chain_id, collection);
|
|
CREATE INDEX idx_nft_sales_token ON nft_sales(chain_id, collection, token_id);
|
|
CREATE INDEX idx_nft_sales_seller ON nft_sales(chain_id, seller);
|
|
CREATE INDEX idx_nft_sales_buyer ON nft_sales(chain_id, buyer);
|
|
CREATE INDEX idx_nft_sales_protocol ON nft_sales(chain_id, protocol);
|
|
CREATE INDEX idx_nft_sales_timestamp ON nft_sales(chain_id, timestamp DESC);
|
|
CREATE INDEX idx_nft_sales_block ON nft_sales(chain_id, block_number DESC);
|
|
CREATE INDEX idx_nft_sales_price ON nft_sales(chain_id, collection, price DESC);
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- NFT Collections (Marketplace Stats)
|
|
--------------------------------------------------------------------------------
|
|
|
|
CREATE TABLE nft_collections (
|
|
chain_id BIGINT NOT NULL REFERENCES chains(chain_id),
|
|
address BYTEA NOT NULL,
|
|
name VARCHAR(255),
|
|
symbol VARCHAR(50),
|
|
token_standard VARCHAR(20) DEFAULT 'ERC721',
|
|
total_supply BIGINT,
|
|
floor_price NUMERIC(100) DEFAULT 0,
|
|
floor_token BYTEA, -- Payment token for floor price
|
|
royalty_bps INTEGER DEFAULT 0, -- Royalty in basis points
|
|
royalty_recipient BYTEA,
|
|
volume_24h NUMERIC(100) DEFAULT 0,
|
|
volume_7d NUMERIC(100) DEFAULT 0,
|
|
volume_30d NUMERIC(100) DEFAULT 0,
|
|
volume_total NUMERIC(100) DEFAULT 0,
|
|
sales_24h BIGINT DEFAULT 0,
|
|
sales_7d BIGINT DEFAULT 0,
|
|
sales_30d BIGINT DEFAULT 0,
|
|
sales_total BIGINT DEFAULT 0,
|
|
listing_count BIGINT DEFAULT 0,
|
|
owner_count BIGINT DEFAULT 0,
|
|
avg_price_24h NUMERIC(100),
|
|
price_change_24h NUMERIC(10, 4), -- Percentage
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
PRIMARY KEY (chain_id, address)
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_nft_collections_volume ON nft_collections(chain_id, volume_total DESC);
|
|
CREATE INDEX idx_nft_collections_floor ON nft_collections(chain_id, floor_price DESC);
|
|
CREATE INDEX idx_nft_collections_sales ON nft_collections(chain_id, sales_total DESC);
|
|
CREATE INDEX idx_nft_collections_name ON nft_collections USING gin (name gin_trgm_ops);
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- NFT Collection Activity (Aggregated)
|
|
--------------------------------------------------------------------------------
|
|
|
|
CREATE TABLE nft_collection_activity (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
chain_id BIGINT NOT NULL,
|
|
collection BYTEA NOT NULL,
|
|
activity_type VARCHAR(20) NOT NULL, -- sale, listing, bid, transfer, mint
|
|
from_address BYTEA,
|
|
to_address BYTEA,
|
|
token_id NUMERIC(78),
|
|
quantity BIGINT DEFAULT 1,
|
|
price NUMERIC(100),
|
|
payment_token BYTEA,
|
|
protocol VARCHAR(50),
|
|
block_number BIGINT NOT NULL,
|
|
tx_hash BYTEA NOT NULL,
|
|
log_index INTEGER NOT NULL,
|
|
timestamp BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
FOREIGN KEY (chain_id, collection) REFERENCES nft_collections(chain_id, address)
|
|
);
|
|
|
|
CREATE INDEX idx_nft_activity_collection ON nft_collection_activity(chain_id, collection);
|
|
CREATE INDEX idx_nft_activity_type ON nft_collection_activity(chain_id, activity_type);
|
|
CREATE INDEX idx_nft_activity_timestamp ON nft_collection_activity(chain_id, timestamp DESC);
|
|
CREATE INDEX idx_nft_activity_token ON nft_collection_activity(chain_id, collection, token_id);
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- NFT Marketplace Stats (Aggregated daily/hourly)
|
|
--------------------------------------------------------------------------------
|
|
|
|
CREATE TABLE nft_marketplace_stats (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
chain_id BIGINT NOT NULL REFERENCES chains(chain_id),
|
|
period_type VARCHAR(10) NOT NULL, -- hour, day, week
|
|
period_start TIMESTAMPTZ NOT NULL,
|
|
period_end TIMESTAMPTZ NOT NULL,
|
|
total_volume NUMERIC(100) DEFAULT 0,
|
|
total_sales BIGINT DEFAULT 0,
|
|
unique_buyers BIGINT DEFAULT 0,
|
|
unique_sellers BIGINT DEFAULT 0,
|
|
avg_price NUMERIC(100),
|
|
max_price NUMERIC(100),
|
|
volume_by_protocol JSONB, -- {"seaport": "123", "blur": "456"}
|
|
sales_by_protocol JSONB, -- {"seaport": 10, "blur": 20}
|
|
top_collections JSONB, -- [{"address": "0x...", "volume": "123"}]
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
UNIQUE(chain_id, period_type, period_start)
|
|
);
|
|
|
|
CREATE INDEX idx_nft_stats_period ON nft_marketplace_stats(chain_id, period_type, period_start DESC);
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- NFT Royalty Settings
|
|
--------------------------------------------------------------------------------
|
|
|
|
CREATE TABLE nft_royalties (
|
|
chain_id BIGINT NOT NULL,
|
|
collection BYTEA NOT NULL,
|
|
recipient BYTEA NOT NULL,
|
|
basis_points INTEGER NOT NULL,
|
|
source VARCHAR(50), -- eip2981, rarible, manifold, custom
|
|
block_number BIGINT,
|
|
tx_hash BYTEA,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
PRIMARY KEY (chain_id, collection, recipient)
|
|
);
|
|
|
|
CREATE INDEX idx_nft_royalties_collection ON nft_royalties(chain_id, collection);
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Views for Common Queries
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Active listings by collection
|
|
CREATE VIEW v_nft_active_listings AS
|
|
SELECT
|
|
o.chain_id,
|
|
o.collection,
|
|
o.token_id,
|
|
o.price,
|
|
o.payment_token,
|
|
o.maker,
|
|
o.protocol,
|
|
o.end_time,
|
|
o.created_at
|
|
FROM nft_orders o
|
|
WHERE o.status = 'active'
|
|
AND o.order_type = 'listing'
|
|
AND (o.end_time IS NULL OR o.end_time > NOW());
|
|
|
|
-- Floor prices by collection
|
|
CREATE VIEW v_nft_floor_prices AS
|
|
SELECT
|
|
chain_id,
|
|
collection,
|
|
MIN(price) as floor_price,
|
|
COUNT(*) as listing_count
|
|
FROM nft_orders
|
|
WHERE status = 'active'
|
|
AND order_type = 'listing'
|
|
AND (end_time IS NULL OR end_time > NOW())
|
|
GROUP BY chain_id, collection;
|
|
|
|
-- Recent sales
|
|
CREATE VIEW v_nft_recent_sales AS
|
|
SELECT
|
|
s.chain_id,
|
|
s.collection,
|
|
s.token_id,
|
|
s.seller,
|
|
s.buyer,
|
|
s.price,
|
|
s.payment_token,
|
|
s.protocol,
|
|
s.tx_hash,
|
|
s.timestamp,
|
|
c.name as collection_name
|
|
FROM nft_sales s
|
|
LEFT JOIN nft_collections c ON s.chain_id = c.chain_id AND s.collection = c.address
|
|
ORDER BY s.timestamp DESC;
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Triggers for Auto-Updates
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- Function to update collection stats on sale
|
|
CREATE OR REPLACE FUNCTION update_collection_on_sale()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
INSERT INTO nft_collections (chain_id, address, volume_total, sales_total)
|
|
VALUES (NEW.chain_id, NEW.collection, NEW.price, 1)
|
|
ON CONFLICT (chain_id, address) DO UPDATE
|
|
SET volume_total = nft_collections.volume_total + NEW.price,
|
|
sales_total = nft_collections.sales_total + 1,
|
|
updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_nft_sale_update_collection
|
|
AFTER INSERT ON nft_sales
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_collection_on_sale();
|
|
|
|
-- Function to update listing counts
|
|
CREATE OR REPLACE FUNCTION update_collection_listings()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF TG_OP = 'INSERT' AND NEW.order_type = 'listing' AND NEW.status = 'active' THEN
|
|
UPDATE nft_collections
|
|
SET listing_count = listing_count + 1,
|
|
updated_at = NOW()
|
|
WHERE chain_id = NEW.chain_id AND address = NEW.collection;
|
|
ELSIF TG_OP = 'UPDATE' AND OLD.status = 'active' AND NEW.status != 'active' THEN
|
|
UPDATE nft_collections
|
|
SET listing_count = GREATEST(listing_count - 1, 0),
|
|
updated_at = NOW()
|
|
WHERE chain_id = NEW.chain_id AND address = NEW.collection;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_nft_order_update_listings
|
|
AFTER INSERT OR UPDATE ON nft_orders
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_collection_listings();
|