import type { FulfillmentDataResponse, ProtocolData } from "../orders/types" import { getFulfillListingPayload, getFulfillmentDataPath, getFulfillOfferPayload, } from "../orders/utils" import { type Chain, OrderSide } from "../types" import type { Camelize } from "../utils/case" import { getCancelOrderPath, getOrderByHashPath, getPostListingPath, getPostOfferPath, } from "./apiPaths" import type { Fetcher } from "./fetcher" import type { CancelOrderResponse, GetOrderByHashResponse, Listing, Offer, } from "./types" /** * Order-related API operations */ export class OrdersAPI { constructor( private fetcher: Fetcher, private chain: Chain, ) {} /** * Gets a single order by its order hash. * Returns the raw API response which can be either an Offer or Listing. */ async getOrderByHash( orderHash: string, protocolAddress: string, chain: Chain = this.chain, ): Promise { const response = await this.fetcher.get<{ order: GetOrderByHashResponse }>(getOrderByHashPath(chain, protocolAddress, orderHash)) return response.order } /** * Generate the data needed to fulfill a listing or an offer onchain. */ async generateFulfillmentData( fulfillerAddress: string, orderHash: string, protocolAddress: string, side: OrderSide, assetContractAddress?: string, tokenId?: string, unitsToFill?: string, recipientAddress?: string, includeOptionalCreatorFees: boolean = false, ): Promise> { const payload = side === OrderSide.LISTING ? getFulfillListingPayload( fulfillerAddress, orderHash, protocolAddress, this.chain, assetContractAddress, tokenId, unitsToFill, recipientAddress, includeOptionalCreatorFees, ) : getFulfillOfferPayload( fulfillerAddress, orderHash, protocolAddress, this.chain, assetContractAddress, tokenId, unitsToFill, includeOptionalCreatorFees, ) return this.fetcher.post( getFulfillmentDataPath(side), payload, ) } /** * Post a listing to OpenSea. * * The body uses mixed casing per the OpenSea OpenAPI spec: the outer * envelope (`protocol_address`) is snake_case, but the inner Seaport * struct (`parameters.startTime`, `parameters.orderType`, `offer[].itemType`, * etc.) mirrors the on-chain struct names and must stay camelCase on the * wire. We pass `snakeizeBody: false` and emit the body in exact wire shape. */ async postListing( order: ProtocolData, protocolAddress: string, ): Promise { if (!order) { throw new Error("order data is required") } if (!protocolAddress || !/^0x[a-fA-F0-9]{40}$/.test(protocolAddress)) { throw new Error("Invalid protocol address format") } return this.fetcher.post( getPostListingPath(this.chain, "seaport"), { ...order, protocol_address: protocolAddress, }, undefined, { snakeizeBody: false }, ) } /** * Post an offer to OpenSea. * * Same mixed-casing wire shape as {@link postListing}: outer * `protocol_address` is snake_case but the inner Seaport struct stays * camelCase. We opt out of automatic snakeize so `parameters` is sent * verbatim. */ async postOffer( order: ProtocolData, protocolAddress: string, ): Promise { if (!order) { throw new Error("order data is required") } if (!protocolAddress || !/^0x[a-fA-F0-9]{40}$/.test(protocolAddress)) { throw new Error("Invalid protocol address format") } return this.fetcher.post( getPostOfferPath(this.chain, "seaport"), { ...order, protocol_address: protocolAddress, }, undefined, { snakeizeBody: false }, ) } /** * Offchain cancel an order, offer or listing, by its order hash when protected by the SignedZone. * * The OpenSea OpenAPI `CancelRequest` schema uses camelCase * `offererSignature`, so we opt out of automatic snakeize. */ async offchainCancelOrder( protocolAddress: string, orderHash: string, chain: Chain = this.chain, offererSignature?: string, ): Promise { return this.fetcher.post( getCancelOrderPath(chain, protocolAddress, orderHash), { offererSignature }, undefined, { snakeizeBody: false }, ) } }