Skip to content

Commit 6c1a12e

Browse files
authored
feat(orderbook): Add createTraitBid, getTraitBid and listTraitBid functions for orderbook (#2854)
1 parent dd19810 commit 6c1a12e

File tree

13 files changed

+633
-3
lines changed

13 files changed

+633
-3
lines changed

packages/orderbook/src/api-client/api-client.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
ListingResult,
99
ListListingsResult,
1010
ListTradeResult,
11+
ListTraitBidsResult,
1112
OrdersService,
1213
TradeResult,
14+
TraitBidResult,
1315
} from '../openapi/sdk';
1416
import { FulfillableOrder } from '../openapi/sdk/models/FulfillableOrder';
1517
import { FulfillmentDataRequest } from '../openapi/sdk/models/FulfillmentDataRequest';
@@ -23,10 +25,13 @@ import {
2325
} from '../seaport/map-to-immutable-order';
2426
import {
2527
CreateBidParams,
28+
CreateCollectionBidParams,
2629
CreateListingParams,
30+
CreateTraitBidParams,
2731
ListBidsParams,
2832
ListCollectionBidsParams,
2933
ListListingsParams,
34+
ListTraitBidsParams,
3035
ListTradesParams,
3136
} from '../types';
3237

@@ -72,6 +77,13 @@ export class ImmutableApiClient {
7277
});
7378
}
7479

80+
async getTraitBid(traitBidId: string): Promise<TraitBidResult> {
81+
return this.orderbookService.getTraitBid({
82+
chainName: this.chainName,
83+
traitBidId,
84+
});
85+
}
86+
7587
async getTrade(tradeId: string): Promise<TradeResult> {
7688
return this.orderbookService.getTrade({
7789
chainName: this.chainName,
@@ -106,6 +118,15 @@ export class ImmutableApiClient {
106118
});
107119
}
108120

121+
async listTraitBids(
122+
listOrderParams: ListTraitBidsParams,
123+
): Promise<ListTraitBidsResult> {
124+
return this.orderbookService.listTraitBids({
125+
chainName: this.chainName,
126+
...listOrderParams,
127+
});
128+
}
129+
109130
async listTrades(
110131
listTradesParams: ListTradesParams,
111132
): Promise<ListTradeResult> {
@@ -243,7 +264,7 @@ export class ImmutableApiClient {
243264
orderComponents,
244265
orderSignature,
245266
makerFees,
246-
}: CreateBidParams): Promise<CollectionBidResult> {
267+
}: CreateCollectionBidParams): Promise<CollectionBidResult> {
247268
if (orderComponents.offer.length !== 1) {
248269
throw new Error('Only one item can be listed for a collection bid');
249270
}
@@ -293,4 +314,69 @@ export class ImmutableApiClient {
293314
},
294315
});
295316
}
317+
318+
async createTraitBid({
319+
orderHash,
320+
orderComponents,
321+
orderSignature,
322+
makerFees,
323+
traitCriteria,
324+
}: CreateTraitBidParams): Promise<TraitBidResult> {
325+
if (orderComponents.offer.length !== 1) {
326+
throw new Error('Only one item can be listed for a trait bid');
327+
}
328+
329+
if (orderComponents.consideration.length !== 1) {
330+
throw new Error('Only one item can be used as currency for a trait bid');
331+
}
332+
333+
if (ItemType.ERC20 !== orderComponents.offer[0].itemType) {
334+
throw new Error('Only ERC20 tokens can be used as the currency item in a trait bid');
335+
}
336+
337+
if (![ItemType.ERC721_WITH_CRITERIA, ItemType.ERC1155_WITH_CRITERIA]
338+
.includes(orderComponents.consideration[0].itemType)
339+
) {
340+
throw new Error('Only ERC721 / ERC1155 collection based tokens can be bid against');
341+
}
342+
343+
if (!traitCriteria?.length) {
344+
throw new Error('At least one trait criterion is required for a trait bid');
345+
}
346+
347+
return this.orderbookService.createTraitBid({
348+
chainName: this.chainName,
349+
requestBody: {
350+
account_address: orderComponents.offerer,
351+
buy: orderComponents.consideration.map(mapSeaportItemToImmutableAssetCollectionItem),
352+
fees: makerFees.map((f) => ({
353+
type: Fee.type.MAKER_ECOSYSTEM,
354+
amount: f.amount,
355+
recipient_address: f.recipientAddress,
356+
})),
357+
end_at: new Date(
358+
parseInt(`${orderComponents.endTime.toString()}000`, 10),
359+
).toISOString(),
360+
order_hash: orderHash,
361+
protocol_data: {
362+
order_type:
363+
mapSeaportOrderTypeToImmutableProtocolDataOrderType(orderComponents.orderType),
364+
zone_address: orderComponents.zone,
365+
seaport_address: this.seaportAddress,
366+
seaport_version: SEAPORT_CONTRACT_VERSION_V1_5,
367+
counter: orderComponents.counter.toString(),
368+
},
369+
salt: orderComponents.salt,
370+
sell: orderComponents.offer.map(mapSeaportItemToImmutableERC20Item),
371+
signature: orderSignature,
372+
start_at: new Date(
373+
parseInt(`${orderComponents.startTime.toString()}000`, 10),
374+
).toISOString(),
375+
trait_criteria: traitCriteria.map((c) => ({
376+
trait_type: c.traitType,
377+
values: c.values,
378+
})),
379+
},
380+
});
381+
}
296382
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Fee } from './sdk';
2+
import { Order as OpenApiOrder } from './sdk/models/Order';
3+
import { ProtocolData } from './sdk/models/ProtocolData';
4+
import { mapOrderFromOpenApiOrder, mapTraitBidFromOpenApiOrder } from './mapper';
5+
6+
describe('mapTraitBidFromOpenApiOrder', () => {
7+
const baseOrder: OpenApiOrder = {
8+
id: '018792c9-4ad7-8ec4-4038-9e05c598534a',
9+
type: OpenApiOrder.type.TRAIT_BID,
10+
account_address: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
11+
chain: { id: 'eip155:11155111', name: 'imtbl-zkevm-testnet' },
12+
created_at: '2022-03-07T07:20:50.52Z',
13+
updated_at: '2022-03-07T07:20:50.52Z',
14+
start_at: '2022-03-09T05:00:50.52Z',
15+
end_at: '2022-03-10T05:00:50.52Z',
16+
order_hash: '0xabc',
17+
salt: '1',
18+
signature: '0x',
19+
status: { name: 'ACTIVE' },
20+
fill_status: { numerator: '0', denominator: '1' },
21+
fees: [
22+
{
23+
type: Fee.type.MAKER_ECOSYSTEM,
24+
amount: '1',
25+
recipient_address: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
26+
},
27+
],
28+
protocol_data: {
29+
order_type: ProtocolData.order_type.PARTIAL_RESTRICTED,
30+
counter: '0',
31+
zone_address: '0x12',
32+
seaport_address: '0x34',
33+
seaport_version: '1.5',
34+
},
35+
sell: [
36+
{
37+
type: 'ERC20',
38+
contract_address: '0x0165878a594ca255338adfa4d48449f69242eb8f',
39+
amount: '1000',
40+
},
41+
],
42+
buy: [
43+
{
44+
type: 'ERC721_COLLECTION',
45+
contract_address: '0x692edad005237c7e737bb2c0f3d8cccc10d3479e',
46+
amount: '1',
47+
},
48+
],
49+
};
50+
51+
it('maps trait criteria from snake_case API fields', () => {
52+
const order: OpenApiOrder = {
53+
...baseOrder,
54+
trait_criteria: [
55+
{ trait_type: 'Background', values: ['Blue', 'Red'] },
56+
],
57+
};
58+
59+
const mapped = mapTraitBidFromOpenApiOrder(order);
60+
61+
expect(mapped.traitCriteria).toEqual([
62+
{ traitType: 'Background', values: ['Blue', 'Red'] },
63+
]);
64+
expect(mapped.type).toBe('TRAIT_BID');
65+
expect(mapped.sell[0]).toEqual({
66+
type: 'ERC20',
67+
contractAddress: '0x0165878a594ca255338adfa4d48449f69242eb8f',
68+
amount: '1000',
69+
});
70+
});
71+
72+
it('defaults trait criteria when omitted', () => {
73+
const mapped = mapTraitBidFromOpenApiOrder(baseOrder);
74+
expect(mapped.traitCriteria).toEqual([]);
75+
});
76+
77+
it('routes TRAIT_BID through mapOrderFromOpenApiOrder', () => {
78+
const order: OpenApiOrder = {
79+
...baseOrder,
80+
trait_criteria: [{ trait_type: 'Rarity', values: ['Legendary'] }],
81+
};
82+
const mapped = mapOrderFromOpenApiOrder(order);
83+
expect(mapped.type).toBe('TRAIT_BID');
84+
if (mapped.type === 'TRAIT_BID') {
85+
expect(mapped.traitCriteria).toEqual([{ traitType: 'Rarity', values: ['Legendary'] }]);
86+
}
87+
});
88+
});

packages/orderbook/src/openapi/mapper.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import {
1212
Order,
1313
Page,
1414
Trade,
15+
TraitBid,
16+
TraitFilter,
1517
} from '../types';
1618
import { exhaustiveSwitch } from '../utils';
1719
import { Order as OpenApiOrder } from './sdk/models/Order';
20+
import type { TraitFilter as OpenApiTraitFilter } from './sdk/models/TraitFilter';
1821
import { Page as OpenApiPage } from './sdk/models/Page';
1922
import { Trade as OpenApiTrade } from './sdk/models/Trade';
2023

@@ -163,6 +166,18 @@ export function mapBidFromOpenApiOrder(order: OpenApiOrder): Bid {
163166
};
164167
}
165168

169+
function mapTraitCriteriaFromOpenApi(
170+
criteria: OpenApiTraitFilter[] | undefined,
171+
): TraitFilter[] {
172+
if (!criteria?.length) {
173+
return [];
174+
}
175+
return criteria.map((c) => ({
176+
traitType: c.trait_type,
177+
values: [...c.values],
178+
}));
179+
}
180+
166181
export function mapCollectionBidFromOpenApiOrder(order: OpenApiOrder): CollectionBid {
167182
if (order.type !== OpenApiOrder.type.COLLECTION_BID) {
168183
throw new Error('Order type must be COLLECTION_BID');
@@ -231,6 +246,75 @@ export function mapCollectionBidFromOpenApiOrder(order: OpenApiOrder): Collectio
231246
};
232247
}
233248

249+
export function mapTraitBidFromOpenApiOrder(order: OpenApiOrder): TraitBid {
250+
if (order.type !== OpenApiOrder.type.TRAIT_BID) {
251+
throw new Error('Order type must be TRAIT_BID');
252+
}
253+
254+
const sellItems: ERC20Item[] = order.sell.map((item) => {
255+
if (item.type === 'ERC20') {
256+
return {
257+
type: 'ERC20',
258+
contractAddress: item.contract_address,
259+
amount: item.amount,
260+
};
261+
}
262+
263+
throw new Error('Trait bid sell items must be ERC20');
264+
});
265+
266+
const buyItems: (ERC721CollectionItem | ERC1155CollectionItem)[] = order.buy.map((item) => {
267+
if (item.type === 'ERC721_COLLECTION') {
268+
return {
269+
type: 'ERC721_COLLECTION',
270+
contractAddress: item.contract_address,
271+
amount: item.amount,
272+
};
273+
}
274+
275+
if (item.type === 'ERC1155_COLLECTION') {
276+
return {
277+
type: 'ERC1155_COLLECTION',
278+
contractAddress: item.contract_address,
279+
amount: item.amount,
280+
};
281+
}
282+
283+
throw new Error('Trait bid buy items must either ERC721_COLLECTION or ERC1155_COLLECTION');
284+
});
285+
286+
return {
287+
id: order.id,
288+
type: order.type,
289+
chain: order.chain,
290+
accountAddress: order.account_address,
291+
sell: sellItems,
292+
buy: buyItems,
293+
fees: order.fees.map((fee) => ({
294+
amount: fee.amount,
295+
recipientAddress: fee.recipient_address,
296+
type: fee.type as unknown as FeeType,
297+
})),
298+
status: order.status,
299+
fillStatus: order.fill_status,
300+
startAt: order.start_at,
301+
endAt: order.end_at,
302+
salt: order.salt,
303+
signature: order.signature,
304+
orderHash: order.order_hash,
305+
protocolData: {
306+
orderType: order.protocol_data.order_type,
307+
counter: order.protocol_data.counter,
308+
seaportAddress: order.protocol_data.seaport_address,
309+
seaportVersion: order.protocol_data.seaport_version,
310+
zoneAddress: order.protocol_data.zone_address,
311+
},
312+
createdAt: order.created_at,
313+
updatedAt: order.updated_at,
314+
traitCriteria: mapTraitCriteriaFromOpenApi(order.trait_criteria),
315+
};
316+
}
317+
234318
export function mapOrderFromOpenApiOrder(order: OpenApiOrder): Order {
235319
switch (order.type) {
236320
case OpenApiOrder.type.LISTING:
@@ -239,6 +323,8 @@ export function mapOrderFromOpenApiOrder(order: OpenApiOrder): Order {
239323
return mapBidFromOpenApiOrder(order);
240324
case OpenApiOrder.type.COLLECTION_BID:
241325
return mapCollectionBidFromOpenApiOrder(order);
326+
case OpenApiOrder.type.TRAIT_BID:
327+
return mapTraitBidFromOpenApiOrder(order);
242328
default:
243329
return exhaustiveSwitch(order.type);
244330
}

packages/orderbook/src/openapi/sdk/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type { ChainName } from './models/ChainName';
2121
export type { CollectionBidResult } from './models/CollectionBidResult';
2222
export type { CreateBidRequestBody } from './models/CreateBidRequestBody';
2323
export type { CreateCollectionBidRequestBody } from './models/CreateCollectionBidRequestBody';
24+
export type { CreateTraitBidRequestBody } from './models/CreateTraitBidRequestBody';
2425
export type { CreateListingRequestBody } from './models/CreateListingRequestBody';
2526
export type { ERC1155CollectionItem } from './models/ERC1155CollectionItem';
2627
export type { ERC1155Item } from './models/ERC1155Item';
@@ -39,6 +40,7 @@ export type { InactiveOrderStatus } from './models/InactiveOrderStatus';
3940
export type { Item } from './models/Item';
4041
export type { ListBidsResult } from './models/ListBidsResult';
4142
export type { ListCollectionBidsResult } from './models/ListCollectionBidsResult';
43+
export type { ListTraitBidsResult } from './models/ListTraitBidsResult';
4244
export type { ListingResult } from './models/ListingResult';
4345
export type { ListListingsResult } from './models/ListListingsResult';
4446
export type { ListTradeResult } from './models/ListTradeResult';
@@ -54,6 +56,8 @@ export { ProtocolData } from './models/ProtocolData';
5456
export type { Trade } from './models/Trade';
5557
export type { TradeBlockchainMetadata } from './models/TradeBlockchainMetadata';
5658
export type { TradeResult } from './models/TradeResult';
59+
export type { TraitBidResult } from './models/TraitBidResult';
60+
export type { TraitFilter } from './models/TraitFilter';
5761
export type { UnfulfillableOrder } from './models/UnfulfillableOrder';
5862

5963
export { OrdersService } from './services/OrdersService';

0 commit comments

Comments
 (0)