From b143166c33d89099a103fc8992d45d504207ef71 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 8 May 2024 14:57:38 -0700 Subject: [PATCH] - add List Collections API (#1460) - add Cancel Order API --- package.json | 2 +- src/api/api.ts | 61 ++++++++++++++++++++++++++ src/api/apiPaths.ts | 12 +++++ src/api/types.ts | 39 ++++++++++++++++ src/sdk.ts | 19 +++++++- test/integration/getCollection.spec.ts | 23 ++++++++++ test/integration/postOrder.spec.ts | 10 +++++ 7 files changed, 164 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 650c4b499..12f62340b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opensea-js", - "version": "7.1.7", + "version": "7.1.8", "description": "TypeScript 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 40deb4eb2..718083e57 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,6 +1,7 @@ import { ethers } from "ethers"; import { getCollectionPath, + getCollectionsPath, getOrdersAPIPath, getPostCollectionOfferPath, getBuildOfferPath, @@ -18,10 +19,12 @@ import { getAccountPath, getCollectionStatsPath, getBestListingsAPIPath, + getCancelOrderPath, } from "./apiPaths"; import { BuildOfferResponse, GetCollectionResponse, + GetCollectionsResponse, ListNFTsResponse, GetNFTResponse, ListCollectionOffersResponse, @@ -31,6 +34,9 @@ import { GetOffersResponse, GetListingsResponse, CollectionOffer, + CollectionOrderByOption, + CancelOrderResponse, + GetCollectionsArgs, } from "./types"; import { API_BASE_MAINNET, API_BASE_TESTNET } from "../constants"; import { @@ -532,6 +538,40 @@ export class OpenSeaAPI { return collectionFromJSON(response); } + /** + * Fetch a list of OpenSea collections. + * @param orderBy The order to return the collections in. Default: CREATED_DATE + * @param chain The chain to filter the collections on. Default: all chains + * @param creatorUsername The creator's OpenSea username to filter the collections on. + * @param includeHidden If hidden collections should be returned. Default: false + * @param limit The limit of collections to return. + * @param next The cursor for the next page of results. This is returned from a previous request. + * @returns List of {@link OpenSeaCollection} returned by the API. + */ + public async getCollections( + orderBy: CollectionOrderByOption = CollectionOrderByOption.CREATED_DATE, + chain?: Chain, + creatorUsername?: string, + includeHidden: boolean = false, + limit?: number, + next?: string, + ): Promise { + const path = getCollectionsPath(); + const args: GetCollectionsArgs = { + order_by: orderBy, + chain, + creator_username: creatorUsername, + include_hidden: includeHidden, + limit, + next, + }; + const response = await this.get(path, args); + response.collections = response.collections.map((collection) => + collectionFromJSON(collection), + ); + return response; + } + /** * Fetch stats for an OpenSea collection. * @param slug The slug (identifier) of the collection. @@ -592,6 +632,27 @@ export class OpenSeaAPI { return response; } + /** + * Offchain cancel an order, offer or listing, by its order hash when protected by the SignedZone. + * Protocol and Chain are required to prevent hash collisions. + * Please note cancellation is only assured if a fulfillment signature was not vended prior to cancellation. + * @param protocolAddress The Seaport address for the order. + * @param orderHash The order hash, or external identifier, of the order. + * @param chain The chain where the order is located. + * @returns The response from the API. + */ + public async offchainCancelOrder( + protocolAddress: string, + orderHash: string, + chain: Chain = this.chain, + ): Promise { + const response = await this.post( + getCancelOrderPath(chain, protocolAddress, orderHash), + {}, + ); + return response; + } + /** * Generic fetch method for any API endpoint * @param apiPath Path to URL endpoint under API diff --git a/src/api/apiPaths.ts b/src/api/apiPaths.ts index c0f386573..6425e8803 100644 --- a/src/api/apiPaths.ts +++ b/src/api/apiPaths.ts @@ -40,6 +40,10 @@ export const getCollectionPath = (slug: string) => { return `/api/v2/collections/${slug}`; }; +export const getCollectionsPath = () => { + return "/api/v2/collections"; +}; + export const getCollectionStatsPath = (slug: string) => { return `/api/v2/collections/${slug}/stats`; }; @@ -91,3 +95,11 @@ export const getRefreshMetadataPath = ( ) => { return `/v2/chain/${chain}/contract/${address}/nfts/${identifier}/refresh`; }; + +export const getCancelOrderPath = ( + chain: Chain, + protocolAddress: string, + orderHash: string, +) => { + return `/v2/orders/chain/${chain}/protocol/${protocolAddress}/${orderHash}/cancel`; +}; diff --git a/src/api/types.ts b/src/api/types.ts index 705e39896..83dee4a2e 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -54,6 +54,19 @@ type ContractCriteria = { address: string; }; +/** + * Query args for Get Collections + * @category API Query Args + */ +export interface GetCollectionsArgs { + order_by?: string; + limit?: number; + next?: string; + chain?: string; + creator_username?: string; + include_hidden?: boolean; +} + /** * Response from OpenSea API for fetching a single collection. * @category API Response Types @@ -63,6 +76,24 @@ export type GetCollectionResponse = { collection: OpenSeaCollection; }; +/** + * Response from OpenSea API for fetching a list of collections. + * @category API Response Types + */ +export type GetCollectionsResponse = QueryCursorsV2 & { + /** List of collections. See {@link OpenSeaCollection} */ + collections: OpenSeaCollection[]; +}; + +export enum CollectionOrderByOption { + CREATED_DATE = "created_date", + ONE_DAY_CHANGE = "one_day_change", + SEVEN_DAY_VOLUME = "seven_day_volume", + SEVEN_DAY_CHANGE = "seven_day_change", + NUM_OWNERS = "num_owners", + MARKET_CAP = "market_cap", +} + /** * Base Order type shared between Listings and Offers. * @category API Models @@ -190,6 +221,14 @@ export type GetBestOfferResponse = Offer | CollectionOffer; */ export type GetBestListingResponse = Listing; +/** + * Response from OpenSea API for offchain canceling an order. + * @category API Response Types + */ +export type CancelOrderResponse = { + last_signature_issued_valid_until: string | null; +}; + /** * NFT type returned by OpenSea API. * @category API Models diff --git a/src/sdk.ts b/src/sdk.ts index 8686dab97..3551576a0 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -877,7 +877,7 @@ export class OpenSeaSDK { } /** - * Cancel an order on-chain, preventing it from ever being fulfilled. + * Cancel an order onchain, preventing it from ever being fulfilled. * @param options * @param options.order The order to cancel * @param options.accountAddress The account address that will be cancelling the order. @@ -916,6 +916,23 @@ export class OpenSeaSDK { ); } + /** + * Offchain cancel an order, offer or listing, by its order hash when protected by the SignedZone. + * Protocol and Chain are required to prevent hash collisions. + * Please note cancellation is only assured if a fulfillment signature was not vended prior to cancellation. + * @param protocolAddress The Seaport address for the order. + * @param orderJash The order hash, or external identifier, of the order. + * @param chain The chain where the order is located. + * @returns The response from the API. + */ + public async offchainCancelOrder( + protocolAddress: string, + orderHash: string, + chain: Chain = this.chain, + ) { + return this.api.offchainCancelOrder(protocolAddress, orderHash, chain); + } + /** * Returns whether an order is fulfillable. * An order may not be fulfillable if a target item's transfer function diff --git a/test/integration/getCollection.spec.ts b/test/integration/getCollection.spec.ts index 038d6cc18..e6a8df26a 100644 --- a/test/integration/getCollection.spec.ts +++ b/test/integration/getCollection.spec.ts @@ -1,6 +1,7 @@ import { assert } from "chai"; import { suite, test } from "mocha"; import { sdk } from "./setup"; +import { CollectionOrderByOption } from "../../src/api/types"; import { SafelistStatus } from "../../src/types"; suite("SDK: getCollection", () => { @@ -18,6 +19,28 @@ suite("SDK: getCollection", () => { ); }); + test("Get Collections", async () => { + const response = await sdk.api.getCollections(); + const { collections, next } = response; + assert(collections[0], "Collection should not be null"); + assert(collections[0].name, "Collection name should exist"); + assert(next, "Next cursor should be included"); + + const response2 = await sdk.api.getCollections( + CollectionOrderByOption.MARKET_CAP, + ); + const { collections: collectionsByMarketCap, next: nextByMarketCap } = + response2; + assert(collectionsByMarketCap[0], "Collection should not be null"); + assert(collectionsByMarketCap[0].name, "Collection name should exist"); + assert(nextByMarketCap, "Next cursor should be included"); + + assert( + collectionsByMarketCap[0].name != collections[0].name, + "Collection order should differ", + ); + }); + test("Get Collection Stats", async () => { const slug = "cool-cats-nft"; const stats = await sdk.api.getCollectionStats(slug); diff --git a/test/integration/postOrder.spec.ts b/test/integration/postOrder.spec.ts index 14cfee25a..29789609c 100644 --- a/test/integration/postOrder.spec.ts +++ b/test/integration/postOrder.spec.ts @@ -115,6 +115,16 @@ suite("SDK: order posting", () => { }; const offerResponse = await sdk.createCollectionOffer(postOrderRequest); expect(offerResponse).to.exist.and.to.have.property("protocol_data"); + + // Cancel the order + const { protocol_address, order_hash } = offerResponse!; + const cancelResponse = await sdk.offchainCancelOrder( + protocol_address, + order_hash, + ); + expect(cancelResponse).to.exist.and.to.have.property( + "last_signature_issued_valid_until", + ); }); test("Post Collection Offer - Polygon", async () => {