From f9317030b66200f967a7b93e6de39f4e8ac917c6 Mon Sep 17 00:00:00 2001 From: Leslie Fung Date: Wed, 18 Sep 2024 11:56:33 +1000 Subject: [PATCH] refactor: [TD-1668] Update `orderbook` OpenAPI SDK (#2152) --- packages/orderbook/src/openapi/mapper.ts | 237 +++++++++- packages/orderbook/src/openapi/sdk/index.ts | 9 + .../openapi/sdk/models/AssetCollectionItem.ts | 9 + .../src/openapi/sdk/models/BidResult.ts | 10 + .../openapi/sdk/models/CollectionBidResult.ts | 10 + .../sdk/models/CreateBidRequestBody.ts | 42 ++ .../models/CreateCollectionBidRequestBody.ts | 43 ++ .../sdk/models/ERC1155CollectionItem.ts | 19 + .../sdk/models/ERC721CollectionItem.ts | 19 + .../sdk/models/FulfillmentDataRequest.ts | 4 + .../orderbook/src/openapi/sdk/models/Item.ts | 4 +- .../src/openapi/sdk/models/ListBidsResult.ts | 12 + .../sdk/models/ListCollectionBidsResult.ts | 12 + .../orderbook/src/openapi/sdk/models/Order.ts | 2 + .../src/openapi/sdk/services/OrdersService.ts | 302 ++++++++++++ packages/orderbook/src/orderbook.ts | 136 +++--- .../src/seaport/map-to-seaport-order.ts | 214 ++++++--- .../orderbook/src/seaport/seaport.test.ts | 9 +- packages/orderbook/src/seaport/seaport.ts | 323 ++++++++----- packages/orderbook/src/types.ts | 443 ++++++++++-------- packages/orderbook/src/utils.ts | 4 + sdk/package.json | 1 + tests/func-tests/zkevm/README.md | 11 +- .../{TestToken.sol => TestERC721Token.sol} | 4 +- tests/func-tests/zkevm/package.json | 1 + .../zkevm/step-definitions/order.steps.ts | 3 +- .../zkevm/utils/orderbook/config.ts | 5 +- .../zkevm/utils/orderbook/erc1155.ts | 9 +- .../zkevm/utils/orderbook/erc721.ts | 18 +- yarn.lock | 1 + 30 files changed, 1432 insertions(+), 484 deletions(-) create mode 100644 packages/orderbook/src/openapi/sdk/models/AssetCollectionItem.ts create mode 100644 packages/orderbook/src/openapi/sdk/models/BidResult.ts create mode 100644 packages/orderbook/src/openapi/sdk/models/CollectionBidResult.ts create mode 100644 packages/orderbook/src/openapi/sdk/models/CreateBidRequestBody.ts create mode 100644 packages/orderbook/src/openapi/sdk/models/CreateCollectionBidRequestBody.ts create mode 100644 packages/orderbook/src/openapi/sdk/models/ERC1155CollectionItem.ts create mode 100644 packages/orderbook/src/openapi/sdk/models/ERC721CollectionItem.ts create mode 100644 packages/orderbook/src/openapi/sdk/models/ListBidsResult.ts create mode 100644 packages/orderbook/src/openapi/sdk/models/ListCollectionBidsResult.ts create mode 100644 packages/orderbook/src/utils.ts rename tests/func-tests/zkevm/contracts/{TestToken.sol => TestERC721Token.sol} (87%) diff --git a/packages/orderbook/src/openapi/mapper.ts b/packages/orderbook/src/openapi/mapper.ts index 02eb2514ca..f61b6fd364 100644 --- a/packages/orderbook/src/openapi/mapper.ts +++ b/packages/orderbook/src/openapi/mapper.ts @@ -1,19 +1,57 @@ -import { Order as OpenApiOrder } from './sdk/models/Order'; -import { Trade as OpenApiTrade } from './sdk/models/Trade'; -import { Page as OpenApiPage } from './sdk/models/Page'; import { + Bid, + CollectionBid, + ERC1155CollectionItem, + ERC1155Item, ERC20Item, - NativeItem, - Order, + ERC721CollectionItem, ERC721Item, FeeType, + Listing, + NativeItem, + Order, Page, Trade, - ERC1155Item, } from '../types'; +import { exhaustiveSwitch } from '../utils'; +import { Order as OpenApiOrder } from './sdk/models/Order'; +import { Page as OpenApiPage } from './sdk/models/Page'; +import { Trade as OpenApiTrade } from './sdk/models/Trade'; + +export function mapListingFromOpenApiOrder(order: OpenApiOrder): Listing { + if (order.type !== OpenApiOrder.type.LISTING) { + throw new Error('Order type must be LISTING'); + } + + const sellItems: (ERC721Item | ERC1155Item)[] = order.sell.map((item) => { + if (item.type === 'ERC721') { + return { + type: 'ERC721', + contractAddress: item.contract_address, + tokenId: item.token_id, + }; + } + + if (item.type === 'ERC1155') { + return { + type: 'ERC1155', + contractAddress: item.contract_address, + tokenId: item.token_id, + amount: item.amount, + }; + } + + throw new Error('Listing sell items must either ERC721 or ERC1155'); + }); -export function mapFromOpenApiOrder(order: OpenApiOrder): Order { const buyItems: (ERC20Item | NativeItem)[] = order.buy.map((item) => { + if (item.type === 'NATIVE') { + return { + type: 'NATIVE', + amount: item.amount, + }; + } + if (item.type === 'ERC20') { return { type: 'ERC20', @@ -22,17 +60,58 @@ export function mapFromOpenApiOrder(order: OpenApiOrder): Order { }; } - if (item.type === 'NATIVE') { + throw new Error('Listing buy items must be either NATIVE or ERC20'); + }); + + return { + id: order.id, + type: order.type, + chain: order.chain, + accountAddress: order.account_address, + sell: sellItems, + buy: buyItems, + fees: order.fees.map((fee) => ({ + amount: fee.amount, + recipientAddress: fee.recipient_address, + type: fee.type as unknown as FeeType, + })), + status: order.status, + fillStatus: order.fill_status, + startAt: order.start_at, + endAt: order.end_at, + salt: order.salt, + signature: order.signature, + orderHash: order.order_hash, + protocolData: { + orderType: order.protocol_data.order_type, + counter: order.protocol_data.counter, + seaportAddress: order.protocol_data.seaport_address, + seaportVersion: order.protocol_data.seaport_version, + zoneAddress: order.protocol_data.zone_address, + }, + createdAt: order.created_at, + updatedAt: order.updated_at, + }; +} + +export function mapBidFromOpenApiOrder(order: OpenApiOrder): Bid { + if (order.type !== OpenApiOrder.type.BID) { + throw new Error('Order type must be BID'); + } + + const sellItems: ERC20Item[] = order.buy.map((item) => { + if (item.type === 'ERC20') { return { - type: 'NATIVE', + type: 'ERC20', + contractAddress: item.contract_address, amount: item.amount, }; } - throw new Error('Buy items must be either ERC20 or NATIVE'); + throw new Error('Bid sell items must be ERC20'); }); - const sellItems: (ERC721Item | ERC1155Item)[] = order.sell.map((item) => { + const buyItems: (ERC721Item | ERC1155Item)[] = order.sell.map((item) => { if (item.type === 'ERC721') { return { type: 'ERC721', @@ -50,24 +129,27 @@ export function mapFromOpenApiOrder(order: OpenApiOrder): Order { }; } - throw new Error('Sell items must ERC721 or ERC1155'); + throw new Error('Bid buy items must either ERC721 or ERC1155'); }); return { id: order.id, type: order.type, + chain: order.chain, accountAddress: order.account_address, - buy: buyItems, sell: sellItems, + buy: buyItems, fees: order.fees.map((fee) => ({ amount: fee.amount, recipientAddress: fee.recipient_address, type: fee.type as unknown as FeeType, })), + status: order.status, fillStatus: order.fill_status, - chain: order.chain, - createdAt: order.created_at, + startAt: order.start_at, endAt: order.end_at, + salt: order.salt, + signature: order.signature, orderHash: order.order_hash, protocolData: { orderType: order.protocol_data.order_type, @@ -76,16 +158,17 @@ export function mapFromOpenApiOrder(order: OpenApiOrder): Order { seaportVersion: order.protocol_data.seaport_version, zoneAddress: order.protocol_data.zone_address, }, - salt: order.salt, - signature: order.signature, - startAt: order.start_at, - status: order.status, + createdAt: order.created_at, updatedAt: order.updated_at, }; } -export function mapFromOpenApiTrade(trade: OpenApiTrade): Trade { - const buyItems: (ERC20Item | NativeItem)[] = trade.buy.map((item) => { +export function mapCollectionBidFromOpenApiOrder(order: OpenApiOrder): CollectionBid { + if (order.type !== OpenApiOrder.type.COLLECTION_BID) { + throw new Error('Order type must be COLLECTION_BID'); + } + + const sellItems: ERC20Item[] = order.buy.map((item) => { if (item.type === 'ERC20') { return { type: 'ERC20', @@ -94,6 +177,75 @@ export function mapFromOpenApiTrade(trade: OpenApiTrade): Trade { }; } + throw new Error('Collection bid sell items must be ERC20'); + }); + + const buyItems: (ERC721CollectionItem | ERC1155CollectionItem)[] = order.sell.map((item) => { + if (item.type === 'ERC721_COLLECTION') { + return { + type: 'ERC721_COLLECTION', + contractAddress: item.contract_address, + amount: item.amount, + }; + } + + if (item.type === 'ERC1155_COLLECTION') { + return { + type: 'ERC1155_COLLECTION', + contractAddress: item.contract_address, + amount: item.amount, + }; + } + + throw new Error('Collection bid buy items must either ERC721_COLLECTION or ERC1155_COLLECTION'); + }); + + return { + id: order.id, + type: order.type, + chain: order.chain, + accountAddress: order.account_address, + sell: sellItems, + buy: buyItems, + fees: order.fees.map((fee) => ({ + amount: fee.amount, + recipientAddress: fee.recipient_address, + type: fee.type as unknown as FeeType, + })), + status: order.status, + fillStatus: order.fill_status, + startAt: order.start_at, + endAt: order.end_at, + salt: order.salt, + signature: order.signature, + orderHash: order.order_hash, + protocolData: { + orderType: order.protocol_data.order_type, + counter: order.protocol_data.counter, + seaportAddress: order.protocol_data.seaport_address, + seaportVersion: order.protocol_data.seaport_version, + zoneAddress: order.protocol_data.zone_address, + }, + createdAt: order.created_at, + updatedAt: order.updated_at, + }; +} + +export function mapOrderFromOpenApiOrder(order: OpenApiOrder): Order { + switch (order.type) { + case OpenApiOrder.type.LISTING: + return mapListingFromOpenApiOrder(order); + case OpenApiOrder.type.BID: + return mapBidFromOpenApiOrder(order); + case OpenApiOrder.type.COLLECTION_BID: + return mapCollectionBidFromOpenApiOrder(order); + default: + return exhaustiveSwitch(order.type); + } +} + +export function mapFromOpenApiTrade(trade: OpenApiTrade): Trade { + const buyItems: (ERC20Item | NativeItem | ERC721Item | ERC1155Item)[] = trade.buy.map((item) => { if (item.type === 'NATIVE') { return { type: 'NATIVE', @@ -101,10 +253,14 @@ export function mapFromOpenApiTrade(trade: OpenApiTrade): Trade { }; } - throw new Error('Buy items must be either ERC20 or NATIVE'); - }); + if (item.type === 'ERC20') { + return { + type: 'ERC20', + contractAddress: item.contract_address, + amount: item.amount, + }; + } - const sellItems: (ERC721Item | ERC1155Item)[] = trade.sell.map((item) => { if (item.type === 'ERC721') { return { type: 'ERC721', @@ -122,9 +278,40 @@ export function mapFromOpenApiTrade(trade: OpenApiTrade): Trade { }; } - throw new Error('Sell items must ERC721'); + throw new Error('Buy items must be NATIVE, ERC20, ERC721 or ERC1155'); }); + const sellItems: (ERC20Item | ERC721Item | ERC1155Item)[] = trade.sell.map( + (item) => { + if (item.type === 'ERC20') { + return { + type: 'ERC20', + contractAddress: item.contract_address, + amount: item.amount, + }; + } + + if (item.type === 'ERC721') { + return { + type: 'ERC721', + contractAddress: item.contract_address, + tokenId: item.token_id, + }; + } + + if (item.type === 'ERC1155') { + return { + type: 'ERC1155', + contractAddress: item.contract_address, + tokenId: item.token_id, + amount: item.amount, + }; + } + + throw new Error('Sell items must be ERC20, ERC721 or ERC1155'); + }, + ); + return { id: trade.id, orderId: trade.order_id, diff --git a/packages/orderbook/src/openapi/sdk/index.ts b/packages/orderbook/src/openapi/sdk/index.ts index ccc881a689..339c2b7eef 100644 --- a/packages/orderbook/src/openapi/sdk/index.ts +++ b/packages/orderbook/src/openapi/sdk/index.ts @@ -10,15 +10,22 @@ export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; export type { ActiveOrderStatus } from './models/ActiveOrderStatus'; +export type { AssetCollectionItem } from './models/AssetCollectionItem'; +export type { BidResult } from './models/BidResult'; export { CancelledOrderStatus } from './models/CancelledOrderStatus'; export type { CancelOrdersRequestBody } from './models/CancelOrdersRequestBody'; export type { CancelOrdersResult } from './models/CancelOrdersResult'; export type { CancelOrdersResultData } from './models/CancelOrdersResultData'; export type { Chain } from './models/Chain'; export type { ChainName } from './models/ChainName'; +export type { CollectionBidResult } from './models/CollectionBidResult'; +export type { CreateBidRequestBody } from './models/CreateBidRequestBody'; +export type { CreateCollectionBidRequestBody } from './models/CreateCollectionBidRequestBody'; export type { CreateListingRequestBody } from './models/CreateListingRequestBody'; +export type { ERC1155CollectionItem } from './models/ERC1155CollectionItem'; export type { ERC1155Item } from './models/ERC1155Item'; export type { ERC20Item } from './models/ERC20Item'; +export type { ERC721CollectionItem } from './models/ERC721CollectionItem'; export type { ERC721Item } from './models/ERC721Item'; export type { Error } from './models/Error'; export type { ExpiredOrderStatus } from './models/ExpiredOrderStatus'; @@ -30,6 +37,8 @@ export type { FulfillableOrder } from './models/FulfillableOrder'; export type { FulfillmentDataRequest } from './models/FulfillmentDataRequest'; export type { InactiveOrderStatus } from './models/InactiveOrderStatus'; export type { Item } from './models/Item'; +export type { ListBidsResult } from './models/ListBidsResult'; +export type { ListCollectionBidsResult } from './models/ListCollectionBidsResult'; export type { ListingResult } from './models/ListingResult'; export type { ListListingsResult } from './models/ListListingsResult'; export type { ListTradeResult } from './models/ListTradeResult'; diff --git a/packages/orderbook/src/openapi/sdk/models/AssetCollectionItem.ts b/packages/orderbook/src/openapi/sdk/models/AssetCollectionItem.ts new file mode 100644 index 0000000000..9c7f056ecb --- /dev/null +++ b/packages/orderbook/src/openapi/sdk/models/AssetCollectionItem.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ERC1155CollectionItem } from './ERC1155CollectionItem'; +import type { ERC721CollectionItem } from './ERC721CollectionItem'; + +export type AssetCollectionItem = (ERC721CollectionItem | ERC1155CollectionItem); + diff --git a/packages/orderbook/src/openapi/sdk/models/BidResult.ts b/packages/orderbook/src/openapi/sdk/models/BidResult.ts new file mode 100644 index 0000000000..36a76b5c59 --- /dev/null +++ b/packages/orderbook/src/openapi/sdk/models/BidResult.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Order } from './Order'; + +export type BidResult = { + result: Order; +}; + diff --git a/packages/orderbook/src/openapi/sdk/models/CollectionBidResult.ts b/packages/orderbook/src/openapi/sdk/models/CollectionBidResult.ts new file mode 100644 index 0000000000..4e351a70dd --- /dev/null +++ b/packages/orderbook/src/openapi/sdk/models/CollectionBidResult.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Order } from './Order'; + +export type CollectionBidResult = { + result: Order; +}; + diff --git a/packages/orderbook/src/openapi/sdk/models/CreateBidRequestBody.ts b/packages/orderbook/src/openapi/sdk/models/CreateBidRequestBody.ts new file mode 100644 index 0000000000..4bbb97fd1d --- /dev/null +++ b/packages/orderbook/src/openapi/sdk/models/CreateBidRequestBody.ts @@ -0,0 +1,42 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Fee } from './Fee'; +import type { Item } from './Item'; +import type { ProtocolData } from './ProtocolData'; + +export type CreateBidRequestBody = { + account_address: string; + order_hash: string; + /** + * Buy item for listing should either be ERC721 or ERC1155 item + */ + buy: Array; + /** + * Buy fees should only include maker marketplace fees and should be no more than two entries as more entires will incur more gas. It is best practice to have this as few as possible. + */ + fees: Array; + /** + * Time after which the Order is considered expired + */ + end_at: string; + protocol_data: ProtocolData; + /** + * A random value added to the create Order request + */ + salt: string; + /** + * Sell item for listing should be an ERC20 item + */ + sell: Array; + /** + * Digital signature generated by the user for the specific Order + */ + signature: string; + /** + * Time after which Order is considered active + */ + start_at: string; +}; + diff --git a/packages/orderbook/src/openapi/sdk/models/CreateCollectionBidRequestBody.ts b/packages/orderbook/src/openapi/sdk/models/CreateCollectionBidRequestBody.ts new file mode 100644 index 0000000000..2058cf4380 --- /dev/null +++ b/packages/orderbook/src/openapi/sdk/models/CreateCollectionBidRequestBody.ts @@ -0,0 +1,43 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { AssetCollectionItem } from './AssetCollectionItem'; +import type { ERC20Item } from './ERC20Item'; +import type { Fee } from './Fee'; +import type { ProtocolData } from './ProtocolData'; + +export type CreateCollectionBidRequestBody = { + account_address: string; + order_hash: string; + /** + * Buy item for listing should either be ERC721 or ERC1155 collection item + */ + buy: Array; + /** + * Buy fees should only include maker marketplace fees and should be no more than two entries as more entires will incur more gas. It is best practice to have this as few as possible. + */ + fees: Array; + /** + * Time after which the Order is considered expired + */ + end_at: string; + protocol_data: ProtocolData; + /** + * A random value added to the create Order request + */ + salt: string; + /** + * Sell item for listing should be an ERC20 item + */ + sell: Array; + /** + * Digital signature generated by the user for the specific Order + */ + signature: string; + /** + * Time after which Order is considered active + */ + start_at: string; +}; + diff --git a/packages/orderbook/src/openapi/sdk/models/ERC1155CollectionItem.ts b/packages/orderbook/src/openapi/sdk/models/ERC1155CollectionItem.ts new file mode 100644 index 0000000000..a4b6dbded1 --- /dev/null +++ b/packages/orderbook/src/openapi/sdk/models/ERC1155CollectionItem.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ERC1155CollectionItem = { + /** + * Token type user is offering, which in this case is ERC1155 + */ + type: 'ERC1155_COLLECTION'; + /** + * Address of ERC1155 collection + */ + contract_address: string; + /** + * A string representing the price at which the user is willing to sell the token. This value is provided in the smallest unit of the token (e.g., wei for Ethereum). + */ + amount: string; +}; + diff --git a/packages/orderbook/src/openapi/sdk/models/ERC721CollectionItem.ts b/packages/orderbook/src/openapi/sdk/models/ERC721CollectionItem.ts new file mode 100644 index 0000000000..b2d9b6b612 --- /dev/null +++ b/packages/orderbook/src/openapi/sdk/models/ERC721CollectionItem.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ERC721CollectionItem = { + /** + * Token type user is offering, which in this case is ERC721 + */ + type: 'ERC721_COLLECTION'; + /** + * Address of ERC721 collection + */ + contract_address: string; + /** + * A string representing the price at which the user is willing to sell the token. This value is provided in the smallest unit of the token (e.g., wei for Ethereum). + */ + amount: string; +}; + diff --git a/packages/orderbook/src/openapi/sdk/models/FulfillmentDataRequest.ts b/packages/orderbook/src/openapi/sdk/models/FulfillmentDataRequest.ts index e2a57b46e7..4d95e9d099 100644 --- a/packages/orderbook/src/openapi/sdk/models/FulfillmentDataRequest.ts +++ b/packages/orderbook/src/openapi/sdk/models/FulfillmentDataRequest.ts @@ -11,5 +11,9 @@ export type FulfillmentDataRequest = { */ taker_address: string; fees: Array; + /** + * Token ID for the ERC721 or ERC1155 token + */ + token_id?: string; }; diff --git a/packages/orderbook/src/openapi/sdk/models/Item.ts b/packages/orderbook/src/openapi/sdk/models/Item.ts index 21e0f6fcef..958cede280 100644 --- a/packages/orderbook/src/openapi/sdk/models/Item.ts +++ b/packages/orderbook/src/openapi/sdk/models/Item.ts @@ -2,10 +2,12 @@ /* tslint:disable */ /* eslint-disable */ +import type { ERC1155CollectionItem } from './ERC1155CollectionItem'; import type { ERC1155Item } from './ERC1155Item'; import type { ERC20Item } from './ERC20Item'; +import type { ERC721CollectionItem } from './ERC721CollectionItem'; import type { ERC721Item } from './ERC721Item'; import type { NativeItem } from './NativeItem'; -export type Item = (NativeItem | ERC20Item | ERC721Item | ERC1155Item); +export type Item = (NativeItem | ERC20Item | ERC721Item | ERC1155Item | ERC721CollectionItem | ERC1155CollectionItem); diff --git a/packages/orderbook/src/openapi/sdk/models/ListBidsResult.ts b/packages/orderbook/src/openapi/sdk/models/ListBidsResult.ts new file mode 100644 index 0000000000..92326e56a6 --- /dev/null +++ b/packages/orderbook/src/openapi/sdk/models/ListBidsResult.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Order } from './Order'; +import type { Page } from './Page'; + +export type ListBidsResult = { + page: Page; + result: Array; +}; + diff --git a/packages/orderbook/src/openapi/sdk/models/ListCollectionBidsResult.ts b/packages/orderbook/src/openapi/sdk/models/ListCollectionBidsResult.ts new file mode 100644 index 0000000000..13c63a2b31 --- /dev/null +++ b/packages/orderbook/src/openapi/sdk/models/ListCollectionBidsResult.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Order } from './Order'; +import type { Page } from './Page'; + +export type ListCollectionBidsResult = { + page: Page; + result: Array; +}; + diff --git a/packages/orderbook/src/openapi/sdk/models/Order.ts b/packages/orderbook/src/openapi/sdk/models/Order.ts index eb12e694f7..21042f819d 100644 --- a/packages/orderbook/src/openapi/sdk/models/Order.ts +++ b/packages/orderbook/src/openapi/sdk/models/Order.ts @@ -60,6 +60,8 @@ export namespace Order { */ export enum type { LISTING = 'LISTING', + BID = 'BID', + COLLECTION_BID = 'COLLECTION_BID', } diff --git a/packages/orderbook/src/openapi/sdk/services/OrdersService.ts b/packages/orderbook/src/openapi/sdk/services/OrdersService.ts index b24368cc32..87b3362512 100644 --- a/packages/orderbook/src/openapi/sdk/services/OrdersService.ts +++ b/packages/orderbook/src/openapi/sdk/services/OrdersService.ts @@ -1,12 +1,18 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { BidResult } from '../models/BidResult'; import type { CancelOrdersRequestBody } from '../models/CancelOrdersRequestBody'; import type { CancelOrdersResult } from '../models/CancelOrdersResult'; import type { ChainName } from '../models/ChainName'; +import type { CollectionBidResult } from '../models/CollectionBidResult'; +import type { CreateBidRequestBody } from '../models/CreateBidRequestBody'; +import type { CreateCollectionBidRequestBody } from '../models/CreateCollectionBidRequestBody'; import type { CreateListingRequestBody } from '../models/CreateListingRequestBody'; import type { FulfillableOrder } from '../models/FulfillableOrder'; import type { FulfillmentDataRequest } from '../models/FulfillmentDataRequest'; +import type { ListBidsResult } from '../models/ListBidsResult'; +import type { ListCollectionBidsResult } from '../models/ListCollectionBidsResult'; import type { ListingResult } from '../models/ListingResult'; import type { ListListingsResult } from '../models/ListListingsResult'; import type { ListTradeResult } from '../models/ListTradeResult'; @@ -183,6 +189,240 @@ export class OrdersService { }); } + /** + * List a paginated array of bids with optional filter parameters + * List a paginated array of bids with optional filter parameters + * @returns ListBidsResult OK response. + * @throws ApiError + */ + public listBids({ + chainName, + status, + buyItemContractAddress, + sellItemContractAddress, + accountAddress, + buyItemMetadataId, + buyItemTokenId, + fromUpdatedAt, + pageSize, + sortBy, + sortDirection, + pageCursor, + }: { + chainName: ChainName, + /** + * Order status to filter by + */ + status?: OrderStatusName, + /** + * Buy item contract address to filter by + */ + buyItemContractAddress?: string, + /** + * Sell item contract address to filter by + */ + sellItemContractAddress?: string, + /** + * The account address of the user who created the bid + */ + accountAddress?: string, + /** + * The metadata_id of the buy item + */ + buyItemMetadataId?: string, + /** + * buy item token identifier to filter by + */ + buyItemTokenId?: string, + /** + * From updated at including given date + */ + fromUpdatedAt?: string, + /** + * Maximum number of orders to return per page + */ + pageSize?: PageSize, + /** + * Order field to sort by. `sell_item_amount` sorts by per token price, for example if 10eth is offered for 5 ERC1155 items, it’s sorted as 2eth for `sell_item_amount`. + */ + sortBy?: 'created_at' | 'updated_at' | 'sell_item_amount', + /** + * Ascending or descending direction for sort + */ + sortDirection?: 'asc' | 'desc', + /** + * Page cursor to retrieve previous or next page. Use the value returned in the response. + */ + pageCursor?: PageCursor, + }): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/v1/chains/{chain_name}/orders/bids', + path: { + 'chain_name': chainName, + }, + query: { + 'status': status, + 'buy_item_contract_address': buyItemContractAddress, + 'sell_item_contract_address': sellItemContractAddress, + 'account_address': accountAddress, + 'buy_item_metadata_id': buyItemMetadataId, + 'buy_item_token_id': buyItemTokenId, + 'from_updated_at': fromUpdatedAt, + 'page_size': pageSize, + 'sort_by': sortBy, + 'sort_direction': sortDirection, + 'page_cursor': pageCursor, + }, + errors: { + 400: `Bad Request (400)`, + 404: `The specified resource was not found (404)`, + 500: `Internal Server Error (500)`, + }, + }); + } + + /** + * Create a bid + * Create a bid + * @returns BidResult Created response. + * @throws ApiError + */ + public createBid({ + chainName, + requestBody, + }: { + chainName: ChainName, + requestBody: CreateBidRequestBody, + }): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/v1/chains/{chain_name}/orders/bids', + path: { + 'chain_name': chainName, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Bad Request (400)`, + 404: `The specified resource was not found (404)`, + 500: `Internal Server Error (500)`, + 501: `Not Implemented Error (501)`, + }, + }); + } + + /** + * List a paginated array of collection bids with optional filter parameters + * List a paginated array of collection bids with optional filter parameters + * @returns ListCollectionBidsResult OK response. + * @throws ApiError + */ + public listCollectionBids({ + chainName, + status, + buyItemContractAddress, + sellItemContractAddress, + accountAddress, + fromUpdatedAt, + pageSize, + sortBy, + sortDirection, + pageCursor, + }: { + chainName: ChainName, + /** + * Order status to filter by + */ + status?: OrderStatusName, + /** + * Buy item contract address to filter by + */ + buyItemContractAddress?: string, + /** + * Sell item contract address to filter by + */ + sellItemContractAddress?: string, + /** + * The account address of the user who created the bid + */ + accountAddress?: string, + /** + * From updated at including given date + */ + fromUpdatedAt?: string, + /** + * Maximum number of orders to return per page + */ + pageSize?: PageSize, + /** + * Order field to sort by. `sell_item_amount` sorts by per token price, for example if 10eth is offered for 5 ERC1155 items, it’s sorted as 2eth for `sell_item_amount`. + */ + sortBy?: 'created_at' | 'updated_at' | 'sell_item_amount', + /** + * Ascending or descending direction for sort + */ + sortDirection?: 'asc' | 'desc', + /** + * Page cursor to retrieve previous or next page. Use the value returned in the response. + */ + pageCursor?: PageCursor, + }): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/v1/chains/{chain_name}/orders/collection-bids', + path: { + 'chain_name': chainName, + }, + query: { + 'status': status, + 'buy_item_contract_address': buyItemContractAddress, + 'sell_item_contract_address': sellItemContractAddress, + 'account_address': accountAddress, + 'from_updated_at': fromUpdatedAt, + 'page_size': pageSize, + 'sort_by': sortBy, + 'sort_direction': sortDirection, + 'page_cursor': pageCursor, + }, + errors: { + 400: `Bad Request (400)`, + 404: `The specified resource was not found (404)`, + 500: `Internal Server Error (500)`, + }, + }); + } + + /** + * Create a collection bid + * Create a collection bid + * @returns CollectionBidResult Created response. + * @throws ApiError + */ + public createCollectionBid({ + chainName, + requestBody, + }: { + chainName: ChainName, + requestBody: CreateCollectionBidRequestBody, + }): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/v1/chains/{chain_name}/orders/collection-bids', + path: { + 'chain_name': chainName, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Bad Request (400)`, + 404: `The specified resource was not found (404)`, + 500: `Internal Server Error (500)`, + 501: `Not Implemented Error (501)`, + }, + }); + } + /** * Get a single listing by ID * Get a single listing by ID @@ -214,6 +454,68 @@ export class OrdersService { }); } + /** + * Get a single bid by ID + * Get a single bid by ID + * @returns BidResult OK response. + * @throws ApiError + */ + public getBid({ + chainName, + bidId, + }: { + chainName: ChainName, + /** + * Global Bid identifier + */ + bidId: string, + }): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/v1/chains/{chain_name}/orders/bids/{bid_id}', + path: { + 'chain_name': chainName, + 'bid_id': bidId, + }, + errors: { + 400: `Bad Request (400)`, + 404: `The specified resource was not found (404)`, + 500: `Internal Server Error (500)`, + }, + }); + } + + /** + * Get a single collection bid by ID + * Get a single collection bid by ID + * @returns CollectionBidResult OK response. + * @throws ApiError + */ + public getCollectionBid({ + chainName, + collectionBidId, + }: { + chainName: ChainName, + /** + * Global Collection Bid identifier + */ + collectionBidId: string, + }): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/v1/chains/{chain_name}/orders/collection-bids/{collection_bid_id}', + path: { + 'chain_name': chainName, + 'collection_bid_id': collectionBidId, + }, + errors: { + 400: `Bad Request (400)`, + 404: `The specified resource was not found (404)`, + 500: `Internal Server Error (500)`, + }, + }); + } + /** * Retrieve fulfillment data for orders * Retrieve signed fulfillment data based on the list of order IDs and corresponding fees. diff --git a/packages/orderbook/src/orderbook.ts b/packages/orderbook/src/orderbook.ts index 74b5a7e039..49c57345f3 100644 --- a/packages/orderbook/src/orderbook.ts +++ b/packages/orderbook/src/orderbook.ts @@ -7,23 +7,25 @@ import { OrderbookModuleConfiguration, OrderbookOverrides, } from './config/config'; -import { CancelOrdersResult, Fee as OpenApiFee } from './openapi/sdk'; import { - mapFromOpenApiOrder, mapFromOpenApiPage, mapFromOpenApiTrade, + mapListingFromOpenApiOrder, + mapOrderFromOpenApiOrder, } from './openapi/mapper'; +import { CancelOrdersResult, Fee as OpenApiFee } from './openapi/sdk'; import { Seaport } from './seaport'; import { getBulkSeaportOrderSignatures } from './seaport/components'; import { SeaportLibFactory } from './seaport/seaport-lib-factory'; import { + Action, ActionType, CancelOrdersOnChainResponse, CreateListingParams, - FeeType, FeeValue, FulfillBulkOrdersResponse, FulfillmentListing, + FulfillmentOrder, FulfillOrderResponse, ListingResult, ListListingsParams, @@ -31,13 +33,13 @@ import { ListTradesParams, ListTradesResult, OrderStatusName, - PrepareCancelOrdersResponse, - PrepareListingParams, PrepareBulkListingsParams, PrepareBulkListingsResponse, + PrepareCancelOrdersResponse, + PrepareListingParams, PrepareListingResponse, SignablePurpose, - TradeResult, Action, + TradeResult, } from './types'; /** @@ -114,34 +116,34 @@ export class Orderbook { } /** - * Get an order by ID + * Get a listing by ID * @param {string} listingId - The listingId to find. - * @return {ListingResult} The returned order result. + * @return {ListingResult} The returned listing result. */ async getListing(listingId: string): Promise { const apiListing = await this.apiClient.getListing(listingId); return { - result: mapFromOpenApiOrder(apiListing.result), + result: mapListingFromOpenApiOrder(apiListing.result), }; } /** * Get a trade by ID * @param {string} tradeId - The tradeId to find. - * @return {TradeResult} The returned order result. + * @return {TradeResult} The returned trade result. */ async getTrade(tradeId: string): Promise { - const apiListing = await this.apiClient.getTrade(tradeId); + const apiTrade = await this.apiClient.getTrade(tradeId); return { - result: mapFromOpenApiTrade(apiListing.result), + result: mapFromOpenApiTrade(apiTrade.result), }; } /** - * List orders. This method is used to get a list of orders filtered by conditions specified + * List listings. This method is used to get a list of listings filtered by conditions specified * in the params object. * @param {ListListingsParams} listOrderParams - Filtering, ordering and page parameters. - * @return {ListListingsResult} The paged orders. + * @return {ListListingsResult} The paged listings. */ async listListings( listOrderParams: ListListingsParams, @@ -149,7 +151,7 @@ export class Orderbook { const apiListings = await this.apiClient.listListings(listOrderParams); return { page: mapFromOpenApiPage(apiListings.page), - result: apiListings.result.map(mapFromOpenApiOrder), + result: apiListings.result.map(mapListingFromOpenApiOrder), }; } @@ -162,28 +164,30 @@ export class Orderbook { async listTrades( listTradesParams: ListTradesParams, ): Promise { - const apiListings = await this.apiClient.listTrades(listTradesParams); + const apiTrades = await this.apiClient.listTrades(listTradesParams); return { - page: mapFromOpenApiPage(apiListings.page), - result: apiListings.result.map(mapFromOpenApiTrade), + page: mapFromOpenApiPage(apiTrades.page), + result: apiTrades.result.map(mapFromOpenApiTrade), }; } /** * Get required transactions and messages for signing to facilitate creating bulk listings. - * Once the transactions are submitted and the message signed, call the completeListings method - * provided in the return type with the signature. This method supports up to 20 listing creations + * Once the transactions are submitted and the message signed, call the + * {@linkcode PrepareBulkListingsResponse.completeListings} method provided in the return + * type with the signature. This method supports up to 20 listing creations * at a time. It can also be used for individual listings to simplify integration code paths. * * Bulk listings created using an EOA (Metamask) will require a single listing confirmation * signature. * Bulk listings creating using a smart contract wallet will require multiple listing confirmation - * signatures(as many as the number of orders). + * signatures (as many as the number of orders). * @param {PrepareBulkListingsParams} prepareBulkListingsParams - Details about the listings * to be created. - * @return {PrepareBulkListingsResponse} PrepareListingResponse includes + * @return {PrepareBulkListingsResponse} PrepareBulkListingsResponse includes * any unsigned approval transactions, the typed bulk order message for signing and - * the createListings method that can be called with the signature(s) to create the listings. + * the {@linkcode PrepareBulkListingsResponse.completeListings} method that can be called with + * the signature(s) to create the listings. */ async prepareBulkListings( { @@ -205,6 +209,7 @@ export class Orderbook { makerAddress, listingParams[0].sell, listingParams[0].buy, + listingParams[0].sell.type === 'ERC1155', new Date(), listingParams[0].orderExpiry || Orderbook.defaultOrderExpiry(), ); @@ -242,6 +247,7 @@ export class Orderbook { makerAddress, listing.sell, listing.buy, + listing.sell.type === 'ERC1155', new Date(), listing.orderExpiry || Orderbook.defaultOrderExpiry(), ))); @@ -292,7 +298,7 @@ export class Orderbook { success: !!apiListingResponse, orderHash: prepareListingResponses[i].orderHash, // eslint-disable-next-line max-len - order: apiListingResponse ? mapFromOpenApiOrder(apiListingResponse.result) : undefined, + order: apiListingResponse ? mapListingFromOpenApiOrder(apiListingResponse.result) : undefined, })), }; }, @@ -301,11 +307,12 @@ export class Orderbook { // Bulk listings (with multiple listings) code path for EOA wallets. track('orderbookmr', 'bulkListings', { walletType: 'EOA', makerAddress, listingsCount: listingParams.length }); - const { actions, preparedListings } = await this.seaport.prepareBulkSeaportOrders( + const { actions, preparedOrders } = await this.seaport.prepareBulkSeaportOrders( makerAddress, listingParams.map((orderParam) => ({ - listingItem: orderParam.sell, + offerItem: orderParam.sell, considerationItem: orderParam.buy, + allowPartialFills: orderParam.sell.type === 'ERC1155', orderStart: new Date(), orderExpiry: orderParam.orderExpiry || Orderbook.defaultOrderExpiry(), })), @@ -319,7 +326,7 @@ export class Orderbook { throw new Error('Only a single signature is expected for bulk listing creation'); } - const orderComponents = preparedListings.map((orderParam) => orderParam.orderComponents); + const orderComponents = preparedOrders.map((orderParam) => orderParam.orderComponents); const signature = signatureIsArray ? signatures[0] : signatures; const bulkOrderSignatures = getBulkSeaportOrderSignatures( signature, @@ -329,7 +336,7 @@ export class Orderbook { const createOrdersApiListingResponse = await Promise.all( orderComponents.map((orderComponent, i) => { const sig = bulkOrderSignatures[i]; - const listing = preparedListings[i]; + const listing = preparedOrders[i]; const listingParam = listingParams[i]; return this.apiClient.createListing({ orderComponents: orderComponent, @@ -344,8 +351,10 @@ export class Orderbook { return { result: createOrdersApiListingResponse.map((apiListingResponse, i) => ({ success: !!apiListingResponse, - orderHash: preparedListings[i].orderHash, - order: apiListingResponse ? mapFromOpenApiOrder(apiListingResponse.result) : undefined, + orderHash: preparedOrders[i].orderHash, + order: apiListingResponse + ? mapListingFromOpenApiOrder(apiListingResponse.result) + : undefined, })), }; }, @@ -354,11 +363,11 @@ export class Orderbook { /** * Get required transactions and messages for signing prior to creating a listing - * through the createListing method + * through the {@linkcode createListing} method * @param {PrepareListingParams} prepareListingParams - Details about the listing to be created. * @return {PrepareListingResponse} PrepareListingResponse includes * the unsigned approval transaction, the typed order message for signing and - * the order components that can be submitted to `createListing` with a signature. + * the order components that can be submitted to {@linkcode createListing} with a signature. */ async prepareListing({ makerAddress, @@ -370,6 +379,7 @@ export class Orderbook { makerAddress, sell, buy, + sell.type === 'ERC1155', // Default order start to now new Date(), // Default order expiry to 2 years from now @@ -378,9 +388,9 @@ export class Orderbook { } /** - * Create an order - * @param {CreateListingParams} createListingParams - create an order with the given params. - * @return {ListingResult} The result of the order created in the Immutable services. + * Create a listing + * @param {CreateListingParams} createListingParams - create a listing with the given params. + * @return {ListingResult} The result of the listing created in the Immutable services. */ async createListing( createListingParams: CreateListingParams, @@ -390,7 +400,7 @@ export class Orderbook { }); return { - result: mapFromOpenApiOrder(apiListingResponse.result), + result: mapListingFromOpenApiOrder(apiListingResponse.result), }; } @@ -398,7 +408,7 @@ export class Orderbook { * Get unsigned transactions that can be submitted to fulfil an open order. If the approval * transaction exists it must be signed and submitted to the chain before the fulfilment * transaction can be submitted or it will be reverted. - * @param {string} listingId - The listingId to fulfil. + * @param {string} orderId - The orderId to fulfil. * @param {string} takerAddress - The address of the account fulfilling the order. * @param {FeeValue[]} takerFees - Taker ecosystem fees to be paid. * @param {string} amountToFill - Amount of the order to fill, defaults to sell item amount. @@ -406,19 +416,18 @@ export class Orderbook { * @return {FulfillOrderResponse} Approval and fulfilment transactions. */ async fulfillOrder( - listingId: string, + orderId: string, takerAddress: string, takerFees: FeeValue[], amountToFill?: string, ): Promise { const fulfillmentDataRes = await this.apiClient.fulfillmentData([ { - order_id: listingId, + order_id: orderId, taker_address: takerAddress, fees: takerFees.map((fee) => ({ + type: OpenApiFee.type.TAKER_ECOSYSTEM, amount: fee.amount, - type: - FeeType.TAKER_ECOSYSTEM as unknown as OpenApiFee.type.TAKER_ECOSYSTEM, recipient_address: fee.recipientAddress, })), }, @@ -448,23 +457,28 @@ export class Orderbook { * Get unsigned transactions that can be submitted to fulfil multiple open orders. If approval * transactions exist, they must be signed and submitted to the chain before the fulfilment * transaction can be submitted or it will be reverted. - * @param {Array} listings - The details of the listings to fulfil, amounts + * @param {FulfillmentOrder[]} orders - The details of the orders to fulfil, amounts * to fill and taker ecosystem fees to be paid. * @param {string} takerAddress - The address of the account fulfilling the order. * @return {FulfillBulkOrdersResponse} Approval and fulfilment transactions. */ async fulfillBulkOrders( - listings: Array, + orders: FulfillmentOrder[] | FulfillmentListing[], takerAddress: string, ): Promise { + const mappedOrders = orders.map((order): FulfillmentOrder => ({ + orderId: 'listingId' in order ? order.listingId : order.orderId, + takerFees: order.takerFees, + amountToFill: order.amountToFill, + })); + const fulfillmentDataRes = await this.apiClient.fulfillmentData( - listings.map((listingRequest) => ({ - order_id: listingRequest.listingId, + mappedOrders.map((fulfillmentRequest) => ({ + order_id: fulfillmentRequest.orderId, taker_address: takerAddress, - fees: listingRequest.takerFees.map((fee) => ({ + fees: fulfillmentRequest.takerFees.map((fee) => ({ + type: OpenApiFee.type.TAKER_ECOSYSTEM, amount: fee.amount, - type: - FeeType.TAKER_ECOSYSTEM as unknown as OpenApiFee.type.TAKER_ECOSYSTEM, recipient_address: fee.recipientAddress, })), })), @@ -473,16 +487,16 @@ export class Orderbook { try { const fulfillableOrdersWithUnits = fulfillmentDataRes.result.fulfillable_orders .map((fulfillmentData) => { - // Find the listing that corresponds to the order for the units - const listing = listings.find((l) => l.listingId === fulfillmentData.order.id); - if (!listing) { - throw new Error(`Could not find listing for order ${fulfillmentData.order.id}`); + // Find the order that corresponds to the order for the units + const order = mappedOrders.find((l) => l.orderId === fulfillmentData.order.id); + if (!order) { + throw new Error(`Could not find order for order ${fulfillmentData.order.id}`); } return { extraData: fulfillmentData.extra_data, order: fulfillmentData.order, - unitsToFill: listing.amountToFill, + unitsToFill: order.amountToFill, }; }); @@ -492,7 +506,7 @@ export class Orderbook { takerAddress, )), fulfillableOrders: fulfillmentDataRes.result.fulfillable_orders.map( - (o) => mapFromOpenApiOrder(o.order), + (o) => mapOrderFromOpenApiOrder(o.order), ), unfulfillableOrders: fulfillmentDataRes.result.unfulfillable_orders.map( (o) => ({ @@ -507,7 +521,7 @@ export class Orderbook { if (String(e).includes('The fulfiller does not have the balances needed to fulfill.')) { return { fulfillableOrders: fulfillmentDataRes.result.fulfillable_orders.map( - (o) => mapFromOpenApiOrder(o.order), + (o) => mapOrderFromOpenApiOrder(o.order), ), unfulfillableOrders: fulfillmentDataRes.result.unfulfillable_orders.map( (o) => ({ @@ -528,8 +542,8 @@ export class Orderbook { /** * Cancelling orders is a gasless alternative to on-chain cancellation exposed with - * `cancelOrdersOnChain`. For the orderbook to authenticate the cancellation, the creator - * of the orders must sign an EIP712 message containing the orderIds + * {@linkcode cancelOrdersOnChain}. For the orderbook to authenticate the cancellation, + * the creator of the orders must sign an EIP712 message containing the orderIds. * @param {string} orderIds - The orderIds to attempt to cancel. * @return {PrepareCancelOrdersResponse} The signable action to cancel the orders. */ @@ -573,10 +587,10 @@ export class Orderbook { /** * Cancelling orders is a gasless alternative to on-chain cancellation exposed with - * `cancelOrdersOnChain`. Orders cancelled this way cannot be fulfilled and will be removed - * from the orderbook. If there is pending fulfillment data outstanding for the order, its - * cancellation will be pending until the fulfillment window has passed. - * `prepareOffchainOrderCancellations` can be used to get the signable action that is signed + * {@linkcode cancelOrdersOnChain}. Orders cancelled this way cannot be fulfilled and + * will be removed from the orderbook. If there is pending fulfillment data outstanding + * for the order, its cancellation will be pending until the fulfillment window has passed. + * {@linkcode prepareOrderCancellations} can be used to get the signable action that is signed * to get the signature required for this call. * @param {string[]} orderIds - The orderIds to attempt to cancel. * @param {string} accountAddress - The address of the account cancelling the orders. diff --git a/packages/orderbook/src/seaport/map-to-seaport-order.ts b/packages/orderbook/src/seaport/map-to-seaport-order.ts index 8d751639d5..85ef762407 100644 --- a/packages/orderbook/src/seaport/map-to-seaport-order.ts +++ b/packages/orderbook/src/seaport/map-to-seaport-order.ts @@ -1,100 +1,172 @@ import { ConsiderationItem, + OfferItem, OrderComponents, TipInputItem, } from '@opensea/seaport-js/lib/types'; import { constants } from 'ethers'; +import { Item, Order, ProtocolData } from '../openapi/sdk'; +import { exhaustiveSwitch } from '../utils'; import { ItemType, OrderType } from './constants'; -import { ERC721Item, ERC1155Item, Order } from '../openapi/sdk'; + +function mapImmutableItemToSeaportOfferItem(item: Item): OfferItem { + switch (item.type) { + case 'NATIVE': + throw new Error('NATIVE items are not supported in the offer'); + case 'ERC20': + return { + itemType: ItemType.ERC20, + token: item.contract_address, + identifierOrCriteria: '0', + startAmount: item.amount, + endAmount: item.amount, + }; + case 'ERC721': + return { + itemType: ItemType.ERC721, + token: item.contract_address, + identifierOrCriteria: item.token_id, + startAmount: '1', + endAmount: '1', + }; + case 'ERC1155': + return { + itemType: ItemType.ERC1155, + token: item.contract_address, + identifierOrCriteria: item.token_id, + startAmount: item.amount, + endAmount: item.amount, + }; + case 'ERC721_COLLECTION': + throw new Error('ERC721_COLLECTION items are not supported in the offer'); + case 'ERC1155_COLLECTION': + throw new Error('ERC1155_COLLECTION items are not supported in the offer'); + default: + return exhaustiveSwitch(item); + } +} + +function mapImmutableItemToSeaportConsiderationItem( + item: Item, + recipient: string, +): ConsiderationItem { + switch (item.type) { + case 'NATIVE': + return { + itemType: ItemType.NATIVE, + startAmount: item.amount, + endAmount: item.amount, + token: constants.AddressZero, + identifierOrCriteria: '0', + recipient, + }; + case 'ERC20': + return { + itemType: ItemType.ERC20, + startAmount: item.amount, + endAmount: item.amount, + token: item.contract_address, + identifierOrCriteria: '0', + recipient, + }; + case 'ERC721': + return { + itemType: ItemType.ERC721, + startAmount: '1', + endAmount: '1', + token: item.contract_address, + identifierOrCriteria: item.token_id, + recipient, + }; + case 'ERC1155': + return { + itemType: ItemType.ERC1155, + startAmount: item.amount, + endAmount: item.amount, + token: item.contract_address, + identifierOrCriteria: item.token_id, + recipient, + }; + case 'ERC721_COLLECTION': + return { + itemType: ItemType.ERC721_WITH_CRITERIA, + startAmount: item.amount, + endAmount: item.amount, + token: item.contract_address, + identifierOrCriteria: '0', + recipient, + }; + case 'ERC1155_COLLECTION': + return { + itemType: ItemType.ERC1155_WITH_CRITERIA, + startAmount: item.amount, + endAmount: item.amount, + token: item.contract_address, + identifierOrCriteria: '0', + recipient, + }; + default: + return exhaustiveSwitch(item); + } +} export function mapImmutableOrderToSeaportOrderComponents( order: Order, ): { orderComponents: OrderComponents, tips: Array } { - const considerationItems: ConsiderationItem[] = order.buy.map((buyItem) => { - switch (buyItem.type) { - case 'NATIVE': - return { - startAmount: buyItem.amount, - endAmount: buyItem.amount, - itemType: ItemType.NATIVE, - recipient: order.account_address, - token: constants.AddressZero, - identifierOrCriteria: '0', - }; - case 'ERC20': - return { - startAmount: buyItem.amount, - endAmount: buyItem.amount, - itemType: ItemType.ERC20, - recipient: order.account_address, - token: buyItem.contract_address! || constants.AddressZero, - identifierOrCriteria: '0', - }; - default: // ERC721 - return { - startAmount: '1', - endAmount: '1', - itemType: ItemType.ERC721, - recipient: order.account_address, - token: buyItem.contract_address! || constants.AddressZero, - identifierOrCriteria: '0', - }; + const offerItems: OfferItem[] = order.sell.map(mapImmutableItemToSeaportOfferItem); + // eslint-disable-next-line max-len + const considerationItems: ConsiderationItem[] = order.buy.map((item): ConsiderationItem => mapImmutableItemToSeaportConsiderationItem(item, order.account_address)); + + // eslint-disable-next-line func-names + const currencyItem = (function (ot: Order.type): OfferItem | ConsiderationItem { + switch (ot) { + case Order.type.LISTING: + return considerationItems[0]; + case Order.type.BID: + case Order.type.COLLECTION_BID: + return offerItems[0]; + default: + return exhaustiveSwitch(ot); } - }); + }(order.type)); + + // eslint-disable-next-line func-names + const seaportOrderType = (function (ot: ProtocolData.order_type): OrderType { + switch (ot) { + case ProtocolData.order_type.FULL_RESTRICTED: + return OrderType.FULL_RESTRICTED; + case ProtocolData.order_type.PARTIAL_RESTRICTED: + return OrderType.PARTIAL_RESTRICTED; + default: + return exhaustiveSwitch(ot); + } + }(order.protocol_data.order_type)); const fees: TipInputItem[] = order.fees.map((fee) => ({ amount: fee.amount, - itemType: - order.buy[0].type === 'ERC20' ? ItemType.ERC20 : ItemType.NATIVE, + itemType: currencyItem.itemType, recipient: fee.recipient_address, - token: - order.buy[0].type === 'ERC20' - ? order.buy[0].contract_address! - : constants.AddressZero, - identifierOrCriteria: '0', + token: currencyItem.token, + identifierOrCriteria: currencyItem.identifierOrCriteria, })); return { orderComponents: { - conduitKey: constants.HashZero, - consideration: [...considerationItems], - offer: order.sell.map((sellItem) => { - let tokenItem; - switch (sellItem.type) { - case 'ERC1155': - tokenItem = sellItem as ERC1155Item; - return { - startAmount: tokenItem.amount, - endAmount: tokenItem.amount, - itemType: ItemType.ERC1155, - recipient: order.account_address, - token: tokenItem.contract_address!, - identifierOrCriteria: tokenItem.token_id, - }; - default: // ERC721 - tokenItem = sellItem as ERC721Item; - return { - startAmount: '1', - endAmount: '1', - itemType: ItemType.ERC721, - recipient: order.account_address, - token: tokenItem.contract_address!, - identifierOrCriteria: tokenItem.token_id, - }; - } - }), - counter: order.protocol_data.counter, - endTime: Math.round(new Date(order.end_at).getTime() / 1000).toString(), + offerer: order.account_address, + zone: order.protocol_data.zone_address, + offer: offerItems, + consideration: considerationItems, + orderType: seaportOrderType, startTime: Math.round( new Date(order.start_at).getTime() / 1000, ).toString(), + endTime: Math.round(new Date(order.end_at).getTime() / 1000).toString(), + zoneHash: constants.HashZero, salt: order.salt, - offerer: order.account_address, - zone: order.protocol_data.zone_address, + conduitKey: constants.HashZero, + counter: order.protocol_data.counter, // this should be the fee exclusive number of items the user signed for totalOriginalConsiderationItems: considerationItems.length, - orderType: order.sell[0].type === 'ERC1155' ? OrderType.PARTIAL_RESTRICTED : OrderType.FULL_RESTRICTED, - zoneHash: constants.HashZero, }, tips: fees, }; diff --git a/packages/orderbook/src/seaport/seaport.test.ts b/packages/orderbook/src/seaport/seaport.test.ts index b5a12eae65..e7d049ae6a 100644 --- a/packages/orderbook/src/seaport/seaport.test.ts +++ b/packages/orderbook/src/seaport/seaport.test.ts @@ -135,6 +135,7 @@ describe('Seaport', () => { offerer, listingItem, considerationItem, + false, orderStart, orderExpiry, ); @@ -149,6 +150,7 @@ describe('Seaport', () => { offerer, listingItem, considerationItem, + false, orderStart, orderExpiry, ); @@ -174,6 +176,7 @@ describe('Seaport', () => { offerer, listingItem, considerationItem, + false, orderStart, orderExpiry, ); @@ -185,6 +188,7 @@ describe('Seaport', () => { offerer, listingItem, considerationItem, + false, orderStart, orderExpiry, ); @@ -271,7 +275,6 @@ describe('Seaport', () => { ], consideration: [ { - token: undefined, amount: considerationItem.amount, recipient: offerer, }, @@ -303,6 +306,7 @@ describe('Seaport', () => { offerer, listingItem, considerationItem, + false, orderStart, orderExpiry, ); @@ -324,6 +328,7 @@ describe('Seaport', () => { offerer, listingItem, considerationItem, + false, orderStart, orderExpiry, ); @@ -350,6 +355,7 @@ describe('Seaport', () => { offerer, listingItem, considerationItem, + false, orderStart, orderExpiry, ); @@ -361,6 +367,7 @@ describe('Seaport', () => { offerer, listingItem, considerationItem, + false, orderStart, orderExpiry, ); diff --git a/packages/orderbook/src/seaport/seaport.ts b/packages/orderbook/src/seaport/seaport.ts index 320572cb8b..c7f065a02e 100644 --- a/packages/orderbook/src/seaport/seaport.ts +++ b/packages/orderbook/src/seaport/seaport.ts @@ -1,41 +1,130 @@ import { Seaport as SeaportLib } from '@opensea/seaport-js'; import type { - ApprovalAction, + ConsiderationInputItem, + CreateBulkOrdersAction, CreateInputItem, CreateOrderAction, - CreateBulkOrdersAction, ExchangeAction, OrderComponents, OrderUseCase, } from '@opensea/seaport-js/lib/types'; import { providers } from 'ethers'; -import { mapFromOpenApiOrder } from '../openapi/mapper'; +import { mapOrderFromOpenApiOrder } from '../openapi/mapper'; +import { Order as OpenApiOrder } from '../openapi/sdk'; import { Action, ActionType, + ERC1155CollectionItem, ERC1155Item, ERC20Item, + ERC721CollectionItem, ERC721Item, FulfillOrderResponse, NativeItem, PrepareBulkSeaportOrders, - PrepareListingResponse, + PrepareOrderResponse, SignableAction, SignablePurpose, TransactionAction, TransactionPurpose, } from '../types'; -import { Order } from '../openapi/sdk'; +import { exhaustiveSwitch } from '../utils'; +import { + getBulkOrderComponentsFromMessage, + getOrderComponentsFromMessage, +} from './components'; import { EIP_712_ORDER_TYPE, ItemType, SEAPORT_CONTRACT_NAME, SEAPORT_CONTRACT_VERSION_V1_5, } from './constants'; -import { getBulkOrderComponentsFromMessage, getOrderComponentsFromMessage } from './components'; +import { mapImmutableOrderToSeaportOrderComponents } from './map-to-seaport-order'; import { SeaportLibFactory } from './seaport-lib-factory'; import { prepareTransaction } from './transaction'; -import { mapImmutableOrderToSeaportOrderComponents } from './map-to-seaport-order'; + +function mapImmutableSdkItemToSeaportSdkCreateInputItem( + item: ERC20Item | ERC721Item | ERC1155Item, +): CreateInputItem { + switch (item.type) { + case 'ERC20': + return { + token: item.contractAddress, + amount: item.amount, + }; + case 'ERC721': + return { + itemType: ItemType.ERC721, + token: item.contractAddress, + identifier: item.tokenId, + }; + case 'ERC1155': + return { + itemType: ItemType.ERC1155, + token: item.contractAddress, + identifier: item.tokenId, + amount: item.amount, + }; + default: + return exhaustiveSwitch(item); + } +} + +function mapImmutableSdkItemToSeaportSdkConsiderationInputItem( + item: + | NativeItem + | ERC20Item + | ERC721Item + | ERC1155Item + | ERC721CollectionItem + | ERC1155CollectionItem, + recipient: string, +): ConsiderationInputItem { + switch (item.type) { + case 'NATIVE': + return { + amount: item.amount, + recipient, + }; + case 'ERC20': + return { + token: item.contractAddress, + amount: item.amount, + recipient, + }; + case 'ERC721': + return { + itemType: ItemType.ERC721, + token: item.contractAddress, + identifier: item.tokenId, + recipient, + }; + case 'ERC1155': + return { + itemType: ItemType.ERC1155, + token: item.contractAddress, + identifier: item.tokenId, + amount: item.amount, + recipient, + }; + case 'ERC721_COLLECTION': + return { + itemType: ItemType.ERC721, + token: item.contractAddress, + amount: item.amount, + recipient, + }; + case 'ERC1155_COLLECTION': + return { + itemType: ItemType.ERC1155, + token: item.contractAddress, + amount: item.amount, + recipient, + }; + default: + return exhaustiveSwitch(item); + } +} export class Seaport { constructor( @@ -44,15 +133,22 @@ export class Seaport { private seaportContractAddress: string, private zoneContractAddress: string, private rateLimitingKey?: string, - ) { } + ) {} async prepareBulkSeaportOrders( offerer: string, orderInputs: { - listingItem: ERC721Item | ERC1155Item, - considerationItem: ERC20Item | NativeItem, - orderStart: Date, - orderExpiry: Date, + offerItem: ERC20Item | ERC721Item | ERC1155Item; + considerationItem: + | NativeItem + | ERC20Item + | ERC721Item + | ERC1155Item + | ERC721CollectionItem + | ERC1155CollectionItem; + allowPartialFills: boolean; + orderStart: Date; + orderExpiry: Date; }[], ): Promise { const { actions: seaportActions } = await this.createSeaportOrders( @@ -60,12 +156,12 @@ export class Seaport { orderInputs, ); - const approvalActions = seaportActions.filter((action) => action.type === 'approval') as - | ApprovalAction[] - | []; + const approvalActions = seaportActions.filter( + (action) => action.type === 'approval', + ); const network = await this.provider.getNetwork(); - const listingActions: Action[] = approvalActions.map((approvalAction) => ({ + const actions: Action[] = approvalActions.map((approvalAction) => ({ type: ActionType.TRANSACTION, purpose: TransactionPurpose.APPROVAL, buildTransaction: prepareTransaction( @@ -75,9 +171,7 @@ export class Seaport { ), })); - const createAction: CreateBulkOrdersAction | undefined = seaportActions.find( - (action) => action.type === 'createBulk', - ) as CreateBulkOrdersAction | undefined; + const createAction: CreateBulkOrdersAction | undefined = seaportActions.find((action) => action.type === 'createBulk'); if (!createAction) { throw new Error('No create bulk order action found'); @@ -86,15 +180,15 @@ export class Seaport { const orderMessageToSign = await createAction.getMessageToSign(); const { components, types, value } = getBulkOrderComponentsFromMessage(orderMessageToSign); - listingActions.push({ + actions.push({ type: ActionType.SIGNABLE, - purpose: SignablePurpose.CREATE_LISTING, + purpose: SignablePurpose.CREATE_ORDER, message: await this.getTypedDataFromBulkOrderComponents(types, value), }); return { - actions: listingActions, - preparedListings: components.map((orderComponent) => ({ + actions, + preparedOrders: components.map((orderComponent) => ({ orderComponents: orderComponent, orderHash: this.getSeaportLib().getOrderHash(orderComponent), })), @@ -103,27 +197,35 @@ export class Seaport { async prepareSeaportOrder( offerer: string, - listingItem: ERC721Item | ERC1155Item, - considerationItem: ERC20Item | NativeItem, + offerItem: ERC20Item | ERC721Item | ERC1155Item, + considerationItem: + | NativeItem + | ERC20Item + | ERC721Item + | ERC1155Item + | ERC721CollectionItem + | ERC1155CollectionItem, + allowPartialFills: boolean, orderStart: Date, orderExpiry: Date, - ): Promise { + ): Promise { const { actions: seaportActions } = await this.createSeaportOrder( offerer, - listingItem, + offerItem, considerationItem, + allowPartialFills, orderStart, orderExpiry, ); - const listingActions: Action[] = []; + const actions: Action[] = []; - const approvalAction = seaportActions.find((action) => action.type === 'approval') as - | ApprovalAction - | undefined; + const approvalAction = seaportActions.find( + (action) => action.type === 'approval', + ); if (approvalAction) { - listingActions.push({ + actions.push({ type: ActionType.TRANSACTION, purpose: TransactionPurpose.APPROVAL, buildTransaction: prepareTransaction( @@ -136,7 +238,7 @@ export class Seaport { const createAction: CreateOrderAction | undefined = seaportActions.find( (action) => action.type === 'create', - ) as CreateOrderAction | undefined; + ); if (!createAction) { throw new Error('No create order action found'); @@ -145,21 +247,21 @@ export class Seaport { const orderMessageToSign = await createAction.getMessageToSign(); const orderComponents = getOrderComponentsFromMessage(orderMessageToSign); - listingActions.push({ + actions.push({ type: ActionType.SIGNABLE, - purpose: SignablePurpose.CREATE_LISTING, + purpose: SignablePurpose.CREATE_ORDER, message: await this.getTypedDataFromOrderComponents(orderComponents), }); return { - actions: listingActions, + actions, orderComponents, orderHash: this.getSeaportLib().getOrderHash(orderComponents), }; } async fulfillOrder( - order: Order, + order: OpenApiOrder, account: string, extraData: string, unitsToFill?: string, @@ -184,9 +286,9 @@ export class Seaport { const fulfillmentActions: TransactionAction[] = []; - const approvalAction = seaportActions.find((action) => action.type === 'approval') as - | ApprovalAction - | undefined; + const approvalAction = seaportActions.find( + (action) => action.type === 'approval', + ); if (approvalAction) { fulfillmentActions.push({ @@ -202,7 +304,7 @@ export class Seaport { const fulfilOrderAction: ExchangeAction | undefined = seaportActions.find( (action) => action.type === 'exchange', - ) as ExchangeAction | undefined; + ); if (!fulfilOrderAction) { throw new Error('No exchange action found'); @@ -221,15 +323,15 @@ export class Seaport { return { actions: fulfillmentActions, expiration: Seaport.getExpirationISOTimeFromExtraData(extraData), - order: mapFromOpenApiOrder(order), + order: mapOrderFromOpenApiOrder(order), }; } async fulfillBulkOrders( fulfillingOrders: { extraData: string; - order: Order; - unitsToFill?: string + order: OpenApiOrder; + unitsToFill?: string; }[], account: string, ): Promise<{ @@ -257,9 +359,9 @@ export class Seaport { const fulfillmentActions: TransactionAction[] = []; - const approvalAction = seaportActions.find((action) => action.type === 'approval') as - | ApprovalAction - | undefined; + const approvalAction = seaportActions.find( + (action) => action.type === 'approval', + ); if (approvalAction) { fulfillmentActions.push({ @@ -275,7 +377,7 @@ export class Seaport { const fulfilOrderAction: ExchangeAction | undefined = seaportActions.find( (action) => action.type === 'exchange', - ) as ExchangeAction | undefined; + ); if (!fulfilOrderAction) { throw new Error('No exchange action found'); @@ -300,13 +402,19 @@ export class Seaport { }; } - async cancelOrders(orders: Order[], account: string): Promise { + async cancelOrders( + orders: OpenApiOrder[], + account: string, + ): Promise { const orderComponents = orders.map( (order) => mapImmutableOrderToSeaportOrderComponents(order).orderComponents, ); const seaportLib = this.getSeaportLib(orders[0]); - const cancellationTransaction = await seaportLib.cancelOrders(orderComponents, account); + const cancellationTransaction = await seaportLib.cancelOrders( + orderComponents, + account, + ); return { type: ActionType.TRANSACTION, @@ -322,84 +430,75 @@ export class Seaport { private createSeaportOrders( offerer: string, orderInputs: { - listingItem: ERC721Item | ERC1155Item, - considerationItem: ERC20Item | NativeItem, - orderStart: Date, - orderExpiry: Date, + offerItem: ERC20Item | ERC721Item | ERC1155Item; + considerationItem: + | NativeItem + | ERC20Item + | ERC721Item + | ERC1155Item + | ERC721CollectionItem + | ERC1155CollectionItem; + allowPartialFills: boolean; + orderStart: Date; + orderExpiry: Date; }[], ): Promise> { const seaportLib = this.getSeaportLib(); - return seaportLib.createBulkOrders(orderInputs.map((orderInput) => { - const { - listingItem, considerationItem, orderStart, orderExpiry, - } = orderInput; - - const offerItem: CreateInputItem = listingItem.type === 'ERC721' - ? { - itemType: ItemType.ERC721, - token: listingItem.contractAddress, - identifier: listingItem.tokenId, - } - : { - itemType: ItemType.ERC1155, - token: listingItem.contractAddress, - identifier: listingItem.tokenId, - amount: listingItem.amount, + return seaportLib.createBulkOrders( + orderInputs.map((orderInput) => { + const { + offerItem, + considerationItem, + allowPartialFills, + orderStart, + orderExpiry, + } = orderInput; + + return { + allowPartialFills, + offer: [mapImmutableSdkItemToSeaportSdkCreateInputItem(offerItem)], + consideration: [ + mapImmutableSdkItemToSeaportSdkConsiderationInputItem( + considerationItem, + offerer, + ), + ], + startTime: (orderStart.getTime() / 1000).toFixed(0), + endTime: (orderExpiry.getTime() / 1000).toFixed(0), + zone: this.zoneContractAddress, + restrictedByZone: true, }; - - return { - allowPartialFills: listingItem.type === 'ERC1155', - offer: [offerItem], - consideration: [ - { - token: - considerationItem.type === 'ERC20' ? considerationItem.contractAddress : undefined, - amount: considerationItem.amount, - recipient: offerer, - }, - ], - startTime: (orderStart.getTime() / 1000).toFixed(0), - endTime: (orderExpiry.getTime() / 1000).toFixed(0), - zone: this.zoneContractAddress, - restrictedByZone: true, - }; - }), offerer); + }), + offerer, + ); } private createSeaportOrder( offerer: string, - listingItem: ERC721Item | ERC1155Item, - considerationItem: ERC20Item | NativeItem, + offerItem: ERC20Item | ERC721Item | ERC1155Item, + considerationItem: + | NativeItem + | ERC20Item + | ERC721Item + | ERC1155Item + | ERC721CollectionItem + | ERC1155CollectionItem, + allowPartialFills: boolean, orderStart: Date, orderExpiry: Date, ): Promise> { const seaportLib = this.getSeaportLib(); - const offerItem: CreateInputItem = listingItem.type === 'ERC721' - ? { - itemType: ItemType.ERC721, - token: listingItem.contractAddress, - identifier: listingItem.tokenId, - } - : { - itemType: ItemType.ERC1155, - token: listingItem.contractAddress, - identifier: listingItem.tokenId, - amount: listingItem.amount, - }; - return seaportLib.createOrder( { - allowPartialFills: listingItem.type === 'ERC1155', - offer: [offerItem], + allowPartialFills, + offer: [mapImmutableSdkItemToSeaportSdkCreateInputItem(offerItem)], consideration: [ - { - token: - considerationItem.type === 'ERC20' ? considerationItem.contractAddress : undefined, - amount: considerationItem.amount, - recipient: offerer, - }, + mapImmutableSdkItemToSeaportSdkConsiderationInputItem( + considerationItem, + offerer, + ), ], startTime: (orderStart.getTime() / 1000).toFixed(0), endTime: (orderExpiry.getTime() / 1000).toFixed(0), @@ -454,7 +553,7 @@ export class Seaport { }; } - private getSeaportLib(order?: Order): SeaportLib { + private getSeaportLib(order?: OpenApiOrder): SeaportLib { const seaportAddress = order?.protocol_data?.seaport_address ?? this.seaportContractAddress; return this.seaportLibFactory.create(seaportAddress, this.rateLimitingKey); } diff --git a/packages/orderbook/src/types.ts b/packages/orderbook/src/types.ts index d33328958b..c0e082e9ca 100644 --- a/packages/orderbook/src/types.ts +++ b/packages/orderbook/src/types.ts @@ -5,10 +5,16 @@ import { Fee as OpenapiFee, OrdersService, OrderStatus } from './openapi/sdk'; // Strictly re-export only the OrderStatusName enum from the openapi types export { OrderStatusName } from './openapi/sdk'; -export interface ERC1155Item { - type: 'ERC1155'; +/* Items */ + +export interface NativeItem { + type: 'NATIVE'; + amount: string; +} + +export interface ERC20Item { + type: 'ERC20'; contractAddress: string; - tokenId: string; amount: string; } @@ -18,74 +24,150 @@ export interface ERC721Item { tokenId: string; } -export interface ERC20Item { - type: 'ERC20'; +export interface ERC1155Item { + type: 'ERC1155'; contractAddress: string; + tokenId: string; amount: string; } -export interface NativeItem { - type: 'NATIVE'; +export interface ERC721CollectionItem { + type: 'ERC721_COLLECTION'; + contractAddress: string; amount: string; } -export interface RoyaltyInfo { - recipient: string; - amountRequired: string; +export interface ERC1155CollectionItem { + type: 'ERC1155_COLLECTION'; + contractAddress: string; + amount: string; } -export interface PrepareListingParams { - makerAddress: string; - sell: ERC721Item | ERC1155Item; - buy: ERC20Item | NativeItem; - orderExpiry?: Date; +/* Orders */ + +export type Order = Listing | Bid | CollectionBid; + +export interface Listing extends OrderFields { + type: 'LISTING'; + sell: (ERC721Item | ERC1155Item)[]; + buy: (NativeItem | ERC20Item)[]; } -export interface PrepareListingResponse { - actions: Action[]; - orderComponents: OrderComponents; - orderHash: string; +export interface Bid extends OrderFields { + type: 'BID'; + sell: ERC20Item[]; + buy: (ERC721Item | ERC1155Item)[]; } -export interface PrepareBulkListingsParams { - makerAddress: string; - listingParams: { - sell: ERC721Item | ERC1155Item; - buy: ERC20Item | NativeItem; - makerFees: FeeValue[]; - orderExpiry?: Date; - }[] +export interface CollectionBid extends OrderFields { + type: 'COLLECTION_BID'; + sell: ERC20Item[]; + buy: (ERC721CollectionItem | ERC1155CollectionItem)[]; } -export interface PrepareBulkListingsResponse { - actions: Action[]; - completeListings(signatures: string[]): Promise; +interface OrderFields { + id: string; + chain: { + id: string; + name: string; + }; + accountAddress: string; + fees: Fee[]; + status: OrderStatus; + fillStatus: { + numerator: string; + denominator: string; + }; /** - * @deprecated Pass a string[] to `completeListings` instead to enable - * smart contract wallets + * Time after which the Order is considered active */ - completeListings(signature: string): Promise; + startAt: string; + /** + * Time after which the Order is expired + */ + endAt: string; + salt: string; + signature: string; + orderHash: string; + protocolData: { + orderType: 'FULL_RESTRICTED' | 'PARTIAL_RESTRICTED'; + zoneAddress: string; + counter: string; + seaportAddress: string; + seaportVersion: string; + }; + createdAt: string; + updatedAt: string; } -export interface PrepareBulkSeaportOrders { - actions: Action[]; - preparedListings: { - orderComponents: OrderComponents; - orderHash: string; - }[] +/* Trades */ + +export interface Trade { + id: string; + orderId: string; + chain: { + id: string; + name: string; + }; + buy: (NativeItem | ERC20Item | ERC721Item | ERC1155Item)[]; + sell: (ERC20Item | ERC721Item | ERC1155Item)[]; + buyerFees: Fee[]; + sellerAddress: string; + buyerAddress: string; + makerAddress: string; + takerAddress: string; + /** + * Time the on-chain event was indexed by the Immutable order book service + */ + indexedAt: string; + blockchainMetadata: { + /** + * The transaction hash of the trade + */ + transactionHash: string; + /** + * EVM block number (uint64 as string) + */ + blockNumber: string; + /** + * Transaction index in a block (uint32 as string) + */ + transactionIndex: string; + /** + * The log index of the fulfillment event in a block (uint32 as string) + */ + logIndex: string; + }; } -export interface PrepareCancelOrdersResponse { - signableAction: SignableAction; +/* Fees */ + +export interface Fee extends FeeValue { + type: FeeType; } -export interface CreateListingParams { +export interface FeeValue { + recipientAddress: string; + amount: string; +} + +export enum FeeType { + MAKER_ECOSYSTEM = OpenapiFee.type.MAKER_ECOSYSTEM, + TAKER_ECOSYSTEM = OpenapiFee.type.TAKER_ECOSYSTEM, + PROTOCOL = OpenapiFee.type.PROTOCOL, + ROYALTY = OpenapiFee.type.ROYALTY, +} + +/* Generic Order Ops */ + +export interface PrepareOrderResponse { + actions: Action[]; orderComponents: OrderComponents; orderHash: string; - orderSignature: string; - makerFees: FeeValue[]; } +/* Listings Ops */ + // Expose the list order filtering and ordering directly from the openAPI SDK, except // chainName is omitted as its configured as a part of the client export type ListListingsParams = Omit< @@ -93,71 +175,79 @@ Parameters[0], 'chainName' >; -export type ListTradesParams = Omit< -Parameters[0], -'chainName' ->; - -export enum FeeType { - MAKER_ECOSYSTEM = OpenapiFee.type.MAKER_ECOSYSTEM, - TAKER_ECOSYSTEM = OpenapiFee.type.TAKER_ECOSYSTEM, - PROTOCOL = OpenapiFee.type.PROTOCOL, - ROYALTY = OpenapiFee.type.ROYALTY, +export interface ListingResult { + result: Listing; } -export interface FeeValue { - recipientAddress: string; - amount: string; +export interface ListListingsResult { + page: Page; + result: Listing[]; } -export interface Fee extends FeeValue { - type: FeeType; +export interface PrepareListingParams { + makerAddress: string; + sell: ERC721Item | ERC1155Item; + buy: NativeItem | ERC20Item; + orderExpiry?: Date; } -export enum TransactionPurpose { - APPROVAL = 'APPROVAL', - FULFILL_ORDER = 'FULFILL_ORDER', - CANCEL = 'CANCEL', -} +export type PrepareListingResponse = PrepareOrderResponse; -export enum SignablePurpose { - CREATE_LISTING = 'CREATE_LISTING', - OFF_CHAIN_CANCELLATION = 'OFF_CHAIN_CANCELLATION', +export interface PrepareBulkListingsParams { + makerAddress: string; + listingParams: { + sell: ERC721Item | ERC1155Item; + buy: NativeItem | ERC20Item; + makerFees: FeeValue[]; + orderExpiry?: Date; + }[]; } -export enum ActionType { - TRANSACTION = 'TRANSACTION', - SIGNABLE = 'SIGNABLE', +export interface PrepareBulkListingsResponse { + actions: Action[]; + completeListings(signatures: string[]): Promise; + /** + * @deprecated Pass a `string[]` to {@linkcode completeListings} instead to enable + * smart contract wallets + */ + completeListings(signature: string): Promise; } -export type TransactionBuilder = () => Promise; - -export interface TransactionAction { - type: ActionType.TRANSACTION; - purpose: TransactionPurpose; - buildTransaction: TransactionBuilder; +export interface CreateListingParams { + orderComponents: OrderComponents; + orderHash: string; + orderSignature: string; + makerFees: FeeValue[]; } -export interface SignableAction { - type: ActionType.SIGNABLE; - purpose: SignablePurpose; - message: { - domain: TypedDataDomain; - types: Record; - value: Record; - }; +export interface BulkListingsResult { + result: { + success: boolean; + orderHash: string; + order?: Listing; + }[]; } -export type Action = TransactionAction | SignableAction; +/* Fulfilment Ops */ + +export interface FulfillmentOrder { + orderId: string; + takerFees: FeeValue[]; + amountToFill?: string; +} +/** + * @deprecated Use {@linkcode FulfillmentOrder} instead + */ export interface FulfillmentListing { - listingId: string, - takerFees: Array, - amountToFill?: string, + listingId: string; + takerFees: FeeValue[]; + amountToFill?: string; } -export type FulfillBulkOrdersResponse - = FulfillBulkOrdersInsufficientBalanceResponse | FulfillBulkOrdersSufficientBalanceResponse; +export type FulfillBulkOrdersResponse = + | FulfillBulkOrdersInsufficientBalanceResponse + | FulfillBulkOrdersSufficientBalanceResponse; export interface FulfillBulkOrdersSufficientBalanceResponse { sufficientBalance: true; @@ -173,11 +263,6 @@ export interface FulfillBulkOrdersInsufficientBalanceResponse { unfulfillableOrders: UnfulfillableOrder[]; } -export interface UnfulfillableOrder { - orderId: string, - reason: string, -} - export interface FulfillOrderResponse { actions: Action[]; /** @@ -189,63 +274,84 @@ export interface FulfillOrderResponse { order: Order; } -export interface CancelOrdersOnChainResponse { - cancellationAction: TransactionAction +export interface UnfulfillableOrder { + orderId: string; + reason: string; } -export interface Order { - id: string; - type: 'LISTING'; - accountAddress: string; - buy: (ERC20Item | NativeItem)[]; - sell: (ERC721Item | ERC1155Item)[]; - fees: Fee[]; - chain: { - id: string; - name: string; - }; - createdAt: string; - updatedAt: string; - fillStatus: { - numerator: string, - denominator: string, - }; - /** - * Time after which the Order is considered active - */ - startAt: string; - /** - * Time after which the Order is expired - */ - endAt: string; - orderHash: string; - protocolData: { - orderType: 'FULL_RESTRICTED' | 'PARTIAL_RESTRICTED'; - zoneAddress: string; - counter: string; - seaportAddress: string; - seaportVersion: string; - }; - salt: string; - signature: string; - status: OrderStatus; +/* Order Cancel Ops */ + +export interface PrepareCancelOrdersResponse { + signableAction: SignableAction; } -export interface ListingResult { - result: Order; +export interface CancelOrdersOnChainResponse { + cancellationAction: TransactionAction; } -export interface BulkListingsResult { - result: { - success: boolean; - orderHash: string; - order?: Order; - }[]; +/* Trade Ops */ + +export type ListTradesParams = Omit< +Parameters[0], +'chainName' +>; + +export interface TradeResult { + result: Trade; } -export interface ListListingsResult { +export interface ListTradesResult { page: Page; - result: Order[]; + result: Trade[]; +} + +/* Action Ops */ + +export type Action = TransactionAction | SignableAction; + +export enum ActionType { + TRANSACTION = 'TRANSACTION', + SIGNABLE = 'SIGNABLE', +} + +export interface TransactionAction { + type: ActionType.TRANSACTION; + purpose: TransactionPurpose; + buildTransaction: TransactionBuilder; +} + +export enum TransactionPurpose { + APPROVAL = 'APPROVAL', + FULFILL_ORDER = 'FULFILL_ORDER', + CANCEL = 'CANCEL', +} + +export type TransactionBuilder = () => Promise; + +export interface SignableAction { + type: ActionType.SIGNABLE; + purpose: SignablePurpose; + message: { + domain: TypedDataDomain; + types: Record; + value: Record; + }; +} + +export enum SignablePurpose { + /** + * @deprecated Use {@linkcode CREATE_ORDER} instead + */ + CREATE_LISTING = 'CREATE_ORDER', + CREATE_ORDER = 'CREATE_ORDER', + OFF_CHAIN_CANCELLATION = 'OFF_CHAIN_CANCELLATION', +} + +/* Misc */ + +export interface RoyaltyInfo { + recipient: string; + amountRequired: string; } export interface Page { @@ -259,49 +365,10 @@ export interface Page { nextCursor: string | null; } -export interface Trade { - id: string; - orderId: string; - chain: { - id: string; - name: string; - }; - buy: (ERC20Item | NativeItem)[]; - sell: (ERC721Item | ERC1155Item)[]; - buyerFees: Fee[]; - sellerAddress: string; - buyerAddress: string; - makerAddress: string; - takerAddress: string; - /** - * Time the on-chain event was indexed by the Immutable order book service - */ - indexedAt: string; - blockchainMetadata: { - /** - * The transaction hash of the trade - */ - transactionHash: string; - /** - * EVM block number (uint64 as string) - */ - blockNumber: string; - /** - * Transaction index in a block (uint32 as string) - */ - transactionIndex: string; - /** - * The log index of the fulfillment event in a block (uint32 as string) - */ - logIndex: string; - }; -} - -export interface TradeResult { - result: Trade; -} - -export interface ListTradesResult { - page: Page; - result: Trade[]; +export interface PrepareBulkSeaportOrders { + actions: Action[]; + preparedOrders: { + orderComponents: OrderComponents; + orderHash: string; + }[]; } diff --git a/packages/orderbook/src/utils.ts b/packages/orderbook/src/utils.ts new file mode 100644 index 0000000000..69fa633e1c --- /dev/null +++ b/packages/orderbook/src/utils.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function exhaustiveSwitch(param: never): never { + throw new Error('Unreachable'); +} diff --git a/sdk/package.json b/sdk/package.json index 46f6f6080a..4b6e233cb2 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -8,6 +8,7 @@ "dependencies": { "@0xsequence/abi": "^1.4.3", "@0xsequence/core": "^1.4.3", + "@0xsquid/sdk": "^2.8.24", "@biom3/design-tokens": "^0.4.2", "@biom3/react": "^0.25.0", "@ethersproject/abi": "^5.7.0", diff --git a/tests/func-tests/zkevm/README.md b/tests/func-tests/zkevm/README.md index 2767bd205d..b3e59ae823 100644 --- a/tests/func-tests/zkevm/README.md +++ b/tests/func-tests/zkevm/README.md @@ -7,30 +7,33 @@ Functional tests using Cucumber and Gherkin 1. Open the repository root folder in VS Code 2. Install dependencies: `yarn` (husky needs `node_modules` at the repo root to run) 3. Build the SDK: `yarn build` -4. cd into `tests/func-tests/zkevm` +4. cd into `tests/func-tests/zkevm` 5. Install dependencies: `yarn` (this also configures husky) ### Required ENV values +``` ZKEVM_ORDERBOOK_BANKER=0x // banker private key used to fund accounts for listings and trades ZKEVM_ORDERBOOK_ERC721=0x // Address of the ERC721 contract that the bank can mint (can be redeployed with `npx ts-node utils/orderbook/deploy-erc721.ts`) ZKEVM_ORDERBOOK_ERC1155=0x // Address of the ERC1155 contract that the bank can mint (can be redeployed with `npx ts-node utils/orderbook/deploy-erc1155.ts`) SEAPORT_CONTRACT_ADDRESS=0x ZONE_CONTRACT_ADDRESS=0x + // The following are devnet values, if running against testnet need to modify ZKEVM_RPC_ENDPOINT=https://rpc.dev.immutable.com ORDERBOOK_MR_API_URL=https://order-book-mr.dev.imtbl.com ZKEVM_CHAIN_NAME=imtbl-zkevm-devnet +``` ## Running the tests -1. Run the tests: `yarn test` +1. Run the tests: `yarn func-test` -**Note:** Certain tests are skipped on CI because of the time they take to run. To run only these, use `yarn test:ci` +**Note:** Certain tests are skipped on CI because of the time they take to run. To run only these, use `yarn func-test:ci` ## Filtering tests -By default, all tests that do not have the `@skip` tag are run. In other words, the tag filter is set to `not @skip`. +By default, all tests that do not have the `@skip` tag are run. In other words, the tag filter is set to `not @skip`. You can change the tag filter on the command line: `TAGS="" yarn test`, or more permanently, by editing your .env file directly. diff --git a/tests/func-tests/zkevm/contracts/TestToken.sol b/tests/func-tests/zkevm/contracts/TestERC721Token.sol similarity index 87% rename from tests/func-tests/zkevm/contracts/TestToken.sol rename to tests/func-tests/zkevm/contracts/TestERC721Token.sol index 7537ce1c14..b4e35a40c6 100644 --- a/tests/func-tests/zkevm/contracts/TestToken.sol +++ b/tests/func-tests/zkevm/contracts/TestERC721Token.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import "@imtbl/contracts/contracts/token/erc721/preset/ImmutableERC721MintByID.sol"; -contract TestToken is ImmutableERC721MintByID { +contract TestERC721Token is ImmutableERC721MintByID { constructor( address owner, string memory name, @@ -10,7 +10,7 @@ contract TestToken is ImmutableERC721MintByID { string memory baseURI, string memory contractURI, address operatorAllowlist, - address receiver, + address receiver, uint96 feeNumerator ) ImmutableERC721MintByID( owner, diff --git a/tests/func-tests/zkevm/package.json b/tests/func-tests/zkevm/package.json index ac114a9c2d..3b2f35f130 100644 --- a/tests/func-tests/zkevm/package.json +++ b/tests/func-tests/zkevm/package.json @@ -32,6 +32,7 @@ "scripts": { "compile": "npx hardhat compile", "func-test": "yarn compile && jest", + "func-test:only": "yarn compile && TAGS=\"@only\" jest", "func-test:ci": "yarn compile && TAGS=\"not @skip and not @slow\" jest", "postpack": "pinst --enable", "prepack": "pinst --disable" diff --git a/tests/func-tests/zkevm/step-definitions/order.steps.ts b/tests/func-tests/zkevm/step-definitions/order.steps.ts index 90214d47c5..39a6fc9bca 100644 --- a/tests/func-tests/zkevm/step-definitions/order.steps.ts +++ b/tests/func-tests/zkevm/step-definitions/order.steps.ts @@ -32,13 +32,12 @@ defineFeature(feature, (test) => { const provider = new RetryProvider(rpcUrl); const bankerWallet = new Wallet(bankerKey, provider); - const orderbookConfig = getConfigFromEnv(); const sdk = new orderbook.Orderbook({ baseConfig: { environment: Environment.SANDBOX, }, overrides: { - ...orderbookConfig, + ...getConfigFromEnv(), }, }); diff --git a/tests/func-tests/zkevm/utils/orderbook/config.ts b/tests/func-tests/zkevm/utils/orderbook/config.ts index 3f865103c1..9c37e105c3 100644 --- a/tests/func-tests/zkevm/utils/orderbook/config.ts +++ b/tests/func-tests/zkevm/utils/orderbook/config.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies -import dotenv from 'dotenv'; import { orderbook } from '@imtbl/sdk'; +import dotenv from 'dotenv'; +import { providers } from 'ethers'; dotenv.config(); @@ -20,6 +21,6 @@ export function getConfigFromEnv(): orderbook.OrderbookModuleConfiguration { chainName: process.env.ZKEVM_CHAIN_NAME, seaportContractAddress: process.env.SEAPORT_CONTRACT_ADDRESS, zoneContractAddress: process.env.ZONE_CONTRACT_ADDRESS, - jsonRpcProviderUrl: process.env.ZKEVM_RPC_ENDPOINT, + provider: new providers.JsonRpcProvider(process.env.ZKEVM_RPC_ENDPOINT), }; } diff --git a/tests/func-tests/zkevm/utils/orderbook/erc1155.ts b/tests/func-tests/zkevm/utils/orderbook/erc1155.ts index b9b296b3bf..c22750cb5f 100644 --- a/tests/func-tests/zkevm/utils/orderbook/erc1155.ts +++ b/tests/func-tests/zkevm/utils/orderbook/erc1155.ts @@ -1,13 +1,10 @@ /* eslint-disable */ import { Wallet } from 'ethers'; -import { GAS_OVERRIDES } from './gas'; -import { randomBytes } from 'crypto'; -import hre from 'hardhat' +import hre from 'hardhat'; import { - OperatorAllowlistUpgradeable__factory, TestERC1155Token, TestERC1155Token__factory, - TestToken, - TestToken__factory + OperatorAllowlistUpgradeable__factory, TestERC1155Token, TestERC1155Token__factory } from '../../typechain-types'; +import { GAS_OVERRIDES } from './gas'; export async function connectToTestERC1155Token(deployer: Wallet, tokenAddress: string): Promise { const hreEthers = (hre as any).ethers; diff --git a/tests/func-tests/zkevm/utils/orderbook/erc721.ts b/tests/func-tests/zkevm/utils/orderbook/erc721.ts index f42df03091..3001acf6e0 100644 --- a/tests/func-tests/zkevm/utils/orderbook/erc721.ts +++ b/tests/func-tests/zkevm/utils/orderbook/erc721.ts @@ -1,18 +1,18 @@ /* eslint-disable */ +import { randomBytes } from 'crypto'; import { Wallet } from 'ethers'; +import hre from 'hardhat'; +import { OperatorAllowlistUpgradeable__factory, TestERC721Token, TestERC721Token__factory } from '../../typechain-types'; import { GAS_OVERRIDES } from './gas'; -import { randomBytes } from 'crypto'; -import hre from 'hardhat' -import { OperatorAllowlistUpgradeable__factory, TestToken, TestToken__factory } from '../../typechain-types'; export function getRandomTokenId(): string { return BigInt('0x' + randomBytes(4).toString('hex')).toString(10); } -export async function connectToTestERC721Token(deployer: Wallet, tokenAddress: string): Promise { +export async function connectToTestERC721Token(deployer: Wallet, tokenAddress: string): Promise { const hreEthers = (hre as any).ethers; - const testTokenContractFactory = await hreEthers.getContractFactory("TestToken") as TestToken__factory; - return testTokenContractFactory.connect(deployer).attach(tokenAddress) as unknown as TestToken + const testTokenContractFactory = await hreEthers.getContractFactory("TestERC721Token") as TestERC721Token__factory; + return testTokenContractFactory.connect(deployer).attach(tokenAddress) as unknown as TestERC721Token } /** @@ -35,10 +35,10 @@ export async function deployERC721Token(deployer: Wallet, seaportAddress: string const tx = await allowlist.addAddressesToAllowlist([seaportAddress], GAS_OVERRIDES); await tx.wait(1); - const testTokenContractFactory = await hreEthers.getContractFactory("TestToken") as TestToken__factory; + const testTokenContractFactory = await hreEthers.getContractFactory("TestERC721Token") as TestERC721Token__factory; const testTokenContract = await testTokenContractFactory.connect(deployer).deploy( deployerAddress, - "Test", + "TestERC721", "TEST", "", "", @@ -55,4 +55,4 @@ export async function deployERC721Token(deployer: Wallet, seaportAddress: string await minterRoleTx.wait() console.log(`Minter role granted to ${deployerAddress}`) -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 83a6299d9a..106f2a44fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4774,6 +4774,7 @@ __metadata: dependencies: "@0xsequence/abi": ^1.4.3 "@0xsequence/core": ^1.4.3 + "@0xsquid/sdk": ^2.8.24 "@babel/core": ^7.21.0 "@babel/plugin-transform-class-properties": ^7.24.1 "@babel/plugin-transform-private-methods": ^7.24.1