Skip to content

Commit

Permalink
feat: bulk listing creation from orderbook SDK (#1885)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sam-Jeston authored Jun 18, 2024
1 parent b77bb22 commit ea1483d
Show file tree
Hide file tree
Showing 9 changed files with 535 additions and 10 deletions.
102 changes: 102 additions & 0 deletions packages/orderbook/src/orderbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -31,6 +32,8 @@ import {
OrderStatusName,
PrepareCancelOrdersResponse,
PrepareListingParams,
PrepareBulkListingsParams,
PrepareBulkListingsResponse,
PrepareListingResponse,
SignablePurpose,
TradeResult,
Expand Down Expand Up @@ -160,6 +163,105 @@ export class Orderbook {
};
}

/**
* 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.
* @return {PrepareBulkListingsResponse} PrepareListingResponse includes
* any unsigned approval transactions, the typed bulk order message for signing and
* the createListings method that can be called with the signature to create the listings.
*/
async prepareBulkListings(
{
makerAddress,
listingParams,
}: PrepareBulkListingsParams,
): Promise<PrepareBulkListingsResponse> {
// 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
// gas efficient
if (listingParams.length === 1) {
const prepareListingResponse = await this.seaport.prepareSeaportOrder(
makerAddress,
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: listingParams[0].makerFees,
orderComponents: prepareListingResponse.orderComponents,
orderHash: prepareListingResponse.orderHash,
orderSignature: signature,
});

return {
result: [{
success: true,
orderHash: prepareListingResponse.orderHash,
order: createListingResult.result,
}],
};
},
};
}

const { actions, preparedListings } = await this.seaport.prepareBulkSeaportOrders(
makerAddress,
listingParams.map((orderParam) => ({
listingItem: orderParam.sell,
considerationItem: orderParam.buy,
orderStart: new Date(),
orderExpiry: orderParam.orderExpiry || new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 2),
})),
);

return {
actions,
completeListings: 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 = 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 the caller as failed
}).catch(() => undefined);
}),
);

return {
result: createOrdersApiListingResponse.map((apiListingResponse, i) => ({
success: !!apiListingResponse,
orderHash: preparedListings[i].orderHash,
order: apiListingResponse ? mapFromOpenApiOrder(apiListingResponse.result) : undefined,
})),
};
},
};
}

/**
* Get required transactions and messages for signing prior to creating a listing
* through the createListing method
Expand Down
28 changes: 28 additions & 0 deletions packages/orderbook/src/seaport/components.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OrderComponents } from '@opensea/seaport-js/lib/types';
import { getBulkOrderTree } from '@opensea/seaport-js/lib/utils/eip712/bulk-orders';
import { BigNumber } from 'ethers';

export function getOrderComponentsFromMessage(orderMessage: string): OrderComponents {
Expand All @@ -9,3 +10,30 @@ export function getOrderComponentsFromMessage(orderMessage: string): OrderCompon

return 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`
// when creating the listings
.filter((o: OrderComponents) => o.offerer !== '0x0000000000000000000000000000000000000000');

// eslint-disable-next-line no-restricted-syntax
for (const orderComponent of orderComponents) {
orderComponent.salt = BigNumber.from(orderComponent.salt).toHexString();
}

return { components: orderComponents, types: data.types, value: data.message };
}

export function getBulkSeaportOrderSignatures(
signature: string,
orderComponents: OrderComponents[],
): string[] {
const tree = getBulkOrderTree(orderComponents);
return orderComponents.map((_, i) => tree.getEncodedProofAndSignature(i, signature));
}
132 changes: 131 additions & 1 deletion packages/orderbook/src/seaport/seaport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ApprovalAction,
CreateInputItem,
CreateOrderAction,
CreateBulkOrdersAction,
ExchangeAction,
OrderComponents,
OrderUseCase,
Expand All @@ -17,6 +18,7 @@ import {
ERC721Item,
FulfillOrderResponse,
NativeItem,
PrepareBulkSeaportOrders,
PrepareListingResponse,
SignableAction,
SignablePurpose,
Expand All @@ -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';
Expand All @@ -44,6 +46,61 @@ export class Seaport {
private rateLimitingKey?: string,
) {}

async prepareBulkSeaportOrders(
offerer: string,
orderInputs: {
listingItem: ERC721Item | ERC1155Item,
considerationItem: ERC20Item | NativeItem,
orderStart: Date,
orderExpiry: Date,
}[],
): Promise<PrepareBulkSeaportOrders> {
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();
const { components, types, value } = getBulkOrderComponentsFromMessage(orderMessageToSign);

listingActions.push({
type: ActionType.SIGNABLE,
purpose: SignablePurpose.CREATE_LISTING,
message: await this.getTypedDataFromBulkOrderComponents(types, value),
});

return {
actions: listingActions,
preparedListings: components.map((orderComponent) => ({
orderComponents: orderComponent,
orderHash: this.getSeaportLib().getOrderHash(orderComponent),
})),
};
}

async prepareSeaportOrder(
offerer: string,
listingItem: ERC721Item | ERC1155Item,
Expand Down Expand Up @@ -262,6 +319,54 @@ export class Seaport {
};
}

private createSeaportOrders(
offerer: string,
orderInputs: {
listingItem: ERC721Item | ERC1155Item,
considerationItem: ERC20Item | NativeItem,
orderStart: Date,
orderExpiry: Date,
}[],
): Promise<OrderUseCase<CreateBulkOrdersAction>> {
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,
Expand Down Expand Up @@ -305,6 +410,31 @@ export class Seaport {
);
}

// Types and value are JSON parsed from the seaport-js string, so the types are
// reflected as any
private async getTypedDataFromBulkOrderComponents(
types: any,
value: any,
): Promise<SignableAction['message']> {
// 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 = {
name: SEAPORT_CONTRACT_NAME,
version: SEAPORT_CONTRACT_VERSION_V1_5,
chainId,
verifyingContract: this.seaportContractAddress,
};

return {
domain: domainData,
types,
value,
};
}

private async getTypedDataFromOrderComponents(
orderComponents: OrderComponents,
): Promise<SignableAction['message']> {
Expand Down
Loading

0 comments on commit ea1483d

Please sign in to comment.