From 99466bf63ff07b34b1d26a36c5405d6b07d5046f Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Fri, 7 Jun 2024 15:49:34 +1000 Subject: [PATCH 01/17] WIP: bulk order creation --- packages/orderbook/src/orderbook.ts | 24 ++++ packages/orderbook/src/seaport/components.ts | 12 ++ packages/orderbook/src/seaport/seaport.ts | 134 ++++++++++++++++++- packages/orderbook/src/test/manual.e2e.ts | 63 +++++++++ packages/orderbook/src/types.ts | 13 ++ 5 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 packages/orderbook/src/test/manual.e2e.ts diff --git a/packages/orderbook/src/orderbook.ts b/packages/orderbook/src/orderbook.ts index 85790e1d08..513cfb1d18 100644 --- a/packages/orderbook/src/orderbook.ts +++ b/packages/orderbook/src/orderbook.ts @@ -31,6 +31,8 @@ import { OrderStatusName, PrepareCancelOrdersResponse, PrepareListingParams, + PrepareBulkListingsParams, + PrepareBulkListingsResponse, PrepareListingResponse, SignablePurpose, TradeResult, @@ -160,6 +162,28 @@ export class Orderbook { }; } + /** + * TODO + */ + async prepareBulkListings( + { + makerAddress, + orderParams, + }: PrepareBulkListingsParams, + ): Promise { + // TODO: If a single order is passed, delegate to prepareListing as the signature + // will be more gase efficient at settlement time + return this.seaport.prepareBulkSeaportOrders( + makerAddress, + orderParams.map((orderParam) => ({ + listingItem: orderParam.sell, + considerationItem: orderParam.buy, + orderStart: new Date(), + orderExpiry: orderParam.orderExpiry || new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 2), + })), + ); + } + /** * Get required transactions and messages for signing prior to creating a listing * through the createListing method diff --git a/packages/orderbook/src/seaport/components.ts b/packages/orderbook/src/seaport/components.ts index 1fe6f065ef..79d43193a3 100644 --- a/packages/orderbook/src/seaport/components.ts +++ b/packages/orderbook/src/seaport/components.ts @@ -9,3 +9,15 @@ export function getOrderComponentsFromMessage(orderMessage: string): OrderCompon return orderComponents; } + +export function getBulkOrderComponentsFromMessage(orderMessage: string): OrderComponents[] { + const data = JSON.parse(orderMessage); + const orderComponents: OrderComponents[] = data.message.tree; + + // eslint-disable-next-line no-restricted-syntax + for (const orderComponent of orderComponents) { + orderComponent.salt = BigNumber.from(orderComponent.salt).toHexString(); + } + + return orderComponents; +} diff --git a/packages/orderbook/src/seaport/seaport.ts b/packages/orderbook/src/seaport/seaport.ts index 3beb91615d..70d0bad7f6 100644 --- a/packages/orderbook/src/seaport/seaport.ts +++ b/packages/orderbook/src/seaport/seaport.ts @@ -3,6 +3,7 @@ import { ApprovalAction, CreateInputItem, CreateOrderAction, + CreateBulkOrdersAction, ExchangeAction, OrderComponents, OrderUseCase, @@ -17,6 +18,7 @@ import { ERC721Item, FulfillOrderResponse, NativeItem, + PrepareBulkListingsResponse, PrepareListingResponse, SignableAction, SignablePurpose, @@ -30,7 +32,7 @@ import { SEAPORT_CONTRACT_NAME, SEAPORT_CONTRACT_VERSION_V1_5, } from './constants'; -import { getOrderComponentsFromMessage } from './components'; +import { getBulkOrderComponentsFromMessage, getOrderComponentsFromMessage } from './components'; import { SeaportLibFactory } from './seaport-lib-factory'; import { prepareTransaction } from './transaction'; import { mapImmutableOrderToSeaportOrderComponents } from './map-to-seaport-order'; @@ -44,6 +46,68 @@ export class Seaport { private rateLimitingKey?: string, ) {} + async prepareBulkSeaportOrders( + offerer: string, + orderInputs: { + listingItem: ERC721Item | ERC1155Item, + considerationItem: ERC20Item | NativeItem, + orderStart: Date, + orderExpiry: Date, + }[], + ): Promise { + const { actions: seaportActions } = await this.createSeaportOrders( + offerer, + orderInputs, + ); + + const approvalActions = seaportActions.filter((action) => action.type === 'approval') as + | ApprovalAction[] + | []; + + const network = await this.provider.getNetwork(); + const listingActions: Action[] = approvalActions.map((approvalAction) => ({ + type: ActionType.TRANSACTION, + purpose: TransactionPurpose.APPROVAL, + buildTransaction: prepareTransaction( + approvalAction.transactionMethods, + network.chainId, + offerer, + ), + })); + + const createAction: CreateBulkOrdersAction | undefined = seaportActions.find( + (action) => action.type === 'createBulk', + ) as CreateBulkOrdersAction | undefined; + + if (!createAction) { + throw new Error('No create bulk order action found'); + } + + const orderMessageToSign = await createAction.getMessageToSign(); + // The tree root is a zero property order that we dont need submitted to the API + const orders = getBulkOrderComponentsFromMessage(orderMessageToSign) + .filter((o) => o.offerer !== '0x0000000000000000000000000000000000000000'); + + // TODO: Comments to explain + const message = JSON.parse(orderMessageToSign); + delete message.types.EIP712Domain; + message.value = message.message; + + listingActions.push({ + type: ActionType.SIGNABLE, + purpose: SignablePurpose.CREATE_LISTING, + message, + }); + + return { + actions: listingActions, + preparedOrders: orders.map((orderComponent) => ({ + orderComponents: orderComponent, + orderHash: this.getSeaportLib().getOrderHash(orderComponent), + })), + }; + } + async prepareSeaportOrder( offerer: string, listingItem: ERC721Item | ERC1155Item, @@ -284,6 +348,54 @@ export class Seaport { }; } + private createSeaportOrders( + offerer: string, + orderInputs: { + listingItem: ERC721Item | ERC1155Item, + considerationItem: ERC20Item | NativeItem, + 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 { + 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); + } + private createSeaportOrder( offerer: string, listingItem: ERC721Item | ERC1155Item, @@ -327,6 +439,26 @@ export class Seaport { ); } + private async getTypedDataFromBulkOrderComponents( + orderComponents: OrderComponents, + message: string, + ): Promise { + const { chainId } = await this.provider.getNetwork(); + + const domainData = { + name: SEAPORT_CONTRACT_NAME, + version: SEAPORT_CONTRACT_VERSION_V1_5, + chainId, + verifyingContract: this.seaportContractAddress, + }; + + return { + domain: domainData, + types: EIP_712_ORDER_TYPE, + value: orderComponents, + }; + } + private async getTypedDataFromOrderComponents( orderComponents: OrderComponents, ): Promise { diff --git a/packages/orderbook/src/test/manual.e2e.ts b/packages/orderbook/src/test/manual.e2e.ts new file mode 100644 index 0000000000..94afeb9bfb --- /dev/null +++ b/packages/orderbook/src/test/manual.e2e.ts @@ -0,0 +1,63 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-restricted-syntax */ +import { Environment } from '@imtbl/config'; +import { OrderStatusName } from 'openapi/sdk'; +import { Orderbook } from 'orderbook'; +import { getLocalhostProvider } from './helpers/provider'; +import { getOffererWallet } from './helpers/signers'; +import { deployTestToken } from './helpers/erc721'; +import { waitForOrderToBeOfStatus } from './helpers/order'; +import { getConfigFromEnv } from './helpers'; +import { actionAll } from './helpers/actions'; + +describe('prepareListing and createOrder bulk e2e', () => { + it('should create the order', async () => { + const provider = getLocalhostProvider(); + const offerer = getOffererWallet(provider); + + const localConfigOverrides = getConfigFromEnv(); + const sdk = new Orderbook({ + baseConfig: { + environment: Environment.SANDBOX, + }, + overrides: { + ...localConfigOverrides, + }, + }); + + const { contract } = await deployTestToken(offerer); + await contract.safeMint(offerer.address); + + const bulkListings = await sdk.prepareBulkListings({ + makerAddress: offerer.address, + orderParams: [ + { + buy: { + amount: '1000000', + type: 'NATIVE', + }, + sell: { + contractAddress: contract.address, + tokenId: '0', + type: 'ERC721', + }, + }, + ], + }); + + const signatures = await actionAll(bulkListings.actions, offerer); + console.log(JSON.stringify(bulkListings.preparedOrders, null, 2)); + + const res = await sdk.createListing({ + orderComponents: bulkListings.preparedOrders[0].orderComponents, + orderHash: bulkListings.preparedOrders[0].orderHash, + orderSignature: signatures[0], + makerFees: [], + }).catch(e => { + console.log(JSON.stringify(e, null, 2)); + throw e + }); + + // await waitForOrderToBeOfStatus(sdk, orderId, OrderStatusName.ACTIVE); + }, 30_000); +}); diff --git a/packages/orderbook/src/types.ts b/packages/orderbook/src/types.ts index ee08e442cb..7b6b4e7595 100644 --- a/packages/orderbook/src/types.ts +++ b/packages/orderbook/src/types.ts @@ -47,6 +47,19 @@ export interface PrepareListingResponse { orderHash: string; } +export interface PrepareBulkListingsParams { + makerAddress: string; + orderParams: Omit[]; +} + +export interface PrepareBulkListingsResponse { + actions: Action[]; + preparedOrders: { + orderComponents: OrderComponents; + orderHash: string; + }[] +} + export interface PrepareCancelOrdersResponse { signableAction: SignableAction; } From 64f796e9bcf9a56b2afaa8e44bc6fc3211852a50 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 11 Jun 2024 13:37:40 +1000 Subject: [PATCH 02/17] feat: functional bulk listing creation --- packages/orderbook/src/orderbook.ts | 99 ++++++++++++++++++- packages/orderbook/src/seaport/components.ts | 9 ++ packages/orderbook/src/seaport/seaport.ts | 23 +++-- .../{manual.e2e.ts => bulk-listings.e2e.ts} | 41 +++++--- packages/orderbook/src/test/helpers/config.ts | 7 +- packages/orderbook/src/types.ts | 13 +++ tests/func-tests/zkevm/features/order.feature | 11 +++ .../zkevm/step-definitions/order.steps.ts | 44 ++++++++- .../zkevm/step-definitions/shared.ts | 72 +++++++++++++- tests/func-tests/zkevm/yarn.lock | 34 +++++-- 10 files changed, 309 insertions(+), 44 deletions(-) rename packages/orderbook/src/test/{manual.e2e.ts => bulk-listings.e2e.ts} (57%) diff --git a/packages/orderbook/src/orderbook.ts b/packages/orderbook/src/orderbook.ts index 513cfb1d18..ab96ed8642 100644 --- a/packages/orderbook/src/orderbook.ts +++ b/packages/orderbook/src/orderbook.ts @@ -13,6 +13,7 @@ import { mapFromOpenApiTrade, } from './openapi/mapper'; import { Seaport } from './seaport'; +import { getBulkSeaportOrderSignatures } from './seaport/components'; import { SeaportLibFactory } from './seaport/seaport-lib-factory'; import { ActionType, @@ -36,6 +37,8 @@ import { PrepareListingResponse, SignablePurpose, TradeResult, + CreateBulkListingsParams, + BulkListingsResult, } from './types'; /** @@ -163,7 +166,14 @@ export class Orderbook { } /** - * TODO + * Get required transactions and messages for signing prior to creating bulk listings + * through the createBulkListings method. This method only supports up to 10 listings + * at a time. It can also be used for individual listings to simplify integration code paths. + * @param {PrepareBulkListingsParams} prepareBulkListingsParams - Details about the listings + * to be created. + * @return {PrepareBulkListingsResponse} PrepareListingResponse includes + * any unsigned approval transactions, the typed bulk order message for signing and + * the order components that can be submitted to `createBulkListings` with the signature. */ async prepareBulkListings( { @@ -171,8 +181,32 @@ export class Orderbook { orderParams, }: PrepareBulkListingsParams, ): Promise { - // TODO: If a single order is passed, delegate to prepareListing as the signature - // will be more gase efficient at settlement time + // Limit bulk listing creation to 10 orders to prevent API and order evaluation spam + if (orderParams.length > 10) { + throw new Error('Bulk listing creation is limited to 10 orders'); + } + + // In the event of a single order, delegate to prepareListing as the signature is more + // gas efficient + if (orderParams.length === 1) { + const prepareListingResponse = await this.prepareListing({ + buy: orderParams[0].buy, + makerAddress, + sell: orderParams[0].sell, + orderExpiry: orderParams[0].orderExpiry, + }); + + return { + actions: prepareListingResponse.actions, + preparedOrders: [ + { + orderComponents: prepareListingResponse.orderComponents, + orderHash: prepareListingResponse.orderHash, + }, + ], + }; + } + return this.seaport.prepareBulkSeaportOrders( makerAddress, orderParams.map((orderParam) => ({ @@ -184,6 +218,65 @@ export class Orderbook { ); } + /** + * Create bulk listings. + * @param {CreateBulkListingsParams} createBulkListingParams - create bulk listings + * from with the given signature and order components. The createOrderParams array *must* + * be in the same order as the result of the prepareBulkListings method. + * @return {BulkListingsResult} The result of the listing creations from the Immutable orderbook + * API. + */ + async createBulkListings( + { bulkOrderSignature, createOrderParams }: CreateBulkListingsParams, + ): Promise { + // In the event of a single order, delegate to createListing as the signature will not + // be generated from a tree + if (createOrderParams.length === 1) { + const createOrderResponse = await this.createListing({ + ...createOrderParams[0], + orderSignature: bulkOrderSignature, + }); + + return { + result: [ + { + success: !!createOrderResponse.result, + orderHash: createOrderParams[0].orderHash, + order: createOrderResponse.result, + }, + ], + }; + } + + const orderComponents = createOrderParams.map((orderParam) => orderParam.orderComponents); + const signatures = getBulkSeaportOrderSignatures( + bulkOrderSignature, + orderComponents, + ); + + const createOrdersApiListingResponse = await Promise.all( + orderComponents.map((orderComponent, i) => { + const sig = signatures[i]; + const listingParams = createOrderParams[i]; + return this.apiClient.createListing({ + orderComponents: orderComponent, + orderHash: listingParams.orderHash, + orderSignature: sig, + makerFees: listingParams.makerFees, + // Swallow failed creations - this gets mapped in the response to caller as failed + }).catch(() => undefined); + }), + ); + + return { + result: createOrdersApiListingResponse.map((apiListingResponse, i) => ({ + success: !!apiListingResponse, + orderHash: createOrderParams[i].orderHash, + order: apiListingResponse ? mapFromOpenApiOrder(apiListingResponse.result) : undefined, + })), + }; + } + /** * Get required transactions and messages for signing prior to creating a listing * through the createListing method diff --git a/packages/orderbook/src/seaport/components.ts b/packages/orderbook/src/seaport/components.ts index 79d43193a3..ad8a5c4f8e 100644 --- a/packages/orderbook/src/seaport/components.ts +++ b/packages/orderbook/src/seaport/components.ts @@ -1,4 +1,5 @@ import { OrderComponents } from '@opensea/seaport-js/lib/types'; +import { getBulkOrderTree } from '@opensea/seaport-js/src/utils/eip712/bulk-orders'; import { BigNumber } from 'ethers'; export function getOrderComponentsFromMessage(orderMessage: string): OrderComponents { @@ -21,3 +22,11 @@ export function getBulkOrderComponentsFromMessage(orderMessage: string): OrderCo return orderComponents; } + +export function getBulkSeaportOrderSignatures( + signature: string, + orderComponents: OrderComponents[], +): string[] { + const tree = getBulkOrderTree(orderComponents); + return orderComponents.map((_, i) => tree.getEncodedProofAndSignature(i, signature)); +} diff --git a/packages/orderbook/src/seaport/seaport.ts b/packages/orderbook/src/seaport/seaport.ts index 70d0bad7f6..5ed3d2f2c1 100644 --- a/packages/orderbook/src/seaport/seaport.ts +++ b/packages/orderbook/src/seaport/seaport.ts @@ -84,19 +84,12 @@ export class Seaport { } const orderMessageToSign = await createAction.getMessageToSign(); - // The tree root is a zero property order that we dont need submitted to the API - const orders = getBulkOrderComponentsFromMessage(orderMessageToSign) - .filter((o) => o.offerer !== '0x0000000000000000000000000000000000000000'); - - // TODO: Comments to explain - const message = JSON.parse(orderMessageToSign); - delete message.types.EIP712Domain; - message.value = message.message; + const orders = getBulkOrderComponentsFromMessage(orderMessageToSign); listingActions.push({ type: ActionType.SIGNABLE, purpose: SignablePurpose.CREATE_LISTING, - message, + message: await this.getTypedDataFromBulkOrderComponents({ tree: orders }, orderInputs.length), }); return { @@ -440,8 +433,8 @@ export class Seaport { } private async getTypedDataFromBulkOrderComponents( - orderComponents: OrderComponents, - message: string, + orderComponents: { tree: OrderComponents[] }, + numberOfOrders: number, ): Promise { const { chainId } = await this.provider.getNetwork(); @@ -452,9 +445,15 @@ export class Seaport { verifyingContract: this.seaportContractAddress, }; + const bulkOrderType = [{ name: 'tree', type: `OrderComponents[${numberOfOrders}]` }]; + return { domain: domainData, - types: EIP_712_ORDER_TYPE, + types: { + // eslint-disable-next-line @typescript-eslint/naming-convention + BulkOrder: bulkOrderType, + ...EIP_712_ORDER_TYPE, + }, value: orderComponents, }; } diff --git a/packages/orderbook/src/test/manual.e2e.ts b/packages/orderbook/src/test/bulk-listings.e2e.ts similarity index 57% rename from packages/orderbook/src/test/manual.e2e.ts rename to packages/orderbook/src/test/bulk-listings.e2e.ts index 94afeb9bfb..3208031e59 100644 --- a/packages/orderbook/src/test/manual.e2e.ts +++ b/packages/orderbook/src/test/bulk-listings.e2e.ts @@ -21,12 +21,17 @@ describe('prepareListing and createOrder bulk e2e', () => { environment: Environment.SANDBOX, }, overrides: { - ...localConfigOverrides, + apiEndpoint: localConfigOverrides.apiEndpoint, + chainName: localConfigOverrides.chainName, + jsonRpcProviderUrl: localConfigOverrides.jsonRpcProviderUrl, + seaportContractAddress: localConfigOverrides.seaportContractAddress, + zoneContractAddress: localConfigOverrides.zoneContractAddress, }, }); const { contract } = await deployTestToken(offerer); await contract.safeMint(offerer.address); + await contract.safeMint(offerer.address); const bulkListings = await sdk.prepareBulkListings({ makerAddress: offerer.address, @@ -42,22 +47,36 @@ describe('prepareListing and createOrder bulk e2e', () => { type: 'ERC721', }, }, + { + buy: { + amount: '2000000', + type: 'NATIVE', + }, + sell: { + contractAddress: contract.address, + tokenId: '1', + type: 'ERC721', + }, + }, ], }); const signatures = await actionAll(bulkListings.actions, offerer); - console.log(JSON.stringify(bulkListings.preparedOrders, null, 2)); - const res = await sdk.createListing({ - orderComponents: bulkListings.preparedOrders[0].orderComponents, - orderHash: bulkListings.preparedOrders[0].orderHash, - orderSignature: signatures[0], - makerFees: [], - }).catch(e => { - console.log(JSON.stringify(e, null, 2)); - throw e + const res = await sdk.createBulkListings({ + bulkOrderSignature: signatures[0], + createOrderParams: bulkListings.preparedOrders.map((preparedOrder) => ({ + makerFees: [], + orderComponents: preparedOrder.orderComponents, + orderHash: preparedOrder.orderHash, + })), }); - // await waitForOrderToBeOfStatus(sdk, orderId, OrderStatusName.ACTIVE); + for (const result of res.result) { + if (!result.order) { + throw new Error('Order not created'); + } + await waitForOrderToBeOfStatus(sdk, result.order.id, OrderStatusName.ACTIVE); + } }, 30_000); }); diff --git a/packages/orderbook/src/test/helpers/config.ts b/packages/orderbook/src/test/helpers/config.ts index f7103ae3aa..23a32f54f2 100644 --- a/packages/orderbook/src/test/helpers/config.ts +++ b/packages/orderbook/src/test/helpers/config.ts @@ -1,11 +1,10 @@ // eslint-disable-next-line import/no-extraneous-dependencies import dotenv from 'dotenv'; -import { OrderbookModuleConfiguration } from 'config'; -import { getLocalhostProvider } from './provider'; +import { OrderbookOverrides } from 'config'; dotenv.config(); -export function getConfigFromEnv(): OrderbookModuleConfiguration { +export function getConfigFromEnv(): OrderbookOverrides { if ( !process.env.ORDERBOOK_MR_API_URL || !process.env.SEAPORT_CONTRACT_ADDRESS @@ -21,6 +20,6 @@ export function getConfigFromEnv(): OrderbookModuleConfiguration { chainName: process.env.CHAIN_NAME, seaportContractAddress: process.env.SEAPORT_CONTRACT_ADDRESS, zoneContractAddress: process.env.ZONE_CONTRACT_ADDRESS, - provider: getLocalhostProvider(), + jsonRpcProviderUrl: process.env.RPC_ENDPOINT, }; } diff --git a/packages/orderbook/src/types.ts b/packages/orderbook/src/types.ts index 7b6b4e7595..b9fc01108c 100644 --- a/packages/orderbook/src/types.ts +++ b/packages/orderbook/src/types.ts @@ -64,6 +64,11 @@ export interface PrepareCancelOrdersResponse { signableAction: SignableAction; } +export interface CreateBulkListingsParams { + bulkOrderSignature: string; + createOrderParams: Omit[]; +} + export interface CreateListingParams { orderComponents: OrderComponents; orderHash: string; @@ -220,6 +225,14 @@ export interface ListingResult { result: Order; } +export interface BulkListingsResult { + result: { + success: boolean; + orderHash: string; + order?: Order; + }[]; +} + export interface ListListingsResult { page: Page; result: Order[]; diff --git a/tests/func-tests/zkevm/features/order.feature b/tests/func-tests/zkevm/features/order.feature index 31a0b39ee8..122aece59a 100644 --- a/tests/func-tests/zkevm/features/order.feature +++ b/tests/func-tests/zkevm/features/order.feature @@ -13,6 +13,17 @@ Feature: orderbook And 1 ERC721 token should be transferred to the fulfiller And 1 trade should be available + Scenario: bulk creating and fulfilling ERC721 listings + Given I have a funded offerer account + And the offerer account has 2 ERC721 token + And I have a funded fulfiller account + When I bulk create listings to sell 2 ERC721 token + Then the listing should be of status active + When I fulfill the listing to buy 1 token + Then the listing should be of status filled + And 1 ERC721 token should be transferred to the fulfiller + And 1 trade should be available + Scenario: create and completely fill a ERC1155 listing Given I have a funded offerer account And the offerer account has 100 ERC1155 tokens diff --git a/tests/func-tests/zkevm/step-definitions/order.steps.ts b/tests/func-tests/zkevm/step-definitions/order.steps.ts index 9d49e2199f..4c94cffa84 100644 --- a/tests/func-tests/zkevm/step-definitions/order.steps.ts +++ b/tests/func-tests/zkevm/step-definitions/order.steps.ts @@ -14,6 +14,7 @@ import { whenICreateAListing, whenIFulfillTheListingToBuy, andERC1155TokensShouldBeTransferredToTheFulfiller, thenTheListingsShouldBeOfStatus, whenIFulfillBulkListings, + whenICreateABulkListing, } from './shared'; const feature = loadFeature('features/order.feature', { tagFilter: process.env.TAGS }); @@ -62,7 +63,7 @@ defineFeature(feature, (test) => { givenIHaveAFundedOffererAccount(given, bankerWallet, offerer); - andTheOffererAccountHasERC721Token(and, bankerWallet, offerer, erc721ContractAddress, testTokenId); + andTheOffererAccountHasERC721Token(and, bankerWallet, offerer, erc721ContractAddress, [testTokenId]); andIHaveAFundedFulfillerAccount(and, bankerWallet, fulfiller); @@ -79,6 +80,45 @@ defineFeature(feature, (test) => { andTradeShouldBeAvailable(and, sdk, fulfiller, getListingId); }, 120_000); + test('bulk creating and fulfilling ERC721 listings', async ({ + given, + when, + then, + and, + }) => { + const offerer = new Wallet(Wallet.createRandom().privateKey, provider); + const fulfiller = new Wallet(Wallet.createRandom().privateKey, provider); + const testTokenId1 = getRandomTokenId(); + const testTokenId2 = getRandomTokenId(); + + let listingId: string = ''; + + // these callback functions are required to update / retrieve test level state variables from shared steps. + const setListingId = (id: string) => { + listingId = id; + }; + + const getListingId = () => listingId; + + givenIHaveAFundedOffererAccount(given, bankerWallet, offerer); + + andTheOffererAccountHasERC721Token(and, bankerWallet, offerer, erc721ContractAddress, [testTokenId1, testTokenId2]); + + andIHaveAFundedFulfillerAccount(and, bankerWallet, fulfiller); + + whenICreateABulkListing(when, sdk, offerer, erc721ContractAddress, [testTokenId1, testTokenId2], setListingId); + + thenTheListingShouldBeOfStatus(then, sdk, getListingId); + + whenIFulfillTheListingToBuy(when, sdk, fulfiller, getListingId); + + thenTheListingShouldBeOfStatus(then, sdk, getListingId); + + andERC721TokenShouldBeTransferredToTheFulfiller(and, bankerWallet, erc721ContractAddress, testTokenId2, fulfiller); + + andTradeShouldBeAvailable(and, sdk, fulfiller, getListingId); + }, 120_000); + test('create and completely fill a ERC1155 listing', ({ given, when, @@ -196,7 +236,7 @@ defineFeature(feature, (test) => { andTheOffererAccountHasERC1155Tokens(and, bankerWallet, offerer, erc1155ContractAddress, testERC1155TokenId); - andTheOffererAccountHasERC721Token(and, bankerWallet, offerer, erc721ContractAddress, testERC721TokenId); + andTheOffererAccountHasERC721Token(and, bankerWallet, offerer, erc721ContractAddress, [testERC721TokenId]); andIHaveAFundedFulfillerAccount(and, bankerWallet, fulfiller); diff --git a/tests/func-tests/zkevm/step-definitions/shared.ts b/tests/func-tests/zkevm/step-definitions/shared.ts index 4c8c8d9c8d..ac9eb79dc1 100644 --- a/tests/func-tests/zkevm/step-definitions/shared.ts +++ b/tests/func-tests/zkevm/step-definitions/shared.ts @@ -35,12 +35,16 @@ export const andTheOffererAccountHasERC721Token = ( banker: Wallet, offerer: Wallet, contractAddress: string, - tokenId: string, + tokenIds: string[], ) => { and(/^the offerer account has (\d+) ERC721 token$/, async () => { const testToken = await connectToTestERC721Token(banker, contractAddress); - const mintTx = await testToken.mint(offerer.address, tokenId, GAS_OVERRIDES); - await mintTx.wait(1); + for (const tokenId of tokenIds) { + // eslint-disable-next-line no-await-in-loop + const mintTx = await testToken.mint(offerer.address, tokenId, GAS_OVERRIDES); + // eslint-disable-next-line no-await-in-loop + await mintTx.wait(1); + } }); }; @@ -120,6 +124,68 @@ export const whenICreateAListing = ( }); }; +export const whenICreateABulkListing = ( + when: DefineStepFunction, + sdk: orderbook.Orderbook, + offerer: Wallet, + contractAddress: string, + tokenIds: string[], + setListingId: (listingId: string) => void, +) => { + when(/^I bulk create listings to sell (\d+) (\w+) tokens?$/, async (amount, tokenType): Promise => { + const orderParams: any[] = []; + for (const tokenId of tokenIds) { + let sellItem; + if (tokenType === 'ERC721') { + sellItem = { + contractAddress, + tokenId, + type: 'ERC721', + } as orderbook.ERC721Item; + } else { + sellItem = { + contractAddress, + tokenId, + type: 'ERC1155', + amount: amount.toString(), + } as orderbook.ERC1155Item; + } + + orderParams.push({ + buy: { + amount: `${listingPrice}`, + type: 'NATIVE', + }, + sell: sellItem, + }); + } + + const listing = await sdk.prepareBulkListings({ + makerAddress: offerer.address, + orderParams, + }); + + const signatures = await actionAll(listing.actions, offerer); + const { result } = await sdk.createBulkListings({ + bulkOrderSignature: signatures[0], + createOrderParams: listing.preparedOrders.map((or) => ({ + makerFees: [], + orderComponents: or.orderComponents, + orderHash: or.orderHash, + })), + }); + + for (const res of result) { + if (!res.success) { + throw new Error(`Failed to create listing for order hash: ${res.orderHash}`); + } + } + + // Set the listing ID as the second order created to be filled in the next steps + setListingId(result[1].order?.id!); + }); +}; + export const thenTheListingShouldBeOfStatus = ( then: DefineStepFunction, sdk: orderbook.Orderbook, diff --git a/tests/func-tests/zkevm/yarn.lock b/tests/func-tests/zkevm/yarn.lock index 6d95bb792d..063589b5fc 100644 --- a/tests/func-tests/zkevm/yarn.lock +++ b/tests/func-tests/zkevm/yarn.lock @@ -1105,7 +1105,7 @@ __metadata: "@imtbl/sdk@file:../../../sdk::locator=func-tests-imx%40workspace%3A.": version: 0.0.0 - resolution: "@imtbl/sdk@file:../../../sdk#../../../sdk::hash=1967e6&locator=func-tests-imx%40workspace%3A." + resolution: "@imtbl/sdk@file:../../../sdk#../../../sdk::hash=e4abd4&locator=func-tests-imx%40workspace%3A." dependencies: "@0xsequence/abi": ^1.4.3 "@0xsequence/core": ^1.4.3 @@ -1123,6 +1123,7 @@ __metadata: "@metamask/detect-provider": ^2.0.0 "@opensea/seaport-js": 4.0.0 "@rive-app/react-canvas-lite": ^4.9.0 + "@stdlib/number-float64-base-normalize": 0.0.8 "@uniswap/router-sdk": ^1.4.0 "@uniswap/sdk-core": ^3.0.1 "@uniswap/v3-sdk": ^3.9.0 @@ -1145,7 +1146,7 @@ __metadata: jwt-decode: ^3.1.2 lru-memorise: 0.3.0 magic-sdk: ^21.2.0 - oidc-client-ts: 2.2.1 + oidc-client-ts: 2.4.0 os-browserify: ^0.3.0 pako: ^2.1.0 pg: ^8.11.5 @@ -1161,7 +1162,7 @@ __metadata: optional: true prisma: optional: true - checksum: b75200fbe949534cd7dc7e51c9f75bf8d99d91e131646e4c407bd5d87277f014a70b73ef2dcf42817486aa30056ff8207d84264c21e9533b272077d40d23655f + checksum: e891dff51ef06c93c5085c60647bd1e08b78bba5dec6f441378f5208c22316f5dd5657a16d4bc0c00f786f972a41857a7f83eafd8ecf855029075478b9324dcb languageName: node linkType: hard @@ -3992,6 +3993,21 @@ __metadata: languageName: node linkType: hard +"@stdlib/number-float64-base-normalize@npm:0.0.8": + version: 0.0.8 + resolution: "@stdlib/number-float64-base-normalize@npm:0.0.8" + dependencies: + "@stdlib/constants-float64-smallest-normal": ^0.0.x + "@stdlib/math-base-assert-is-infinite": ^0.0.x + "@stdlib/math-base-assert-is-nan": ^0.0.x + "@stdlib/math-base-special-abs": ^0.0.x + "@stdlib/types": ^0.0.x + "@stdlib/utils-define-nonenumerable-read-only-property": ^0.0.x + checksum: 677062923b00824bdb13263fdd1a9e721cacd4add2a9db014652a9cd77a053e531725f35313dbc80507f1b5a812d77b2ca6860008e38346d16f710fd427471f8 + conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows) + languageName: node + linkType: hard + "@stdlib/number-float64-base-normalize@npm:^0.0.x": version: 0.0.9 resolution: "@stdlib/number-float64-base-normalize@npm:0.0.9" @@ -7369,7 +7385,7 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.1.1, crypto-js@npm:^4.2.0": +"crypto-js@npm:^4.2.0": version: 4.2.0 resolution: "crypto-js@npm:4.2.0" checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774 @@ -13655,13 +13671,13 @@ __metadata: languageName: node linkType: hard -"oidc-client-ts@npm:2.2.1": - version: 2.2.1 - resolution: "oidc-client-ts@npm:2.2.1" +"oidc-client-ts@npm:2.4.0": + version: 2.4.0 + resolution: "oidc-client-ts@npm:2.4.0" dependencies: - crypto-js: ^4.1.1 + crypto-js: ^4.2.0 jwt-decode: ^3.1.2 - checksum: 83bc815d59a1221bdf2a39aff505941f1bf9aba542a59cc9cbe677b85fc06d4f555d55c1b0a6ad8ca76c57bacc423e1b03de5b7cef3d93c403126143f1152943 + checksum: 8467db689298221f706d3358961efb0ddc789f6bd7d4765e71ae5fe62067999d2ce6e8e7584b9d991b8caa6f7fb383f75841e1cfa9e05808c34632de374f5e68 languageName: node linkType: hard From d7b7a14bce37f48d1cce1dd5a59911759a40aac6 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 11 Jun 2024 13:41:32 +1000 Subject: [PATCH 03/17] remove unused test file --- .../orderbook/src/test/bulk-listings.e2e.ts | 82 ------------------- 1 file changed, 82 deletions(-) delete mode 100644 packages/orderbook/src/test/bulk-listings.e2e.ts diff --git a/packages/orderbook/src/test/bulk-listings.e2e.ts b/packages/orderbook/src/test/bulk-listings.e2e.ts deleted file mode 100644 index 3208031e59..0000000000 --- a/packages/orderbook/src/test/bulk-listings.e2e.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* eslint-disable no-await-in-loop */ -/* eslint-disable no-restricted-syntax */ -import { Environment } from '@imtbl/config'; -import { OrderStatusName } from 'openapi/sdk'; -import { Orderbook } from 'orderbook'; -import { getLocalhostProvider } from './helpers/provider'; -import { getOffererWallet } from './helpers/signers'; -import { deployTestToken } from './helpers/erc721'; -import { waitForOrderToBeOfStatus } from './helpers/order'; -import { getConfigFromEnv } from './helpers'; -import { actionAll } from './helpers/actions'; - -describe('prepareListing and createOrder bulk e2e', () => { - it('should create the order', async () => { - const provider = getLocalhostProvider(); - const offerer = getOffererWallet(provider); - - const localConfigOverrides = getConfigFromEnv(); - const sdk = new Orderbook({ - baseConfig: { - environment: Environment.SANDBOX, - }, - overrides: { - apiEndpoint: localConfigOverrides.apiEndpoint, - chainName: localConfigOverrides.chainName, - jsonRpcProviderUrl: localConfigOverrides.jsonRpcProviderUrl, - seaportContractAddress: localConfigOverrides.seaportContractAddress, - zoneContractAddress: localConfigOverrides.zoneContractAddress, - }, - }); - - const { contract } = await deployTestToken(offerer); - await contract.safeMint(offerer.address); - await contract.safeMint(offerer.address); - - const bulkListings = await sdk.prepareBulkListings({ - makerAddress: offerer.address, - orderParams: [ - { - buy: { - amount: '1000000', - type: 'NATIVE', - }, - sell: { - contractAddress: contract.address, - tokenId: '0', - type: 'ERC721', - }, - }, - { - buy: { - amount: '2000000', - type: 'NATIVE', - }, - sell: { - contractAddress: contract.address, - tokenId: '1', - type: 'ERC721', - }, - }, - ], - }); - - const signatures = await actionAll(bulkListings.actions, offerer); - - const res = await sdk.createBulkListings({ - bulkOrderSignature: signatures[0], - createOrderParams: bulkListings.preparedOrders.map((preparedOrder) => ({ - makerFees: [], - orderComponents: preparedOrder.orderComponents, - orderHash: preparedOrder.orderHash, - })), - }); - - for (const result of res.result) { - if (!result.order) { - throw new Error('Order not created'); - } - await waitForOrderToBeOfStatus(sdk, result.order.id, OrderStatusName.ACTIVE); - } - }, 30_000); -}); From 07a3bab35f066c5a6ee863651757f1f106774f02 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 11 Jun 2024 13:42:03 +1000 Subject: [PATCH 04/17] Revert unneeded config diff --- packages/orderbook/src/test/helpers/config.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/orderbook/src/test/helpers/config.ts b/packages/orderbook/src/test/helpers/config.ts index 23a32f54f2..f7103ae3aa 100644 --- a/packages/orderbook/src/test/helpers/config.ts +++ b/packages/orderbook/src/test/helpers/config.ts @@ -1,10 +1,11 @@ // eslint-disable-next-line import/no-extraneous-dependencies import dotenv from 'dotenv'; -import { OrderbookOverrides } from 'config'; +import { OrderbookModuleConfiguration } from 'config'; +import { getLocalhostProvider } from './provider'; dotenv.config(); -export function getConfigFromEnv(): OrderbookOverrides { +export function getConfigFromEnv(): OrderbookModuleConfiguration { if ( !process.env.ORDERBOOK_MR_API_URL || !process.env.SEAPORT_CONTRACT_ADDRESS @@ -20,6 +21,6 @@ export function getConfigFromEnv(): OrderbookOverrides { chainName: process.env.CHAIN_NAME, seaportContractAddress: process.env.SEAPORT_CONTRACT_ADDRESS, zoneContractAddress: process.env.ZONE_CONTRACT_ADDRESS, - jsonRpcProviderUrl: process.env.RPC_ENDPOINT, + provider: getLocalhostProvider(), }; } From 13ad74d2b805d137ce270efa04cde9d0932016d8 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 11 Jun 2024 15:02:46 +1000 Subject: [PATCH 05/17] Fix: EIP712 message generation for > 2 listings when bulk creating --- packages/orderbook/src/orderbook.ts | 34 ++--- packages/orderbook/src/seaport/components.ts | 7 +- packages/orderbook/src/seaport/constants.ts | 32 +++++ packages/orderbook/src/seaport/seaport.ts | 13 +- .../orderbook/src/test/bulk-listings.e2e.ts | 123 ++++++++++++++++++ packages/orderbook/src/types.ts | 6 +- .../zkevm/step-definitions/shared.ts | 12 +- tests/func-tests/zkevm/yarn.lock | 4 +- 8 files changed, 196 insertions(+), 35 deletions(-) create mode 100644 packages/orderbook/src/test/bulk-listings.e2e.ts diff --git a/packages/orderbook/src/orderbook.ts b/packages/orderbook/src/orderbook.ts index ab96ed8642..469b136dc2 100644 --- a/packages/orderbook/src/orderbook.ts +++ b/packages/orderbook/src/orderbook.ts @@ -178,27 +178,27 @@ export class Orderbook { async prepareBulkListings( { makerAddress, - orderParams, + listingParams, }: PrepareBulkListingsParams, ): Promise { // Limit bulk listing creation to 10 orders to prevent API and order evaluation spam - if (orderParams.length > 10) { + if (listingParams.length > 10) { throw new Error('Bulk listing creation is limited to 10 orders'); } // In the event of a single order, delegate to prepareListing as the signature is more // gas efficient - if (orderParams.length === 1) { + if (listingParams.length === 1) { const prepareListingResponse = await this.prepareListing({ - buy: orderParams[0].buy, + buy: listingParams[0].buy, makerAddress, - sell: orderParams[0].sell, - orderExpiry: orderParams[0].orderExpiry, + sell: listingParams[0].sell, + orderExpiry: listingParams[0].orderExpiry, }); return { actions: prepareListingResponse.actions, - preparedOrders: [ + preparedListings: [ { orderComponents: prepareListingResponse.orderComponents, orderHash: prepareListingResponse.orderHash, @@ -209,7 +209,7 @@ export class Orderbook { return this.seaport.prepareBulkSeaportOrders( makerAddress, - orderParams.map((orderParam) => ({ + listingParams.map((orderParam) => ({ listingItem: orderParam.sell, considerationItem: orderParam.buy, orderStart: new Date(), @@ -227,13 +227,13 @@ export class Orderbook { * API. */ async createBulkListings( - { bulkOrderSignature, createOrderParams }: CreateBulkListingsParams, + { bulkOrderSignature, listingParams }: CreateBulkListingsParams, ): Promise { // In the event of a single order, delegate to createListing as the signature will not // be generated from a tree - if (createOrderParams.length === 1) { + if (listingParams.length === 1) { const createOrderResponse = await this.createListing({ - ...createOrderParams[0], + ...listingParams[0], orderSignature: bulkOrderSignature, }); @@ -241,14 +241,14 @@ export class Orderbook { result: [ { success: !!createOrderResponse.result, - orderHash: createOrderParams[0].orderHash, + orderHash: listingParams[0].orderHash, order: createOrderResponse.result, }, ], }; } - const orderComponents = createOrderParams.map((orderParam) => orderParam.orderComponents); + const orderComponents = listingParams.map((orderParam) => orderParam.orderComponents); const signatures = getBulkSeaportOrderSignatures( bulkOrderSignature, orderComponents, @@ -257,12 +257,12 @@ export class Orderbook { const createOrdersApiListingResponse = await Promise.all( orderComponents.map((orderComponent, i) => { const sig = signatures[i]; - const listingParams = createOrderParams[i]; + const listing = listingParams[i]; return this.apiClient.createListing({ orderComponents: orderComponent, - orderHash: listingParams.orderHash, + orderHash: listing.orderHash, orderSignature: sig, - makerFees: listingParams.makerFees, + makerFees: listing.makerFees, // Swallow failed creations - this gets mapped in the response to caller as failed }).catch(() => undefined); }), @@ -271,7 +271,7 @@ export class Orderbook { return { result: createOrdersApiListingResponse.map((apiListingResponse, i) => ({ success: !!apiListingResponse, - orderHash: createOrderParams[i].orderHash, + orderHash: listingParams[i].orderHash, order: apiListingResponse ? mapFromOpenApiOrder(apiListingResponse.result) : undefined, })), }; diff --git a/packages/orderbook/src/seaport/components.ts b/packages/orderbook/src/seaport/components.ts index ad8a5c4f8e..d4d8c21d6d 100644 --- a/packages/orderbook/src/seaport/components.ts +++ b/packages/orderbook/src/seaport/components.ts @@ -1,5 +1,5 @@ import { OrderComponents } from '@opensea/seaport-js/lib/types'; -import { getBulkOrderTree } from '@opensea/seaport-js/src/utils/eip712/bulk-orders'; +import { getBulkOrderTree } from '@opensea/seaport-js/lib/utils/eip712/bulk-orders'; import { BigNumber } from 'ethers'; export function getOrderComponentsFromMessage(orderMessage: string): OrderComponents { @@ -13,7 +13,10 @@ export function getOrderComponentsFromMessage(orderMessage: string): OrderCompon export function getBulkOrderComponentsFromMessage(orderMessage: string): OrderComponents[] { const data = JSON.parse(orderMessage); - const orderComponents: OrderComponents[] = data.message.tree; + const orderComponents: OrderComponents[] = data.message.tree.flat(Infinity) + // Filter off the zero nodes in the tree. The will get rebuilt bu `getBulkOrderTree` + // when creating the listings + .filter((o: OrderComponents) => o.offerer !== '0x0000000000000000000000000000000000000000'); // eslint-disable-next-line no-restricted-syntax for (const orderComponent of orderComponents) { diff --git a/packages/orderbook/src/seaport/constants.ts b/packages/orderbook/src/seaport/constants.ts index da32e6f143..d5d9b9324d 100644 --- a/packages/orderbook/src/seaport/constants.ts +++ b/packages/orderbook/src/seaport/constants.ts @@ -36,6 +36,38 @@ export const EIP_712_ORDER_TYPE = { ], }; +export const EIP_712_BULK_ORDER_TYPE = { + BulkOrder: [{ name: "tree", type: "OrderComponents[2][2][2][2][2][2][2]" }], + OrderComponents: [ + { name: "offerer", type: "address" }, + { name: "zone", type: "address" }, + { name: "offer", type: "OfferItem[]" }, + { name: "consideration", type: "ConsiderationItem[]" }, + { name: "orderType", type: "uint8" }, + { name: "startTime", type: "uint256" }, + { name: "endTime", type: "uint256" }, + { name: "zoneHash", type: "bytes32" }, + { name: "salt", type: "uint256" }, + { name: "conduitKey", type: "bytes32" }, + { name: "counter", type: "uint256" }, + ], + OfferItem: [ + { name: "itemType", type: "uint8" }, + { name: "token", type: "address" }, + { name: "identifierOrCriteria", type: "uint256" }, + { name: "startAmount", type: "uint256" }, + { name: "endAmount", type: "uint256" }, + ], + ConsiderationItem: [ + { name: "itemType", type: "uint8" }, + { name: "token", type: "address" }, + { name: "identifierOrCriteria", type: "uint256" }, + { name: "startAmount", type: "uint256" }, + { name: "endAmount", type: "uint256" }, + { name: "recipient", type: "address" }, + ], +}; + export enum OrderType { FULL_OPEN = 0, // No partial fills, anyone can execute PARTIAL_OPEN = 1, // Partial fills supported, anyone can execute diff --git a/packages/orderbook/src/seaport/seaport.ts b/packages/orderbook/src/seaport/seaport.ts index 5ed3d2f2c1..3c2f539bb2 100644 --- a/packages/orderbook/src/seaport/seaport.ts +++ b/packages/orderbook/src/seaport/seaport.ts @@ -8,6 +8,7 @@ import { OrderComponents, OrderUseCase, } from '@opensea/seaport-js/lib/types'; +import { getBulkOrderTree, getBulkOrderTreeHeight } from '@opensea/seaport-js/lib/utils/eip712/bulk-orders'; import { providers } from 'ethers'; import { mapFromOpenApiOrder } from 'openapi/mapper'; import { @@ -89,12 +90,12 @@ export class Seaport { listingActions.push({ type: ActionType.SIGNABLE, purpose: SignablePurpose.CREATE_LISTING, - message: await this.getTypedDataFromBulkOrderComponents({ tree: orders }, orderInputs.length), + message: await this.getTypedDataFromBulkOrderComponents(orders, orderInputs.length), }); return { actions: listingActions, - preparedOrders: orders.map((orderComponent) => ({ + preparedListings: orders.map((orderComponent) => ({ orderComponents: orderComponent, orderHash: this.getSeaportLib().getOrderHash(orderComponent), })), @@ -433,7 +434,7 @@ export class Seaport { } private async getTypedDataFromBulkOrderComponents( - orderComponents: { tree: OrderComponents[] }, + orderComponents: OrderComponents[], numberOfOrders: number, ): Promise { const { chainId } = await this.provider.getNetwork(); @@ -445,7 +446,9 @@ export class Seaport { verifyingContract: this.seaportContractAddress, }; - const bulkOrderType = [{ name: 'tree', type: `OrderComponents[${numberOfOrders}]` }]; + const treeHeight = getBulkOrderTreeHeight(numberOfOrders); + const bulkOrderType = [{ name: 'tree', type: `OrderComponents${'[2]'.repeat(treeHeight)}` }]; + const tree = getBulkOrderTree(orderComponents); return { domain: domainData, @@ -454,7 +457,7 @@ export class Seaport { BulkOrder: bulkOrderType, ...EIP_712_ORDER_TYPE, }, - value: orderComponents, + value: { tree: tree.getDataToSign() }, }; } diff --git a/packages/orderbook/src/test/bulk-listings.e2e.ts b/packages/orderbook/src/test/bulk-listings.e2e.ts new file mode 100644 index 0000000000..3df0517b54 --- /dev/null +++ b/packages/orderbook/src/test/bulk-listings.e2e.ts @@ -0,0 +1,123 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-restricted-syntax */ +import { Environment } from '@imtbl/config'; +import { OrderStatusName } from 'openapi/sdk'; +import { Orderbook } from 'orderbook'; +import { getLocalhostProvider } from './helpers/provider'; +import { getOffererWallet } from './helpers/signers'; +import { deployTestToken } from './helpers/erc721'; +import { waitForOrderToBeOfStatus } from './helpers/order'; +import { getConfigFromEnv } from './helpers'; +import { actionAll } from './helpers/actions'; +import { PrepareBulkListingsParams } from '../types'; + +describe('prepareListing and createOrder bulk e2e', () => { + it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])('should create %d listings', async (numberOfListings) => { + const provider = getLocalhostProvider(); + const offerer = getOffererWallet(provider); + + const localConfigOverrides = getConfigFromEnv(); + const sdk = new Orderbook({ + baseConfig: { + environment: Environment.SANDBOX, + }, + overrides: { + ...localConfigOverrides, + }, + }); + + const { contract } = await deployTestToken(offerer); + + // Build the order params while minting the tokens + const orderParams: PrepareBulkListingsParams['listingParams'] = []; + let i = 0; + while (i < numberOfListings) { + await contract.safeMint(offerer.address); + + orderParams.push({ + buy: { + amount: '1000000', + type: 'NATIVE', + }, + sell: { + contractAddress: contract.address, + tokenId: `${i}`, + type: 'ERC721', + }, + }); + // eslint-disable-next-line no-plusplus + i++; + } + + await contract.safeMint(offerer.address); + + const bulkListings = await sdk.prepareBulkListings({ + makerAddress: offerer.address, + listingParams: orderParams, + }); + + const signatures = await actionAll(bulkListings.actions, offerer); + + const res = await sdk.createBulkListings({ + bulkOrderSignature: signatures[0], + listingParams: bulkListings.preparedListings.map((preparedOrder) => ({ + makerFees: [], + orderComponents: preparedOrder.orderComponents, + orderHash: preparedOrder.orderHash, + })), + }); + + for (const result of res.result) { + if (!result.order) { + throw new Error('Order not created'); + } + await waitForOrderToBeOfStatus(sdk, result.order.id, OrderStatusName.ACTIVE); + } + }, 30_000); + + it('should create fail to prepare more than 10 listings', async () => { + const provider = getLocalhostProvider(); + const offerer = getOffererWallet(provider); + + const localConfigOverrides = getConfigFromEnv(); + const sdk = new Orderbook({ + baseConfig: { + environment: Environment.SANDBOX, + }, + overrides: { + ...localConfigOverrides, + }, + }); + + const { contract } = await deployTestToken(offerer); + + // Build the order params while minting the tokens + const orderParams: PrepareBulkListingsParams['listingParams'] = []; + let i = 0; + const tooManyListings = 11; + while (i < tooManyListings) { + await contract.safeMint(offerer.address); + + orderParams.push({ + buy: { + amount: '1000000', + type: 'NATIVE', + }, + sell: { + contractAddress: contract.address, + tokenId: `${i}`, + type: 'ERC721', + }, + }); + // eslint-disable-next-line no-plusplus + i++; + } + + await contract.safeMint(offerer.address); + + await expect(sdk.prepareBulkListings({ + makerAddress: offerer.address, + listingParams: orderParams, + })).rejects.toEqual(new Error('Bulk listing creation is limited to 10 orders')); + }); +}); diff --git a/packages/orderbook/src/types.ts b/packages/orderbook/src/types.ts index b9fc01108c..2cfb148018 100644 --- a/packages/orderbook/src/types.ts +++ b/packages/orderbook/src/types.ts @@ -49,12 +49,12 @@ export interface PrepareListingResponse { export interface PrepareBulkListingsParams { makerAddress: string; - orderParams: Omit[]; + listingParams: Omit[]; } export interface PrepareBulkListingsResponse { actions: Action[]; - preparedOrders: { + preparedListings: { orderComponents: OrderComponents; orderHash: string; }[] @@ -66,7 +66,7 @@ export interface PrepareCancelOrdersResponse { export interface CreateBulkListingsParams { bulkOrderSignature: string; - createOrderParams: Omit[]; + listingParams: Omit[]; } export interface CreateListingParams { diff --git a/tests/func-tests/zkevm/step-definitions/shared.ts b/tests/func-tests/zkevm/step-definitions/shared.ts index ac9eb79dc1..fd17978a09 100644 --- a/tests/func-tests/zkevm/step-definitions/shared.ts +++ b/tests/func-tests/zkevm/step-definitions/shared.ts @@ -133,7 +133,7 @@ export const whenICreateABulkListing = ( setListingId: (listingId: string) => void, ) => { when(/^I bulk create listings to sell (\d+) (\w+) tokens?$/, async (amount, tokenType): Promise => { - const orderParams: any[] = []; + const listingParams: any[] = []; for (const tokenId of tokenIds) { let sellItem; if (tokenType === 'ERC721') { @@ -151,7 +151,7 @@ export const whenICreateABulkListing = ( } as orderbook.ERC1155Item; } - orderParams.push({ + listingParams.push({ buy: { amount: `${listingPrice}`, type: 'NATIVE', @@ -160,15 +160,15 @@ export const whenICreateABulkListing = ( }); } - const listing = await sdk.prepareBulkListings({ + const { actions, preparedListings } = await sdk.prepareBulkListings({ makerAddress: offerer.address, - orderParams, + listingParams, }); - const signatures = await actionAll(listing.actions, offerer); + const signatures = await actionAll(actions, offerer); const { result } = await sdk.createBulkListings({ bulkOrderSignature: signatures[0], - createOrderParams: listing.preparedOrders.map((or) => ({ + listingParams: preparedListings.map((or) => ({ makerFees: [], orderComponents: or.orderComponents, orderHash: or.orderHash, diff --git a/tests/func-tests/zkevm/yarn.lock b/tests/func-tests/zkevm/yarn.lock index 063589b5fc..2b19c48c5e 100644 --- a/tests/func-tests/zkevm/yarn.lock +++ b/tests/func-tests/zkevm/yarn.lock @@ -1105,7 +1105,7 @@ __metadata: "@imtbl/sdk@file:../../../sdk::locator=func-tests-imx%40workspace%3A.": version: 0.0.0 - resolution: "@imtbl/sdk@file:../../../sdk#../../../sdk::hash=e4abd4&locator=func-tests-imx%40workspace%3A." + resolution: "@imtbl/sdk@file:../../../sdk#../../../sdk::hash=218f6f&locator=func-tests-imx%40workspace%3A." dependencies: "@0xsequence/abi": ^1.4.3 "@0xsequence/core": ^1.4.3 @@ -1162,7 +1162,7 @@ __metadata: optional: true prisma: optional: true - checksum: e891dff51ef06c93c5085c60647bd1e08b78bba5dec6f441378f5208c22316f5dd5657a16d4bc0c00f786f972a41857a7f83eafd8ecf855029075478b9324dcb + checksum: a22cdb89e45fcf98a7833e00c7ceb3523bf22020a9c439c5c3fda7da8b466cadc161bf28474379ee95c4638b7de74d9d050fe42e1ea06402464046eb5e38005b languageName: node linkType: hard From b42712bdb5b73eabd4e08a087d45c7411a0f44d4 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 11 Jun 2024 15:05:31 +1000 Subject: [PATCH 06/17] remove unused constant --- packages/orderbook/src/seaport/constants.ts | 32 --------------------- 1 file changed, 32 deletions(-) diff --git a/packages/orderbook/src/seaport/constants.ts b/packages/orderbook/src/seaport/constants.ts index d5d9b9324d..da32e6f143 100644 --- a/packages/orderbook/src/seaport/constants.ts +++ b/packages/orderbook/src/seaport/constants.ts @@ -36,38 +36,6 @@ export const EIP_712_ORDER_TYPE = { ], }; -export const EIP_712_BULK_ORDER_TYPE = { - BulkOrder: [{ name: "tree", type: "OrderComponents[2][2][2][2][2][2][2]" }], - OrderComponents: [ - { name: "offerer", type: "address" }, - { name: "zone", type: "address" }, - { name: "offer", type: "OfferItem[]" }, - { name: "consideration", type: "ConsiderationItem[]" }, - { name: "orderType", type: "uint8" }, - { name: "startTime", type: "uint256" }, - { name: "endTime", type: "uint256" }, - { name: "zoneHash", type: "bytes32" }, - { name: "salt", type: "uint256" }, - { name: "conduitKey", type: "bytes32" }, - { name: "counter", type: "uint256" }, - ], - OfferItem: [ - { name: "itemType", type: "uint8" }, - { name: "token", type: "address" }, - { name: "identifierOrCriteria", type: "uint256" }, - { name: "startAmount", type: "uint256" }, - { name: "endAmount", type: "uint256" }, - ], - ConsiderationItem: [ - { name: "itemType", type: "uint8" }, - { name: "token", type: "address" }, - { name: "identifierOrCriteria", type: "uint256" }, - { name: "startAmount", type: "uint256" }, - { name: "endAmount", type: "uint256" }, - { name: "recipient", type: "address" }, - ], -}; - export enum OrderType { FULL_OPEN = 0, // No partial fills, anyone can execute PARTIAL_OPEN = 1, // Partial fills supported, anyone can execute From 2a710412b2132d17fa7d65f2bce7cd00a373fb17 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Wed, 12 Jun 2024 09:32:42 +1000 Subject: [PATCH 07/17] Improve usability of bulk listing interfaces --- packages/orderbook/src/orderbook.ts | 114 ++++++++---------- packages/orderbook/src/seaport/seaport.ts | 4 +- .../orderbook/src/test/bulk-listings.e2e.ts | 19 ++- packages/orderbook/src/types.ts | 17 ++- .../zkevm/step-definitions/shared.ts | 11 +- tests/func-tests/zkevm/yarn.lock | 4 +- 6 files changed, 74 insertions(+), 95 deletions(-) diff --git a/packages/orderbook/src/orderbook.ts b/packages/orderbook/src/orderbook.ts index 469b136dc2..8225edc899 100644 --- a/packages/orderbook/src/orderbook.ts +++ b/packages/orderbook/src/orderbook.ts @@ -37,8 +37,6 @@ import { PrepareListingResponse, SignablePurpose, TradeResult, - CreateBulkListingsParams, - BulkListingsResult, } from './types'; /** @@ -166,14 +164,15 @@ export class Orderbook { } /** - * Get required transactions and messages for signing prior to creating bulk listings - * through the createBulkListings method. This method only supports up to 10 listings + * Get required transactions and messages for signing prior to creating bulk listings. + * Once the transactions are submitting and the message signed, call the createListings method + * provided in the return type with the signature. This method only supports up to 10 listings * at a time. It can also be used for individual listings to simplify integration code paths. * @param {PrepareBulkListingsParams} prepareBulkListingsParams - Details about the listings * to be created. * @return {PrepareBulkListingsResponse} PrepareListingResponse includes * any unsigned approval transactions, the typed bulk order message for signing and - * the order components that can be submitted to `createBulkListings` with the signature. + * the createListings method that can be called with the signature to create the listings. */ async prepareBulkListings( { @@ -198,16 +197,26 @@ export class Orderbook { return { actions: prepareListingResponse.actions, - preparedListings: [ - { + createListings: async (signature: string) => { + const createListingResult = await this.createListing({ + makerFees: [], orderComponents: prepareListingResponse.orderComponents, orderHash: prepareListingResponse.orderHash, - }, - ], + orderSignature: signature, + }); + + return { + result: [{ + success: !!createListingResult.result, + orderHash: prepareListingResponse.orderHash, + order: createListingResult.result, + }], + }; + }, }; } - return this.seaport.prepareBulkSeaportOrders( + const { actions, preparedListings } = await this.seaport.prepareBulkSeaportOrders( makerAddress, listingParams.map((orderParam) => ({ listingItem: orderParam.sell, @@ -216,64 +225,39 @@ export class Orderbook { orderExpiry: orderParam.orderExpiry || new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 2), })), ); - } - /** - * Create bulk listings. - * @param {CreateBulkListingsParams} createBulkListingParams - create bulk listings - * from with the given signature and order components. The createOrderParams array *must* - * be in the same order as the result of the prepareBulkListings method. - * @return {BulkListingsResult} The result of the listing creations from the Immutable orderbook - * API. - */ - async createBulkListings( - { bulkOrderSignature, listingParams }: CreateBulkListingsParams, - ): Promise { - // In the event of a single order, delegate to createListing as the signature will not - // be generated from a tree - if (listingParams.length === 1) { - const createOrderResponse = await this.createListing({ - ...listingParams[0], - orderSignature: bulkOrderSignature, - }); - - return { - result: [ - { - success: !!createOrderResponse.result, - orderHash: listingParams[0].orderHash, - order: createOrderResponse.result, - }, - ], - }; - } - - const orderComponents = listingParams.map((orderParam) => orderParam.orderComponents); - const signatures = getBulkSeaportOrderSignatures( - bulkOrderSignature, - orderComponents, - ); + return { + actions, + createListings: async (bulkOrderSignature: string) => { + const orderComponents = preparedListings.map((orderParam) => orderParam.orderComponents); + const signatures = getBulkSeaportOrderSignatures( + bulkOrderSignature, + orderComponents, + ); - const createOrdersApiListingResponse = await Promise.all( - orderComponents.map((orderComponent, i) => { - const sig = signatures[i]; - const listing = listingParams[i]; - return this.apiClient.createListing({ - orderComponents: orderComponent, - orderHash: listing.orderHash, - orderSignature: sig, - makerFees: listing.makerFees, - // Swallow failed creations - this gets mapped in the response to caller as failed - }).catch(() => undefined); - }), - ); + const createOrdersApiListingResponse = await Promise.all( + orderComponents.map((orderComponent, i) => { + const sig = signatures[i]; + const listing = preparedListings[i]; + const listingParam = listingParams[i]; + return this.apiClient.createListing({ + orderComponents: orderComponent, + orderHash: listing.orderHash, + orderSignature: sig, + makerFees: listingParam.makerFees, + // Swallow failed creations - this gets mapped in the response to caller as failed + }).catch(() => undefined); + }), + ); - return { - result: createOrdersApiListingResponse.map((apiListingResponse, i) => ({ - success: !!apiListingResponse, - orderHash: listingParams[i].orderHash, - order: apiListingResponse ? mapFromOpenApiOrder(apiListingResponse.result) : undefined, - })), + return { + result: createOrdersApiListingResponse.map((apiListingResponse, i) => ({ + success: !!apiListingResponse, + orderHash: preparedListings[i].orderHash, + order: apiListingResponse ? mapFromOpenApiOrder(apiListingResponse.result) : undefined, + })), + }; + }, }; } diff --git a/packages/orderbook/src/seaport/seaport.ts b/packages/orderbook/src/seaport/seaport.ts index 3c2f539bb2..54a8cea715 100644 --- a/packages/orderbook/src/seaport/seaport.ts +++ b/packages/orderbook/src/seaport/seaport.ts @@ -19,7 +19,7 @@ import { ERC721Item, FulfillOrderResponse, NativeItem, - PrepareBulkListingsResponse, + PrepareBulkSeaportOrders, PrepareListingResponse, SignableAction, SignablePurpose, @@ -55,7 +55,7 @@ export class Seaport { orderStart: Date, orderExpiry: Date, }[], - ): Promise { + ): Promise { const { actions: seaportActions } = await this.createSeaportOrders( offerer, orderInputs, diff --git a/packages/orderbook/src/test/bulk-listings.e2e.ts b/packages/orderbook/src/test/bulk-listings.e2e.ts index 3df0517b54..09008c8285 100644 --- a/packages/orderbook/src/test/bulk-listings.e2e.ts +++ b/packages/orderbook/src/test/bulk-listings.e2e.ts @@ -44,6 +44,10 @@ describe('prepareListing and createOrder bulk e2e', () => { tokenId: `${i}`, type: 'ERC721', }, + makerFees: [{ + amount: '10000', + recipientAddress: offerer.address, + }], }); // eslint-disable-next-line no-plusplus i++; @@ -51,21 +55,13 @@ describe('prepareListing and createOrder bulk e2e', () => { await contract.safeMint(offerer.address); - const bulkListings = await sdk.prepareBulkListings({ + const { actions, createListings } = await sdk.prepareBulkListings({ makerAddress: offerer.address, listingParams: orderParams, }); - const signatures = await actionAll(bulkListings.actions, offerer); - - const res = await sdk.createBulkListings({ - bulkOrderSignature: signatures[0], - listingParams: bulkListings.preparedListings.map((preparedOrder) => ({ - makerFees: [], - orderComponents: preparedOrder.orderComponents, - orderHash: preparedOrder.orderHash, - })), - }); + const signatures = await actionAll(actions, offerer); + const res = await createListings(signatures[0]); for (const result of res.result) { if (!result.order) { @@ -108,6 +104,7 @@ describe('prepareListing and createOrder bulk e2e', () => { tokenId: `${i}`, type: 'ERC721', }, + makerFees: [], }); // eslint-disable-next-line no-plusplus i++; diff --git a/packages/orderbook/src/types.ts b/packages/orderbook/src/types.ts index 2cfb148018..189cf2c328 100644 --- a/packages/orderbook/src/types.ts +++ b/packages/orderbook/src/types.ts @@ -49,10 +49,20 @@ export interface PrepareListingResponse { export interface PrepareBulkListingsParams { makerAddress: string; - listingParams: Omit[]; + listingParams: { + sell: ERC721Item | ERC1155Item; + buy: ERC20Item | NativeItem; + makerFees: FeeValue[]; + orderExpiry?: Date; + }[] } export interface PrepareBulkListingsResponse { + actions: Action[]; + createListings: (signature: string) => Promise; +} + +export interface PrepareBulkSeaportOrders { actions: Action[]; preparedListings: { orderComponents: OrderComponents; @@ -64,11 +74,6 @@ export interface PrepareCancelOrdersResponse { signableAction: SignableAction; } -export interface CreateBulkListingsParams { - bulkOrderSignature: string; - listingParams: Omit[]; -} - export interface CreateListingParams { orderComponents: OrderComponents; orderHash: string; diff --git a/tests/func-tests/zkevm/step-definitions/shared.ts b/tests/func-tests/zkevm/step-definitions/shared.ts index fd17978a09..9e3d82b4d5 100644 --- a/tests/func-tests/zkevm/step-definitions/shared.ts +++ b/tests/func-tests/zkevm/step-definitions/shared.ts @@ -160,20 +160,13 @@ export const whenICreateABulkListing = ( }); } - const { actions, preparedListings } = await sdk.prepareBulkListings({ + const { actions, createListings } = await sdk.prepareBulkListings({ makerAddress: offerer.address, listingParams, }); const signatures = await actionAll(actions, offerer); - const { result } = await sdk.createBulkListings({ - bulkOrderSignature: signatures[0], - listingParams: preparedListings.map((or) => ({ - makerFees: [], - orderComponents: or.orderComponents, - orderHash: or.orderHash, - })), - }); + const { result } = await createListings(signatures[0]); for (const res of result) { if (!res.success) { diff --git a/tests/func-tests/zkevm/yarn.lock b/tests/func-tests/zkevm/yarn.lock index 2b19c48c5e..d3757d45d9 100644 --- a/tests/func-tests/zkevm/yarn.lock +++ b/tests/func-tests/zkevm/yarn.lock @@ -1105,7 +1105,7 @@ __metadata: "@imtbl/sdk@file:../../../sdk::locator=func-tests-imx%40workspace%3A.": version: 0.0.0 - resolution: "@imtbl/sdk@file:../../../sdk#../../../sdk::hash=218f6f&locator=func-tests-imx%40workspace%3A." + resolution: "@imtbl/sdk@file:../../../sdk#../../../sdk::hash=86877a&locator=func-tests-imx%40workspace%3A." dependencies: "@0xsequence/abi": ^1.4.3 "@0xsequence/core": ^1.4.3 @@ -1162,7 +1162,7 @@ __metadata: optional: true prisma: optional: true - checksum: a22cdb89e45fcf98a7833e00c7ceb3523bf22020a9c439c5c3fda7da8b466cadc161bf28474379ee95c4638b7de74d9d050fe42e1ea06402464046eb5e38005b + checksum: 051bfbebe60446e4af3d16ae2e7ce620a04b198e6a0052c081a691912ba8857869658cf8a74ede6a3a5f8b38da8c898440b4d81e1bd9ae0f2f47d70b3b7db4dd languageName: node linkType: hard From 0ff473d4c4b9ac069b672854580e3ac2afefd395 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Wed, 12 Jun 2024 10:55:01 +1000 Subject: [PATCH 08/17] fix: func tests --- tests/func-tests/zkevm/step-definitions/shared.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/func-tests/zkevm/step-definitions/shared.ts b/tests/func-tests/zkevm/step-definitions/shared.ts index 9e3d82b4d5..74f949cedd 100644 --- a/tests/func-tests/zkevm/step-definitions/shared.ts +++ b/tests/func-tests/zkevm/step-definitions/shared.ts @@ -157,6 +157,7 @@ export const whenICreateABulkListing = ( type: 'NATIVE', }, sell: sellItem, + makerFees: [], }); } From 4115a9485bf6f29ac1a33966fa452eabcbfd2879 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Mon, 17 Jun 2024 12:36:30 +1000 Subject: [PATCH 09/17] bump listing limit to 20 --- packages/orderbook/src/orderbook.ts | 6 +++--- packages/orderbook/src/test/bulk-listings.e2e.ts | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/orderbook/src/orderbook.ts b/packages/orderbook/src/orderbook.ts index 8225edc899..bc1c1fcdb6 100644 --- a/packages/orderbook/src/orderbook.ts +++ b/packages/orderbook/src/orderbook.ts @@ -180,9 +180,9 @@ export class Orderbook { listingParams, }: PrepareBulkListingsParams, ): Promise { - // Limit bulk listing creation to 10 orders to prevent API and order evaluation spam - if (listingParams.length > 10) { - throw new Error('Bulk listing creation is limited to 10 orders'); + // Limit bulk listing creation to 20 orders to prevent API and order evaluation spam + if (listingParams.length > 20) { + throw new Error('Bulk listing creation is limited to 20 orders'); } // In the event of a single order, delegate to prepareListing as the signature is more diff --git a/packages/orderbook/src/test/bulk-listings.e2e.ts b/packages/orderbook/src/test/bulk-listings.e2e.ts index 09008c8285..b63e95d092 100644 --- a/packages/orderbook/src/test/bulk-listings.e2e.ts +++ b/packages/orderbook/src/test/bulk-listings.e2e.ts @@ -11,8 +11,11 @@ import { getConfigFromEnv } from './helpers'; import { actionAll } from './helpers/actions'; import { PrepareBulkListingsParams } from '../types'; +// An array of each number between 1 and 20 +const supportedListings = Array.from({ length: 20 }, (_, i) => i + 1); + describe('prepareListing and createOrder bulk e2e', () => { - it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])('should create %d listings', async (numberOfListings) => { + it.each(supportedListings)('should create %d listings', async (numberOfListings) => { const provider = getLocalhostProvider(); const offerer = getOffererWallet(provider); @@ -71,7 +74,7 @@ describe('prepareListing and createOrder bulk e2e', () => { } }, 30_000); - it('should create fail to prepare more than 10 listings', async () => { + it('should create fail to prepare more than 20 listings', async () => { const provider = getLocalhostProvider(); const offerer = getOffererWallet(provider); @@ -90,7 +93,7 @@ describe('prepareListing and createOrder bulk e2e', () => { // Build the order params while minting the tokens const orderParams: PrepareBulkListingsParams['listingParams'] = []; let i = 0; - const tooManyListings = 11; + const tooManyListings = 21; while (i < tooManyListings) { await contract.safeMint(offerer.address); @@ -115,6 +118,6 @@ describe('prepareListing and createOrder bulk e2e', () => { await expect(sdk.prepareBulkListings({ makerAddress: offerer.address, listingParams: orderParams, - })).rejects.toEqual(new Error('Bulk listing creation is limited to 10 orders')); - }); + })).rejects.toEqual(new Error('Bulk listing creation is limited to 20 orders')); + }, 30_000); }); From 88064954d50f4720ecb2a35620eec04cfae4a4d1 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 18 Jun 2024 09:42:50 +1000 Subject: [PATCH 10/17] PR review --- .github/CODEOWNERS | 1 + packages/blockchain-data/sdk/package.json | 2 +- packages/orderbook/src/orderbook.ts | 14 +++++----- packages/orderbook/src/seaport/components.ts | 8 ++++-- packages/orderbook/src/seaport/seaport.ts | 28 ++++++++----------- .../orderbook/src/test/bulk-listings.e2e.ts | 2 +- packages/orderbook/src/types.ts | 2 +- packages/webhook/sdk/package.json | 6 ++-- .../zkevm/step-definitions/shared.ts | 4 +-- 9 files changed, 34 insertions(+), 33 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a2a1f560e9..7631098b92 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,7 @@ /packages/passport @immutable/passport /packages/x-provider @immutable/commerce /packages/checkout @immutable/commerce +/packages/orderbook @immutable/traders /packages/checkout/widgets-lib @immutable/commerce /packages/blockchain-data @immutable/assets /packages/minting-backend @shineli1984 diff --git a/packages/blockchain-data/sdk/package.json b/packages/blockchain-data/sdk/package.json index d2eb4d5400..a0fa0e2bbf 100644 --- a/packages/blockchain-data/sdk/package.json +++ b/packages/blockchain-data/sdk/package.json @@ -38,8 +38,8 @@ "generate-types": "typechain --target=ethers-v5 --out-dir=src/typechain/types 'abi/*.json'", "lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0", "test": "jest", - "test:watch": "jest --watch", "test:e2e": "jest --runInBand --testMatch \"**/?(*.)+(e2e).[jt]s?(x)\"", + "test:watch": "jest --watch", "typecheck": "tsc --noEmit --jsx preserve" }, "type": "module", diff --git a/packages/orderbook/src/orderbook.ts b/packages/orderbook/src/orderbook.ts index bc1c1fcdb6..7de4a133ec 100644 --- a/packages/orderbook/src/orderbook.ts +++ b/packages/orderbook/src/orderbook.ts @@ -164,9 +164,9 @@ export class Orderbook { } /** - * Get required transactions and messages for signing prior to creating bulk listings. - * Once the transactions are submitting and the message signed, call the createListings method - * provided in the return type with the signature. This method only supports up to 10 listings + * 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 * at a time. It can also be used for individual listings to simplify integration code paths. * @param {PrepareBulkListingsParams} prepareBulkListingsParams - Details about the listings * to be created. @@ -197,7 +197,7 @@ export class Orderbook { return { actions: prepareListingResponse.actions, - createListings: async (signature: string) => { + completeListings: async (signature: string) => { const createListingResult = await this.createListing({ makerFees: [], orderComponents: prepareListingResponse.orderComponents, @@ -207,7 +207,7 @@ export class Orderbook { return { result: [{ - success: !!createListingResult.result, + success: true, orderHash: prepareListingResponse.orderHash, order: createListingResult.result, }], @@ -228,7 +228,7 @@ export class Orderbook { return { actions, - createListings: async (bulkOrderSignature: string) => { + completeListings: async (bulkOrderSignature: string) => { const orderComponents = preparedListings.map((orderParam) => orderParam.orderComponents); const signatures = getBulkSeaportOrderSignatures( bulkOrderSignature, @@ -245,7 +245,7 @@ export class Orderbook { orderHash: listing.orderHash, orderSignature: sig, makerFees: listingParam.makerFees, - // Swallow failed creations - this gets mapped in the response to caller as failed + // Swallow failed creations - this gets mapped in the response to the caller as failed }).catch(() => undefined); }), ); diff --git a/packages/orderbook/src/seaport/components.ts b/packages/orderbook/src/seaport/components.ts index d4d8c21d6d..0fb8b5d16b 100644 --- a/packages/orderbook/src/seaport/components.ts +++ b/packages/orderbook/src/seaport/components.ts @@ -11,7 +11,11 @@ export function getOrderComponentsFromMessage(orderMessage: string): OrderCompon return orderComponents; } -export function getBulkOrderComponentsFromMessage(orderMessage: string): OrderComponents[] { +export function getBulkOrderComponentsFromMessage(orderMessage: string): { + components: OrderComponents[], + types: any, + value: any +} { const data = JSON.parse(orderMessage); const orderComponents: OrderComponents[] = data.message.tree.flat(Infinity) // Filter off the zero nodes in the tree. The will get rebuilt bu `getBulkOrderTree` @@ -23,7 +27,7 @@ export function getBulkOrderComponentsFromMessage(orderMessage: string): OrderCo orderComponent.salt = BigNumber.from(orderComponent.salt).toHexString(); } - return orderComponents; + return { components: orderComponents, types: data.types, value: data.message }; } export function getBulkSeaportOrderSignatures( diff --git a/packages/orderbook/src/seaport/seaport.ts b/packages/orderbook/src/seaport/seaport.ts index 2b87b344ff..4437f56574 100644 --- a/packages/orderbook/src/seaport/seaport.ts +++ b/packages/orderbook/src/seaport/seaport.ts @@ -8,7 +8,6 @@ import { OrderComponents, OrderUseCase, } from '@opensea/seaport-js/lib/types'; -import { getBulkOrderTree, getBulkOrderTreeHeight } from '@opensea/seaport-js/lib/utils/eip712/bulk-orders'; import { providers } from 'ethers'; import { mapFromOpenApiOrder } from 'openapi/mapper'; import { @@ -85,17 +84,17 @@ export class Seaport { } const orderMessageToSign = await createAction.getMessageToSign(); - const orders = getBulkOrderComponentsFromMessage(orderMessageToSign); + const { components, types, value } = getBulkOrderComponentsFromMessage(orderMessageToSign); listingActions.push({ type: ActionType.SIGNABLE, purpose: SignablePurpose.CREATE_LISTING, - message: await this.getTypedDataFromBulkOrderComponents(orders, orderInputs.length), + message: await this.getTypedDataFromBulkOrderComponents(types, value), }); return { actions: listingActions, - preparedListings: orders.map((orderComponent) => ({ + preparedListings: components.map((orderComponent) => ({ orderComponents: orderComponent, orderHash: this.getSeaportLib().getOrderHash(orderComponent), })), @@ -411,10 +410,15 @@ export class Seaport { ); } + // Types and value are JSON parsed from the seaport-js string, so the types are + // reflected as any private async getTypedDataFromBulkOrderComponents( - orderComponents: OrderComponents[], - numberOfOrders: number, + types: any, + value: any, ): Promise { + // We must remove EIP712Domain from the types object + // eslint-disable-next-line no-param-reassign + delete types.EIP712Domain; const { chainId } = await this.provider.getNetwork(); const domainData = { @@ -424,18 +428,10 @@ export class Seaport { verifyingContract: this.seaportContractAddress, }; - const treeHeight = getBulkOrderTreeHeight(numberOfOrders); - const bulkOrderType = [{ name: 'tree', type: `OrderComponents${'[2]'.repeat(treeHeight)}` }]; - const tree = getBulkOrderTree(orderComponents); - return { domain: domainData, - types: { - // eslint-disable-next-line @typescript-eslint/naming-convention - BulkOrder: bulkOrderType, - ...EIP_712_ORDER_TYPE, - }, - value: { tree: tree.getDataToSign() }, + types, + value, }; } diff --git a/packages/orderbook/src/test/bulk-listings.e2e.ts b/packages/orderbook/src/test/bulk-listings.e2e.ts index b63e95d092..ad895882f7 100644 --- a/packages/orderbook/src/test/bulk-listings.e2e.ts +++ b/packages/orderbook/src/test/bulk-listings.e2e.ts @@ -58,7 +58,7 @@ describe('prepareListing and createOrder bulk e2e', () => { await contract.safeMint(offerer.address); - const { actions, createListings } = await sdk.prepareBulkListings({ + const { actions, completeListings: createListings } = await sdk.prepareBulkListings({ makerAddress: offerer.address, listingParams: orderParams, }); diff --git a/packages/orderbook/src/types.ts b/packages/orderbook/src/types.ts index 189cf2c328..cb49a845e9 100644 --- a/packages/orderbook/src/types.ts +++ b/packages/orderbook/src/types.ts @@ -59,7 +59,7 @@ export interface PrepareBulkListingsParams { export interface PrepareBulkListingsResponse { actions: Action[]; - createListings: (signature: string) => Promise; + completeListings: (signature: string) => Promise; } export interface PrepareBulkSeaportOrders { diff --git a/packages/webhook/sdk/package.json b/packages/webhook/sdk/package.json index 49f316f5a4..b84b6e62ec 100644 --- a/packages/webhook/sdk/package.json +++ b/packages/webhook/sdk/package.json @@ -25,6 +25,9 @@ "typechain": "^8.1.1", "typescript": "^4.9.5" }, + "files": [ + "dist" + ], "homepage": "https://github.com/immutable/ts-immutable-sdk#readme", "license": "Apache-2.0", "main": "dist/index.js", @@ -39,9 +42,6 @@ "test:e2e": "jest --runInBand --testMatch \"**/?(*.)+(e2e).[jt]s?(x)\"", "typecheck": "tsc --noEmit --jsx preserve" }, - "files": [ - "dist" - ], "type": "module", "types": "dist/index.d.ts" } diff --git a/tests/func-tests/zkevm/step-definitions/shared.ts b/tests/func-tests/zkevm/step-definitions/shared.ts index 74f949cedd..8034ddfed5 100644 --- a/tests/func-tests/zkevm/step-definitions/shared.ts +++ b/tests/func-tests/zkevm/step-definitions/shared.ts @@ -161,13 +161,13 @@ export const whenICreateABulkListing = ( }); } - const { actions, createListings } = await sdk.prepareBulkListings({ + const { actions, completeListings } = await sdk.prepareBulkListings({ makerAddress: offerer.address, listingParams, }); const signatures = await actionAll(actions, offerer); - const { result } = await createListings(signatures[0]); + const { result } = await completeListings(signatures[0]); for (const res of result) { if (!res.success) { From 3aa12d230b62ae78ed914fd5bfe4afb91a09b961 Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 18 Jun 2024 09:44:40 +1000 Subject: [PATCH 11/17] revert bad merge diff --- packages/blockchain-data/sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blockchain-data/sdk/package.json b/packages/blockchain-data/sdk/package.json index a0fa0e2bbf..d2eb4d5400 100644 --- a/packages/blockchain-data/sdk/package.json +++ b/packages/blockchain-data/sdk/package.json @@ -38,8 +38,8 @@ "generate-types": "typechain --target=ethers-v5 --out-dir=src/typechain/types 'abi/*.json'", "lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0", "test": "jest", - "test:e2e": "jest --runInBand --testMatch \"**/?(*.)+(e2e).[jt]s?(x)\"", "test:watch": "jest --watch", + "test:e2e": "jest --runInBand --testMatch \"**/?(*.)+(e2e).[jt]s?(x)\"", "typecheck": "tsc --noEmit --jsx preserve" }, "type": "module", From 188ef04d961414ad93d9700ed73873c0ebbdf2ba Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 18 Jun 2024 10:27:09 +1000 Subject: [PATCH 12/17] remove codeowner diff --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 55a2dd9012..2d38d69382 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,7 +17,6 @@ /packages/passport @immutable/passport /packages/x-provider @immutable/commerce /packages/checkout @immutable/commerce -/packages/orderbook @immutable/traders /packages/checkout/widgets-lib @immutable/commerce /packages/blockchain-data @immutable/assets /packages/minting-backend @shineli1984 From 8f98518af80b21c52d100c99aa6d2b0692fa7e5d Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 18 Jun 2024 10:28:58 +1000 Subject: [PATCH 13/17] revert webhook merge diff --- packages/webhook/sdk/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webhook/sdk/package.json b/packages/webhook/sdk/package.json index b84b6e62ec..49f316f5a4 100644 --- a/packages/webhook/sdk/package.json +++ b/packages/webhook/sdk/package.json @@ -25,9 +25,6 @@ "typechain": "^8.1.1", "typescript": "^4.9.5" }, - "files": [ - "dist" - ], "homepage": "https://github.com/immutable/ts-immutable-sdk#readme", "license": "Apache-2.0", "main": "dist/index.js", @@ -42,6 +39,9 @@ "test:e2e": "jest --runInBand --testMatch \"**/?(*.)+(e2e).[jt]s?(x)\"", "typecheck": "tsc --noEmit --jsx preserve" }, + "files": [ + "dist" + ], "type": "module", "types": "dist/index.d.ts" } From 71bdd846ddbe19dcd2b76736312bd1e4586e148d Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 18 Jun 2024 10:34:25 +1000 Subject: [PATCH 14/17] cleanup test code --- packages/orderbook/src/test/bulk-listings.e2e.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/orderbook/src/test/bulk-listings.e2e.ts b/packages/orderbook/src/test/bulk-listings.e2e.ts index ad895882f7..1d1607a6ec 100644 --- a/packages/orderbook/src/test/bulk-listings.e2e.ts +++ b/packages/orderbook/src/test/bulk-listings.e2e.ts @@ -58,13 +58,13 @@ describe('prepareListing and createOrder bulk e2e', () => { await contract.safeMint(offerer.address); - const { actions, completeListings: createListings } = await sdk.prepareBulkListings({ + const { actions, completeListings } = await sdk.prepareBulkListings({ makerAddress: offerer.address, listingParams: orderParams, }); - const signatures = await actionAll(actions, offerer); - const res = await createListings(signatures[0]); + const [bulkOrderSignature] = await actionAll(actions, offerer); + const res = await completeListings(bulkOrderSignature); for (const result of res.result) { if (!result.order) { From da055e0fb9a2b5b255129d04d28c2f1ed6f867ed Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 18 Jun 2024 11:09:58 +1000 Subject: [PATCH 15/17] lift internal seaport call for matching param manipulation --- packages/orderbook/src/orderbook.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/orderbook/src/orderbook.ts b/packages/orderbook/src/orderbook.ts index 7de4a133ec..29501dd650 100644 --- a/packages/orderbook/src/orderbook.ts +++ b/packages/orderbook/src/orderbook.ts @@ -188,18 +188,19 @@ export class Orderbook { // In the event of a single order, delegate to prepareListing as the signature is more // gas efficient if (listingParams.length === 1) { - const prepareListingResponse = await this.prepareListing({ - buy: listingParams[0].buy, + const prepareListingResponse = await this.seaport.prepareSeaportOrder( makerAddress, - sell: listingParams[0].sell, - orderExpiry: listingParams[0].orderExpiry, - }); + listingParams[0].sell, + listingParams[0].buy, + new Date(), + listingParams[0].orderExpiry || new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 2), + ); return { actions: prepareListingResponse.actions, completeListings: async (signature: string) => { const createListingResult = await this.createListing({ - makerFees: [], + makerFees: listingParams[0].makerFees, orderComponents: prepareListingResponse.orderComponents, orderHash: prepareListingResponse.orderHash, orderSignature: signature, From d78e5beb67179e37d9a29b595535d2a5f087e67b Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 18 Jun 2024 11:12:24 +1000 Subject: [PATCH 16/17] bump fees --- tests/func-tests/zkevm/utils/orderbook/gas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/func-tests/zkevm/utils/orderbook/gas.ts b/tests/func-tests/zkevm/utils/orderbook/gas.ts index 8b3ef80edf..c27c51f3ca 100644 --- a/tests/func-tests/zkevm/utils/orderbook/gas.ts +++ b/tests/func-tests/zkevm/utils/orderbook/gas.ts @@ -1,6 +1,6 @@ import { BigNumber } from 'ethers'; export const GAS_OVERRIDES = { - maxFeePerGas: BigNumber.from(101e9), - maxPriorityFeePerGas: BigNumber.from(100e9), + maxFeePerGas: BigNumber.from(15e8), + maxPriorityFeePerGas: BigNumber.from(12e8), }; From 66282881ccc4f6527b657b64ff7273efaa4b322c Mon Sep 17 00:00:00 2001 From: Sam Jeston Date: Tue, 18 Jun 2024 13:15:33 +1000 Subject: [PATCH 17/17] fix: fee bump --- tests/func-tests/zkevm/step-definitions/shared.ts | 4 ++-- tests/func-tests/zkevm/utils/orderbook/gas.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/func-tests/zkevm/step-definitions/shared.ts b/tests/func-tests/zkevm/step-definitions/shared.ts index 8034ddfed5..b75e36d338 100644 --- a/tests/func-tests/zkevm/step-definitions/shared.ts +++ b/tests/func-tests/zkevm/step-definitions/shared.ts @@ -11,8 +11,8 @@ import { import { GAS_OVERRIDES } from '../utils/orderbook/gas'; import { actionAll } from '../utils/orderbook/actions'; -const imxForApproval = 0.02 * 1e18; -const imxForFulfillment = 0.05 * 1e18; +const imxForApproval = 0.03 * 1e18; +const imxForFulfillment = 0.08 * 1e18; const listingPrice = 0.0001 * 1e18; export const givenIHaveAFundedOffererAccount = ( diff --git a/tests/func-tests/zkevm/utils/orderbook/gas.ts b/tests/func-tests/zkevm/utils/orderbook/gas.ts index c27c51f3ca..c5f73cf3cd 100644 --- a/tests/func-tests/zkevm/utils/orderbook/gas.ts +++ b/tests/func-tests/zkevm/utils/orderbook/gas.ts @@ -1,6 +1,6 @@ import { BigNumber } from 'ethers'; export const GAS_OVERRIDES = { - maxFeePerGas: BigNumber.from(15e8), - maxPriorityFeePerGas: BigNumber.from(12e8), + maxFeePerGas: BigNumber.from(150e9), + maxPriorityFeePerGas: BigNumber.from(120e9), };