From d5740e43cec15091287ee215c8f12dabf8504c16 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 3 Apr 2024 14:47:40 -0700 Subject: [PATCH] add trait offers (#1433) --- developerDocs/getting-started.md | 4 ++++ src/api/api.ts | 30 ++++++++++++++++++++++++++++-- src/orders/utils.ts | 24 ++++++++++++++++++++++-- src/sdk.ts | 19 ++++++++++++++++++- test/integration/postOrder.spec.ts | 20 ++++++++++++++++++++ 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/developerDocs/getting-started.md b/developerDocs/getting-started.md index 113c8cbd0..45b864f31 100644 --- a/developerDocs/getting-started.md +++ b/developerDocs/getting-started.md @@ -95,6 +95,10 @@ The units for `startAmount` are Ether (ETH). If you want to specify another ERC- See [Listening to Events](#listening-to-events) to respond to the setup transactions that occur the first time a user sells an item. +### Creating Collection and Trait Offers + +Criteria offers, consisting of collection and trait offers, are supported with `openseaSDK.createCollectionOffer()`. For trait offers, include `traitType` as the trait name and `traitValue` as the required value for the offer. + #### Creating English Auctions English Auctions are auctions that start at a small amount (we recommend even doing 0!) and increase with every bid. At expiration time, the item sells to the highest bidder. diff --git a/src/api/api.ts b/src/api/api.ts index bda0ede5c..112857cc2 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -355,6 +355,8 @@ export class OpenSeaAPI { * @param quantity The number of NFTs requested in the offer. * @param collectionSlug The slug (identifier) of the collection to build the offer for. * @param offerProtectionEnabled Build the offer on OpenSea's signed zone to provide offer protections from receiving an item which is disabled from trading. + * @param traitType If defined, the trait name to create the collection offer for. + * @param traitValue If defined, the trait value to create the collection offer for. * @returns The {@link BuildOfferResponse} returned by the API. */ public async buildOffer( @@ -362,12 +364,23 @@ export class OpenSeaAPI { quantity: number, collectionSlug: string, offerProtectionEnabled = true, + traitType?: string, + traitValue?: string, ): Promise { + if (traitType || traitValue) { + if (!traitType || !traitValue) { + throw new Error( + "Both traitType and traitValue must be defined if one is defined.", + ); + } + } const payload = getBuildCollectionOfferPayload( offererAddress, quantity, collectionSlug, offerProtectionEnabled, + traitType, + traitValue, ); const response = await this.post( getBuildOfferPath(), @@ -393,13 +406,22 @@ export class OpenSeaAPI { * Post a collection offer to OpenSea. * @param order The collection offer to post. * @param slug The slug (identifier) of the collection to post the offer for. + * @param traitType If defined, the trait name to create the collection offer for. + * @param traitValue If defined, the trait value to create the collection offer for. * @returns The {@link Offer} returned to the API. */ public async postCollectionOffer( order: ProtocolData, slug: string, + traitType?: string, + traitValue?: string, ): Promise { - const payload = getPostCollectionOfferPayload(slug, order); + const payload = getPostCollectionOfferPayload( + slug, + order, + traitType, + traitValue, + ); return await this.post( getPostCollectionOfferPath(), payload, @@ -656,7 +678,11 @@ export class OpenSeaAPI { // If an errors array is returned, throw with the error messages. const errors = response.bodyJson?.errors; if (errors?.length > 0) { - throw new Error(`Server Error: ${errors.join(", ")}`); + let errorMessage = errors.join(", "); + if (errorMessage === "[object Object]") { + errorMessage = JSON.stringify(errors); + } + throw new Error(`Server Error: ${errorMessage}`); } else { // Otherwise, let ethers throw a SERVER_ERROR since it will include // more context about the request and response. diff --git a/src/orders/utils.ts b/src/orders/utils.ts index 100aa0874..8a3daa023 100644 --- a/src/orders/utils.ts +++ b/src/orders/utils.ts @@ -14,14 +14,24 @@ export const DEFAULT_SEAPORT_CONTRACT_ADDRESS = export const getPostCollectionOfferPayload = ( collectionSlug: string, protocol_data: ProtocolData, + traitType?: string, + traitValue?: string, ) => { - return { + const payload = { criteria: { collection: { slug: collectionSlug }, }, protocol_data, protocol_address: DEFAULT_SEAPORT_CONTRACT_ADDRESS, }; + if (traitType && traitValue) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (payload.criteria as any).trait = { + type: traitType, + value: traitValue, + }; + } + return payload; }; export const getBuildCollectionOfferPayload = ( @@ -29,8 +39,10 @@ export const getBuildCollectionOfferPayload = ( quantity: number, collectionSlug: string, offerProtectionEnabled: boolean, + traitType?: string, + traitValue?: string, ) => { - return { + const payload = { offerer: offererAddress, quantity, criteria: { @@ -41,6 +53,14 @@ export const getBuildCollectionOfferPayload = ( protocol_address: DEFAULT_SEAPORT_CONTRACT_ADDRESS, offer_protection_enabled: offerProtectionEnabled, }; + if (traitType && traitValue) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (payload.criteria as any).trait = { + type: traitType, + value: traitValue, + }; + } + return payload; }; export const getFulfillmentDataPath = (side: OrderSide) => { diff --git a/src/sdk.ts b/src/sdk.ts index 3b778ad06..8686dab97 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -564,6 +564,9 @@ export class OpenSeaSDK { * @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. * @param options.excludeOptionalCreatorFees If true, optional creator fees will be excluded from the offer. Default: false. + * @param options.offerProtectionEnabled Build the offer on OpenSea's signed zone to provide offer protections from receiving an item which is disabled from trading. + * @param options.traitType If defined, the trait name to create the collection offer for. + * @param options.traitValue If defined, the trait value to create the collection offer for. * @returns The {@link CollectionOffer} that was created. */ public async createCollectionOffer({ @@ -576,6 +579,9 @@ export class OpenSeaSDK { expirationTime, paymentTokenAddress = getWETHAddress(this.chain), excludeOptionalCreatorFees = false, + offerProtectionEnabled = true, + traitType, + traitValue, }: { collectionSlug: string; accountAddress: string; @@ -586,6 +592,9 @@ export class OpenSeaSDK { expirationTime?: number | string; paymentTokenAddress: string; excludeOptionalCreatorFees?: boolean; + offerProtectionEnabled?: boolean; + traitType?: string; + traitValue?: string; }): Promise { await this._requireAccountIsAvailable(accountAddress); @@ -594,6 +603,9 @@ export class OpenSeaSDK { accountAddress, quantity, collectionSlug, + offerProtectionEnabled, + traitType, + traitValue, ); const item = buildOfferResult.partialParameters.consideration[0]; const convertedConsiderationItem = { @@ -647,7 +659,12 @@ export class OpenSeaSDK { ); const order = await executeAllActions(); - return this.api.postCollectionOffer(order, collectionSlug); + return this.api.postCollectionOffer( + order, + collectionSlug, + traitType, + traitValue, + ); } /** diff --git a/test/integration/postOrder.spec.ts b/test/integration/postOrder.spec.ts index ae8b4c9dd..14cfee25a 100644 --- a/test/integration/postOrder.spec.ts +++ b/test/integration/postOrder.spec.ts @@ -131,4 +131,24 @@ suite("SDK: order posting", () => { await sdkPolygon.createCollectionOffer(postOrderRequest); expect(offerResponse).to.exist.and.to.have.property("protocol_data"); }); + + test("Post Trait Offer - Ethereum", async () => { + const collection = await sdk.api.getCollection("cool-cats-nft"); + const paymentTokenAddress = getWETHAddress(sdk.chain); + const postOrderRequest = { + collectionSlug: collection.collection, + accountAddress: walletAddress, + amount: OFFER_AMOUNT, + quantity: 1, + paymentTokenAddress, + traitType: "face", + traitValue: "tvface bobross", + }; + const offerResponse = await sdk.createCollectionOffer(postOrderRequest); + expect(offerResponse).to.exist.and.to.have.property("protocol_data"); + expect(offerResponse?.criteria.trait).to.deep.equal({ + type: "face", + value: "tvface bobross", + }); + }); });