Skip to content

Commit

Permalink
✨ Turn GET /income to aggregating query and rename the original one t…
Browse files Browse the repository at this point in the history
…o GET /income/detail
  • Loading branch information
williamchong committed Apr 19, 2023
1 parent 92adbd1 commit 6e64ef4
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 71 deletions.
Empty file added build.sh
Empty file.
150 changes: 117 additions & 33 deletions db/nft.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,31 +276,61 @@ func GetNftEvents(conn *pgxpool.Conn, q QueryEventsRequest, p PageRequest) (Quer
e.id, e.action, e.class_id, e.nft_id, e.sender,
e.receiver, e.timestamp, e.tx_hash, e.events, e.price,
e.memo
FROM nft_event as e
JOIN nft_class as c
ON e.class_id = c.class_id
JOIN iscn AS i
ON i.iscn_id_prefix = c.parent_iscn_id_prefix
JOIN iscn_latest_version
ON i.iscn_id_prefix = iscn_latest_version.iscn_id_prefix
AND i.version = iscn_latest_version.latest_version
WHERE ($4 = '' OR e.class_id = $4)
AND ($12::text[] IS NULL OR cardinality($12::text[]) = 0 OR i.owner = ANY($12))
AND (nft_id = '' OR $5 = '' OR nft_id = $5)
AND ($6 = '' OR c.parent_iscn_id_prefix = $6)
AND ($10::text[] IS NULL OR cardinality($10::text[]) = 0 OR e.sender = ANY($10))
AND ($11::text[] IS NULL OR cardinality($11::text[]) = 0 OR e.receiver = ANY($11))
AND ($13::text[] IS NULL OR cardinality($13::text[]) = 0
OR e.sender = ANY($13)
OR e.receiver = ANY($13)
OR i.owner = ANY($13)
FROM (
(
SELECT DISTINCT ON (e.id) e.*
FROM nft_event as e
JOIN nft_class as c
ON e.class_id = c.class_id
JOIN iscn AS i
ON i.iscn_id_prefix = c.parent_iscn_id_prefix
JOIN iscn_latest_version
ON i.iscn_id_prefix = iscn_latest_version.iscn_id_prefix
AND i.version = iscn_latest_version.latest_version
WHERE ($4 = '' OR e.class_id = $4)
AND ($12::text[] IS NULL OR cardinality($12::text[]) = 0 OR i.owner = ANY($12))
AND (nft_id = '' OR $5 = '' OR nft_id = $5)
AND ($6 = '' OR c.parent_iscn_id_prefix = $6)
AND ($10::text[] IS NULL OR cardinality($10::text[]) = 0 OR e.sender = ANY($10))
AND ($11::text[] IS NULL OR cardinality($11::text[]) = 0 OR e.receiver = ANY($11))
AND ($13::text[] IS NULL OR cardinality($13::text[]) = 0
OR e.sender = ANY($13)
OR e.receiver = ANY($13)
)
AND ($1 = 0 OR e.id > $1)
AND ($2 = 0 OR e.id < $2)
AND ($7::text[] IS NULL OR cardinality($7::text[]) = 0 OR e.action = ANY($7))
AND ($8::text[] IS NULL OR cardinality($8::text[]) = 0 OR e.sender != ALL($8))
AND ($9::text[] IS NULL OR cardinality($9::text[]) = 0 OR e.receiver != ALL($9))
ORDER BY e.id %[1]s
LIMIT $3
) UNION ALL (
SELECT DISTINCT ON (e.id) e.*
FROM nft_event as e
JOIN nft_class as c
ON e.class_id = c.class_id
JOIN iscn AS i
ON i.iscn_id_prefix = c.parent_iscn_id_prefix
JOIN iscn_latest_version
ON i.iscn_id_prefix = iscn_latest_version.iscn_id_prefix
AND i.version = iscn_latest_version.latest_version
WHERE ($4 = '' OR e.class_id = $4)
AND ($12::text[] IS NULL OR cardinality($12::text[]) = 0 OR i.owner = ANY($12))
AND (nft_id = '' OR $5 = '' OR nft_id = $5)
AND ($6 = '' OR c.parent_iscn_id_prefix = $6)
AND ($10::text[] IS NULL OR cardinality($10::text[]) = 0 OR e.sender = ANY($10))
AND ($11::text[] IS NULL OR cardinality($11::text[]) = 0 OR e.receiver = ANY($11))
AND ($13::text[] IS NULL OR cardinality($13::text[]) = 0 OR i.owner = ANY($13))
AND ($1 = 0 OR e.id > $1)
AND ($2 = 0 OR e.id < $2)
AND ($7::text[] IS NULL OR cardinality($7::text[]) = 0 OR e.action = ANY($7))
AND ($8::text[] IS NULL OR cardinality($8::text[]) = 0 OR e.sender != ALL($8))
AND ($9::text[] IS NULL OR cardinality($9::text[]) = 0 OR e.receiver != ALL($9))
ORDER BY e.id %[1]s
LIMIT $3
)
AND ($1 = 0 OR e.id > $1)
AND ($2 = 0 OR e.id < $2)
AND ($7::text[] IS NULL OR cardinality($7::text[]) = 0 OR e.action = ANY($7))
AND ($8::text[] IS NULL OR cardinality($8::text[]) = 0 OR e.sender != ALL($8))
AND ($9::text[] IS NULL OR cardinality($9::text[]) = 0 OR e.receiver != ALL($9))
ORDER BY e.id %s
) AS e
ORDER BY e.id %[1]s
LIMIT $3
`, p.Order())

Expand Down Expand Up @@ -352,6 +382,60 @@ func GetNftEvents(conn *pgxpool.Conn, q QueryEventsRequest, p PageRequest) (Quer
func GetNftIncomes(conn *pgxpool.Conn, q QueryIncomesRequest, p PageRequest) (QueryIncomesResponse, error) {
ownerVariations := utils.ConvertAddressPrefixes(q.Owner, AddressPrefixes)
stakeholderVariations := utils.ConvertAddressPrefixes(q.Address, AddressPrefixes)

sql := fmt.Sprintf(`
SELECT i.address, SUM(i.amount) AS amount
FROM nft_event AS e
JOIN nft_income AS i
ON e.class_id = i.class_id
AND e.nft_id = i.nft_id
AND e.tx_hash = i.tx_hash
WHERE ($2 = 0 OR i.id > $2)
AND ($3 = 0 OR i.id < $3)
AND ($4 = '' OR e.class_id = $4)
AND ($5 = '' OR e.nft_id = $5)
AND ($6::text[] IS NULL OR cardinality($6::text[]) = 0 OR e.receiver = ANY($6))
AND ($7::text[] IS NULL OR cardinality($7::text[]) = 0 OR i.address = ANY($7))
AND ($8 = 0 OR (e.timestamp IS NOT NULL AND e.timestamp > to_timestamp($8)))
AND ($9 = 0 OR (e.timestamp IS NOT NULL AND e.timestamp < to_timestamp($9)))
AND ($10::text[] IS NULL OR cardinality($10::text[]) = 0 OR e.action = ANY($10))
GROUP BY i.address
ORDER BY amount %[1]s
LIMIT $1
`, p.Order())

ctx, cancel := GetTimeoutContext()
defer cancel()

rows, err := conn.Query(
ctx, sql,
p.Limit, p.After(), p.Before(), q.ClassId, q.NftId,
ownerVariations, stakeholderVariations, q.After, q.Before, q.ActionType,
)
if err != nil {
logger.L.Errorw("Failed to query nft incomes", "error", err)
return QueryIncomesResponse{}, fmt.Errorf("query nft incomes error: %w", err)
}

res := QueryIncomesResponse{
Incomes: make([]NftIncomeResponse, 0),
}
for rows.Next() {
var r NftIncomeResponse
if err = rows.Scan(&r.Address, &r.Amount); err != nil {
logger.L.Errorw("failed to scan nft incomes", "error", err, "q", q)
return QueryIncomesResponse{}, fmt.Errorf("query nft incomes data failed: %w", err)
}
res.Incomes = append(res.Incomes, r)
res.TotalAmount += r.Amount
}
res.Pagination.Count = len(res.Incomes)
return res, nil
}

func GetNftIncomeDetails(conn *pgxpool.Conn, q QueryIncomeDetailsRequest, p PageRequest) (QueryIncomeDetailsResponse, error) {
ownerVariations := utils.ConvertAddressPrefixes(q.Owner, AddressPrefixes)
stakeholderVariations := utils.ConvertAddressPrefixes(q.Address, AddressPrefixes)
orderBy := "i.id"
switch q.OrderBy {
case "price":
Expand Down Expand Up @@ -393,25 +477,25 @@ func GetNftIncomes(conn *pgxpool.Conn, q QueryIncomesRequest, p PageRequest) (Qu
ownerVariations, stakeholderVariations, q.After, q.Before, q.ActionType,
)
if err != nil {
logger.L.Errorw("Failed to query nft incomes", "error", err)
return QueryIncomesResponse{}, fmt.Errorf("query nft royalties error: %w", err)
logger.L.Errorw("Failed to query nft income details", "error", err)
return QueryIncomeDetailsResponse{}, fmt.Errorf("query nft income details error: %w", err)
}

res := QueryIncomesResponse{
Incomes: make([]NftIncomeResponse, 0),
res := QueryIncomeDetailsResponse{
IncomeDetails: make([]NftIncomeDetailResponse, 0),
}
for rows.Next() {
var r NftIncomeResponse
var r NftIncomeDetailResponse
if err = rows.Scan(
&r.ClassId, &r.NftId, &r.TxHash, &r.Timestamp, &r.Owner,
&r.Address, &r.Price, &r.Amount,
); err != nil {
logger.L.Errorw("failed to scan nft incomes", "error", err, "q", q)
return QueryIncomesResponse{}, fmt.Errorf("query nft incomes data failed: %w", err)
logger.L.Errorw("failed to scan nft income details", "error", err, "q", q)
return QueryIncomeDetailsResponse{}, fmt.Errorf("query nft income details data failed: %w", err)
}
res.Incomes = append(res.Incomes, r)
res.IncomeDetails = append(res.IncomeDetails, r)
}
res.Pagination.Count = len(res.Incomes)
res.Pagination.Count = len(res.IncomeDetails)
return res, nil
}

Expand Down
1 change: 1 addition & 0 deletions db/schema/v015.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX idx_nft_event_class_id ON nft_event (class_id);
29 changes: 25 additions & 4 deletions db/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,31 @@ type QueryIncomesRequest struct {
After int64 `form:"after"`
Before int64 `form:"before"`
ActionType []NftEventAction `form:"action_type"`
OrderBy string `form:"order_by"`
}

type NftIncomeResponse struct {
Address string `json:"address"`
Amount uint64 `json:"amount"`
}

type QueryIncomesResponse struct {
TotalAmount uint64 `json:"total_amount"`
Incomes []NftIncomeResponse `json:"incomes"`
Pagination PageResponse `json:"pagination"`
}

type QueryIncomeDetailsRequest struct {
ClassId string `form:"class_id"`
NftId string `form:"nft_id"`
Owner string `form:"owner"`
Address string `form:"address"`
After int64 `form:"after"`
Before int64 `form:"before"`
ActionType []NftEventAction `form:"action_type"`
OrderBy string `form:"order_by"`
}

type NftIncomeDetailResponse struct {
ClassId string `json:"class_id"`
NftId string `json:"nft_id"`
TxHash string `json:"tx_hash"`
Expand All @@ -299,9 +320,9 @@ type NftIncomeResponse struct {
Amount uint64 `json:"amount"`
}

type QueryIncomesResponse struct {
Incomes []NftIncomeResponse `json:"incomes"`
Pagination PageResponse `json:"pagination"`
type QueryIncomeDetailsResponse struct {
IncomeDetails []NftIncomeDetailResponse `json:"income_details"`
Pagination PageResponse `json:"pagination"`
}

type QueryRankingRequest struct {
Expand Down
62 changes: 44 additions & 18 deletions extractor/marketplace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,23 +337,36 @@ func TestListing(t *testing.T) {
require.Len(t, eventsRes.Events, 1)
require.Equal(t, updatedPrice1, eventsRes.Events[0].Price)

royalties, err := GetNftIncomes(Conn,
incomesRes, err := GetNftIncomes(Conn,
QueryIncomesRequest{
ClassId: nftClasses[0].Id,
}, PageRequest{Limit: 10},
)
require.NoError(t, err)
require.Len(t, incomesRes.Incomes, 2)
require.Equal(t, stakeholder1, incomesRes.Incomes[0].Address)
require.Equal(t, royalty1, incomesRes.Incomes[0].Amount)
require.Equal(t, stakeholder2, incomesRes.Incomes[1].Address)
require.Equal(t, royalty2, incomesRes.Incomes[1].Amount)
require.Equal(t, updatedPrice1, incomesRes.TotalAmount)

incomeDetailsRes, err := GetNftIncomeDetails(Conn,
QueryIncomeDetailsRequest{
ClassId: nftClasses[0].Id,
NftId: nfts[0].NftId,
OrderBy: "income",
ActionType: []NftEventAction{ACTION_BUY},
}, PageRequest{Limit: 10},
)
require.NoError(t, err)
require.Len(t, royalties.Incomes, 2)
require.Equal(t, nftClasses[0].Id, royalties.Incomes[0].ClassId)
require.Equal(t, nfts[0].NftId, royalties.Incomes[0].NftId)
require.Equal(t, "AAAAAE", royalties.Incomes[0].TxHash)
require.Equal(t, stakeholder1, royalties.Incomes[0].Address)
require.Equal(t, royalty1, royalties.Incomes[0].Amount)
require.Equal(t, stakeholder2, royalties.Incomes[1].Address)
require.Equal(t, royalty2, royalties.Incomes[1].Amount)
require.Len(t, incomeDetailsRes.IncomeDetails, 2)
require.Equal(t, nftClasses[0].Id, incomeDetailsRes.IncomeDetails[0].ClassId)
require.Equal(t, nfts[0].NftId, incomeDetailsRes.IncomeDetails[0].NftId)
require.Equal(t, "AAAAAE", incomeDetailsRes.IncomeDetails[0].TxHash)
require.Equal(t, stakeholder1, incomeDetailsRes.IncomeDetails[0].Address)
require.Equal(t, royalty1, incomeDetailsRes.IncomeDetails[0].Amount)
require.Equal(t, stakeholder2, incomeDetailsRes.IncomeDetails[1].Address)
require.Equal(t, royalty2, incomeDetailsRes.IncomeDetails[1].Amount)
}

func TestOffer(t *testing.T) {
Expand Down Expand Up @@ -545,21 +558,34 @@ func TestOffer(t *testing.T) {
require.Len(t, eventsRes.Events, 1)
require.Equal(t, updatedPrice1, eventsRes.Events[0].Price)

incomes, err := GetNftIncomes(Conn,
incomesRes, err := GetNftIncomes(Conn,
QueryIncomesRequest{
ClassId: nftClasses[0].Id,
}, PageRequest{Limit: 10},
)
require.NoError(t, err)
require.Len(t, incomesRes.Incomes, 2)
require.Equal(t, stakeholder1, incomesRes.Incomes[0].Address)
require.Equal(t, royalty1, incomesRes.Incomes[0].Amount)
require.Equal(t, stakeholder2, incomesRes.Incomes[1].Address)
require.Equal(t, royalty2, incomesRes.Incomes[1].Amount)
require.Equal(t, updatedPrice1, incomesRes.TotalAmount)

incomeDetailsRes, err := GetNftIncomeDetails(Conn,
QueryIncomeDetailsRequest{
ClassId: nftClasses[0].Id,
NftId: nfts[0].NftId,
OrderBy: "income",
ActionType: []NftEventAction{ACTION_SELL},
}, PageRequest{Limit: 10},
)
require.NoError(t, err)
require.Len(t, incomes.Incomes, 2)
require.Equal(t, nftClasses[0].Id, incomes.Incomes[0].ClassId)
require.Equal(t, nfts[0].NftId, incomes.Incomes[0].NftId)
require.Equal(t, "AAAAAE", incomes.Incomes[0].TxHash)
require.Equal(t, stakeholder1, incomes.Incomes[0].Address)
require.Equal(t, royalty1, incomes.Incomes[0].Amount)
require.Equal(t, stakeholder2, incomes.Incomes[1].Address)
require.Equal(t, royalty2, incomes.Incomes[1].Amount)
require.Len(t, incomeDetailsRes.IncomeDetails, 2)
require.Equal(t, nftClasses[0].Id, incomeDetailsRes.IncomeDetails[0].ClassId)
require.Equal(t, nfts[0].NftId, incomeDetailsRes.IncomeDetails[0].NftId)
require.Equal(t, "AAAAAE", incomeDetailsRes.IncomeDetails[0].TxHash)
require.Equal(t, stakeholder1, incomeDetailsRes.IncomeDetails[0].Address)
require.Equal(t, royalty1, incomeDetailsRes.IncomeDetails[0].Amount)
require.Equal(t, stakeholder2, incomeDetailsRes.IncomeDetails[1].Address)
require.Equal(t, royalty2, incomeDetailsRes.IncomeDetails[1].Amount)
}
43 changes: 28 additions & 15 deletions extractor/nft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ func TestSendNftWithPrice(t *testing.T) {
receiver := ADDR_01_LIKE
stakeholder1 := ADDR_02_LIKE
stakeholder2 := ADDR_03_LIKE
price := 100
royalty1 := 78
price := uint64(100)
royalty1 := uint64(78)
royalty2 := price - royalty1
txs := []string{
fmt.Sprintf(`
Expand Down Expand Up @@ -252,30 +252,43 @@ func TestSendNftWithPrice(t *testing.T) {
require.Equal(t, ADDR_02_LIKE, eventRes.Events[0].Receiver)
require.Equal(t, "AAAAAA", eventRes.Events[0].TxHash)
require.Equal(t, ACTION_SEND, eventRes.Events[0].Action)
require.Equal(t, uint64(price), eventRes.Events[0].Price)
require.Equal(t, price, eventRes.Events[0].Price)

incomeRes, err := GetNftIncomes(Conn, QueryIncomesRequest{
incomesRes, err := GetNftIncomes(Conn,
QueryIncomesRequest{
ClassId: nftClasses[0].Id,
}, PageRequest{Limit: 10, Reverse: true},
)
require.NoError(t, err)
require.Len(t, incomesRes.Incomes, 2)
require.Equal(t, stakeholder1, incomesRes.Incomes[0].Address)
require.Equal(t, royalty1, incomesRes.Incomes[0].Amount)
require.Equal(t, stakeholder2, incomesRes.Incomes[1].Address)
require.Equal(t, royalty2, incomesRes.Incomes[1].Amount)
require.Equal(t, price, incomesRes.TotalAmount)

incomeDetailsRes, err := GetNftIncomeDetails(Conn, QueryIncomeDetailsRequest{
ClassId: nftClasses[0].Id,
OrderBy: "royalty",
OrderBy: "income",
ActionType: []NftEventAction{ACTION_SEND},
}, PageRequest{Limit: 10, Reverse: true})
require.NoError(t, err)
require.Len(t, incomeRes.Incomes, 2)
require.Equal(t, nfts[0].ClassId, incomeRes.Incomes[0].ClassId)
require.Equal(t, nfts[0].NftId, incomeRes.Incomes[0].NftId)
require.Equal(t, ADDR_02_LIKE, incomeRes.Incomes[0].Owner)
require.Equal(t, stakeholder1, incomeRes.Incomes[0].Address)
require.Equal(t, uint64(royalty1), incomeRes.Incomes[0].Amount)
require.Equal(t, stakeholder2, incomeRes.Incomes[1].Address)
require.Equal(t, uint64(royalty2), incomeRes.Incomes[1].Amount)
require.Equal(t, uint64(price), incomeRes.Incomes[0].Amount+incomeRes.Incomes[1].Amount)
require.Len(t, incomeDetailsRes.IncomeDetails, 2)
require.Equal(t, nfts[0].ClassId, incomeDetailsRes.IncomeDetails[0].ClassId)
require.Equal(t, nfts[0].NftId, incomeDetailsRes.IncomeDetails[0].NftId)
require.Equal(t, ADDR_02_LIKE, incomeDetailsRes.IncomeDetails[0].Owner)
require.Equal(t, stakeholder1, incomeDetailsRes.IncomeDetails[0].Address)
require.Equal(t, royalty1, incomeDetailsRes.IncomeDetails[0].Amount)
require.Equal(t, stakeholder2, incomeDetailsRes.IncomeDetails[1].Address)
require.Equal(t, royalty2, incomeDetailsRes.IncomeDetails[1].Amount)
require.Equal(t, price, incomeDetailsRes.IncomeDetails[0].Amount+incomeDetailsRes.IncomeDetails[1].Amount)

row := Conn.QueryRow(context.Background(), `SELECT latest_price, price_updated_at FROM nft WHERE class_id = $1 AND nft_id = $2`, nftClasses[0].Id, nfts[0].NftId)
var lastPrice uint64
var priceUpdatedAt time.Time
err = row.Scan(&lastPrice, &priceUpdatedAt)
require.NoError(t, err)
require.Equal(t, uint64(price), lastPrice)
require.Equal(t, price, lastPrice)
require.Equal(t, timestamp.UTC(), priceUpdatedAt.UTC())
}

Expand Down
Loading

0 comments on commit 6e64ef4

Please sign in to comment.