diff --git a/.eslintrc.js b/.eslintrc.js index d0c4f7ba1..7a3f67fb5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,6 +40,7 @@ module.exports = { "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-var-requires": "off", + "import/no-unused-modules": "off", "import/order": [ "error", { @@ -51,9 +52,7 @@ module.exports = { }, }, ], - "import/no-unused-modules": [1, { unusedExports: true }], "no-control-regex": "off", - "object-shorthand": ["error", "always"], }, settings: { diff --git a/src/api/api.ts b/src/api/api.ts index d970a4ecc..176dc6fa8 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,4 +1,33 @@ import { ethers } from "ethers"; +import { API_BASE_MAINNET, API_BASE_TESTNET } from "../constants"; +import { + ProtocolData, + OrdersQueryOptions, + OrdersQueryResponse, + OrderV2, + FulfillmentDataResponse, + OrderAPIOptions, + OrdersPostQueryResponse, +} from "../orders/types"; +import { + getFulfillListingPayload, + getFulfillOfferPayload, + getFulfillmentDataPath, + getBuildCollectionOfferPayload, + getPostCollectionOfferPayload, + serializeOrdersQueryOptions, + deserializeOrder, +} from "../orders/utils"; +import { + Chain, + OpenSeaAPIConfig, + OpenSeaCollection, + OpenSeaCollectionStats, + OpenSeaPaymentToken, + OpenSeaAccount, + OrderSide, + OrderProtocol, +} from "../types"; import { getCollectionPath, getCollectionsPath, @@ -38,35 +67,6 @@ import { CancelOrderResponse, GetCollectionsArgs, } from "./types"; -import { API_BASE_MAINNET, API_BASE_TESTNET } from "../constants"; -import { - FulfillmentDataResponse, - OrderAPIOptions, - OrdersPostQueryResponse, - OrdersQueryOptions, - OrdersQueryResponse, - OrderV2, - ProtocolData, -} from "../orders/types"; -import { - serializeOrdersQueryOptions, - deserializeOrder, - getFulfillmentDataPath, - getFulfillListingPayload, - getFulfillOfferPayload, - getBuildCollectionOfferPayload, - getPostCollectionOfferPayload, -} from "../orders/utils"; -import { - Chain, - OpenSeaAPIConfig, - OpenSeaAccount, - OpenSeaCollection, - OpenSeaCollectionStats, - OpenSeaPaymentToken, - OrderSide, -} from "../types"; -import { OrderProtocol } from "../orders/types"; import { paymentTokenFromJSON, collectionFromJSON, @@ -136,7 +136,7 @@ export class OpenSeaAPI { */ public async getOrder({ side, - protocol = "seaport", + protocol = OrderProtocol.SEAPORT, orderDirection = "desc", orderBy = "created_date", ...restOptions @@ -174,7 +174,7 @@ export class OpenSeaAPI { */ public async getOrders({ side, - protocol = "seaport", + protocol = OrderProtocol.SEAPORT, orderDirection = "desc", orderBy = "created_date", ...restOptions @@ -347,23 +347,38 @@ export class OpenSeaAPI { if (!order || !apiOptions) { throw new Error("Order and API options are required"); } - + // Protocol validation if (apiOptions.protocol && apiOptions.protocol !== OrderProtocol.SEAPORT) { - throw new Error(`Invalid protocol specified. Must be ${OrderProtocol.SEAPORT}`); + throw new Error( + `Invalid protocol specified. Must be ${OrderProtocol.SEAPORT}`, + ); } // Side validation - if (!apiOptions.side || (apiOptions.side !== OrderSide.LISTING && apiOptions.side !== OrderSide.OFFER)) { - throw new Error(`Invalid order side specified. Must be either ${OrderSide.LISTING} or ${OrderSide.OFFER}`); + if ( + !apiOptions.side || + (apiOptions.side !== OrderSide.LISTING && + apiOptions.side !== OrderSide.OFFER) + ) { + throw new Error( + `Invalid order side specified. Must be either ${OrderSide.LISTING} or ${OrderSide.OFFER}`, + ); } // Protocol address validation - if (!apiOptions.protocolAddress || !ethers.isAddress(apiOptions.protocolAddress)) { + if ( + !apiOptions.protocolAddress || + !ethers.isAddress(apiOptions.protocolAddress) + ) { throw new Error("Invalid protocol address provided"); } - const { protocol = OrderProtocol.SEAPORT, side, protocolAddress } = apiOptions; + const { + protocol = OrderProtocol.SEAPORT, + side, + protocolAddress, + } = apiOptions; const response = await this.post( getOrdersAPIPath(this.chain, protocol, side), { ...order, protocol_address: protocolAddress }, diff --git a/src/api/apiPaths.ts b/src/api/apiPaths.ts index ba4547ed9..751696662 100644 --- a/src/api/apiPaths.ts +++ b/src/api/apiPaths.ts @@ -1,5 +1,4 @@ -import { OrderProtocol } from "../orders/types"; -import { Chain, OrderSide } from "../types"; +import { Chain, OrderSide, OrderProtocol } from "../types"; export const getOrdersAPIPath = ( chain: Chain, diff --git a/src/api/types.ts b/src/api/types.ts index 83dee4a2e..d2bcf3b35 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,11 +1,11 @@ import { ConsiderationItem } from "@opensea/seaport-js/lib/types"; -import { +import type { OrderType, OrderV2, ProtocolData, QueryCursors, } from "../orders/types"; -import { OpenSeaCollection } from "../types"; +import type { OpenSeaCollection } from "../types"; /** * Response from OpenSea API for building an offer. diff --git a/src/constants.ts b/src/constants.ts index aa13b6b5f..42c328db7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,7 +7,6 @@ export const MAX_EXPIRATION_MONTHS = 1; export const API_BASE_MAINNET = "https://api.opensea.io"; export const API_BASE_TESTNET = "https://testnets-api.opensea.io"; -// eslint-disable-next-line import/no-unused-modules export const SIGNED_ZONE = "0x000056f7000000ece9003ca63978907a00ffd100"; export const ENGLISH_AUCTION_ZONE_MAINNETS = diff --git a/src/index.ts b/src/index.ts index b6ab3e34e..4b9edb43e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ import { OpenSeaSDK } from "./sdk"; /** * @example - * // Example Setup * ```ts * import { ethers } from 'ethers' * import { OpenSeaSDK, Chain } from 'opensea-js' @@ -12,11 +11,11 @@ import { OpenSeaSDK } from "./sdk"; * }) * ``` */ -export { - // Main SDK export - OpenSeaSDK, -}; +// Export main SDK +export { OpenSeaSDK }; + +// Export types export * from "./types"; export * from "./api/types"; -export * from "./orders/types"; +export type { OrderType, ProtocolData } from "./orders/types"; diff --git a/src/orders/types.ts b/src/orders/types.ts index c4bd82003..d6a34d3e6 100644 --- a/src/orders/types.ts +++ b/src/orders/types.ts @@ -1,29 +1,27 @@ import { BasicOrderParametersStruct } from "@opensea/seaport-js/lib/typechain-types/seaport/contracts/Seaport"; -import { AdvancedOrder, OrderWithCounter } from "@opensea/seaport-js/lib/types"; -import { OpenSeaAccount, OrderSide } from "../types"; +import { + AdvancedOrder, + OrderWithCounter, + OrderParameters, + Order, +} from "@opensea/seaport-js/lib/types"; +import { BigNumberish } from "ethers"; +import { OpenSeaAccount, OrderSide, OrderProtocol } from "../types"; // Protocol data -export enum OrderProtocol { - SEAPORT = "seaport" -} - -type OrderProtocolToProtocolData = { - [OrderProtocol.SEAPORT]: OrderWithCounter; -}; +type _OrderProtocolToProtocolData = Record< + OrderProtocol, + OrderWithCounter | AdvancedOrder | BasicOrderParametersStruct +>; export type ProtocolData = - OrderProtocolToProtocolData[keyof typeof OrderProtocol]; - -export enum OrderType { - BASIC = "basic", - ENGLISH = "english", - CRITERIA = "criteria", -} - -type OrderFee = { - account: OpenSeaAccount; - basisPoints: string; -}; + | OrderWithCounter + | (Order & { + numerator: bigint; + denominator: bigint; + extraData: string; + parameters: OrderParameters & { counter: BigNumberish }; + }); /** * The latest OpenSea Order schema. @@ -69,6 +67,23 @@ export type OrderV2 = { remainingQuantity: number; }; +/** + * Represents the type of order in the OpenSea marketplace + */ +export enum OrderType { + /** Basic order type for simple transactions */ + BASIC = "basic", + /** English auction order type */ + ENGLISH = "english", + /** Criteria-based order type for collection offers */ + CRITERIA = "criteria", +} + +type OrderFee = { + account: OpenSeaAccount; + basisPoints: string; +}; + export type FulfillmentDataResponse = { protocol: string; fulfillment_data: FulfillmentData; diff --git a/src/sdk.ts b/src/sdk.ts index 2d3341301..b0d18a838 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -52,6 +52,7 @@ import { TokenStandard, AssetWithTokenStandard, AssetWithTokenId, + OrderProtocol, } from "./types"; import { getMaxOrderExpirationTimestamp, @@ -428,7 +429,7 @@ export class OpenSeaSDK { const order = await executeAllActions(); return this.api.postOrder(order, { - protocol: "seaport", + protocol: OrderProtocol.SEAPORT, protocolAddress: DEFAULT_SEAPORT_CONTRACT_ADDRESS, side: OrderSide.OFFER, }); @@ -552,7 +553,7 @@ export class OpenSeaSDK { const order = await executeAllActions(); return this.api.postOrder(order, { - protocol: "seaport", + protocol: OrderProtocol.SEAPORT, protocolAddress: DEFAULT_SEAPORT_CONTRACT_ADDRESS, side: OrderSide.LISTING, }); diff --git a/src/types.ts b/src/types.ts index 07867081a..b67ca375e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -375,3 +375,10 @@ export interface SocialMediaAccount { platform: string; username: string; } + +/** + * Order protocol + */ +export enum OrderProtocol { + SEAPORT = "seaport", +} diff --git a/test/api/api.spec.ts b/test/api/api.spec.ts index 6f51a8957..97b6abd90 100644 --- a/test/api/api.spec.ts +++ b/test/api/api.spec.ts @@ -1,4 +1,4 @@ -import { assert } from "chai"; +import { assert, expect } from "chai"; import { suite, test } from "mocha"; import { Chain } from "../../src"; import { getWETHAddress } from "../../src/utils"; @@ -21,7 +21,9 @@ suite("API", () => { const logPromise = new Promise((resolve, reject) => { mainAPI.logger = (log) => { try { - assert.include(log, `"x-api-key":"${MAINNET_API_KEY}"`); + if (MAINNET_API_KEY) { + assert.include(log, `"x-api-key":"${MAINNET_API_KEY}"`); + } resolve(); } catch (e) { reject(e); @@ -37,11 +39,12 @@ suite("API", () => { }); test("API handles errors", async () => { - // 404 Not found for random token id try { await mainAPI.getNFT(BAYC_CONTRACT_ADDRESS, "404040"); + expect.fail("Should have thrown an error"); } catch (error) { - assert.include((error as Error).message, "not found"); + expect(error).to.be.an.instanceOf(Error); + expect((error as Error).message).to.include("not found"); } }); }); diff --git a/test/api/fulfillment.spec.ts b/test/api/fulfillment.spec.ts index a63c107b5..ca4a96cf9 100644 --- a/test/api/fulfillment.spec.ts +++ b/test/api/fulfillment.spec.ts @@ -1,17 +1,20 @@ import "../utils/setup"; -import { assert } from "chai"; +import { expect } from "chai"; import { suite, test } from "mocha"; -import { OrderSide } from "../../src/types"; +import { OrderProtocol, OrderSide } from "../../src/types"; import { mainAPI } from "../utils/constants"; suite("Generating fulfillment data", () => { test(`Generate fulfillment data for listing`, async () => { const order = await mainAPI.getOrder({ - protocol: "seaport", + protocol: OrderProtocol.SEAPORT, side: OrderSide.LISTING, }); - if (order.orderHash == null) { + expect(order).to.not.be.undefined; + expect(order.orderHash).to.not.be.null; + + if (!order || !order.orderHash) { return; } @@ -22,16 +25,23 @@ suite("Generating fulfillment data", () => { order.side, ); - assert.exists(fulfillment.fulfillment_data.orders[0].signature); + expect(fulfillment).to.not.be.undefined; + expect(fulfillment.fulfillment_data).to.not.be.undefined; + expect(fulfillment.fulfillment_data.orders).to.be.an("array").that.is.not + .empty; + expect(fulfillment.fulfillment_data.orders[0].signature).to.exist; }); test(`Generate fulfillment data for offer`, async () => { const order = await mainAPI.getOrder({ - protocol: "seaport", + protocol: OrderProtocol.SEAPORT, side: OrderSide.OFFER, }); - if (order.orderHash == null) { + expect(order).to.not.be.undefined; + expect(order.orderHash).to.not.be.null; + + if (!order || !order.orderHash) { return; } @@ -42,6 +52,10 @@ suite("Generating fulfillment data", () => { order.side, ); - assert.exists(fulfillment.fulfillment_data.orders[0].signature); + expect(fulfillment).to.not.be.undefined; + expect(fulfillment.fulfillment_data).to.not.be.undefined; + expect(fulfillment.fulfillment_data.orders).to.be.an("array").that.is.not + .empty; + expect(fulfillment.fulfillment_data.orders[0].signature).to.exist; }); }); diff --git a/test/api/getOrders.spec.ts b/test/api/getOrders.spec.ts index cb2ec3854..5c62106d0 100644 --- a/test/api/getOrders.spec.ts +++ b/test/api/getOrders.spec.ts @@ -1,48 +1,65 @@ import "../utils/setup"; import { expect } from "chai"; import { suite, test } from "mocha"; -import { OrderSide } from "../../src/types"; +import { OrderProtocol, OrderSide } from "../../src/types"; import { BAYC_CONTRACT_ADDRESS, BAYC_TOKEN_IDS, + expectValidOrder, mainAPI, } from "../utils/constants"; -import { expectValidOrder } from "../utils/utils"; suite("Getting orders", () => { [OrderSide.LISTING, OrderSide.OFFER].forEach((side) => { test(`getOrder should return a single order > ${side}`, async () => { const order = await mainAPI.getOrder({ - protocol: "seaport", + protocol: OrderProtocol.SEAPORT, side, }); - expectValidOrder(order); + expect(order).to.not.be.undefined; + if (order) { + expectValidOrder(order); + } }); }); - test(`getOrder should throw if no order found`, async () => { - await expect( - mainAPI.getOrder({ - protocol: "seaport", + test(`getOrder should handle not found case`, async () => { + try { + await mainAPI.getOrder({ + protocol: OrderProtocol.SEAPORT, side: OrderSide.LISTING, maker: "0x000000000000000000000000000000000000dEaD", - }), - ) - .to.eventually.be.rejected.and.be.an.instanceOf(Error) - .and.have.property("message", "Not found: no matching order found"); + }); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + if (error instanceof Error) { + expect(error.message).to.include("Not found"); + } + } }); [OrderSide.LISTING, OrderSide.OFFER].forEach((side) => { test(`getOrders should return a list of orders > ${side}`, async () => { const { orders, next, previous } = await mainAPI.getOrders({ - protocol: "seaport", + protocol: OrderProtocol.SEAPORT, side, tokenIds: BAYC_TOKEN_IDS, assetContractAddress: BAYC_CONTRACT_ADDRESS, }); - orders.map((order) => expectValidOrder(order)); - expect(next).to.not.be.undefined; - expect(previous).to.not.be.undefined; + expect(orders).to.be.an("array"); + orders.forEach((order) => { + if (order) { + expectValidOrder(order); + } + }); + // Pagination fields may be undefined based on results + if (next) { + expect(next).to.be.a("string"); + } + if (previous) { + expect(previous).to.be.a("string"); + } }); }); }); diff --git a/test/utils/constants.ts b/test/utils/constants.ts index 70c5147e0..526a24b65 100644 --- a/test/utils/constants.ts +++ b/test/utils/constants.ts @@ -1,5 +1,7 @@ +import { expect } from "chai"; import { ethers } from "ethers"; import { OpenSeaAPI } from "../../src/api"; +import { OrderV2 } from "../../src/orders/types"; import { Chain } from "../../src/types"; export const MAINNET_API_KEY = process.env.OPENSEA_API_KEY; @@ -35,3 +37,13 @@ export const testnetAPI = new OpenSeaAPI( }, process.env.DEBUG ? console.log : undefined, ); + +export const expectValidOrder = (order: OrderV2) => { + expect(order).to.have.property("orderHash"); + expect(order).to.have.property("protocolData"); + expect(order).to.have.property("protocolAddress"); + expect(order).to.have.property("currentPrice"); + expect(order).to.have.property("maker"); + expect(order).to.have.property("side"); + expect(order).to.have.property("orderType"); +};