From d7b91685bb081a31efba8c1b1557c0e95efc9628 Mon Sep 17 00:00:00 2001 From: hugomartinez Date: Fri, 10 Nov 2023 15:24:22 +0100 Subject: [PATCH 1/7] Add CI and deployment --- .github/workflows/main.yaml | 63 +++++++++++++++++++++++++++++ .github/workflows/pull-request.yaml | 15 +++++++ 2 files changed, 78 insertions(+) create mode 100644 .github/workflows/main.yaml create mode 100644 .github/workflows/pull-request.yaml diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 00000000..9dd00d5d --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,63 @@ +name: Main workflow + +on: + push: + branches: + - main + + workflow_dispatch: + inputs: + namespace: + description: 'Namespace to run deployment against' + type: choice + options: + - eos-mainnet-1 + - wax-mainnet-1 + - wax-testnet-1 + - proton-mainnet-1 + - proton-testnet-1 + required: true + +jobs: + test: + name: Test + uses: Spielworks-Market/backend-workflows/.github/workflows/test.yaml@main + secrets: inherit + with: + run_test_e2e: false + run_unit_test: false + + build: + if: contains(fromJSON('["eos-mainnet-1", "wax-mainnet-1", "wax-testnet-1", "proton-mainnet-1", "proton-testnet-1"]'), inputs.namespace) + name: Build + needs: test + uses: Spielworks-Market/backend-workflows/.github/workflows/docker.yaml@main + with: + repository: eosio-contract-api + permissions: + id-token: write + contents: read + + deploy-filler: + if: contains(fromJSON('["eos-mainnet-1", "wax-mainnet-1", "wax-testnet-1", "proton-mainnet-1", "proton-testnet-1"]'), inputs.namespace) && github.ref == 'refs/heads/main' + name: Prod filler + needs: build + uses: Spielworks-Market/eosio-contract-api-deployment/.github/workflows/deployment.yaml + with: + helm-namespace: ${{ inputs.namespace }} + release: filler + permissions: + id-token: write + secrets: inherit + + deploy-server: + if: contains(fromJSON('["eos-mainnet-1", "wax-mainnet-1", "wax-testnet-1", "proton-mainnet-1", "proton-testnet-1"]'), inputs.namespace) && github.ref == 'refs/heads/main' + name: Prod server + needs: build + uses: Spielworks-Market/eosio-contract-api-deployment/.github/workflows/deployment.yaml + with: + helm-namespace: ${{ inputs.namespace }} + release: server + permissions: + id-token: write + secrets: inherit diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml new file mode 100644 index 00000000..cb282b10 --- /dev/null +++ b/.github/workflows/pull-request.yaml @@ -0,0 +1,15 @@ +name: PR workflow + +on: + pull_request: + branches: + - main + +jobs: + test: + name: Test + uses: Spielworks-Market/backend-workflows/.github/workflows/test.yaml@main + secrets: inherit + with: + run_test_e2e: false + run_unit_test: false From 8b12bc1d4c17d9813acefb1ed360a6a289f739e1 Mon Sep 17 00:00:00 2001 From: hugomartinez Date: Mon, 13 Nov 2023 15:43:52 +0100 Subject: [PATCH 2/7] Add branch name --- .github/workflows/main.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 9dd00d5d..7ebd3f17 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -42,7 +42,7 @@ jobs: if: contains(fromJSON('["eos-mainnet-1", "wax-mainnet-1", "wax-testnet-1", "proton-mainnet-1", "proton-testnet-1"]'), inputs.namespace) && github.ref == 'refs/heads/main' name: Prod filler needs: build - uses: Spielworks-Market/eosio-contract-api-deployment/.github/workflows/deployment.yaml + uses: Spielworks-Market/eosio-contract-api-deployment/.github/workflows/deployment.yaml@master with: helm-namespace: ${{ inputs.namespace }} release: filler @@ -54,7 +54,7 @@ jobs: if: contains(fromJSON('["eos-mainnet-1", "wax-mainnet-1", "wax-testnet-1", "proton-mainnet-1", "proton-testnet-1"]'), inputs.namespace) && github.ref == 'refs/heads/main' name: Prod server needs: build - uses: Spielworks-Market/eosio-contract-api-deployment/.github/workflows/deployment.yaml + uses: Spielworks-Market/eosio-contract-api-deployment/.github/workflows/deployment.yaml@master with: helm-namespace: ${{ inputs.namespace }} release: server From 82a01c6e7eee03dcaa1c1c946d82d83a329c17fa Mon Sep 17 00:00:00 2001 From: hugomartinez Date: Fri, 10 Nov 2023 15:01:20 +0100 Subject: [PATCH 3/7] Add template buyoffer processor --- definitions/hasura/atomicmarket.json | 169 ++++++++++++ .../migrations/1.3.21/atomicmarket.sql | 254 ++++++++++++++++++ definitions/migrations/1.3.21/database.sql | 1 + .../atomicmarket_template_buyoffer_mints.sql | 45 ++++ definitions/tables/atomicmarket_tables.sql | 66 +++++ ...atomicmarket_template_buyoffers_master.sql | 67 +++++ src/api/namespaces/atomicmarket/filler.ts | 19 ++ src/api/namespaces/atomicmarket/format.ts | 27 +- .../handlers/template-buyoffers.test.ts | 66 +++++ .../handlers/template-buyoffers.ts | 140 ++++++++++ src/api/namespaces/atomicmarket/index.ts | 10 +- .../atomicmarket/routes/template-buyoffers.ts | 178 ++++++++++++ src/api/namespaces/atomicmarket/test.ts | 70 +++-- src/api/namespaces/atomicmarket/utils.ts | 73 ++++- src/filler/handlers/atomicmarket/index.ts | 19 +- .../handlers/atomicmarket/processors/logs.ts | 31 +++ .../processors/template-buyoffers.ts | 97 +++++++ .../handlers/atomicmarket/types/actions.ts | 18 ++ 18 files changed, 1326 insertions(+), 24 deletions(-) create mode 100644 definitions/migrations/1.3.21/atomicmarket.sql create mode 100644 definitions/migrations/1.3.21/database.sql create mode 100644 definitions/procedures/atomicmarket_template_buyoffer_mints.sql create mode 100644 definitions/views/atomicmarket_template_buyoffers_master.sql create mode 100644 src/api/namespaces/atomicmarket/handlers/template-buyoffers.test.ts create mode 100644 src/api/namespaces/atomicmarket/handlers/template-buyoffers.ts create mode 100644 src/api/namespaces/atomicmarket/routes/template-buyoffers.ts create mode 100644 src/filler/handlers/atomicmarket/processors/template-buyoffers.ts diff --git a/definitions/hasura/atomicmarket.json b/definitions/hasura/atomicmarket.json index b83bb4ac..3b86c2ff 100644 --- a/definitions/hasura/atomicmarket.json +++ b/definitions/hasura/atomicmarket.json @@ -1012,6 +1012,175 @@ } ] }, + { + "table": { + "schema": "public", + "name": "atomicmarket_template_buyoffers" + }, + "object_relationships": [ + { + "name": "atomicassets_collection", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "atomicassets_collections" + }, + "column_mapping": { + "collection_name": "collection_name", + "assets_contract": "contract" + } + } + } + }, + { + "name": "atomicassets_template", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "atomicassets_templates" + }, + "column_mapping": { + "template_id": "template_id", + "assets_contract": "contract" + } + } + } + }, + { + "name": "atomicmarket_marketplace", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "atomicmarket_marketplaces" + }, + "column_mapping": { + "maker_marketplace": "marketplace_name", + "market_contract": "market_contract" + } + } + } + }, + { + "name": "atomicmarketMarketplaceByMarketContractTakerMarketplace", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "atomicmarket_marketplaces" + }, + "column_mapping": { + "taker_marketplace": "marketplace_name", + "market_contract": "market_contract" + } + } + } + }, + { + "name": "atomicmarket_token", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "atomicmarket_tokens" + }, + "column_mapping": { + "token_symbol": "token_symbol", + "market_contract": "market_contract" + } + } + } + } + ], + "array_relationships": [ + { + "name": "atomicmarket_template_buyoffers_assets", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "atomicmarket_template_buyoffers_assets" + }, + "column_mapping": { + "buyoffer_id": "buyoffer_id", + "market_contract": "market_contract" + } + } + } + } + ], + "select_permissions": [ + { + "role": "anonymous", + "permission": { + "columns": [ + "market_contract", + "buyoffer_id", + "buyer", + "seller", + "price", + "token_symbol", + "assets_contract", + "maker_marketplace", + "taker_marketplace", + "collection_name", + "collection_fee", + "template_id", + "state", + "updated_at_block", + "updated_at_time", + "created_at_block", + "created_at_time" + ], + "filter": {}, + "limit": 1000, + "allow_aggregations": true + } + } + ] + }, + { + "table": { + "schema": "public", + "name": "atomicmarket_template_buyoffers_assets" + }, + "object_relationships": [ + { + "name": "atomicassets_asset", + "using": { + "manual_configuration": { + "remote_table": { + "schema": "public", + "name": "atomicassets_assets" + }, + "column_mapping": { + "asset_id": "asset_id", + "assets_contract": "contract" + } + } + } + } + ], + "select_permissions": [ + { + "role": "anonymous", + "permission": { + "columns": [ + "market_contract", + "buyoffer_id", + "assets_contract", + "index", + "asset_id" + ], + "filter": {}, + "limit": 1000, + "allow_aggregations": true + } + } + ] + }, { "table": { "schema": "public", diff --git a/definitions/migrations/1.3.21/atomicmarket.sql b/definitions/migrations/1.3.21/atomicmarket.sql new file mode 100644 index 00000000..fe452bf4 --- /dev/null +++ b/definitions/migrations/1.3.21/atomicmarket.sql @@ -0,0 +1,254 @@ +DROP TABLE IF EXISTS atomicmarket_template_buyoffers CASCADE; +CREATE TABLE atomicmarket_template_buyoffers +( + market_contract character varying(12) NOT NULL, + buyoffer_id bigint NOT NULL, + buyer character varying(12) NOT NULL, + seller character varying(12), + price bigint NOT NULL, + token_symbol character varying(12) NOT NULL, + assets_contract character varying(12) NOT NULL, + maker_marketplace character varying(12) NOT NULL, + taker_marketplace character varying(12), + template_mint int4range, + collection_name character varying(12) NOT NULL, + collection_fee double precision NOT NULL, + template_id bigint NOT NULL, + state smallint NOT NULL, + updated_at_block bigint NOT NULL, + updated_at_time bigint NOT NULL, + created_at_block bigint NOT NULL, + created_at_time bigint NOT NULL, + CONSTRAINT atomicmarket_template_buyoffers_pkey PRIMARY KEY (market_contract, buyoffer_id) +); + +DROP TABLE IF EXISTS atomicmarket_template_buyoffers_assets CASCADE; +CREATE TABLE atomicmarket_template_buyoffers_assets +( + market_contract character varying(12) NOT NULL, + buyoffer_id bigint NOT NULL, + assets_contract character varying(12) NOT NULL, + "index" integer NOT NULL, + asset_id bigint NOT NULL, + CONSTRAINT atomicmarket_template_buyoffers_assets_pkey PRIMARY KEY (market_contract, buyoffer_id, assets_contract, asset_id) +); + +ALTER TABLE ONLY atomicmarket_template_buyoffers + ADD CONSTRAINT atomicmarket_template_buyoffers_token_symbol_fkey FOREIGN KEY (market_contract, token_symbol) + REFERENCES atomicmarket_tokens (market_contract, token_symbol) MATCH SIMPLE ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED NOT VALID; + +ALTER TABLE ONLY atomicmarket_template_buyoffers + ADD CONSTRAINT atomicmarket_template_buyoffers_maker_marketplace_fkey FOREIGN KEY (market_contract, maker_marketplace) + REFERENCES atomicmarket_marketplaces (market_contract, marketplace_name) MATCH SIMPLE ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED NOT VALID; + +ALTER TABLE ONLY atomicmarket_template_buyoffers + ADD CONSTRAINT atomicmarket_template_buyoffers_taker_marketplace_fkey FOREIGN KEY (market_contract, taker_marketplace) + REFERENCES atomicmarket_marketplaces (market_contract, marketplace_name) MATCH SIMPLE ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED NOT VALID; + + + +ALTER TABLE ONLY atomicmarket_template_buyoffers_assets + ADD CONSTRAINT atomicmarket_template_buyoffers_assets_template_buyoffers_fkey FOREIGN KEY (market_contract, buyoffer_id) + REFERENCES atomicmarket_template_buyoffers (market_contract, buyoffer_id) MATCH SIMPLE ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED NOT VALID; + + +CREATE INDEX atomicmarket_template_buyoffers_buyoffer_id ON atomicmarket_template_buyoffers USING btree (buyoffer_id); +CREATE INDEX atomicmarket_template_buyoffers_seller ON atomicmarket_template_buyoffers USING hash (seller); +CREATE INDEX atomicmarket_template_buyoffers_buyer ON atomicmarket_template_buyoffers USING hash (buyer); +CREATE INDEX atomicmarket_template_buyoffers_price ON atomicmarket_template_buyoffers USING btree (price); +CREATE INDEX atomicmarket_template_buyoffers_collection_name ON atomicmarket_template_buyoffers USING btree (collection_name); +CREATE INDEX atomicmarket_template_buyoffers_template_id ON atomicmarket_template_buyoffers USING btree (template_id); +CREATE INDEX atomicmarket_template_buyoffers_state ON atomicmarket_template_buyoffers USING btree (state); +CREATE INDEX atomicmarket_template_buyoffers_updated_at_time ON atomicmarket_template_buyoffers USING btree (updated_at_time); +CREATE INDEX atomicmarket_template_buyoffers_created_at_time ON atomicmarket_template_buyoffers USING btree (created_at_time); + +CREATE INDEX atomicmarket_template_buyoffers_assets_asset_id ON atomicmarket_template_buyoffers_assets USING btree (asset_id); + +CREATE INDEX atomicmarket_template_buyoffers_missing_mint ON atomicmarket_template_buyoffers(assets_contract, buyoffer_id) WHERE template_mint IS NULL AND "state" = 2; + + + + +DROP FUNCTION IF EXISTS update_atomicmarket_stats_markets_by_template_buyoffer CASCADE; +CREATE OR REPLACE FUNCTION update_atomicmarket_stats_markets_by_template_buyoffer() RETURNS TRIGGER AS $$ +DECLARE + affects_stats_markets BOOLEAN; +BEGIN + affects_stats_markets = + (TG_OP IN ('INSERT', 'UPDATE') AND NEW.state = 2) + OR + (TG_OP IN ('DELETE', 'UPDATE') AND OLD.state = 2); + IF (NOT affects_stats_markets) + THEN RETURN NULL; + END IF; + + INSERT INTO atomicmarket_stats_markets_updates(market_contract, listing_type, listing_id) + VALUES ( + CASE TG_OP WHEN 'DELETE' THEN OLD.market_contract ELSE NEW.market_contract END, + 'template_buyoffer', + CASE TG_OP WHEN 'DELETE' THEN OLD.buyoffer_id ELSE NEW.buyoffer_id END + ); + + RETURN NULL; +END +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS atomicmarket_template_buyoffers_update_atomicmarket_stats_markets_tr ON atomicmarket_template_buyoffers; +CREATE TRIGGER atomicmarket_template_buyoffers_update_atomicmarket_stats_markets_tr + AFTER UPDATE OR INSERT OR DELETE ON atomicmarket_template_buyoffers + FOR EACH ROW + EXECUTE FUNCTION update_atomicmarket_stats_markets_by_template_buyoffer(); + + +DROP FUNCTION IF EXISTS update_atomicmarket_stats_market; +CREATE OR REPLACE FUNCTION update_atomicmarket_stats_market() RETURNS INT +LANGUAGE plpgsql +AS $$ +DECLARE + result INT; + current_block_time BIGINT = (SELECT MAX(block_time) FROM contract_readers); +BEGIN + WITH changed_listings AS ( + DELETE FROM atomicmarket_stats_markets_updates u + WHERE refresh_at <= current_block_time + RETURNING market_contract, listing_type, listing_id + ), updated_listings AS ( + SELECT + sale.market_contract, 'sale' listing_type, sale.sale_id listing_id, + sale.buyer, sale.seller, sale.maker_marketplace, sale.taker_marketplace, + sale.assets_contract, sale.collection_name, + sale.settlement_symbol symbol, sale.final_price price, sale.updated_at_time "time", + CASE WHEN COUNT(*) = 1 THEN MIN(asset.schema_name) END AS schema_name, + CASE WHEN COUNT(*) = 1 THEN MIN(asset.template_id) END AS template_id, + CASE WHEN COUNT(*) = 1 THEN MIN(asset.asset_id) END AS asset_id + FROM atomicmarket_sales sale + JOIN atomicassets_offers_assets offer_asset ON sale.offer_id = offer_asset.offer_id AND sale.assets_contract = offer_asset.contract + JOIN atomicassets_assets asset ON offer_asset.asset_id = asset.asset_id AND offer_asset.contract = asset.contract + WHERE sale.final_price IS NOT NULL AND sale.state = 3 + AND (sale.market_contract, sale.sale_id) IN ( + SELECT market_contract, listing_id + FROM changed_listings + WHERE listing_type = 'sale' + ) + GROUP BY sale.market_contract, sale.sale_id + + UNION ALL + + SELECT + auction.market_contract, 'auction' listing_type, auction.auction_id listing_id, + auction.buyer, auction.seller, auction.maker_marketplace, auction.taker_marketplace, + auction.assets_contract, auction.collection_name, + auction.token_symbol symbol, auction.price, (auction.end_time * 1000) "time", + CASE WHEN COUNT(*) = 1 THEN MIN(asset.schema_name) END AS schema_name, + CASE WHEN COUNT(*) = 1 THEN MIN(asset.template_id) END AS template_id, + CASE WHEN COUNT(*) = 1 THEN MIN(asset.asset_id) END AS asset_id + FROM atomicmarket_auctions auction + JOIN atomicmarket_auctions_assets auction_asset ON auction.auction_id = auction_asset.auction_id AND auction.assets_contract = auction_asset.assets_contract + JOIN atomicassets_assets asset ON auction_asset.asset_id = asset.asset_id AND auction_asset.assets_contract = asset.contract + WHERE auction.buyer IS NOT NULL AND auction.state = 1 AND auction.end_time < extract(epoch from now()) + AND (auction.market_contract, auction.auction_id) IN ( + SELECT market_contract, listing_id + FROM changed_listings + WHERE listing_type = 'auction' + ) + GROUP BY auction.market_contract, auction.auction_id + + UNION ALL + + SELECT + buyoffer.market_contract, 'buyoffer' listing_type, buyoffer.buyoffer_id listing_id, + buyoffer.buyer, buyoffer.seller, buyoffer.maker_marketplace, buyoffer.taker_marketplace, + buyoffer.assets_contract, buyoffer.collection_name, + buyoffer.token_symbol symbol, buyoffer.price, buyoffer.updated_at_time "time", + CASE WHEN COUNT(*) = 1 THEN MIN(asset.schema_name) END AS schema_name, + CASE WHEN COUNT(*) = 1 THEN MIN(asset.template_id) END AS template_id, + CASE WHEN COUNT(*) = 1 THEN MIN(asset.asset_id) END AS asset_id + FROM atomicmarket_buyoffers buyoffer + JOIN atomicmarket_buyoffers_assets buyoffer_asset ON buyoffer.buyoffer_id = buyoffer_asset.buyoffer_id AND buyoffer.assets_contract = buyoffer_asset.assets_contract + JOIN atomicassets_assets asset ON buyoffer_asset.asset_id = asset.asset_id AND buyoffer_asset.assets_contract = asset.contract + WHERE buyoffer.state = 3 + AND (buyoffer.market_contract, buyoffer.buyoffer_id) IN ( + SELECT market_contract, listing_id + FROM changed_listings + WHERE listing_type = 'buyoffer' + ) + GROUP BY buyoffer.market_contract, buyoffer.buyoffer_id + + UNION ALL + + SELECT + t_buyoffer.market_contract, 'template_buyoffer' listing_type, t_buyoffer.buyoffer_id listing_id, + t_buyoffer.buyer, t_buyoffer.seller, t_buyoffer.maker_marketplace, t_buyoffer.taker_marketplace, + t_buyoffer.assets_contract, t_buyoffer.collection_name, + t_buyoffer.token_symbol symbol, t_buyoffer.price, t_buyoffer.updated_at_time "time", + CASE WHEN COUNT(*) = 1 THEN MIN(asset.schema_name) END AS schema_name, + t_buyoffer.template_id, + CASE WHEN COUNT(*) = 1 THEN MIN(asset.asset_id) END AS asset_id + FROM atomicmarket_template_buyoffers t_buyoffer + JOIN atomicmarket_template_buyoffers_assets t_buyoffer_asset ON t_buyoffer.buyoffer_id = t_buyoffer_asset.buyoffer_id AND t_buyoffer.assets_contract = t_buyoffer_asset.assets_contract + JOIN atomicassets_assets asset ON t_buyoffer_asset.asset_id = asset.asset_id AND t_buyoffer_asset.assets_contract = asset.contract + WHERE t_buyoffer.state = 2 + AND (t_buyoffer.market_contract, t_buyoffer.buyoffer_id) IN ( + SELECT market_contract, listing_id + FROM changed_listings + WHERE listing_type = 'template_buyoffer' + ) + GROUP BY t_buyoffer.market_contract, t_buyoffer.buyoffer_id + ), ins_upd AS ( + INSERT INTO atomicmarket_stats_markets AS m ( + market_contract, listing_type, listing_id, buyer, seller, + maker_marketplace, taker_marketplace, assets_contract, + collection_name, symbol, price, "time", + schema_name, template_id, asset_id + ) + SELECT + market_contract, listing_type, listing_id, buyer, seller, + maker_marketplace, taker_marketplace, assets_contract, + collection_name, symbol, price, "time", + schema_name, template_id, asset_id + FROM updated_listings + ON CONFLICT (market_contract, listing_type, listing_id) + DO UPDATE SET + buyer = EXCLUDED.buyer, + seller = EXCLUDED.seller, + maker_marketplace = EXCLUDED.maker_marketplace, + taker_marketplace = EXCLUDED.taker_marketplace, + assets_contract = EXCLUDED.assets_contract, + collection_name = EXCLUDED.collection_name, + symbol = EXCLUDED.symbol, + price = EXCLUDED.price, + "time" = EXCLUDED."time", + schema_name = EXCLUDED.schema_name, + template_id = EXCLUDED.template_id, + asset_id = EXCLUDED.asset_id + WHERE + m.buyer IS DISTINCT FROM EXCLUDED.buyer + OR m.seller IS DISTINCT FROM EXCLUDED.seller + OR m.price IS DISTINCT FROM EXCLUDED.price + OR m.maker_marketplace IS DISTINCT FROM EXCLUDED.maker_marketplace + OR m.taker_marketplace IS DISTINCT FROM EXCLUDED.taker_marketplace + OR m.assets_contract IS DISTINCT FROM EXCLUDED.assets_contract + OR m.collection_name IS DISTINCT FROM EXCLUDED.collection_name + OR m.symbol IS DISTINCT FROM EXCLUDED.symbol + OR m.price IS DISTINCT FROM EXCLUDED.price + OR m."time" IS DISTINCT FROM EXCLUDED."time" + OR m.schema_name IS DISTINCT FROM EXCLUDED.schema_name + OR m.template_id IS DISTINCT FROM EXCLUDED.template_id + OR m.asset_id IS DISTINCT FROM EXCLUDED.asset_id + RETURNING market_contract, listing_type, listing_id + ), del AS ( + DELETE FROM atomicmarket_stats_markets + WHERE (market_contract, listing_type, listing_id) IN ( + SELECT market_contract, listing_type, listing_id FROM changed_listings + EXCEPT + SELECT market_contract, listing_type, listing_id FROM updated_listings + ) + RETURNING 1 + ) + SELECT COALESCE((SELECT COUNT(*) FROM ins_upd), 0) + + COALESCE((SELECT COUNT(*) FROM del), 0) + INTO result; + + RETURN result; +END +$$; diff --git a/definitions/migrations/1.3.21/database.sql b/definitions/migrations/1.3.21/database.sql new file mode 100644 index 00000000..c3016123 --- /dev/null +++ b/definitions/migrations/1.3.21/database.sql @@ -0,0 +1 @@ +UPDATE dbinfo SET "value" = '1.3.21' WHERE name = 'version'; diff --git a/definitions/procedures/atomicmarket_template_buyoffer_mints.sql b/definitions/procedures/atomicmarket_template_buyoffer_mints.sql new file mode 100644 index 00000000..50c58b82 --- /dev/null +++ b/definitions/procedures/atomicmarket_template_buyoffer_mints.sql @@ -0,0 +1,45 @@ +CREATE OR REPLACE PROCEDURE update_atomicmarket_template_buyoffer_mints(selected_contract TEXT, last_irreversible_block BIGINT, max_buyoffers_to_update INT = 50000) +LANGUAGE plpgsql +AS $$ +DECLARE + r RECORD; +BEGIN + FOR r IN + WITH buyoffers_to_update AS MATERIALIZED ( + SELECT market_contract, buyoffer_id + FROM atomicmarket_template_buyoffers + WHERE template_mint IS NULL AND state = 2 + AND market_contract = selected_contract + AND created_at_block <= last_irreversible_block + LIMIT max_buyoffers_to_update + ), new_mints AS MATERIALIZED ( + SELECT + buyoffer.market_contract, + buyoffer.buyoffer_id, + MIN(template_mint) min_template_mint, + MAX(template_mint) max_template_mint + FROM buyoffers_to_update buyoffer + JOIN atomicmarket_template_buyoffers_assets asset ON (buyoffer.market_contract = asset.market_contract AND buyoffer.buyoffer_id = asset.buyoffer_id) + JOIN atomicassets_assets assets ON asset.asset_id = assets.asset_id AND asset.assets_contract = assets.contract + GROUP BY buyoffer.market_contract, buyoffer.buyoffer_id + -- filter out buyoffers where assets have a template id, but the mint is not yet set + HAVING NOT BOOL_OR(assets.template_id IS NOT NULL AND assets.template_mint IS NULL) + ) + SELECT * + FROM new_mints + LOOP + UPDATE atomicmarket_template_buyoffers buyoffer + SET template_mint = + CASE WHEN r.min_template_mint IS NULL + THEN 'empty' + ELSE int4range(r.min_template_mint, r.max_template_mint, '[]') + END + WHERE buyoffer.market_contract = r.market_contract + AND buyoffer.buyoffer_id = r.buyoffer_id + ; + + COMMIT; + END LOOP; +END +$$ +; diff --git a/definitions/tables/atomicmarket_tables.sql b/definitions/tables/atomicmarket_tables.sql index 58f8420e..d92ed933 100644 --- a/definitions/tables/atomicmarket_tables.sql +++ b/definitions/tables/atomicmarket_tables.sql @@ -175,6 +175,39 @@ CREATE TABLE atomicmarket_buyoffers_assets CONSTRAINT atomicmarket_buyoffers_assets_pkey PRIMARY KEY (market_contract, buyoffer_id, assets_contract, asset_id) ); +CREATE TABLE atomicmarket_template_buyoffers +( + market_contract character varying(12) NOT NULL, + buyoffer_id bigint NOT NULL, + buyer character varying(12) NOT NULL, + seller character varying(12), + price bigint NOT NULL, + token_symbol character varying(12) NOT NULL, + assets_contract character varying(12) NOT NULL, + maker_marketplace character varying(12) NOT NULL, + taker_marketplace character varying(12), + template_mint int4range, + collection_name character varying(12) NOT NULL, + collection_fee double precision NOT NULL, + template_id bigint NOT NULL, + state smallint NOT NULL, + updated_at_block bigint NOT NULL, + updated_at_time bigint NOT NULL, + created_at_block bigint NOT NULL, + created_at_time bigint NOT NULL, + CONSTRAINT atomicmarket_template_buyoffers_pkey PRIMARY KEY (market_contract, buyoffer_id) +); + +CREATE TABLE atomicmarket_template_buyoffers_assets +( + market_contract character varying(12) NOT NULL, + buyoffer_id bigint NOT NULL, + assets_contract character varying(12) NOT NULL, + "index" integer NOT NULL, + asset_id bigint NOT NULL, + CONSTRAINT atomicmarket_template_buyoffers_assets_pkey PRIMARY KEY (market_contract, buyoffer_id, assets_contract, asset_id) +); + CREATE TABLE atomicmarket_stats_markets ( listing_id bigint not null, price bigint not null, @@ -254,6 +287,26 @@ ALTER TABLE ONLY atomicmarket_buyoffers_assets +ALTER TABLE ONLY atomicmarket_template_buyoffers + ADD CONSTRAINT atomicmarket_template_buyoffers_token_symbol_fkey FOREIGN KEY (market_contract, token_symbol) + REFERENCES atomicmarket_tokens (market_contract, token_symbol) MATCH SIMPLE ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED NOT VALID; + +ALTER TABLE ONLY atomicmarket_template_buyoffers + ADD CONSTRAINT atomicmarket_template_buyoffers_maker_marketplace_fkey FOREIGN KEY (market_contract, maker_marketplace) + REFERENCES atomicmarket_marketplaces (market_contract, marketplace_name) MATCH SIMPLE ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED NOT VALID; + +ALTER TABLE ONLY atomicmarket_template_buyoffers + ADD CONSTRAINT atomicmarket_template_buyoffers_taker_marketplace_fkey FOREIGN KEY (market_contract, taker_marketplace) + REFERENCES atomicmarket_marketplaces (market_contract, marketplace_name) MATCH SIMPLE ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED NOT VALID; + + + +ALTER TABLE ONLY atomicmarket_template_buyoffers_assets + ADD CONSTRAINT atomicmarket_template_buyoffers_assets_template_buyoffers_fkey FOREIGN KEY (market_contract, buyoffer_id) + REFERENCES atomicmarket_template_buyoffers (market_contract, buyoffer_id) MATCH SIMPLE ON UPDATE RESTRICT ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED NOT VALID; + + + -- Indexes CREATE INDEX atomicmarket_auctions_auction_id ON atomicmarket_auctions USING btree (auction_id); CREATE INDEX atomicmarket_auctions_seller ON atomicmarket_auctions USING hash (seller); @@ -293,10 +346,23 @@ CREATE INDEX atomicmarket_buyoffers_created_at_time ON atomicmarket_buyoffers US CREATE INDEX atomicmarket_buyoffers_assets_asset_id ON atomicmarket_buyoffers_assets USING btree (asset_id); +CREATE INDEX atomicmarket_template_buyoffers_buyoffer_id ON atomicmarket_template_buyoffers USING btree (buyoffer_id); +CREATE INDEX atomicmarket_template_buyoffers_seller ON atomicmarket_template_buyoffers USING hash (seller); +CREATE INDEX atomicmarket_template_buyoffers_buyer ON atomicmarket_template_buyoffers USING hash (buyer); +CREATE INDEX atomicmarket_template_buyoffers_price ON atomicmarket_template_buyoffers USING btree (price); +CREATE INDEX atomicmarket_template_buyoffers_collection_name ON atomicmarket_template_buyoffers USING btree (collection_name); +CREATE INDEX atomicmarket_template_buyoffers_template_id ON atomicmarket_template_buyoffers USING btree (template_id); +CREATE INDEX atomicmarket_template_buyoffers_state ON atomicmarket_template_buyoffers USING btree (state); +CREATE INDEX atomicmarket_template_buyoffers_updated_at_time ON atomicmarket_template_buyoffers USING btree (updated_at_time); +CREATE INDEX atomicmarket_template_buyoffers_created_at_time ON atomicmarket_template_buyoffers USING btree (created_at_time); + +CREATE INDEX atomicmarket_template_buyoffers_assets_asset_id ON atomicmarket_template_buyoffers_assets USING btree (asset_id); + CREATE INDEX atomicmarket_sales_missing_mint ON atomicmarket_sales(assets_contract, sale_id, offer_id) WHERE template_mint IS NULL; CREATE INDEX atomicmarket_buyoffers_missing_mint ON atomicmarket_buyoffers(assets_contract, buyoffer_id) WHERE template_mint IS NULL; CREATE INDEX atomicmarket_auctions_missing_mint ON atomicmarket_auctions(assets_contract, auction_id) WHERE template_mint IS NULL; +CREATE INDEX atomicmarket_template_buyoffers_missing_mint ON atomicmarket_template_buyoffers(assets_contract, buyoffer_id) WHERE template_mint IS NULL AND "state" = 2; -- Only SOLD template_buyoffers have an nft CREATE INDEX atomicmarket_stats_markets_collection_name ON atomicmarket_stats_markets USING btree (collection_name); diff --git a/definitions/views/atomicmarket_template_buyoffers_master.sql b/definitions/views/atomicmarket_template_buyoffers_master.sql new file mode 100644 index 00000000..2793255d --- /dev/null +++ b/definitions/views/atomicmarket_template_buyoffers_master.sql @@ -0,0 +1,67 @@ +CREATE OR REPLACE VIEW atomicmarket_template_buyoffers_master AS + SELECT DISTINCT ON (market_contract, buyoffer_id) + t_buyoffer.market_contract, + t_buyoffer.assets_contract, + t_buyoffer.buyoffer_id, + + t_buyoffer.seller, + t_buyoffer.buyer, + + t_buyoffer.price raw_price, + token.token_precision raw_token_precision, + token.token_symbol raw_token_symbol, + + json_build_object( + 'token_contract', token.token_contract, + 'token_symbol', token.token_symbol, + 'token_precision', token.token_precision, + 'amount', t_buyoffer.price::text + ) price, + + ARRAY( + SELECT asset.asset_id + FROM atomicmarket_template_buyoffers_assets asset + WHERE t_buyoffer.buyoffer_id = asset.buyoffer_id AND asset.market_contract = t_buyoffer.market_contract + ORDER BY "index" ASC + ) assets, + + t_buyoffer.maker_marketplace, + t_buyoffer.taker_marketplace, + + t_buyoffer.collection_name, + json_build_object( + 'collection_name', collection.collection_name, + 'name', collection.data->>'name', + 'img', collection.data->>'img', + 'images', collection.data->>'images', + 'author', collection.author, + 'allow_notify', collection.allow_notify, + 'authorized_accounts', collection.authorized_accounts, + 'notify_accounts', collection.notify_accounts, + 'market_fee', t_buyoffer.collection_fee, + 'created_at_block', collection.created_at_block::text, + 'created_at_time', collection.created_at_time::text + ) collection, + + t_buyoffer.template_id, + json_build_object( + 'template_id', "template".template_id::text, + 'max_supply', "template".max_supply::text, + 'is_transferable', "template".transferable, + 'is_burnable', "template".burnable, + 'issued_supply', "template".issued_supply::text, + 'immutable_data', "template".immutable_data, + 'created_at_time', "template".created_at_time::text, + 'created_at_block', "template".created_at_block::text + ) "template", + + t_buyoffer.state buyoffer_state, + + t_buyoffer.updated_at_block, + t_buyoffer.updated_at_time, + t_buyoffer.created_at_block, + t_buyoffer.created_at_time + FROM atomicmarket_template_buyoffers t_buyoffer, atomicassets_collections collection, atomicassets_templates "template", atomicmarket_tokens token + WHERE t_buyoffer.market_contract = token.market_contract AND t_buyoffer.token_symbol = token.token_symbol AND + t_buyoffer.assets_contract = collection.contract AND t_buyoffer.collection_name = collection.collection_name AND + t_buyoffer.assets_contract = "template".contract AND t_buyoffer.template_id = "template".template_id diff --git a/src/api/namespaces/atomicmarket/filler.ts b/src/api/namespaces/atomicmarket/filler.ts index 49887316..151b4406 100644 --- a/src/api/namespaces/atomicmarket/filler.ts +++ b/src/api/namespaces/atomicmarket/filler.ts @@ -41,6 +41,25 @@ export async function fillBuyoffers(db: DB, assetContract: string, buyoffers: an })); } +export async function fillTemplateBuyoffers(db: DB, assetContract: string, buyoffers: any[]): Promise { + const assetIDs: string[] = []; + + for (const buyoffer of buyoffers) { + assetIDs.push(...buyoffer.assets); + } + + const filler = new AssetFiller( + db, assetContract, assetIDs, formatAsset, 'atomicassets_assets_master', + buildAssetFillerHook({fetchPrices: true}) + ); + + return await Promise.all(buyoffers.map(async (buyoffer) => { + buyoffer.assets = await filler.fill(buyoffer.assets); + + return buyoffer; + })); +} + export async function fillSales(db: DB, assetContract: string, sales: any[]): Promise { const assetIDs: string[] = []; diff --git a/src/api/namespaces/atomicmarket/format.ts b/src/api/namespaces/atomicmarket/format.ts index 4f5fc810..0b8fb2df 100644 --- a/src/api/namespaces/atomicmarket/format.ts +++ b/src/api/namespaces/atomicmarket/format.ts @@ -1,6 +1,6 @@ import { formatAsset } from '../atomicassets/format'; -import { AuctionState, BuyofferState, SaleState } from '../../../filler/handlers/atomicmarket'; -import { AuctionApiState, BuyofferApiState, SaleApiState } from './index'; +import { AuctionState, BuyofferState, SaleState, TemplateBuyofferState } from '../../../filler/handlers/atomicmarket'; +import { AuctionApiState, BuyofferApiState, SaleApiState, TemplateBuyofferApiState } from './index'; import { OfferState } from '../../../filler/handlers/atomicassets'; import { DB } from '../../server'; import { FillerHook } from '../atomicassets/filler'; @@ -59,6 +59,29 @@ export function formatBuyoffer(row: any): any { return data; } +export function formatTemplateBuyoffer(row: any): any { + const data = {...row}; + + data.price.amount = row.raw_price; + + if (row.buyoffer_state === TemplateBuyofferState.LISTED.valueOf()) { + data.state = TemplateBuyofferApiState.LISTED.valueOf(); + } else if (row.buyoffer_state === TemplateBuyofferState.CANCELED.valueOf()) { + data.state = TemplateBuyofferApiState.CANCELED.valueOf(); + } else if (row.buyoffer_state === TemplateBuyofferState.SOLD.valueOf()) { + data.state = TemplateBuyofferState.SOLD.valueOf(); + } + + delete data.raw_price; + delete data.raw_token_symbol; + delete data.raw_token_precision; + delete data.collection_name; + delete data.template_id; + delete data.buyoffer_state; + + return data; +} + export function formatSale(row: any): any { const {raw_price, sale_state, offer_state, ...data} = row; diff --git a/src/api/namespaces/atomicmarket/handlers/template-buyoffers.test.ts b/src/api/namespaces/atomicmarket/handlers/template-buyoffers.test.ts new file mode 100644 index 00000000..c2ce9168 --- /dev/null +++ b/src/api/namespaces/atomicmarket/handlers/template-buyoffers.test.ts @@ -0,0 +1,66 @@ +import {expect} from 'chai'; +import {initAtomicMarketTest} from '../test'; +import {RequestValues} from '../../utils'; +import {getTestContext} from '../../../../utils/test'; +import {getTemplateBuyOffersAction} from './template-buyoffers'; + +// TODO add more tests +describe('template buy offer handler', () => { + const {client, txit} = initAtomicMarketTest(); + + async function getBuyOffersIds(values: RequestValues): Promise> { + const testContext = getTestContext(client); + + const result = await getTemplateBuyOffersAction(values, testContext); + + return result.map((s: any) => s.buyoffer_id); + } + + describe('getTemplateBuyOffers', () => { + + txit('orders by asset name', async () => { + const buyOffer1 = await client.createTemplateBuyOffer(); + const buyOffer2 = await client.createTemplateBuyOffer(); + const buyOffer3 = await client.createTemplateBuyOffer(); + + const asset1 = await client.createAsset({ + mutable_data: {name: 'Z'}, + template_id: buyOffer1.template_id, + }); + await client.createTemplateBuyOfferAssets({ + asset_id: asset1.asset_id, + buyoffer_id: buyOffer1.buyoffer_id, + }); + + const asset2 = await client.createAsset({ + immutable_data: {name: 'A'}, + template_id: buyOffer2.template_id, + }); + await client.createTemplateBuyOfferAssets({ + asset_id: asset2.asset_id, + buyoffer_id: buyOffer2.buyoffer_id, + }); + + await client.createTemplate({ + template_id: buyOffer1.template_id, + }); + + await client.createTemplate({ + template_id: buyOffer2.template_id, + }); + + await client.createTemplate({ + template_id: buyOffer3.template_id, + immutable_data: {name: 'H'} + }); + + expect(await getBuyOffersIds({sort: 'name', order: 'asc'})) + .to.deep.equal([buyOffer2.buyoffer_id, buyOffer3.buyoffer_id, buyOffer1.buyoffer_id]); + }); + + }); + + after(async () => { + await client.end(); + }); +}); diff --git a/src/api/namespaces/atomicmarket/handlers/template-buyoffers.ts b/src/api/namespaces/atomicmarket/handlers/template-buyoffers.ts new file mode 100644 index 00000000..feba7219 --- /dev/null +++ b/src/api/namespaces/atomicmarket/handlers/template-buyoffers.ts @@ -0,0 +1,140 @@ +import { buildBoundaryFilter, RequestValues } from '../../utils'; +import { AtomicMarketContext } from '../index'; +import QueryBuilder from '../../../builder'; +import { buildTemplateBuyofferFilter, hasListingFilter } from '../utils'; +import { buildGreylistFilter, hasAssetFilter, hasDataFilters } from '../../atomicassets/utils'; +import { fillTemplateBuyoffers } from '../filler'; +import { formatTemplateBuyoffer } from '../format'; +import { ApiError } from '../../../error'; +import { applyActionGreylistFilters, getContractActionLogs } from '../../../utils'; +import { filterQueryArgs } from '../../validation'; + +export async function getTemplateBuyOffersAction(params: RequestValues, ctx: AtomicMarketContext): Promise { + const maxLimit = ctx.coreArgs.limits?.buyoffers || 100; + const args = await filterQueryArgs(params, { + page: {type: 'int', min: 1, default: 1}, + limit: {type: 'int', min: 1, max: maxLimit, default: Math.min(maxLimit, 100)}, + sort: { + type: 'string', + allowedValues: [ + 'created', 'updated', 'ending', 'buyoffer_id', 'price', + 'template_mint', 'name', + ], + default: 'created' + }, + order: {type: 'string', allowedValues: ['asc', 'desc'], default: 'desc'}, + + count: {type: 'bool'}, + }); + + const query = new QueryBuilder(` + SELECT listing.buyoffer_id + FROM atomicmarket_template_buyoffers listing + JOIN atomicmarket_tokens "token" ON (listing.market_contract = "token".market_contract AND listing.token_symbol = "token".token_symbol) + `); + + if (args.sort === 'name') { + query.appendToBase(` + LEFT OUTER JOIN atomicmarket_template_buyoffers_assets buyoffer_asset ON buyoffer_asset.buyoffer_id = listing.buyoffer_id AND buyoffer_asset.market_contract = listing.market_contract AND buyoffer_asset.index = 1 + LEFT OUTER JOIN atomicassets_assets asset ON asset.asset_id = buyoffer_asset.asset_id AND asset.contract = buyoffer_asset.assets_contract + LEFT OUTER JOIN atomicassets_templates template ON template.contract = listing.assets_contract AND template.template_id = listing.template_id + `); + } + + query.equal('listing.market_contract', ctx.coreArgs.atomicmarket_account); + + await buildTemplateBuyofferFilter(params, query); + await buildGreylistFilter(params, query, {collectionName: 'listing.collection_name'}); + await buildBoundaryFilter( + params, query, 'listing.buyoffer_id', 'int', + args.sort === 'updated' ? 'listing.updated_at_time' : 'listing.created_at_time' + ); + + if (args.count) { + const countQuery = await ctx.db.query( + 'SELECT COUNT(*) counter FROM (' + query.buildString() + ') x', + query.buildValues() + ); + + return countQuery.rows[0].counter; + } + + const sortMapping: {[key: string]: {column: string, nullable: boolean, numericIndex: boolean}} = { + buyoffer_id: {column: 'listing.buyoffer_id', nullable: false, numericIndex: true}, + created: {column: 'listing.created_at_time', nullable: false, numericIndex: true}, + updated: {column: 'listing.updated_at_time', nullable: false, numericIndex: true}, + price: {column: 'listing.price', nullable: false, numericIndex: true}, + template_mint: {column: 'LOWER(listing.template_mint)', nullable: true, numericIndex: true}, + name: {column: `(COALESCE(asset.mutable_data, '{}') || COALESCE(asset.immutable_data, '{}') || COALESCE(template.immutable_data, '{}'))->>'name'`, nullable: true, numericIndex: false}, + }; + + const ignoreIndex = (hasAssetFilter(params) || hasDataFilters(params) || hasListingFilter(params)) && sortMapping[args.sort].numericIndex; + + query.append('ORDER BY ' + sortMapping[args.sort].column + (ignoreIndex ? ' + 1 ' : ' ') + args.order + ' ' + (sortMapping[args.sort].nullable ? 'NULLS LAST' : '') + ', listing.buyoffer_id ASC'); + query.paginate(args.page, args.limit); + + const buyofferResult = await ctx.db.query(query.buildString(), query.buildValues()); + + const buyofferLookup: {[key: string]: any} = {}; + const result = await ctx.db.query( + 'SELECT * FROM atomicmarket_template_buyoffers_master WHERE market_contract = $1 AND buyoffer_id = ANY ($2)', + [ctx.coreArgs.atomicmarket_account, buyofferResult.rows.map(row => row.buyoffer_id)] + ); + + result.rows.reduce((prev, current) => { + prev[String(current.buyoffer_id)] = current; + + return prev; + }, buyofferLookup); + + const buyoffers = await fillTemplateBuyoffers( + ctx.db, ctx.coreArgs.atomicassets_account, + buyofferResult.rows.map((row) => buyofferLookup[String(row.buyoffer_id)]) + ); + + return buyoffers.map(formatTemplateBuyoffer); +} + +export async function getTemplateBuyOffersCountAction(params: RequestValues, ctx: AtomicMarketContext): Promise { + return getTemplateBuyOffersAction({...params, count: 'true'}, ctx); +} + +export async function getTemplateBuyOfferAction(params: RequestValues, ctx: AtomicMarketContext): Promise { + const args = await filterQueryArgs(ctx.pathParams, { + buyoffer_id: {type: 'id'}, + }); + + const query = await ctx.db.query( + 'SELECT * FROM atomicmarket_template_buyoffers_master WHERE market_contract = $1 AND buyoffer_id = $2', + [ctx.coreArgs.atomicmarket_account, args.buyoffer_id] + ); + + if (query.rowCount === 0) { + throw new ApiError('Buyoffer not found', 416); + } + + const buyoffers = await fillTemplateBuyoffers( + ctx.db, ctx.coreArgs.atomicassets_account, query.rows + ); + + return formatTemplateBuyoffer(buyoffers[0]); +} + +export async function getTemplateBuyOfferLogsAction(params: RequestValues, ctx: AtomicMarketContext): Promise { + const maxLimit = ctx.coreArgs.limits?.logs || 100; + const args = await filterQueryArgs({...ctx.pathParams, ...params}, { + buyoffer_id: {type: 'id'}, + page: {type: 'int', min: 1, default: 1}, + limit: {type: 'int', min: 1, max: maxLimit, default: Math.min(maxLimit, 100)}, + order: {type: 'string', allowedValues: ['asc', 'desc'], default: 'asc'}, + action_whitelist: {type: 'string[]', min: 1}, + action_blacklist: {type: 'string[]', min: 1}, + }); + + return await getContractActionLogs( + ctx.db, ctx.coreArgs.atomicmarket_account, + applyActionGreylistFilters(['lognewtbuyo', 'canceltbuyo', 'fulfilltbuyo'], args), + {buyoffer_id: args.buyoffer_id}, + (args.page - 1) * args.limit, args.limit, args.order + ); +} diff --git a/src/api/namespaces/atomicmarket/index.ts b/src/api/namespaces/atomicmarket/index.ts index b9b4de86..9ab872eb 100644 --- a/src/api/namespaces/atomicmarket/index.ts +++ b/src/api/namespaces/atomicmarket/index.ts @@ -18,7 +18,8 @@ import ApiNotificationReceiver from '../../notification'; import { buyoffersEndpoints, buyofferSockets } from './routes/buyoffers'; import { assetsEndpoints } from './routes/assets'; import { ActionHandlerContext } from '../../actionhandler'; -import {ILimits} from '../../../types/config'; +import { ILimits } from '../../../types/config'; +import { templateBuyoffersEndpoints } from './routes/template-buyoffers'; export interface AtomicMarketNamespaceArgs { connected_reader: string; @@ -61,6 +62,12 @@ export enum BuyofferApiState { INVALID = 4 } +export enum TemplateBuyofferApiState { + LISTED = 0, + CANCELED = 1, + SOLD = 2 +} + export type AtomicMarketContext = ActionHandlerContext; export class AtomicMarketNamespace extends ApiNamespace { @@ -108,6 +115,7 @@ export class AtomicMarketNamespace extends ApiNamespace { endpointsDocs.push(salesEndpoints(this, server, router)); endpointsDocs.push(auctionsEndpoints(this, server, router)); endpointsDocs.push(buyoffersEndpoints(this, server, router)); + endpointsDocs.push(templateBuyoffersEndpoints(this, server, router)); endpointsDocs.push(marketplacesEndpoints(this, server, router)); endpointsDocs.push(pricesEndpoints(this, server, router)); endpointsDocs.push(statsEndpoints(this, server, router)); diff --git a/src/api/namespaces/atomicmarket/routes/template-buyoffers.ts b/src/api/namespaces/atomicmarket/routes/template-buyoffers.ts new file mode 100644 index 00000000..925dfc0d --- /dev/null +++ b/src/api/namespaces/atomicmarket/routes/template-buyoffers.ts @@ -0,0 +1,178 @@ +import * as express from 'express'; + +import { AtomicMarketNamespace, TemplateBuyofferApiState } from '../index'; +import { HTTPServer } from '../../../server'; +import { formatTemplateBuyoffer } from '../format'; +import { fillTemplateBuyoffers } from '../filler'; +import { + actionGreylistParameters, + dateBoundaryParameters, + getOpenAPI3Responses, + getPrimaryBoundaryParams, + paginationParameters, +} from '../../../docs'; +import { extendedAssetFilterParameters, atomicDataFilter, baseAssetFilterParameters } from '../../atomicassets/openapi'; +import { listingFilterParameters } from '../openapi'; +import { + createSocketApiNamespace, + extractNotificationIdentifiers, +} from '../../../utils'; +import ApiNotificationReceiver from '../../../notification'; +import { NotificationData } from '../../../../filler/notifier'; +import { + getTemplateBuyOfferAction, + getTemplateBuyOfferLogsAction, + getTemplateBuyOffersAction, + getTemplateBuyOffersCountAction +} from '../handlers/template-buyoffers'; + +export function templateBuyoffersEndpoints(core: AtomicMarketNamespace, server: HTTPServer, router: express.Router): any { + const {caching, returnAsJSON} = server.web; + + router.all('/v1/template_buyoffers', caching(), returnAsJSON(getTemplateBuyOffersAction, core)); + router.all('/v1/template_buyoffers/_count', caching(), returnAsJSON(getTemplateBuyOffersCountAction, core)); + + router.all('/v1/template_buyoffers/:buyoffer_id', caching(), returnAsJSON(getTemplateBuyOfferAction, core)); + + router.all('/v1/template_buyoffers/:buyoffer_id/logs', caching(), returnAsJSON(getTemplateBuyOfferLogsAction, core)); + + return { + tag: { + name: 'template_buyoffers', + description: 'Template buyoffers' + }, + paths: { + '/v1/template_buyoffers': { + get: { + tags: ['template_buyoffers'], + summary: 'Get all template buyoffers.', + description: atomicDataFilter, + parameters: [ + { + name: 'state', + in: 'query', + description: 'Filter by buyoffer state (' + + TemplateBuyofferApiState.LISTED.valueOf() + ': LISTED - Buyoffer is listed, ' + + TemplateBuyofferApiState.CANCELED.valueOf() + ': CANCELED - Buyoffer was canceled, ' + + TemplateBuyofferApiState.SOLD.valueOf() + ': SOLD - Buyoffer has been sold, ' + + ') - separate multiple with ","', + required: false, + schema: {type: 'string'} + }, + ...listingFilterParameters, + ...baseAssetFilterParameters, + ...extendedAssetFilterParameters, + ...getPrimaryBoundaryParams('buyoffer_id'), + ...dateBoundaryParameters, + ...paginationParameters, + { + name: 'sort', + in: 'query', + description: 'Column to sort', + required: false, + schema: { + type: 'string', + enum: [ + 'created', 'updated', 'buyoffer_id', 'price', + 'template_mint', 'name', + ], + default: 'created' + } + } + ], + responses: getOpenAPI3Responses([200, 500], { + type: 'array', + items: {'$ref': '#/components/schemas/TemplateBuyoffer'} + }) + } + }, + '/v1/template_buyoffers/{buyoffer_id}': { + get: { + tags: ['template_buyoffers'], + summary: 'Get a specific template buyoffer by id', + parameters: [ + { + in: 'path', + name: 'buyoffer_id', + description: 'Buyoffer Id', + required: true, + schema: {type: 'integer'} + } + ], + responses: getOpenAPI3Responses([200, 416, 500], {'$ref': '#/components/schemas/TemplateBuyoffer'}) + } + }, + '/v1/template_buyoffers/{buyoffer_id}/logs': { + get: { + tags: ['template_buyoffers'], + summary: 'Fetch template buyoffer logs', + parameters: [ + { + name: 'buyoffer_id', + in: 'path', + description: 'ID of buyoffer', + required: true, + schema: {type: 'integer'} + }, + ...paginationParameters, + ...actionGreylistParameters + ], + responses: getOpenAPI3Responses([200, 500], {type: 'array', items: {'$ref': '#/components/schemas/Log'}}) + } + } + } + }; +} + +export function templateBuyofferSockets(core: AtomicMarketNamespace, server: HTTPServer, notification: ApiNotificationReceiver): void { + const namespace = createSocketApiNamespace(server, core.path + '/v1/template_buyoffers'); + + namespace.on('connection', (socket) => { + socket.on('subscribe', data => { + const availableRooms = ['new_template_buyoffers']; + + for (const room of availableRooms) { + if (data && data[room]) { + socket.join(room); + } else if (socket.rooms.has(room)) { + socket.leave(room); + } + } + }); + }); + + notification.onData('template_buyoffers', async (notifications: NotificationData[]) => { + const buyofferIDs = extractNotificationIdentifiers(notifications, 'buyoffer_id'); + const query = await server.database.query( + 'SELECT * FROM atomicmarket_template_buyoffers_master WHERE market_contract = $1 AND buyoffer_id = ANY($2)', + [core.args.atomicmarket_account, buyofferIDs] + ); + + const buyoffers = await fillTemplateBuyoffers(server, core.args.atomicassets_account, query.rows); + + for (const notification of notifications) { + if (notification.type === 'trace' && notification.data.trace) { + const trace = notification.data.trace; + + if (trace.act.account !== core.args.atomicmarket_account) { + continue; + } + + const buyofferID = (trace.act.data).buyoffer_id; + const buyoffer = buyoffers.find(row => String(row.buyoffer_id) === String(buyofferID)); + + if (trace.act.name === 'lognewtbuyo') { + namespace.in('new_template_buyoffers').emit('new_template_buyoffer', { + transaction: notification.data.tx, + block: notification.data.block, + trace: notification.data.trace, + buyoffer_id: buyofferID, + buyoffer: formatTemplateBuyoffer(buyoffer) + }); + } + } else if (notification.type === 'fork') { + namespace.emit('fork', {block_num: notification.data.block.block_num}); + } + } + }); +} diff --git a/src/api/namespaces/atomicmarket/test.ts b/src/api/namespaces/atomicmarket/test.ts index 0decfe68..5865c47a 100644 --- a/src/api/namespaces/atomicmarket/test.ts +++ b/src/api/namespaces/atomicmarket/test.ts @@ -83,24 +83,24 @@ export class AtomicMarketTestClient extends AtomicAssetsTestClient { async createBuyOffer(values: Record = {}): Promise> { return this.insert('atomicmarket_buyoffers', { - market_contract: 'amtest', - buyoffer_id: values?.buyoffer_id ?? this.getId(), - buyer: 'buyer', - seller: 'seller', - price: Math.floor(Math.random()*100), - token_symbol: 'TEST', - assets_contract: 'aatest', - maker_marketplace: 'marketplace', - taker_marketplace: 'marketplace', - collection_name: values.collection_name ?? (await this.createCollection()).collection_name, - collection_fee: Math.random(), - state: SaleState.SOLD, - memo: 'memo', - decline_memo: 'something', - updated_at_block: Math.floor(Math.random()*100), - updated_at_time: Date.now(), - created_at_block: Math.floor(Math.random()*100), - created_at_time: Date.now(), + market_contract: 'amtest', + buyoffer_id: values?.buyoffer_id ?? this.getId(), + buyer: 'buyer', + seller: 'seller', + price: Math.floor(Math.random()*100), + token_symbol: 'TEST', + assets_contract: 'aatest', + maker_marketplace: 'marketplace', + taker_marketplace: 'marketplace', + collection_name: values.collection_name ?? (await this.createCollection()).collection_name, + collection_fee: Math.random(), + state: SaleState.SOLD, + memo: 'memo', + decline_memo: 'something', + updated_at_block: Math.floor(Math.random()*100), + updated_at_time: Date.now(), + created_at_block: Math.floor(Math.random()*100), + created_at_time: Date.now(), ...values, }); } @@ -116,6 +116,40 @@ export class AtomicMarketTestClient extends AtomicAssetsTestClient { }); } + async createTemplateBuyOffer(values: Record = {}): Promise> { + return this.insert('atomicmarket_template_buyoffers', { + market_contract: 'amtest', + buyoffer_id: values?.buyoffer_id ?? this.getId(), + buyer: 'buyer', + seller: 'seller', + price: Math.floor(Math.random()*100), + token_symbol: 'TEST', + assets_contract: 'aatest', + maker_marketplace: 'marketplace', + taker_marketplace: 'marketplace', + collection_name: values.collection_name ?? (await this.createCollection()).collection_name, + collection_fee: Math.random(), + template_id: this.getId(), + state: SaleState.SOLD, + updated_at_block: Math.floor(Math.random()*100), + updated_at_time: Date.now(), + created_at_block: Math.floor(Math.random()*100), + created_at_time: Date.now(), + ...values, + }); + } + + async createTemplateBuyOfferAssets(values: Record = {}): Promise> { + return this.insert('atomicmarket_template_buyoffers_assets', { + market_contract: 'amtest', + buyoffer_id: values?.buyoffer_id ?? this.getId(), + assets_contract: 'aatest', + index: 1, + asset_id: values?.asset_id ?? this.getId(), + ...values, + }); + } + async createSale(values: Record = {}): Promise> { return await this.insert('atomicmarket_sales', { market_contract: 'amtest', diff --git a/src/api/namespaces/atomicmarket/utils.ts b/src/api/namespaces/atomicmarket/utils.ts index aeef12b9..ec395c87 100644 --- a/src/api/namespaces/atomicmarket/utils.ts +++ b/src/api/namespaces/atomicmarket/utils.ts @@ -1,6 +1,6 @@ import {buildAssetFilter, hasAssetFilter, hasDataFilters} from '../atomicassets/utils'; -import {AuctionApiState, BuyofferApiState, SaleApiState} from './index'; -import {AuctionState, BuyofferState, SaleState} from '../../../filler/handlers/atomicmarket'; +import {AuctionApiState, BuyofferApiState, SaleApiState, TemplateBuyofferApiState} from './index'; +import {AuctionState, BuyofferState, SaleState, TemplateBuyofferState} from '../../../filler/handlers/atomicmarket'; import {OfferState} from '../../../filler/handlers/atomicassets'; import QueryBuilder from '../../builder'; import {ApiError} from '../../error'; @@ -462,3 +462,72 @@ export async function buildBuyofferFilter(values: FilterValues, query: QueryBuil query.addCondition('(' + stateConditions.join(' OR ') + ')'); } } + +export async function buildTemplateBuyofferFilter(values: FilterValues, query: QueryBuilder): Promise { + const args = await filterQueryArgs(values, { + state: {type: 'string', min: 1}, + + symbol: {type: 'string', min: 1}, + min_price: {type: 'float', min: 0}, + max_price: {type: 'float', min: 0}, + + template_id: {type: 'list[id]'}, + }); + + await buildListingFilter(values, query); + + if (hasAssetFilter(values, ['collection_name', 'template_id']) || hasDataFilters(values)) { + const assetQuery = new QueryBuilder( + `SELECT * FROM atomicassets_templates "template" + LEFT JOIN atomicmarket_template_buyoffers_assets buyoffer_asset ON (listing.market_contract = buyoffer_asset.market_contract AND listing.buyoffer_id = buyoffer_asset.buyoffer_id) + LEFT JOIN atomicassets_assets asset ON (asset.contract = buyoffer_asset.assets_contract AND asset.asset_id = buyoffer_asset.asset_id)`, + query.buildValues() + ); + assetQuery.addCondition('"template".contract = listing.assets_contract AND "template".template_id = listing.template_id'); + + await buildAssetFilter(values, assetQuery, { + assetTable: '"asset"', + templateTable: '"template"', + allowDataFilter: true + }); + + query.addCondition('EXISTS(' + assetQuery.buildString() + ')'); + query.setVars(assetQuery.buildValues()); + } + + if (args.template_id.length) { + query.equalMany('listing.template_id', args.template_id); + } + + if (args.symbol) { + query.equal('listing.token_symbol', args.symbol); + + if (args.min_price) { + query.addCondition('listing.price >= 1.0 * ' + query.addVariable(args.min_price) + ' * POWER(10, "token".token_precision)'); + } + + if (args.max_price) { + query.addCondition('listing.price <= 1.0 * ' + query.addVariable(args.max_price) + ' * POWER(10, "token".token_precision)'); + } + } else if (args.min_price || args.max_price) { + throw new ApiError('Price range filters require the "symbol" filter'); + } + + if (args.state) { + const stateConditions: string[] = []; + + if (args.state.split(',').indexOf(String(TemplateBuyofferApiState.LISTED.valueOf())) >= 0) { + stateConditions.push(`(listing.state = ${TemplateBuyofferState.LISTED.valueOf()})`); + } + + if (args.state.split(',').indexOf(String(TemplateBuyofferApiState.CANCELED.valueOf())) >= 0) { + stateConditions.push(`(listing.state = ${TemplateBuyofferState.CANCELED.valueOf()})`); + } + + if (args.state.split(',').indexOf(String(TemplateBuyofferApiState.SOLD.valueOf())) >= 0) { + stateConditions.push(`(listing.state = ${TemplateBuyofferState.SOLD.valueOf()})`); + } + + query.addCondition('(' + stateConditions.join(' OR ') + ')'); + } +} diff --git a/src/filler/handlers/atomicmarket/index.ts b/src/filler/handlers/atomicmarket/index.ts index 3905f6e8..fe0f0711 100644 --- a/src/filler/handlers/atomicmarket/index.ts +++ b/src/filler/handlers/atomicmarket/index.ts @@ -18,6 +18,7 @@ import { saleProcessor } from './processors/sales'; import { buyofferProcessor } from './processors/buyoffers'; import { bonusfeeProcessor } from './processors/bonusfees'; import { JobQueuePriority } from '../../jobqueue'; +import { templateBuyofferProcessor } from './processors/template-buyoffers'; export const ATOMICMARKET_BASE_PRIORITY = Math.max(ATOMICASSETS_BASE_PRIORITY, DELPHIORACLE_BASE_PRIORITY) + 1000; @@ -49,6 +50,12 @@ export enum BuyofferState { ACCEPTED = 3 } +export enum TemplateBuyofferState { + LISTED = 0, + CANCELED = 1, + SOLD = 2 +} + export enum AtomicMarketUpdatePriority { TABLE_BALANCES = ATOMICMARKET_BASE_PRIORITY + 10, TABLE_MARKETPLACES = ATOMICMARKET_BASE_PRIORITY + 10, @@ -57,10 +64,12 @@ export enum AtomicMarketUpdatePriority { ACTION_CREATE_SALE = ATOMICMARKET_BASE_PRIORITY + 20, ACTION_CREATE_AUCTION = ATOMICMARKET_BASE_PRIORITY + 20, ACTION_CREATE_BUYOFFER = ATOMICMARKET_BASE_PRIORITY + 20, + ACTION_CREATE_TEMPLATE_BUYOFFER = ATOMICMARKET_BASE_PRIORITY + 20, TABLE_AUCTIONS = ATOMICMARKET_BASE_PRIORITY + 30, ACTION_UPDATE_SALE = ATOMICMARKET_BASE_PRIORITY + 40, ACTION_UPDATE_AUCTION = ATOMICMARKET_BASE_PRIORITY + 40, ACTION_UPDATE_BUYOFFER = ATOMICMARKET_BASE_PRIORITY + 40, + ACTION_UPDATE_TEMPLATE_BUYOFFER = ATOMICMARKET_BASE_PRIORITY + 40, LOGS = ATOMICMARKET_BASE_PRIORITY } @@ -80,7 +89,8 @@ export default class AtomicMarketHandler extends ContractHandler { const views = [ 'atomicmarket_assets_master', 'atomicmarket_auctions_master', 'atomicmarket_sales_master', 'atomicmarket_sale_prices_master', - 'atomicmarket_stats_prices_master', 'atomicmarket_buyoffers_master' + 'atomicmarket_stats_prices_master', 'atomicmarket_buyoffers_master', + 'atomicmarket_template_buyoffers_master' ]; const procedures = ['atomicmarket_auction_mints', 'atomicmarket_buyoffer_mints', 'atomicmarket_sale_mints']; @@ -137,6 +147,11 @@ export default class AtomicMarketHandler extends ContractHandler { await client.query(fs.readFileSync('./definitions/views/atomicmarket_buyoffers_master.sql', {encoding: 'utf8'})); await client.query(fs.readFileSync('./definitions/views/atomicmarket_sales_master.sql', {encoding: 'utf8'})); } + + if (version === '1.3.21') { + await client.query(fs.readFileSync('./definitions/views/atomicmarket_template_buyoffers_master.sql', {encoding: 'utf8'})); + await client.query(fs.readFileSync('./definitions/procedures/atomicmarket_template_buyoffer_mints.sql', {encoding: 'utf8'})); + } } constructor(filler: Filler, args: {[key: string]: any}) { @@ -237,6 +252,7 @@ export default class AtomicMarketHandler extends ContractHandler { 'atomicmarket_config', 'atomicmarket_delphi_pairs', 'atomicmarket_marketplaces', 'atomicmarket_token_symbols', 'atomicmarket_bonusfees', 'atomicmarket_balances', 'atomicmarket_stats_markets', 'atomicmarket_template_prices', + 'atomicmarket_template_buyoffers', 'atomicmarket_template_buyoffers_assets', ]; for (const table of tables) { @@ -254,6 +270,7 @@ export default class AtomicMarketHandler extends ContractHandler { destructors.push(balanceProcessor(this, processor)); destructors.push(bonusfeeProcessor(this, processor)); destructors.push(buyofferProcessor(this, processor, notifier)); + destructors.push(templateBuyofferProcessor(this, processor, notifier)); destructors.push(configProcessor(this, processor)); destructors.push(marketplaceProcessor(this, processor)); destructors.push(saleProcessor(this, processor, notifier)); diff --git a/src/filler/handlers/atomicmarket/processors/logs.ts b/src/filler/handlers/atomicmarket/processors/logs.ts index 99fff537..c6276104 100644 --- a/src/filler/handlers/atomicmarket/processors/logs.ts +++ b/src/filler/handlers/atomicmarket/processors/logs.ts @@ -135,5 +135,36 @@ export function logProcessor(core: AtomicMarketHandler, processor: DataProcessor }, AtomicMarketUpdatePriority.LOGS.valueOf() )); + /* TEMPLATE BUYOFFERS */ + destructors.push(processor.onActionTrace( + contract, 'lognewtbuyo', + async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { + await db.logTrace(block, tx, trace, { + buyoffer_id: trace.act.data.buyoffer_id, + maker_marketplace: trace.act.data.maker_marketplace, + collection_fee: trace.act.data.collection_fee + }); + }, AtomicMarketUpdatePriority.LOGS.valueOf() + )); + + destructors.push(processor.onActionTrace( + contract, 'canceltbuyo', + async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { + await db.logTrace(block, tx, trace, { + buyoffer_id: trace.act.data.buyoffer_id + }); + }, AtomicMarketUpdatePriority.LOGS.valueOf() + )); + + destructors.push(processor.onActionTrace( + contract, 'fulfilltbuyo', + async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { + await db.logTrace(block, tx, trace, { + buyoffer_id: trace.act.data.buyoffer_id, + taker_marketplace: trace.act.data.taker_marketplace + }); + }, AtomicMarketUpdatePriority.LOGS.valueOf() + )); + return (): any => destructors.map(fn => fn()); } diff --git a/src/filler/handlers/atomicmarket/processors/template-buyoffers.ts b/src/filler/handlers/atomicmarket/processors/template-buyoffers.ts new file mode 100644 index 00000000..ce3e58a2 --- /dev/null +++ b/src/filler/handlers/atomicmarket/processors/template-buyoffers.ts @@ -0,0 +1,97 @@ +import DataProcessor from '../../../processor'; +import { ContractDBTransaction } from '../../../database'; +import { EosioActionTrace, EosioTransaction } from '../../../../types/eosio'; +import { ShipBlock } from '../../../../types/ship'; +import { eosioTimestampToDate } from '../../../../utils/eosio'; +import AtomicMarketHandler, {AtomicMarketUpdatePriority, TemplateBuyofferState} from '../index'; +import ApiNotificationSender from '../../../notifier'; +import { + CancelBuyofferActionData, + FulfillTemplateBuyofferActionData, + LogNewTemplateBuyofferActionData +} from '../types/actions'; +import { preventInt64Overflow } from '../../../../utils/binary'; +import logger from '../../../../utils/winston'; + +export function templateBuyofferProcessor(core: AtomicMarketHandler, processor: DataProcessor, notifier: ApiNotificationSender): () => any { + const destructors: Array<() => any> = []; + const contract = core.args.atomicmarket_account; + + destructors.push(processor.onActionTrace( + contract, 'lognewtbuyo', + async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { + // fix issue that action could be called by a different account + if (trace.act.authorization.find(authorization => authorization.actor !== core.args.atomicmarket_account)) { + logger.warn('Received lognewbuyoffer from invalid authorization'); + + return; + } + + await db.insert('atomicmarket_template_buyoffers', { + market_contract: core.args.atomicmarket_account, + buyoffer_id: trace.act.data.buyoffer_id, + buyer: trace.act.data.buyer, + seller: null, + price: preventInt64Overflow(trace.act.data.price.split(' ')[0].replace('.', '')), + token_symbol: trace.act.data.price.split(' ')[1], + assets_contract: core.args.atomicassets_account, + maker_marketplace: trace.act.data.maker_marketplace, + taker_marketplace: null, + collection_name: trace.act.data.collection_name, + collection_fee: trace.act.data.collection_fee, + template_id: trace.act.data.template_id, + state: TemplateBuyofferState.LISTED.valueOf(), + updated_at_block: block.block_num, + updated_at_time: eosioTimestampToDate(block.timestamp).getTime(), + created_at_block: block.block_num, + created_at_time: eosioTimestampToDate(block.timestamp).getTime() + }, ['market_contract', 'buyoffer_id']); + + notifier.sendActionTrace('template_buyoffer', block, tx, trace); + }, AtomicMarketUpdatePriority.ACTION_CREATE_TEMPLATE_BUYOFFER.valueOf() + )); + + destructors.push(processor.onActionTrace( + contract, 'canceltbuyo', + async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { + await db.update('atomicmarket_template_buyoffers', { + state: TemplateBuyofferState.CANCELED.valueOf(), + updated_at_block: block.block_num, + updated_at_time: eosioTimestampToDate(block.timestamp).getTime() + }, { + str: 'market_contract = $1 AND buyoffer_id = $2', + values: [core.args.atomicmarket_account, trace.act.data.buyoffer_id] + }, ['market_contract', 'buyoffer_id']); + + notifier.sendActionTrace('template_buyoffer', block, tx, trace); + }, AtomicMarketUpdatePriority.ACTION_UPDATE_TEMPLATE_BUYOFFER.valueOf() + )); + + destructors.push(processor.onActionTrace( + contract, 'fulfilltbuyo', + async (db: ContractDBTransaction, block: ShipBlock, tx: EosioTransaction, trace: EosioActionTrace): Promise => { + await db.update('atomicmarket_template_buyoffers', { + seller: trace.act.data.seller, + state: TemplateBuyofferState.SOLD.valueOf(), + taker_marketplace: trace.act.data.taker_marketplace, + updated_at_block: block.block_num, + updated_at_time: eosioTimestampToDate(block.timestamp).getTime() + }, { + str: 'market_contract = $1 AND buyoffer_id = $2', + values: [core.args.atomicmarket_account, trace.act.data.buyoffer_id] + }, ['market_contract', 'buyoffer_id']); + + await db.insert('atomicmarket_template_buyoffers_assets', { + market_contract: core.args.atomicmarket_account, + buyoffer_id: trace.act.data.buyoffer_id, + assets_contract: core.args.atomicassets_account, + index: 1, + asset_id: trace.act.data.asset_id + }, ['market_contract', 'buyoffer_id', 'assets_contract', 'asset_id']); + + notifier.sendActionTrace('template_buyoffer', block, tx, trace); + }, AtomicMarketUpdatePriority.ACTION_UPDATE_TEMPLATE_BUYOFFER.valueOf() + )); + + return (): any => destructors.map(fn => fn()); +} diff --git a/src/filler/handlers/atomicmarket/types/actions.ts b/src/filler/handlers/atomicmarket/types/actions.ts index 6dccc626..32cba297 100644 --- a/src/filler/handlers/atomicmarket/types/actions.ts +++ b/src/filler/handlers/atomicmarket/types/actions.ts @@ -112,3 +112,21 @@ export type AcceptBuyofferActionData = { expected_price: string, taker_marketplace: string } + +export type LogNewTemplateBuyofferActionData = { + buyoffer_id: string; + buyer: string; + price: string; + template_id: string; + maker_marketplace: string; + collection_name: string; + collection_fee: number; +} + +export interface FulfillTemplateBuyofferActionData { + seller: string; + buyoffer_id: string; + asset_id: string; + expected_price: string; + taker_marketplace: string; +} From 4f3246434d1aed76b9850afcc5d087ca5fd6da0b Mon Sep 17 00:00:00 2001 From: hugomartinez Date: Mon, 13 Nov 2023 17:48:00 +0100 Subject: [PATCH 4/7] Add has_template_buyoffer filter for assets --- .../atomicassets/handlers/assets.ts | 20 +++++++++++++++++++ src/api/namespaces/atomicassets/openapi.ts | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/src/api/namespaces/atomicassets/handlers/assets.ts b/src/api/namespaces/atomicassets/handlers/assets.ts index 31b06529..95670b06 100644 --- a/src/api/namespaces/atomicassets/handlers/assets.ts +++ b/src/api/namespaces/atomicassets/handlers/assets.ts @@ -10,6 +10,7 @@ import { buildAssetFilter, buildGreylistFilter, buildHideOffersFilter, hasStrong import { ApiError } from '../../../error'; import { applyActionGreylistFilters, getContractActionLogs } from '../../../utils'; import { filterQueryArgs, FilterValues } from '../../validation'; +import { TemplateBuyofferState } from '../../../../filler/handlers/atomicmarket'; export async function buildAssetQueryCondition( values: FilterValues, query: QueryBuilder, @@ -21,6 +22,7 @@ export async function buildAssetQueryCondition( only_duplicate_templates: {type: 'bool'}, has_backed_tokens: {type: 'bool'}, + has_template_buyoffer: {type: 'bool'}, template_mint: {type: 'int', min: 1}, @@ -75,6 +77,24 @@ export async function buildAssetQueryCondition( } } + if (typeof args.has_template_buyoffer === 'boolean') { + if (args.has_template_buyoffer) { + query.addCondition('EXISTS (' + + 'SELECT * FROM atomicmarket_template_buyoffers t_buyoffer ' + + 'WHERE ' + options.assetTable + '.contract = t_buyoffer.assets_contract ' + + 'AND ' + options.assetTable + '.template_id = t_buyoffer.template_id ' + + 'AND t_buyoffer.state = ' + TemplateBuyofferState.LISTED + + ')'); + } else { + query.addCondition('NOT EXISTS (' + + 'SELECT * FROM atomicmarket_template_buyoffers t_buyoffer ' + + 'WHERE ' + options.assetTable + '.contract = t_buyoffer.assets_contract ' + + 'AND ' + options.assetTable + '.template_id = t_buyoffer.template_id ' + + 'AND t_buyoffer.state = ' + TemplateBuyofferState.LISTED + + ')'); + } + } + await buildHideOffersFilter(values, query, options.assetTable); if (args.template_mint) { diff --git a/src/api/namespaces/atomicassets/openapi.ts b/src/api/namespaces/atomicassets/openapi.ts index 582a9183..c43d8d61 100644 --- a/src/api/namespaces/atomicassets/openapi.ts +++ b/src/api/namespaces/atomicassets/openapi.ts @@ -420,6 +420,15 @@ export const completeAssetFilterParameters = [ type: 'boolean' } }, + { + name: 'has_template_buyoffer', + in: 'query', + description: 'Show only assets that have a listed template buyoffer', + required: false, + schema: { + type: 'boolean' + } + }, { name: 'authorized_account', in: 'query', From cdef18e0763e93eb7343dee2b5033fc4674673f0 Mon Sep 17 00:00:00 2001 From: hugomartinez Date: Tue, 14 Nov 2023 17:03:41 +0100 Subject: [PATCH 5/7] Add template_buyoffers array to nfts on some routes --- definitions/migrations/1.3.22/database.sql | 1 + .../views/atomicmarket_assets_master.sql | 14 ++++++++- docker-compose.yml | 4 +-- src/api/namespaces/atomicmarket/format.ts | 31 +++++++++++++++++-- .../atomicmarket/handlers/assets.ts | 4 +-- src/api/namespaces/atomicmarket/index.ts | 21 +++++++++++-- src/api/namespaces/atomicmarket/openapi.ts | 11 +++++++ src/filler/handlers/atomicmarket/index.ts | 4 +++ 8 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 definitions/migrations/1.3.22/database.sql diff --git a/definitions/migrations/1.3.22/database.sql b/definitions/migrations/1.3.22/database.sql new file mode 100644 index 00000000..7a1d70b8 --- /dev/null +++ b/definitions/migrations/1.3.22/database.sql @@ -0,0 +1 @@ +UPDATE dbinfo SET "value" = '1.3.22' WHERE name = 'version'; diff --git a/definitions/views/atomicmarket_assets_master.sql b/definitions/views/atomicmarket_assets_master.sql index 0ca49527..2ffa37eb 100644 --- a/definitions/views/atomicmarket_assets_master.sql +++ b/definitions/views/atomicmarket_assets_master.sql @@ -23,5 +23,17 @@ CREATE OR REPLACE VIEW atomicmarket_assets_master AS WHERE auction.market_contract = auction_asset.market_contract AND auction.auction_id = auction_asset.auction_id AND auction_asset.assets_contract = asset.contract AND auction_asset.asset_id = asset.asset_id AND auction.state = 1 AND auction.end_time > (extract(epoch from now()) * 1000)::bigint - ) auctions + ) auctions, + ARRAY( + SELECT + (SELECT json_build_object( + 'market_contract', t_buyoffer2.market_contract, + 'buyoffer_id', t_buyoffer2.buyoffer_id, + 'token_symbol', t_buyoffer2.token_symbol + ) FROM atomicmarket_template_buyoffers t_buyoffer2 WHERE t_buyoffer2.template_id = t_buyoffer.template_id AND t_buyoffer2.token_symbol = t_buyoffer.token_symbol AND t_buyoffer2.price = MAX(t_buyoffer.price) AND state = 0) + FROM atomicmarket_template_buyoffers t_buyoffer + WHERE t_buyoffer.assets_contract = asset.contract AND t_buyoffer.template_id = asset.template_id AND + t_buyoffer.state = 0 + GROUP BY template_id, token_symbol + ) template_buyoffers FROM atomicassets_assets_master asset diff --git a/docker-compose.yml b/docker-compose.yml index ac776f7c..53be6074 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: image: redis:5-alpine restart: on-failure ports: - - "6379:6379" + - "127.0.0.1:6379:6379" volumes: - ./docker/redis/data:/data networks: @@ -45,7 +45,7 @@ services: - 'POSTGRES_USER=root' - 'POSTGRES_PASSWORD=changeme' ports: - - "5432:5432" + - "127.0.0.1:5432:5432" volumes: - ./docker/postgres/data:/var/lib/postgresql/data networks: diff --git a/src/api/namespaces/atomicmarket/format.ts b/src/api/namespaces/atomicmarket/format.ts index 0b8fb2df..c544228e 100644 --- a/src/api/namespaces/atomicmarket/format.ts +++ b/src/api/namespaces/atomicmarket/format.ts @@ -110,10 +110,11 @@ export function formatListingAsset(row: any): any { } export function buildAssetFillerHook( - options: {fetchAuctions?: boolean, fetchSales?: boolean, fetchPrices?: boolean} + options: {fetchTemplateBuyoffers?: boolean, fetchAuctions?: boolean, fetchSales?: boolean, fetchPrices?: boolean} ): FillerHook { return async (db: DB, contract: string, rows: any[]): Promise => { const assetIDs = rows.map(asset => asset.asset_id); + const templateIDs = new Set(rows.map(asset => asset.template_id).filter((templateID: any) => templateID !== null && templateID > -1)); const queries = await Promise.all([ options.fetchSales && db.query( @@ -133,6 +134,20 @@ export function buildAssetFillerHook( 'auction.state = ' + AuctionState.LISTED.valueOf() + ' AND auction.end_time > ' + (Date.now() / 1000) + '::BIGINT ', [contract, assetIDs] ), + options.fetchTemplateBuyoffers && db.query( + 'SELECT t_buyoffer.market_contract, t_buyoffer.template_id, t_buyoffer.token_symbol, ' + + 'MAX(t_buyoffer.price) price, ( ' + + 'SELECT t_buyoffer2.buyoffer_id ' + + 'FROM atomicmarket_template_buyoffers t_buyoffer2 ' + + 'WHERE t_buyoffer2.market_contract = t_buyoffer.market_contract AND t_buyoffer2.template_id = t_buyoffer.template_id ' + + 'AND t_buyoffer2.token_symbol = t_buyoffer.token_symbol AND t_buyoffer2.price = MAX(t_buyoffer.price) AND state = 0 ' + + ') buyoffer_id ' + + 'FROM atomicmarket_template_buyoffers t_buyoffer ' + + 'WHERE t_buyoffer.assets_contract = $1 AND t_buyoffer.template_id = ANY($2) AND ' + + 't_buyoffer.state = ' + TemplateBuyofferState.LISTED.valueOf() + ' ' + + 'GROUP BY market_contract, template_id, token_symbol', + [contract, [...templateIDs]] + ), options.fetchPrices && db.query( 'SELECT DISTINCT ON (price.market_contract, price.collection_name, price.template_id, price.symbol) ' + 'price.market_contract, asset.collection_name, asset.template_id, ' + @@ -148,7 +163,7 @@ export function buildAssetFillerHook( ]); const assetData: {[key: string]: {sales: any[], auctions: any[]}} = {}; - const templateData: {[key: string]: {prices: any[]}} = {}; + const templateData: {[key: string]: {prices: any[], template_buyoffers: any[]}} = {}; for (const row of rows) { assetData[row.asset_id] = {sales: [], auctions: []}; @@ -159,7 +174,7 @@ export function buildAssetFillerHook( continue; } - templateData[row.template.template_id] = {prices: []}; + templateData[row.template.template_id] = {prices: [], template_buyoffers: []}; } if (queries[0]) { @@ -176,6 +191,16 @@ export function buildAssetFillerHook( if (queries[2]) { for (const row of queries[2].rows) { + templateData[row.template_id].template_buyoffers.push({ + market_contract: row.market_contract, + buyoffer_id: row.buyoffer_id, + token_symbol: row.token_symbol, + }); + } + } + + if (queries[3]) { + for (const row of queries[3].rows) { templateData[row.template_id].prices.push({ market_contract: row.market_contract, token: { diff --git a/src/api/namespaces/atomicmarket/handlers/assets.ts b/src/api/namespaces/atomicmarket/handlers/assets.ts index ddfbd580..643b036c 100644 --- a/src/api/namespaces/atomicmarket/handlers/assets.ts +++ b/src/api/namespaces/atomicmarket/handlers/assets.ts @@ -22,8 +22,8 @@ export async function getMarketAssetsAction(params: RequestValues, ctx: AtomicMa return await fillAssets( ctx.db, ctx.coreArgs.atomicassets_account, result, - formatListingAsset, 'atomicmarket_assets_master', - buildAssetFillerHook({fetchSales: true, fetchAuctions: true, fetchPrices: true}) + formatListingAsset, 'atomicassets_assets_master', + buildAssetFillerHook({fetchSales: true, fetchAuctions: true, fetchTemplateBuyoffers: true, fetchPrices: true}) ); } diff --git a/src/api/namespaces/atomicmarket/index.ts b/src/api/namespaces/atomicmarket/index.ts index 9ab872eb..94dfaa65 100644 --- a/src/api/namespaces/atomicmarket/index.ts +++ b/src/api/namespaces/atomicmarket/index.ts @@ -124,19 +124,34 @@ export class AtomicMarketNamespace extends ApiNamespace { const assetApi = new AssetApi( this, server, 'ListingAsset', 'atomicassets_assets_master', - formatListingAsset, buildAssetFillerHook({fetchSales: true, fetchAuctions: true, fetchPrices: true}) + formatListingAsset, buildAssetFillerHook({ + fetchSales: true, + fetchAuctions: true, + fetchTemplateBuyoffers: true, + fetchPrices: true, + }), ); const transferApi = new TransferApi( this, server, 'ListingTransfer', 'atomicassets_transfers_master', formatTransfer, 'atomicassets_assets_master', - formatListingAsset, buildAssetFillerHook({fetchSales: true, fetchAuctions: true, fetchPrices: true}) + formatListingAsset, buildAssetFillerHook({ + fetchSales: true, + fetchAuctions: true, + fetchTemplateBuyoffers: true, + fetchPrices: true, + }) ); const offerApi = new OfferApi( this, server, 'ListingOffer', 'atomicassets_offers_master', formatOffer, 'atomicassets_assets_master', - formatListingAsset, buildAssetFillerHook({fetchSales: true, fetchAuctions: true, fetchPrices: true}) + formatListingAsset, buildAssetFillerHook({ + fetchSales: true, + fetchAuctions: true, + fetchTemplateBuyoffers: true, + fetchPrices: true, + }), ); endpointsDocs.push(assetsEndpoints(this, server, router)); diff --git a/src/api/namespaces/atomicmarket/openapi.ts b/src/api/namespaces/atomicmarket/openapi.ts index c602d989..f33e6092 100644 --- a/src/api/namespaces/atomicmarket/openapi.ts +++ b/src/api/namespaces/atomicmarket/openapi.ts @@ -25,6 +25,17 @@ export const atomicmarketComponents = { } } }, + template_buyoffers: { + type: 'array', + items: { + type: 'object', + properties: { + market_contract: {type: 'string'}, + buyoffer_id: {type: 'string'}, + token_symbol: {type: 'string'}, + } + } + }, prices: { type: 'array', items: { diff --git a/src/filler/handlers/atomicmarket/index.ts b/src/filler/handlers/atomicmarket/index.ts index fe0f0711..3e2d272b 100644 --- a/src/filler/handlers/atomicmarket/index.ts +++ b/src/filler/handlers/atomicmarket/index.ts @@ -152,6 +152,10 @@ export default class AtomicMarketHandler extends ContractHandler { await client.query(fs.readFileSync('./definitions/views/atomicmarket_template_buyoffers_master.sql', {encoding: 'utf8'})); await client.query(fs.readFileSync('./definitions/procedures/atomicmarket_template_buyoffer_mints.sql', {encoding: 'utf8'})); } + + if (version === '1.3.22') { + await client.query(fs.readFileSync('./definitions/views/atomicmarket_assets_master.sql', {encoding: 'utf8'})); + } } constructor(filler: Filler, args: {[key: string]: any}) { From 1b2ee5dee9aa5218f34ebc0a05310b48422cd488 Mon Sep 17 00:00:00 2001 From: hugomartinez Date: Wed, 15 Nov 2023 16:33:35 +0100 Subject: [PATCH 6/7] Fix documentation --- src/api/namespaces/atomicmarket/openapi.ts | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/api/namespaces/atomicmarket/openapi.ts b/src/api/namespaces/atomicmarket/openapi.ts index f33e6092..3c00d1d3 100644 --- a/src/api/namespaces/atomicmarket/openapi.ts +++ b/src/api/namespaces/atomicmarket/openapi.ts @@ -203,6 +203,46 @@ export const atomicmarketComponents = { created_at_time: {type: 'string'} } }, + TemplateBuyoffer: { + type: 'object', + properties: { + market_contract: {type: 'string'}, + assets_contract: {type: 'string'}, + buyoffer_id: {type: 'string'}, + + seller: {type: 'string'}, + buyer: {type: 'string'}, + + price: { + type: 'object', + properties: { + amount: {type: 'string'}, + token_precision: {type: 'integer'}, + token_contract: {type: 'string'}, + token_symbol: {type: 'string'} + } + }, + + assets: { + type: 'array', + items: {'$ref': '#/components/schemas/Asset'} + }, + + maker_marketplace: {type: 'string', nullable: true}, + taker_marketplace: {type: 'string', nullable: true}, + + collection: atomicassetsComponents.Asset.properties.collection, + + template: atomicassetsComponents.Asset.properties.template, + + state: {type: 'integer'}, + + updated_at_block: {type: 'string'}, + updated_at_time: {type: 'string'}, + created_at_block: {type: 'string'}, + created_at_time: {type: 'string'} + } + }, Marketplace: { type: 'object', properties: { From 2afd22e8b4578a962676277bfd59ebe247718e6d Mon Sep 17 00:00:00 2001 From: hugomartinez Date: Tue, 28 Nov 2023 10:04:28 +0100 Subject: [PATCH 7/7] Bump version to 1.3.22 Adds template_buyoffers support --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f473ddc6..c2ea6666 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eosio-contract-api", - "version": "1.3.21", + "version": "1.3.22", "description": "EOSIO Contract API", "author": "pink.gg", "license": "AGPL-3.0",