Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bulk listing creation from orderbook SDK #1885

Merged
merged 20 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 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,104 @@ 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
* 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 10 orders to prevent API and order evaluation spam
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 (listingParams.length === 1) {
const prepareListingResponse = await this.prepareListing({
buy: listingParams[0].buy,
makerAddress,
sell: listingParams[0].sell,
orderExpiry: listingParams[0].orderExpiry,
});
lfportal marked this conversation as resolved.
Show resolved Hide resolved

return {
actions: prepareListingResponse.actions,
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,
}],
};
},
};
}

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,
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 = 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: 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
24 changes: 24 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,26 @@ export function getOrderComponentsFromMessage(orderMessage: string): OrderCompon

return orderComponents;
}

export function getBulkOrderComponentsFromMessage(orderMessage: string): OrderComponents[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like bespoke and error prone logic here? Just observation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeh undeniably not ideal - but unfortunately there is no real way around it because of how we expose the signable message to callers, rather than using the inbuilt seaport-js methods

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 orderComponents;
}

export function getBulkSeaportOrderSignatures(
signature: string,
orderComponents: OrderComponents[],
): string[] {
const tree = getBulkOrderTree(orderComponents);
return orderComponents.map((_, i) => tree.getEncodedProofAndSignature(i, signature));
}
136 changes: 135 additions & 1 deletion packages/orderbook/src/seaport/seaport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import {
ApprovalAction,
CreateInputItem,
CreateOrderAction,
CreateBulkOrdersAction,
ExchangeAction,
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 {
Expand All @@ -17,6 +19,7 @@ import {
ERC721Item,
FulfillOrderResponse,
NativeItem,
PrepareBulkSeaportOrders,
PrepareListingResponse,
SignableAction,
SignablePurpose,
Expand All @@ -30,7 +33,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 +47,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 orders = getBulkOrderComponentsFromMessage(orderMessageToSign);

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

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

async prepareSeaportOrder(
offerer: string,
listingItem: ERC721Item | ERC1155Item,
Expand Down Expand Up @@ -284,6 +342,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 @@ -327,6 +433,34 @@ export class Seaport {
);
}

private async getTypedDataFromBulkOrderComponents(
orderComponents: OrderComponents[],
numberOfOrders: number,
): Promise<SignableAction['message']> {
const { chainId } = await this.provider.getNetwork();

const domainData = {
name: SEAPORT_CONTRACT_NAME,
version: SEAPORT_CONTRACT_VERSION_V1_5,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we want to put in to a config/env? Especially now that we use it in multiple places, could be easy to forget if we move to 1.6

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those constant values are all imported from seaport/constants.ts. If we move versions updating there will modify all the places we create this message

chainId,
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() },
};
}

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