From d8934179a60553e280ef072ebc4761d4dedcad23 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Thu, 23 Nov 2023 09:18:13 -0800 Subject: [PATCH] Add get all listings/offers, get best listing/offer (#1292) * add API endpoints: - get all listings and offers - get best listing and offer * bump package.json version * add typedocs * docs updates --- developerDocs/getting-started.md | 35 ++++--- package.json | 2 +- src/api/api.ts | 97 ++++++++++++++++++- src/api/types.ts | 83 ++++++++++++++-- src/orders/types.ts | 5 +- src/orders/utils.ts | 22 +++++ src/sdk.ts | 8 +- test/integration/getListingsAndOffers.spec.ts | 82 ++++++++++++++++ 8 files changed, 302 insertions(+), 32 deletions(-) create mode 100644 test/integration/getListingsAndOffers.spec.ts diff --git a/developerDocs/getting-started.md b/developerDocs/getting-started.md index 67e30f1d2..96ab27d49 100644 --- a/developerDocs/getting-started.md +++ b/developerDocs/getting-started.md @@ -125,32 +125,41 @@ Note that auctions aren't supported with Ether directly due to limitations in Et ### Fetching Orders -To retrieve a list of offers and auctions on an asset, you can use an instance of the `OpenSeaAPI` exposed on the client. Parameters passed into API filter objects are camel-cased and serialized before being sent as [OpenSea API parameters](https://docs.opensea.io/v2.0/reference): +To retrieve a list of offers and auctions on an asset, you can use `getOrders`. Parameters passed into API filter objects are camel-cased and serialized before being sent as [API parameters](https://docs.opensea.io/v2.0/reference): ```typescript -// Get offers (bids), a.k.a. orders where `side == 0` +// Get offers (bids), a.k.a. orders where `side == "bid"` const { orders, count } = await openseaSDK.api.getOrders({ assetContractAddress: tokenAddress, tokenId, side: "bid", }); -// Get page 2 of all auctions, a.k.a. orders where `side == 1` -const { orders, count } = await openseaSDK.api.getOrders( - { - assetContractAddress: tokenAddress, - tokenId, - side: "ask", - }, - 2, -); +// Get page 2 of all auctions, a.k.a. orders where `side == "ask"` +const { orders, count } = await openseaSDK.api.getOrders({ + assetContractAddress: tokenAddress, + tokenId, + side: "ask", +}); ``` Note that the listing price of an asset is equal to the `currentPrice` of the **lowest listing** on the asset. Users can lower their listing price without invalidating previous listing, so all get shipped down until they're canceled, or one is fulfilled. -To learn more about signatures, makers, takers, listingTime vs createdTime and other kinds of order terminology, please read the [**Terminology Section**](https://docs.opensea.io/reference#terminology) of the API Docs. +#### Fetching All Offers and Best Listings for a given collection + +There are two endpoints that return all offers and listings for a given collection, `getAllOffers` and `getAllListings`. -The available API filters for the orders endpoint is documented in the `OrdersQueryOptions` interface. See the main [API Docs](https://docs.opensea.io/reference#reference-getting-started) for a playground, along with more up-to-date and detailed explanations. +```typescript +const { offers } = await openseaSDK.api.getAllOffers(collectionSlug); +``` + +#### Fetching Best Offers and Best Listings for a given NFT + +There are two endpoints that return the best offer or listing, `getBestOffer` and `getBestListing`. + +```typescript +const offer = await openseaSDK.api.getBestOffer(collectionSlug, tokenId); +``` ### Buying Items diff --git a/package.json b/package.json index dc838e279..59a071477 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opensea-js", - "version": "6.1.13", + "version": "6.1.14", "description": "JavaScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data!", "license": "MIT", "author": "OpenSea Developers", diff --git a/src/api/api.ts b/src/api/api.ts index f5cb8f2c3..94964e57f 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,7 +1,6 @@ import { ethers } from "ethers"; import { BuildOfferResponse, - Offer, GetCollectionResponse, ListNFTsResponse, GetNFTResponse, @@ -10,6 +9,11 @@ import { GetOrdersResponse, GetPaymentTokensResponse, GetBundlesResponse, + GetBestOfferResponse, + GetBestListingResponse, + GetOffersResponse, + GetListingsResponse, + CollectionOffer, } from "./types"; import { API_BASE_MAINNET, API_BASE_TESTNET, API_V1_PATH } from "../constants"; import { @@ -40,6 +44,10 @@ import { getRefreshMetadataPath, getCollectionOffersPath, getListNFTsByAccountPath, + getBestOfferAPIPath, + getBestListingAPIPath, + getAllOffersAPIPath, + getAllListingsAPIPath, } from "../orders/utils"; import { Chain, @@ -177,6 +185,82 @@ export class OpenSeaAPI { }; } + /** + * Gets all offers for a given collection. + * @param collectionSlug The slug of the collection. + * @param limit The number of offers to return. Must be between 1 and 100. Default: 100 + * @param next The cursor for the next page of results. This is returned from a previous request. + * @returns The {@link GetOffersResponse} returned by the API. + */ + public async getAllOffers( + collectionSlug: string, + limit?: number, + next?: string, + ): Promise { + const response = await this.get( + getAllOffersAPIPath(collectionSlug), + serializeOrdersQueryOptions({ + limit, + next, + }), + ); + return response; + } + + /** + * Gets all listings for a given collection. + * @param collectionSlug The slug of the collection. + * @param limit The number of listings to return. Must be between 1 and 100. Default: 100 + * @param next The cursor for the next page of results. This is returned from a previous request. + * @returns The {@link GetListingsResponse} returned by the API. + */ + public async getAllListings( + collectionSlug: string, + limit?: number, + next?: string, + ): Promise { + const response = await this.get( + getAllListingsAPIPath(collectionSlug), + serializeOrdersQueryOptions({ + limit, + next, + }), + ); + return response; + } + + /** + * Gets the best offer for a given token. + * @param collectionSlug The slug of the collection. + * @param tokenId The token identifier. + * @returns The {@link GetBestOfferResponse} returned by the API. + */ + public async getBestOffer( + collectionSlug: string, + tokenId: string | number, + ): Promise { + const response = await this.get( + getBestOfferAPIPath(collectionSlug, tokenId), + ); + return response; + } + + /** + * Gets the best listing for a given token. + * @param collectionSlug The slug of the collection. + * @param tokenId The token identifier. + * @returns The {@link GetBestListingResponse} returned by the API. + */ + public async getBestListing( + collectionSlug: string, + tokenId: string | number, + ): Promise { + const response = await this.get( + getBestListingAPIPath(collectionSlug, tokenId), + ); + return response; + } + /** * Generate the data needed to fulfill a listing or an offer onchain. * @param fulfillerAddress The wallet address which will be used to fulfill the order @@ -302,10 +386,13 @@ export class OpenSeaAPI { order: ProtocolData, slug: string, retries = 0, - ): Promise { + ): Promise { const payload = getPostCollectionOfferPayload(slug, order); try { - return await this.post(getPostCollectionOfferPath(), payload); + return await this.post( + getPostCollectionOfferPath(), + payload, + ); } catch (error) { _throwOrContinue(error, retries); await delay(1000); @@ -586,7 +673,7 @@ export class OpenSeaAPI { /** * Fetch list of bundles from the API. - * @param query Query to use for getting bunldes. See {@link OpenSeaAssetBundleQuery}. + * @param query Query to use for getting bundles. See {@link OpenSeaAssetBundleQuery}. * @param page Page number to fetch. Defaults to 1. * @returns The {@link GetBundlesResponse} returned by the API. */ @@ -651,7 +738,7 @@ export class OpenSeaAPI { } /** - * Generic post methd for any API endpoint. + * Generic post method for any API endpoint. * @param apiPath Path to URL endpoint under API * @param body Data to send. * @param opts ethers ConnectionInfo, similar to Fetch API. diff --git a/src/api/types.ts b/src/api/types.ts index 1a0afd9aa..789dae3a5 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,5 +1,10 @@ import { ConsiderationItem } from "@opensea/seaport-js/lib/types"; -import { OrderV2, ProtocolData, QueryCursors } from "../orders/types"; +import { + OrderType, + OrderV2, + ProtocolData, + QueryCursors, +} from "../orders/types"; import { OpenSeaAsset, OpenSeaAssetBundle, @@ -12,7 +17,7 @@ import { * @category API Response Types */ export type BuildOfferResponse = { - /** A portion of the parameters needed to sumbit a criteria offer, i.e. collection offer. */ + /** A portion of the parameters needed to submit a criteria offer, i.e. collection offer. */ partialParameters: PartialParameters; }; @@ -46,29 +51,65 @@ export type GetCollectionResponse = { }; /** - * Collection Offer type. + * Base Order type shared between Listings and Offers. * @category API Models */ -export type Offer = { +export type Order = { /** Offer Identifier */ order_hash: string; /** Chain the offer exists on */ chain: string; - /** Defines which NFTs meet the criteria to fulfill the offer. */ - criteria: Criteria; /** The protocol data for the order. Only 'seaport' is currently supported. */ protocol_data: ProtocolData; /** The contract address of the protocol. */ protocol_address: string; }; +/** + * Offer type. + * @category API Models + */ +export type Offer = Order; + +/** + * Collection Offer type. + * @category API Models + */ +export type CollectionOffer = Offer & { + /** Defines which NFTs meet the criteria to fulfill the offer. */ + criteria: Criteria; +}; + +/** + * Price response. + * @category API Models + */ +export type Price = { + current: { + currency: string; + decimals: number; + value: string; + }; +}; + +/** + * Listing order type. + * @category API Models + */ +export type Listing = Order & { + /** The order type of the listing. */ + type: OrderType; + /** The price of the listing. */ + price: Price; +}; + /** * Response from OpenSea API for fetching a list of collection offers. * @category API Response Types */ export type ListCollectionOffersResponse = { /** List of {@link Offer} */ - offers: Offer[]; + offers: CollectionOffer[]; }; /** @@ -115,6 +156,34 @@ export type GetOrdersResponse = QueryCursors & { orders: OrderV2[]; }; +/** + * Response from OpenSea API for fetching offers. + * @category API Response Types + */ +export type GetOffersResponse = QueryCursors & { + offers: Offer[]; +}; + +/** + * Response from OpenSea API for fetching listings. + * @category API Response Types + */ +export type GetListingsResponse = QueryCursors & { + listings: Listing[]; +}; + +/** + * Response from OpenSea API for fetching a best offer. + * @category API Response Types + */ +export type GetBestOfferResponse = Offer | CollectionOffer; + +/** + * Response from OpenSea API for fetching a best listing. + * @category API Response Types + */ +export type GetBestListingResponse = Listing; + /** * Response from OpenSea API for fetching payment tokens. * @category API Response Types diff --git a/src/orders/types.ts b/src/orders/types.ts index 7c3b851ae..476311811 100644 --- a/src/orders/types.ts +++ b/src/orders/types.ts @@ -12,7 +12,7 @@ export type ProtocolData = OrderProtocolToProtocolData[keyof OrderProtocolToProtocolData]; // Protocol agnostic order data -type OrderType = "basic" | "dutch" | "english" | "criteria"; +export type OrderType = "basic" | "dutch" | "english" | "criteria"; export type OrderSide = "ask" | "bid"; type OrderFee = { account: OpenSeaAccount; @@ -98,8 +98,9 @@ export type OrderAPIOptions = { }; export type OrdersQueryOptions = OrderAPIOptions & { - limit: number; + limit?: number; cursor?: string; + next?: string; paymentTokenAddress?: string; maker?: string; diff --git a/src/orders/utils.ts b/src/orders/utils.ts index 73d9f8621..52fb9bd82 100644 --- a/src/orders/utils.ts +++ b/src/orders/utils.ts @@ -23,6 +23,28 @@ export const getOrdersAPIPath = ( return `/v2/orders/${chain}/${protocol}/${sidePath}`; }; +export const getAllOffersAPIPath = (collectionSlug: string) => { + return `/v2/offers/collection/${collectionSlug}/all`; +}; + +export const getAllListingsAPIPath = (collectionSlug: string) => { + return `/v2/listings/collection/${collectionSlug}/all`; +}; + +export const getBestOfferAPIPath = ( + collectionSlug: string, + tokenId: string | number, +) => { + return `/v2/offers/collection/${collectionSlug}/nfts/${tokenId}/best`; +}; + +export const getBestListingAPIPath = ( + collectionSlug: string, + tokenId: string | number, +) => { + return `/v2/listings/collection/${collectionSlug}/nfts/${tokenId}/best`; +}; + export const getCollectionPath = (slug: string) => { return `/api/v1/collection/${slug}`; }; diff --git a/src/sdk.ts b/src/sdk.ts index 15415b363..9ae884315 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -19,7 +19,7 @@ import { } from "ethers"; import { parseEther } from "ethers/lib/utils"; import { OpenSeaAPI } from "./api/api"; -import { Offer, NFT } from "./api/types"; +import { CollectionOffer, NFT } from "./api/types"; import { INVERSE_BASIS_POINT, DEFAULT_ZONE, @@ -568,7 +568,7 @@ export class OpenSeaSDK { * @param options.salt Arbitrary salt. If not passed in, a random salt will be generated with the first four bytes being the domain hash or empty. * @param options.expirationTime Expiration time for the order, in UTC seconds. * @param options.paymentTokenAddress ERC20 address for the payment token in the order. If unspecified, defaults to WETH. - * @returns The {@link Offer} that was created. + * @returns The {@link CollectionOffer} that was created. */ public async createCollectionOffer({ collectionSlug, @@ -588,7 +588,7 @@ export class OpenSeaSDK { salt?: BigNumberish; expirationTime?: number | string; paymentTokenAddress: string; - }): Promise { + }): Promise { await this._requireAccountIsAvailable(accountAddress); const collection = await this.api.getCollection(collectionSlug); @@ -710,7 +710,7 @@ export class OpenSeaSDK { * @param options * @param options.order The order to fulfill, a.k.a. "take" * @param options.accountAddress Address of the wallet taking the offer. - * @param options.recipientAddress The optional address to receive the order's item(s) or curriencies. If not specified, defaults to accountAddress. + * @param options.recipientAddress The optional address to receive the order's item(s) or currencies. If not specified, defaults to accountAddress. * @param options.domain An optional domain to be hashed and included at the end of fulfillment calldata. This can be used for on-chain order attribution to assist with analytics. * @param options.overrides Transaction overrides, ignored if not set. * @returns Transaction hash of the order. diff --git a/test/integration/getListingsAndOffers.spec.ts b/test/integration/getListingsAndOffers.spec.ts new file mode 100644 index 000000000..94a45e511 --- /dev/null +++ b/test/integration/getListingsAndOffers.spec.ts @@ -0,0 +1,82 @@ +import { assert } from "chai"; +import { suite, test } from "mocha"; +import { sdk } from "./setup"; + +suite("SDK: getAllOffers", () => { + test("Get All Offers", async () => { + const slug = "cool-cats-nft"; + const response = await sdk.api.getAllOffers(slug); + + assert(response, "Response should not be null"); + assert(response.offers[0].order_hash, "Order hash should not be null"); + assert(response.offers[0].chain, "Chain should not be null"); + assert( + response.offers[0].protocol_address, + "Protocol address should not be null", + ); + assert( + response.offers[0].protocol_data, + "Protocol data should not be null", + ); + }); +}); + +suite("SDK: getAllListings", () => { + test("Get All Listings", async () => { + const slug = "cool-cats-nft"; + const response = await sdk.api.getAllListings(slug); + + assert(response, "Response should not be null"); + assert(response.listings[0].order_hash, "Order hash should not be null"); + assert(response.listings[0].chain, "Chain should not be null"); + assert( + response.listings[0].protocol_address, + "Protocol address should not be null", + ); + assert( + response.listings[0].protocol_data, + "Protocol data should not be null", + ); + }); +}); + +suite("SDK: getBestOffer", () => { + test("Get Best Offer", async () => { + const slug = "cool-cats-nft"; + const tokenId = 1; + const response = await sdk.api.getBestOffer(slug, tokenId); + + assert(response, "Response should not be null"); + assert(response.order_hash, "Order hash should not be null"); + assert(response.chain, "Chain should not be null"); + assert(response.protocol_address, "Protocol address should not be null"); + assert(response.protocol_data, "Protocol data should not be null"); + }); +}); + +suite("SDK: getBestListing", () => { + test("Get Best Listing", async () => { + const slug = "cool-cats-nft"; + const { listings } = await sdk.api.getAllListings(slug); + const listing = listings[0]; + const tokenId = + listing.protocol_data.parameters.offer[0].identifierOrCriteria; + const response = await sdk.api.getBestListing(slug, tokenId); + + assert(response, "Response should not be null"); + assert(response.order_hash, "Order hash should not be null"); + assert(response.chain, "Chain should not be null"); + assert(response.protocol_address, "Protocol address should not be null"); + assert(response.protocol_data, "Protocol data should not be null"); + assert.equal( + listing.order_hash, + response.order_hash, + "Order hashes should match", + ); + assert.equal( + listing.protocol_address, + response.protocol_address, + "Protocol addresses should match", + ); + }); +});