diff --git a/database/marketplace.go b/database/marketplace.go index adb45a5d7..a84590fda 100644 --- a/database/marketplace.go +++ b/database/marketplace.go @@ -2,20 +2,21 @@ package database import ( "fmt" + "time" "github.com/forbole/bdjuno/v2/database/utils" ) -func (db *Db) CheckIfNftExists(tokenId uint64, denomId string) error { +func (db *Db) CheckIfNftExists(tokenID uint64, denomID string) error { var rows []string - err := db.Sqlx.Select(&rows, `SELECT denom_id FROM marketplace_nft WHERE token_id=$1 AND denom_id=$2`, tokenId, denomId) + err := db.Sqlx.Select(&rows, `SELECT denom_id FROM marketplace_nft WHERE token_id=$1 AND denom_id=$2`, tokenID, denomID) if err != nil { return err } if len(rows) != 1 { - return fmt.Errorf("Not found.") + return fmt.Errorf("not found") } return nil @@ -27,17 +28,17 @@ func (db *Db) SaveMarketplaceCollection(txHash string, id uint64, denomID, mintR return err } -func (tx *DbTx) ListNft(txHash string, id, tokenId uint64, denomID, price string) error { +func (tx *DbTx) ListNft(txHash string, id, tokenID uint64, denomID, price string) error { _, err := tx.Exec(`UPDATE marketplace_nft SET transaction_hash=$1, id=$2, price=$3 WHERE token_id=$4 AND denom_id=$5`, - txHash, id, price, tokenId, denomID) + txHash, id, price, tokenID, denomID) fmt.Println(err) return err } -func (tx *DbTx) SaveMarketplaceNft(txHash string, tokenId uint64, denomID, uid, price, creator string) error { +func (tx *DbTx) SaveMarketplaceNft(txHash string, tokenID uint64, denomID, uid, price, creator string) error { _, err := tx.Exec(`INSERT INTO marketplace_nft (transaction_hash, uid, token_id, denom_id, price, creator, uniq_id) VALUES($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (token_id, denom_id) DO UPDATE SET price = EXCLUDED.price, id = EXCLUDED.id`, - txHash, uid, tokenId, denomID, price, creator, utils.FormatUniqID(tokenId, denomID)) + txHash, uid, tokenID, denomID, price, creator, utils.FormatUniqID(tokenID, denomID)) return err } @@ -76,6 +77,52 @@ func (tx *DbTx) UnlistNft(id uint64) error { return err } +func (tx *DbTx) SaveMarketplaceAuction(auctionID uint64, tokenID uint64, denomID string, creator string, startTime time.Time, endTime time.Time, auctionInfo string) error { + _, err := tx.Exec(`INSERT INTO marketplace_auction (id, token_id, denom_id, creator, start_time, end_time, auction) + VALUES($1, $2, $3, $4, $5, $6, $7)`, auctionID, tokenID, denomID, creator, startTime, endTime, auctionInfo) + return err +} + +func (tx *DbTx) SaveMarketplaceBid(auctionID uint64, bidder string, price string, timestamp time.Time, txHash string) error { + _, err := tx.Exec(`INSERT INTO marketplace_bid (auction_id, bidder, price, timestamp, transaction_hash) + VALUES($1, $2, $3, $4, $5)`, auctionID, bidder, price, timestamp, txHash) + return err +} + +func (tx *DbTx) SaveMarketplaceAuctionSold(auctionID uint64, timestamp uint64, usdPrice string, btcPrice string, txHashAcceptBid string) error { + var tokenID uint64 + var denomID, seller, buyer, price, txHashPlaceBid string + + if err := tx.QueryRow(`SELECT token_id, denom_id, creator FROM marketplace_auction WHERE id = $1`, auctionID).Scan(&tokenID, &denomID, &seller); err != nil { + return err + } + + if err := tx.QueryRow(`SELECT bidder, transaction_hash, price FROM marketplace_bid WHERE auction_id = $1 ORDER BY timestamp DESC`, auctionID).Scan(&buyer, &txHashPlaceBid, &price); err != nil { + return err + } + + _, err := tx.Exec(`UPDATE marketplace_auction SET sold = true WHERE id = $1`, auctionID) + if err != nil { + return err + } + + txHashBuyNft := txHashAcceptBid + if txHashBuyNft == "" { + txHashBuyNft = txHashPlaceBid + } + + if err := tx.saveMarketplaceNftBuy(txHashBuyNft, buyer, timestamp, tokenID, denomID, price, seller, usdPrice, btcPrice); err != nil { + return err + } + + return tx.UpdateNFTHistory(txHashBuyNft, tokenID, denomID, seller, buyer, timestamp) +} + +func (tx *DbTx) UpdateMarketplaceAuctionInfo(auctionID uint64, auctionInfo string) error { + _, err := tx.Exec(`UPDATE marketplace_auction SET auction = $2 WHERE id = $1`, auctionID, auctionInfo) + return err +} + func (db *Db) UnlistNft(id uint64) error { _, err := db.Sql.Exec(`UPDATE marketplace_nft SET price = '0', id = null WHERE id = $1`, id) return err @@ -95,3 +142,8 @@ func (db *Db) SetMarketplaceCollectionRoyalties(id uint64, mintRoyalties, resale _, err := db.Sql.Exec(`UPDATE marketplace_collection SET mint_royalties = $1, resale_royalties = $2 WHERE id = $3`, mintRoyalties, resaleRoyalties, id) return err } + +func (db *Db) UpdateMarketplaceAuctionInfo(auctionID uint64, auctionInfo string) error { + _, err := db.Sql.Exec(`UPDATE marketplace_auction SET auction = $2 WHERE id = $1`, auctionID, auctionInfo) + return err +} diff --git a/database/migration_test.go b/database/migration_test.go index ef9638dec..6510e080f 100644 --- a/database/migration_test.go +++ b/database/migration_test.go @@ -17,6 +17,9 @@ var expectedAppliedMigrations = []database.Migration{ {ID: int64(9), Name: "008-block_parsed_data.sql", CreatedAt: int64(0)}, {ID: int64(10), Name: "009-cw20token_update.sql", CreatedAt: int64(0)}, {ID: int64(11), Name: "010-nft-uniq-id.sql", CreatedAt: int64(0)}, + {ID: int64(12), Name: "011-nft-migrate-uniq-id-values.sql", CreatedAt: int64(0)}, + {ID: int64(13), Name: "012-marketplace-nft-id-column-unique.sql", CreatedAt: int64(0)}, + {ID: int64(14), Name: "013-marketplace-auctions.sql", CreatedAt: int64(0)}, } func (suite *DbTestSuite) TestExecuteMigrations() { diff --git a/database/scheme/013-marketplace-auctions.sql b/database/scheme/013-marketplace-auctions.sql new file mode 100644 index 000000000..a91ef0bb1 --- /dev/null +++ b/database/scheme/013-marketplace-auctions.sql @@ -0,0 +1,26 @@ +CREATE TABLE marketplace_auction ( + id BIGINT NOT NULL PRIMARY KEY, + token_id BIGINT NOT NULL, + denom_id TEXT NOT NULL REFERENCES nft_denom (id), + creator TEXT NOT NULL, + start_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, + auction TEXT NOT NULL, + sold BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (token_id, denom_id) REFERENCES nft_nft(id, denom_id) +); + +CREATE INDEX marketplace_auction_token_id_denom_id_index ON marketplace_auction (token_id, denom_id); +CREATE INDEX marketplace_auction_creator_index ON marketplace_auction (creator); + +CREATE TABLE marketplace_bid ( + auction_id BIGINT NOT NULL REFERENCES marketplace_auction (id), + bidder TEXT NOT NULL, + price DECIMAL NOT NULL, + timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, + transaction_hash TEXT NOT NULL REFERENCES transaction (hash) +); + +CREATE INDEX marketplace_bid_auction_id_index ON marketplace_bid (auction_id); +CREATE INDEX marketplace_bid_bidder_index ON marketplace_bid (bidder); +CREATE INDEX marketplace_bid_timestamp_index ON marketplace_bid (timestamp desc); diff --git a/database/types/marketplace.go b/database/types/marketplace.go new file mode 100644 index 000000000..3d9ec7961 --- /dev/null +++ b/database/types/marketplace.go @@ -0,0 +1,37 @@ +package types + +import ( + "time" +) + +type MarketplaceAuctionRow struct { + ID uint64 `db:"id"` + TokenID uint64 `db:"token_id"` + DenomID string `db:"denom_id"` + Creator string `db:"creator"` + StartTime time.Time `db:"start_time"` + EndTime time.Time `db:"end_time"` + Auction string `db:"auction"` + Sold bool `db:"sold"` +} + +type MarketplaceBidRow struct { + AuctionID uint64 `db:"auction_id"` + Bidder string `db:"bidder"` + Price string `db:"price"` + Timestamp time.Time `db:"timestamp"` + TxHash string `db:"transaction_hash"` +} + +type MarketplaceNftBuyHistory struct { + TxHash string `db:"transaction_hash"` + TokenID uint64 `db:"token_id"` + DenomID string `db:"denom_id"` + Price string `db:"price"` + Buyer string `db:"buyer"` + Seller string `db:"seller"` + UsdPrice string `db:"usd_price"` + BtcPrice string `db:"btc_price"` + Timestamp uint64 `db:"timestamp"` + UniqID string `db:"uniq_id"` +} diff --git a/database/types/nft.go b/database/types/nft.go new file mode 100644 index 000000000..da3cbc265 --- /dev/null +++ b/database/types/nft.go @@ -0,0 +1,12 @@ +package types + +type NftTransferHistoryRow struct { + ID uint64 `db:"id"` + TxHash string `db:"transaction_hash"` + DenomID string `db:"denom_id"` + Price string `db:"price"` + OldOwner string `db:"old_owner"` + NewOwner string `db:"new_owner"` + Timestamp uint64 `db:"timestamp"` + UniqID string `db:"uniq_id"` +} diff --git a/go.mod b/go.mod index 70d8f59b4..b40716954 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.17 require ( github.com/CosmWasm/wasmd v0.25.0 github.com/CosmWasm/wasmvm v1.0.0-beta10 - github.com/CudoVentures/cudos-node v1.0.1-0.20221205141302-2c6a9098d44b + github.com/CudoVentures/cudos-node v1.0.1-0.20230207102951-f1acfc21d5b9 github.com/althea-net/cosmos-gravity-bridge/module v0.0.0-00010101000000-000000000000 github.com/cosmos/cosmos-sdk v0.45.3 github.com/forbole/juno/v2 v2.0.0-20220223115732-dbb226a91ce9 diff --git a/go.sum b/go.sum index 9d7af48d5..4dcefc6f7 100644 --- a/go.sum +++ b/go.sum @@ -405,10 +405,8 @@ github.com/CudoVentures/cosmos-gravity-bridge/module v0.0.0-20220908134551-c82e5 github.com/CudoVentures/cosmos-gravity-bridge/module v0.0.0-20220908134551-c82e5d5a6ac3/go.mod h1:yhB9hRM0nPHH8k0HN+066glD0xaZjfB9WjgYeijYNBc= github.com/CudoVentures/cosmos-sdk v0.0.0-20220816082327-65532d606824 h1:6IkLkSVwMjH+qsyBZbOUwJRUHh72jXRzVNOMGKN8icI= github.com/CudoVentures/cosmos-sdk v0.0.0-20220816082327-65532d606824/go.mod h1:Zlp+5q+oliIV6SFlfspqcd/28Ra7v51Bt6Wy6H1b8PI= -github.com/CudoVentures/cudos-node v1.0.1-0.20221124124559-4c2b4c80ba67 h1:2assj7VdMtkQpD394PQJaxrmlvm8hH6g0BXiWiZXrhE= -github.com/CudoVentures/cudos-node v1.0.1-0.20221124124559-4c2b4c80ba67/go.mod h1:NSSTwYXNsoRHQT+TtLPMRcuoTv1t1ngwY5MufiyA6Bo= -github.com/CudoVentures/cudos-node v1.0.1-0.20221205141302-2c6a9098d44b h1:e6wx/bp5G8CdVkLS1wxZq7eKdHIzIcDUBBDmpaq62dU= -github.com/CudoVentures/cudos-node v1.0.1-0.20221205141302-2c6a9098d44b/go.mod h1:NSSTwYXNsoRHQT+TtLPMRcuoTv1t1ngwY5MufiyA6Bo= +github.com/CudoVentures/cudos-node v1.0.1-0.20230207102951-f1acfc21d5b9 h1:fD72wHgKt5JkQikt8/og8FQYADuXWrx9Enbw/IZwE40= +github.com/CudoVentures/cudos-node v1.0.1-0.20230207102951-f1acfc21d5b9/go.mod h1:jp1iaVOhA+a78rw68Dpzr90Bk6vV1jjSHVysGysH0X8= github.com/CudoVentures/juno/v2 v2.0.1-0.20220908075630-6618cc96377f h1:yWNnzclSePa0xJfKFCXDMSBdmFOPD84uwlE6zGaXreI= github.com/CudoVentures/juno/v2 v2.0.1-0.20220908075630-6618cc96377f/go.mod h1:lSiplsjZl8aFajq+DnRph947q6hMPHJqfGEb1WH+pGA= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= diff --git a/hasura/metadata/databases/bdjuno/tables/public_marketplace_auction.yaml b/hasura/metadata/databases/bdjuno/tables/public_marketplace_auction.yaml new file mode 100644 index 000000000..2b7a2ce96 --- /dev/null +++ b/hasura/metadata/databases/bdjuno/tables/public_marketplace_auction.yaml @@ -0,0 +1,24 @@ +table: + name: marketplace_auction + schema: public +object_relationships: +- name: nft_denom + using: + foreign_key_constraint_on: denom_id +- name: nft_nft + using: + foreign_key_constraint_on: [token_id, denom_id] +select_permissions: +- permission: + allow_aggregations: true + columns: + - id + - token_id + - denom_id + - creator + - start_time + - end_time + - auction + - sold + filter: {} + role: anonymous diff --git a/hasura/metadata/databases/bdjuno/tables/public_marketplace_bid.yaml b/hasura/metadata/databases/bdjuno/tables/public_marketplace_bid.yaml new file mode 100644 index 000000000..0fad45f17 --- /dev/null +++ b/hasura/metadata/databases/bdjuno/tables/public_marketplace_bid.yaml @@ -0,0 +1,21 @@ +table: + name: marketplace_bid + schema: public +object_relationships: +- name: marketplace_auction + using: + foreign_key_constraint_on: auction_id +- name: transaction + using: + foreign_key_constraint_on: transaction_hash +select_permissions: +- permission: + allow_aggregations: true + columns: + - auction_id + - bidder + - price + - timestamp + - transaction_hash + filter: {} + role: anonymous diff --git a/hasura/metadata/databases/bdjuno/tables/tables.yaml b/hasura/metadata/databases/bdjuno/tables/tables.yaml index f12cded2d..c017351f9 100644 --- a/hasura/metadata/databases/bdjuno/tables/tables.yaml +++ b/hasura/metadata/databases/bdjuno/tables/tables.yaml @@ -65,3 +65,5 @@ - "!include public_cw20token_info.yaml" - "!include public_cw20token_code_id.yaml" - "!include public_nft_transfer_history.yaml" +- "!include public_marketplace_auction.yaml" +- "!include public_marketplace_bid.yaml" diff --git a/modules/marketplace/auction_test.go b/modules/marketplace/auction_test.go new file mode 100644 index 000000000..6d26d775a --- /dev/null +++ b/modules/marketplace/auction_test.go @@ -0,0 +1,223 @@ +package marketplace + +import ( + "strconv" + "testing" + "time" + + "github.com/CudoVentures/cudos-node/simapp" + "github.com/CudoVentures/cudos-node/x/marketplace/types" + sdk "github.com/cosmos/cosmos-sdk/types" + dbtypes "github.com/forbole/bdjuno/v2/database/types" + dbutils "github.com/forbole/bdjuno/v2/database/utils" + "github.com/forbole/bdjuno/v2/utils" + juno "github.com/forbole/juno/v2/types" + "github.com/lib/pq" + "github.com/stretchr/testify/require" + abcitypes "github.com/tendermint/tendermint/abci/types" + tmctypes "github.com/tendermint/tendermint/rpc/core/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +const ( + addr1 = "cudos1" + addr2 = "cudos2" + denomID = "denom" + tokenID = uint64(1) + auctionID = uint64(1) + txHash = "txHash" + txHash2 = "txHash2" + auctionInfo = `{test:test}` + auctionInfoUpdated = `{updated: updated}` +) + +var ( + startTime = time.Date(2222, 1, 1, 1, 0, 0, 0, time.FixedZone("", 0)) + endTime = startTime.Add(time.Hour * 25) +) + +func TestMarketplaceAuctions(t *testing.T) { + // SETUP + db, err := utils.NewTestDb("marketplaceAuctionsTest") + require.NoError(t, err) + + _, err = db.Sql.Exec(`INSERT INTO block (height, hash, timestamp) VALUES ($1, $2, $3)`, 1, "1", time.Now()) + require.NoError(t, err) + + _, err = db.Sql.Exec(`INSERT INTO transaction (hash, height, success, signatures) VALUES ($1, $2, true, $3)`, txHash, 1, pq.Array([]string{"1"})) + require.NoError(t, err) + + _, err = db.Sql.Exec(`INSERT INTO transaction (hash, height, success, signatures) VALUES ($1, $2, true, $3)`, txHash2, 1, pq.Array([]string{"1"})) + require.NoError(t, err) + + _, err = db.Sql.Exec(`INSERT INTO nft_denom (transaction_hash, id, name, schema, symbol, owner, contract_address_signer, traits, minter, description, data_text, data_json) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT DO NOTHING`, txHash, denomID, "name", "schema", "symbol", addr1, "contractAddressSigner", "traits", "minter", "description", "dataText", "{}") + require.NoError(t, err) + + _, err = db.Sql.Exec(`INSERT INTO nft_nft (transaction_hash, id, denom_id, name, uri, owner, data_json, data_text, sender, contract_address_signer, uniq_id) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT DO NOTHING`, txHash, tokenID, denomID, "name", "uri", addr1, "{}", "dataText", addr1, "contractAddressSigner", dbutils.FormatUniqID(tokenID, denomID)) + require.NoError(t, err) + + m := NewModule(simapp.MakeTestEncodingConfig().Marshaler, db) + txb := utils.NewMockTxBuilder(t, startTime, txHash, 1) + + // TEST msgHandlePublishAuction + txb.WithEventPublishAuction(auctionID, startTime, endTime, auctionInfo) + + err = m.handleMsgPublishAuction(0, txb.Build(), &types.MsgPublishAuction{ + TokenId: strconv.FormatUint(tokenID, 10), + DenomId: denomID, + Creator: addr1, + }) + require.NoError(t, err) + + wantAuction := dbtypes.MarketplaceAuctionRow{ + ID: auctionID, + TokenID: tokenID, + DenomID: denomID, + Creator: addr1, + StartTime: startTime, + EndTime: endTime, + Auction: auctionInfo, + Sold: false, + } + + var haveAuctions []dbtypes.MarketplaceAuctionRow + err = db.Sqlx.Select(&haveAuctions, `SELECT * FROM marketplace_auction WHERE id = $1`, auctionID) + require.NoError(t, err) + require.Len(t, haveAuctions, 1) + require.Equal(t, wantAuction, haveAuctions[0]) + + // TEST msgHandlePlaceBid + msgHandleBid := &types.MsgPlaceBid{ + AuctionId: auctionID, + Amount: sdk.NewCoin("acudos", sdk.OneInt()), + Bidder: addr2, + } + + err = m.handleMsgPlaceBid(0, txb.Build(), msgHandleBid) + require.NoError(t, err) + + var haveHistoryRowCount int + err = db.Sqlx.QueryRow(`SELECT COUNT(*) FROM marketplace_nft_buy_history`).Scan(&haveHistoryRowCount) + require.NoError(t, err) + require.Zero(t, haveHistoryRowCount) + + wantBid := dbtypes.MarketplaceBidRow{ + AuctionID: auctionID, + Bidder: addr2, + Price: "1", + Timestamp: startTime, + TxHash: txHash, + } + + var haveBids []dbtypes.MarketplaceBidRow + err = db.Sqlx.Select(&haveBids, `SELECT * FROM marketplace_bid`) + require.NoError(t, err) + require.Len(t, haveBids, 1) + require.Equal(t, wantBid, haveBids[0]) + + // TEST msgHandleBid with EventBuyNft + txb.WithEventBuyNftFromAuction(auctionID, tokenID, denomID, addr2) + err = m.handleMsgPlaceBid(0, txb.Build(), msgHandleBid) + require.NoError(t, err) + + wantTimestamp, err := utils.ISO8601ToTimestamp(txb.Build().Timestamp) + require.NoError(t, err) + + wantBuyHistory := dbtypes.MarketplaceNftBuyHistory{ + TxHash: txHash, + TokenID: tokenID, + DenomID: denomID, + Price: "1", + Buyer: addr2, + Seller: addr1, + Timestamp: uint64(wantTimestamp), + UniqID: dbutils.FormatUniqID(tokenID, denomID), + } + + var haveBuyHistory []dbtypes.MarketplaceNftBuyHistory + err = db.Sqlx.Select(&haveBuyHistory, `SELECT * FROM marketplace_nft_buy_history`) + require.NoError(t, err) + require.Len(t, haveBuyHistory, 1) + haveBuyHistory[0].UsdPrice = "" + haveBuyHistory[0].BtcPrice = "" + require.Equal(t, wantBuyHistory, haveBuyHistory[0]) + + wantTransferHistory := dbtypes.NftTransferHistoryRow{ + ID: 1, + TxHash: txHash, + DenomID: denomID, + OldOwner: addr1, + NewOwner: addr2, + Timestamp: uint64(wantTimestamp), + UniqID: dbutils.FormatUniqID(tokenID, denomID), + } + + var haveTransferHistory []dbtypes.NftTransferHistoryRow + err = db.Sqlx.Select(&haveTransferHistory, `SELECT * FROM nft_transfer_history`) + require.NoError(t, err) + require.Len(t, haveTransferHistory, 1) + require.Equal(t, wantTransferHistory, haveTransferHistory[0]) + + var haveSold bool + err = db.Sqlx.QueryRow(`SELECT sold FROM marketplace_auction`).Scan(&haveSold) + require.NoError(t, err) + require.True(t, haveSold) + + var haveBidRowCount int + err = db.Sqlx.QueryRow(`SELECT COUNT(*) FROM marketplace_bid`).Scan(&haveBidRowCount) + require.NoError(t, err) + require.Equal(t, 2, haveBidRowCount) + + // TEST msgHandleAcceptBid + txb = utils.NewMockTxBuilder(t, startTime, txHash2, 1) + err = m.handleMsgAcceptBid(txb.Build(), &types.MsgAcceptBid{AuctionId: auctionID}) + require.NoError(t, err) + + haveBuyHistory = []dbtypes.MarketplaceNftBuyHistory{} + err = db.Sqlx.Select(&haveBuyHistory, `SELECT * FROM marketplace_nft_buy_history`) + require.NoError(t, err) + require.Len(t, haveBuyHistory, 2) + require.Equal(t, txHash2, haveBuyHistory[1].TxHash) + + haveTransferHistory = []dbtypes.NftTransferHistoryRow{} + + err = db.Sqlx.Select(&haveTransferHistory, `SELECT * FROM nft_transfer_history`) + require.NoError(t, err) + require.Len(t, haveTransferHistory, 2) + require.Equal(t, txHash2, haveTransferHistory[1].TxHash) + + // TEST HandleBlock + block := tmctypes.ResultBlock{Block: &tmtypes.Block{Header: tmtypes.Header{Time: startTime}}} + blockEvents := tmctypes.ResultBlockResults{EndBlockEvents: []abcitypes.Event{ + abcitypes.Event(sdk.NewEvent( + types.EventBuyNftType, + sdk.NewAttribute(types.AttributeAuctionID, strconv.FormatUint(auctionID, 10)), + sdk.NewAttribute(types.AttributeKeyTokenID, strconv.FormatUint(tokenID, 10)), + sdk.NewAttribute(types.AttributeKeyDenomID, denomID), + sdk.NewAttribute(types.AttributeKeyBuyer, addr2), + )), + abcitypes.Event(sdk.NewEvent( + types.EventDutchAuctionPriceDiscountType, + sdk.NewAttribute(types.AttributeAuctionID, strconv.FormatUint(auctionID, 10)), + sdk.NewAttribute(types.AttributeAuctionInfo, auctionInfoUpdated), + )), + }} + + err = m.HandleBlock(&block, &blockEvents, []*juno.Tx{}, &tmctypes.ResultValidators{}) + require.NoError(t, err) + + var haveBuyHistoryRowCount int + err = db.Sqlx.QueryRow(`SELECT COUNT(*) FROM marketplace_nft_buy_history`).Scan(&haveBuyHistoryRowCount) + require.NoError(t, err) + require.Equal(t, 3, haveBuyHistoryRowCount) + + var haveTransferHistoryRowCount int + err = db.Sqlx.QueryRow(`SELECT COUNT(*) FROM nft_transfer_history`).Scan(&haveTransferHistoryRowCount) + require.NoError(t, err) + require.Equal(t, 3, haveBuyHistoryRowCount) + + var haveAuctionInfo string + err = db.Sqlx.QueryRow(`SELECT auction FROM marketplace_auction WHERE id = $1`, auctionID).Scan(&haveAuctionInfo) + require.NoError(t, err) + require.Equal(t, auctionInfoUpdated, haveAuctionInfo) +} diff --git a/modules/marketplace/handle_block.go b/modules/marketplace/handle_block.go new file mode 100644 index 000000000..d5a5884fe --- /dev/null +++ b/modules/marketplace/handle_block.go @@ -0,0 +1,82 @@ +package marketplace + +import ( + "strconv" + + abci "github.com/tendermint/tendermint/abci/types" + + marketplaceTypes "github.com/CudoVentures/cudos-node/x/marketplace/types" + "github.com/forbole/bdjuno/v2/client/coingecko" + "github.com/forbole/bdjuno/v2/database" + juno "github.com/forbole/juno/v2/types" + + tmctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// HandleBlock implements BlockModule +func (m *Module) HandleBlock( + block *tmctypes.ResultBlock, res *tmctypes.ResultBlockResults, tx []*juno.Tx, _ *tmctypes.ResultValidators, +) error { + if err := m.handleSoldAuctions(res.EndBlockEvents, uint64(block.Block.Time.Unix())); err != nil { + return err + } + + return m.handleAuctionPriceDiscounts(res.EndBlockEvents) +} + +func (m *Module) handleSoldAuctions(events []abci.Event, timestamp uint64) error { + for _, event := range juno.FindEventsByType(events, marketplaceTypes.EventBuyNftType) { + auctionIDAttr, err := juno.FindAttributeByKey(event, marketplaceTypes.AttributeAuctionID) + if err != nil { + continue + } + + auctionID, err := strconv.ParseUint(string(auctionIDAttr.Value), 10, 64) + if err != nil { + continue + } + + usdPrice, err := coingecko.GetCUDOSPrice("usd") + if err != nil { + return err + } + + btcPrice, err := coingecko.GetCUDOSPrice("btc") + if err != nil { + return err + } + + if err := m.db.ExecuteTx(func(dbTx *database.DbTx) error { + return dbTx.SaveMarketplaceAuctionSold(auctionID, timestamp, usdPrice, btcPrice, "") + }); err != nil { + return err + } + } + + return nil +} + +func (m *Module) handleAuctionPriceDiscounts(events []abci.Event) error { + for _, event := range juno.FindEventsByType(events, marketplaceTypes.EventDutchAuctionPriceDiscountType) { + auctionIDAttr, err := juno.FindAttributeByKey(event, marketplaceTypes.AttributeAuctionID) + if err != nil { + continue + } + + auctionID, err := strconv.ParseUint(string(auctionIDAttr.Value), 10, 64) + if err != nil { + continue + } + + auctionInfo, err := juno.FindAttributeByKey(event, marketplaceTypes.AttributeAuctionInfo) + if err != nil { + continue + } + + if err := m.db.UpdateMarketplaceAuctionInfo(auctionID, string(auctionInfo.Value)); err != nil { + return err + } + } + + return nil +} diff --git a/modules/marketplace/handle_msg.go b/modules/marketplace/handle_msg.go index 960df708c..893dfe2c6 100644 --- a/modules/marketplace/handle_msg.go +++ b/modules/marketplace/handle_msg.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "strconv" + "time" marketplaceTypes "github.com/CudoVentures/cudos-node/x/marketplace/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -41,6 +42,12 @@ func (m *Module) HandleMsg(index int, msg sdk.Msg, tx *juno.Tx) error { return m.handleMsgUpdateRoyalties(cosmosMsg) case *marketplaceTypes.MsgCreateCollection: return m.handleMsgCreateCollection(index, tx, cosmosMsg) + case *marketplaceTypes.MsgPublishAuction: + return m.handleMsgPublishAuction(index, tx, cosmosMsg) + case *marketplaceTypes.MsgPlaceBid: + return m.handleMsgPlaceBid(index, tx, cosmosMsg) + case *marketplaceTypes.MsgAcceptBid: + return m.handleMsgAcceptBid(tx, cosmosMsg) default: return nil } @@ -218,3 +225,94 @@ func (m *Module) handleMsgCreateCollection(index int, tx *juno.Tx, msg *marketpl return m.db.SaveMarketplaceCollection(tx.TxHash, collectionID, msg.Id, string(mintRoyaltiesJSON), string(resaleRoyaltiesJSON), msg.Creator, msg.Verified) } + +func (m *Module) handleMsgPublishAuction(index int, tx *juno.Tx, msg *marketplaceTypes.MsgPublishAuction) error { + auctionIDStr := utils.GetValueFromLogs(uint32(index), tx.Logs, marketplaceTypes.EventPublishAuctionType, marketplaceTypes.AttributeAuctionID) + + auctionID, err := strconv.ParseUint(auctionIDStr, 10, 64) + if err != nil { + return err + } + + tokenID, err := strconv.ParseUint(msg.TokenId, 10, 64) + if err != nil { + return err + } + + startTimeStr := utils.GetValueFromLogs(uint32(index), tx.Logs, marketplaceTypes.EventPublishAuctionType, marketplaceTypes.AttributeStartTime) + + startTime, err := time.Parse(time.RFC3339, startTimeStr) + if err != nil { + return err + } + + endTimeStr := utils.GetValueFromLogs(uint32(index), tx.Logs, marketplaceTypes.EventPublishAuctionType, marketplaceTypes.AttributeEndTime) + + endTime, err := time.Parse(time.RFC3339, endTimeStr) + if err != nil { + return err + } + + auctionInfo := utils.GetValueFromLogs(uint32(index), tx.Logs, marketplaceTypes.EventPublishAuctionType, marketplaceTypes.AttributeAuctionInfo) + + return m.db.ExecuteTx(func(dbTx *database.DbTx) error { + return dbTx.SaveMarketplaceAuction(auctionID, tokenID, msg.DenomId, msg.Creator, startTime, endTime, auctionInfo) + }) +} + +func (m *Module) handleMsgPlaceBid(index int, tx *juno.Tx, msg *marketplaceTypes.MsgPlaceBid) error { + timestamp, err := time.Parse(time.RFC3339, tx.Timestamp) + if err != nil { + return fmt.Errorf("error while parsing time: %s", err) + } + + return m.db.ExecuteTx(func(dbTx *database.DbTx) error { + err := dbTx.SaveMarketplaceBid(msg.AuctionId, msg.Bidder, msg.Amount.Amount.String(), timestamp, tx.TxHash) + if err != nil { + return err + } + + nftSold := utils.GetValueFromLogs(uint32(index), tx.Logs, marketplaceTypes.EventBuyNftType, marketplaceTypes.AttributeAuctionID) + if nftSold != "" { + timestamp, err := generalUtils.ISO8601ToTimestamp(tx.Timestamp) + if err != nil { + return err + } + + usdPrice, err := coingecko.GetCUDOSPrice("usd") + if err != nil { + return err + } + + btcPrice, err := coingecko.GetCUDOSPrice("btc") + if err != nil { + return err + } + + return dbTx.SaveMarketplaceAuctionSold(msg.AuctionId, uint64(timestamp), usdPrice, btcPrice, "") + } + + return nil + }) +} + +func (m *Module) handleMsgAcceptBid(tx *juno.Tx, msg *marketplaceTypes.MsgAcceptBid) error { + timestamp, err := generalUtils.ISO8601ToTimestamp(tx.Timestamp) + if err != nil { + return err + } + + usdPrice, err := coingecko.GetCUDOSPrice("usd") + if err != nil { + return err + } + + btcPrice, err := coingecko.GetCUDOSPrice("btc") + if err != nil { + return err + } + + return m.db.ExecuteTx(func(dbTx *database.DbTx) error { + return dbTx.SaveMarketplaceAuctionSold(msg.AuctionId, uint64(timestamp), usdPrice, btcPrice, tx.TxHash) + }) +} diff --git a/modules/marketplace/module.go b/modules/marketplace/module.go index 61572369b..038c26ddc 100644 --- a/modules/marketplace/module.go +++ b/modules/marketplace/module.go @@ -10,6 +10,7 @@ import ( var ( _ modules.Module = &Module{} _ modules.MessageModule = &Module{} + _ modules.BlockModule = &Module{} ) // Module represents the nft module diff --git a/utils/mock_tx_builder.go b/utils/mock_tx_builder.go index 5b07c05c9..58d6c6a86 100644 --- a/utils/mock_tx_builder.go +++ b/utils/mock_tx_builder.go @@ -1,6 +1,7 @@ package utils import ( + "strconv" "testing" "time" @@ -10,6 +11,7 @@ import ( juno "github.com/forbole/juno/v2/types" wasm "github.com/CosmWasm/wasmd/x/wasm/types" + marketplace "github.com/CudoVentures/cudos-node/x/marketplace/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/group" ) @@ -28,57 +30,104 @@ func NewMockTxBuilder(t *testing.T, timestamp time.Time, txHash string, height u func (b *MockTxBuilder) WithEventCreateGroup(groupID uint64, address string) *MockTxBuilder { require.NotEmpty(b.t, address) - e, err := sdk.TypedEventToEvent(&group.EventCreateGroup{GroupId: groupID}) - require.NoError(b.t, err) + e := abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventCreateGroup", + sdk.NewAttribute("group_id", strconv.FormatUint(groupID, 10)), + )) - e2, err := sdk.TypedEventToEvent(&group.EventCreateGroupPolicy{Address: address}) - require.NoError(b.t, err) + e2 := abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventCreateGroupPolicy", + sdk.NewAttribute("address", address), + )) - b.events = append(b.events, abcitypes.Event(e), abcitypes.Event(e2)) + b.events = append(b.events, e, e2) return b } func (b *MockTxBuilder) WithEventSubmitProposal(proposalID uint64) *MockTxBuilder { - e, err := sdk.TypedEventToEvent(&group.EventSubmitProposal{ProposalId: proposalID}) - require.NoError(b.t, err) + e := abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventSubmitProposal", + sdk.NewAttribute("proposal_id", strconv.FormatUint(proposalID, 10))), + ) - b.events = append(b.events, abcitypes.Event(e)) + b.events = append(b.events, e) return b } func (b *MockTxBuilder) WithEventExec(result group.ProposalExecutorResult) *MockTxBuilder { - e, err := sdk.TypedEventToEvent(&group.EventExec{Result: result, Logs: "1"}) - require.NoError(b.t, err) + e := abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventExec", + sdk.NewAttribute("result", result.String()), + sdk.NewAttribute("logs", "1"), + )) - b.events = append(b.events, abcitypes.Event(e)) + b.events = append(b.events, e) return b } func (b *MockTxBuilder) WithEventVote() *MockTxBuilder { - e, err := sdk.TypedEventToEvent(&group.EventVote{ProposalId: 1}) - require.NoError(b.t, err) + e := abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventVote", + sdk.NewAttribute("proposal_id", "1"), + )) - b.events = append(b.events, abcitypes.Event(e)) + b.events = append(b.events, e) return b } func (b *MockTxBuilder) WithEventWithdrawProposal() *MockTxBuilder { - e, err := sdk.TypedEventToEvent(&group.EventWithdrawProposal{ProposalId: 1}) - require.NoError(b.t, err) + e := abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventWithdrawProposal", + sdk.NewAttribute("proposal_id", "1"), + )) - b.events = append(b.events, abcitypes.Event(e)) + b.events = append(b.events, e) return b } func (b *MockTxBuilder) WithEventInstantiateContract(contractAddr string) *MockTxBuilder { - e := sdk.NewEvent(wasm.EventTypeInstantiate, sdk.NewAttribute(wasm.AttributeKeyContractAddr, contractAddr)) - b.events = append(b.events, abcitypes.Event(e)) + e := abcitypes.Event(sdk.NewEvent( + wasm.EventTypeInstantiate, + sdk.NewAttribute(wasm.AttributeKeyContractAddr, contractAddr), + )) + + b.events = append(b.events, e) return b } func (b *MockTxBuilder) WithEventWasmAction(msgType string) *MockTxBuilder { - e := sdk.NewEvent(wasm.WasmModuleEventType, sdk.NewAttribute("action", msgType)) - b.events = append(b.events, abcitypes.Event(e)) + e := abcitypes.Event(sdk.NewEvent( + wasm.WasmModuleEventType, + sdk.NewAttribute("action", msgType), + )) + + b.events = append(b.events, e) + return b +} + +func (b *MockTxBuilder) WithEventPublishAuction(auctionID uint64, startTime time.Time, endTime time.Time, auctionInfo string) *MockTxBuilder { + e := abcitypes.Event(sdk.NewEvent( + marketplace.EventPublishAuctionType, + sdk.NewAttribute(marketplace.AttributeAuctionID, strconv.FormatUint(auctionID, 10)), + sdk.NewAttribute(marketplace.AttributeStartTime, startTime.Format(time.RFC3339)), + sdk.NewAttribute(marketplace.AttributeEndTime, endTime.Format(time.RFC3339)), + sdk.NewAttribute(marketplace.AttributeAuctionInfo, auctionInfo), + )) + + b.events = append(b.events, e) + return b +} + +func (b *MockTxBuilder) WithEventBuyNftFromAuction(auctionID uint64, tokenID uint64, denomID string, buyer string) *MockTxBuilder { + e := abcitypes.Event(sdk.NewEvent( + marketplace.EventBuyNftType, + sdk.NewAttribute(marketplace.AttributeAuctionID, strconv.FormatUint(auctionID, 10)), + sdk.NewAttribute(marketplace.AttributeKeyTokenID, strconv.FormatUint(tokenID, 10)), + sdk.NewAttribute(marketplace.AttributeKeyDenomID, denomID), + sdk.NewAttribute(marketplace.AttributeKeyBuyer, buyer), + )) + + b.events = append(b.events, e) return b } diff --git a/utils/mock_tx_builder_test.go b/utils/mock_tx_builder_test.go index 9bad49d09..aa828083c 100644 --- a/utils/mock_tx_builder_test.go +++ b/utils/mock_tx_builder_test.go @@ -5,50 +5,77 @@ import ( "time" wasm "github.com/CosmWasm/wasmd/x/wasm/types" + marketplace "github.com/CudoVentures/cudos-node/x/marketplace/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/group" - "github.com/forbole/bdjuno/v2/modules/utils" "github.com/stretchr/testify/require" + abcitypes "github.com/tendermint/tendermint/abci/types" ) -var ( +const ( num = uint64(1) str = "1" index = uint32(0) resultDefault = group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN ) -func TestMockTxBuilder_Build(t *testing.T) { - timestamp := time.Now() - tx := NewMockTxBuilder(t, timestamp, str, num).WithEventCreateGroup(num, str).WithEventSubmitProposal(num).WithEventExec(resultDefault).WithEventVote().WithEventWithdrawProposal().WithEventInstantiateContract(str).WithEventWasmAction(str).Build() - - expectedEventCount := 8 - actualEventCount := len(tx.Logs[0].Events) - require.Equal(t, expectedEventCount, actualEventCount) - - groupID := utils.GetValueFromLogs(index, tx.Logs, "cosmos.group.v1.EventCreateGroup", "group_id") - require.Equal(t, str, groupID) - - address := utils.GetValueFromLogs(index, tx.Logs, "cosmos.group.v1.EventCreateGroupPolicy", "address") - require.Equal(t, str, address) - - proposalID := utils.GetValueFromLogs(index, tx.Logs, "cosmos.group.v1.EventSubmitProposal", "proposal_id") - require.Equal(t, str, proposalID) - - voteEvent := utils.GetValueFromLogs(index, tx.Logs, "cosmos.group.v1.EventVote", "proposal_id") - require.Equal(t, str, voteEvent) - - executorResult := utils.GetValueFromLogs(index, tx.Logs, "cosmos.group.v1.EventExec", "result") - require.Equal(t, resultDefault.String(), executorResult) - - withdrawEvent := utils.GetValueFromLogs(uint32(index), tx.Logs, "cosmos.group.v1.EventWithdrawProposal", "proposal_id") - require.Equal(t, str, withdrawEvent) +var ( + timestamp = time.Date(2222, 1, 1, 1, 0, 0, 0, time.UTC) + wantLogs = sdk.ABCIMessageLogs{{MsgIndex: 0, Events: sdk.StringifyEvents([]abcitypes.Event{ + abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventCreateGroup", + sdk.NewAttribute("group_id", str), + )), + abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventCreateGroupPolicy", + sdk.NewAttribute("address", str), + )), + abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventSubmitProposal", + sdk.NewAttribute("proposal_id", str), + )), + abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventExec", + sdk.NewAttribute("result", resultDefault.String()), + sdk.NewAttribute("logs", str), + )), + abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventVote", + sdk.NewAttribute("proposal_id", str), + )), + abcitypes.Event(sdk.NewEvent( + "cosmos.group.v1.EventWithdrawProposal", + sdk.NewAttribute("proposal_id", str), + )), + abcitypes.Event(sdk.NewEvent( + wasm.EventTypeInstantiate, + sdk.NewAttribute(wasm.AttributeKeyContractAddr, str), + )), + abcitypes.Event(sdk.NewEvent( + wasm.WasmModuleEventType, + sdk.NewAttribute("action", str), + )), + abcitypes.Event(sdk.NewEvent( + marketplace.EventPublishAuctionType, + sdk.NewAttribute(marketplace.AttributeAuctionID, str), + sdk.NewAttribute(marketplace.AttributeStartTime, timestamp.Format(time.RFC3339)), + sdk.NewAttribute(marketplace.AttributeEndTime, timestamp.Format(time.RFC3339)), + sdk.NewAttribute(marketplace.AttributeAuctionInfo, str), + )), + abcitypes.Event(sdk.NewEvent( + marketplace.EventBuyNftType, + sdk.NewAttribute(marketplace.AttributeAuctionID, str), + sdk.NewAttribute(marketplace.AttributeKeyTokenID, str), + sdk.NewAttribute(marketplace.AttributeKeyDenomID, str), + sdk.NewAttribute(marketplace.AttributeKeyBuyer, str), + )), + })}} +) - instantiateContractEvent := utils.GetValueFromLogs(uint32(index), tx.Logs, wasm.EventTypeInstantiate, wasm.AttributeKeyContractAddr) - require.Equal(t, str, instantiateContractEvent) +func TestMockTxBuilder_Build(t *testing.T) { + tx := NewMockTxBuilder(t, timestamp, str, num).WithEventCreateGroup(num, str).WithEventSubmitProposal(num).WithEventExec(resultDefault).WithEventVote().WithEventWithdrawProposal().WithEventInstantiateContract(str).WithEventWasmAction(str).WithEventPublishAuction(num, timestamp, timestamp, str).WithEventBuyNftFromAuction(num, num, str, str).Build() - expectedTimestamp := timestamp.Format(time.RFC3339) - actualTimestamp := tx.Timestamp - require.Equal(t, expectedTimestamp, actualTimestamp) + require.Equal(t, wantLogs, tx.Logs) actualTxHash := tx.TxHash require.Equal(t, str, actualTxHash)