-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathMarketplaceOrdersEntityService.ts
More file actions
330 lines (303 loc) · 9.82 KB
/
MarketplaceOrdersEntityService.ts
File metadata and controls
330 lines (303 loc) · 9.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import { HypercertExchangeClient } from "@hypercerts-org/marketplace-sdk";
import { Insertable, Selectable, Updateable } from "kysely";
import { inject, injectable } from "tsyringe";
import { EvmClientFactory } from "../../../client/evmClient.js";
import { DataKyselyService, kyselyData } from "../../../client/kysely.js";
import type { GetOrdersArgs } from "../../../graphql/schemas/args/orderArgs.js";
import { SortOrder } from "../../../graphql/schemas/enums/sortEnums.js";
import type { DataDatabase } from "../../../types/kyselySupabaseData.js";
import { createEntityService, EntityService } from "./EntityServiceFactory.js";
export type MarketplaceOrderSelect = Selectable<
DataDatabase["marketplace_orders"]
>;
export type MarketplaceOrderInsert = Insertable<
DataDatabase["marketplace_orders"]
>;
export type MarketplaceOrderUpdate = Updateable<
DataDatabase["marketplace_orders"]
>;
export type MarketplaceOrderNonceSelect = Selectable<
DataDatabase["marketplace_order_nonces"]
>;
export type MarketplaceOrderNonceInsert = Insertable<
DataDatabase["marketplace_order_nonces"]
>;
export type MarketplaceOrderNonceUpdate = Updateable<
DataDatabase["marketplace_order_nonces"]
>;
/**
* Service class for managing marketplace orders in the database.
* Handles CRUD operations for orders and their associated nonces.
*
* This service provides methods to:
* - Query and manage marketplace orders
* - Handle order nonces for transaction validation
* - Validate orders against token IDs
* - Perform batch operations on orders
*
* @injectable
*/
@injectable()
export class MarketplaceOrdersService {
private entityService: EntityService<
DataDatabase["marketplace_orders"],
GetOrdersArgs
>;
/**
* Initializes a new instance of the MarketplaceOrdersService.
* Creates an EntityService instance for the marketplace_orders table.
*
* @param dbService - The database service instance for direct database operations
*/
constructor(@inject(DataKyselyService) private dbService: DataKyselyService) {
this.entityService = createEntityService<
DataDatabase,
"marketplace_orders",
GetOrdersArgs
>("marketplace_orders", "MarketplaceOrdersEntityService", kyselyData);
}
/**
* Retrieves multiple orders based on the provided arguments.
*
* @param args - Query arguments for filtering orders
* @returns Promise resolving to an object containing order data and count
*/
async getOrders(args: GetOrdersArgs) {
return this.entityService.getMany(args);
}
/**
* Retrieves a single order based on the provided arguments.
*
* @param args - Query arguments for filtering the order
* @returns Promise resolving to a single order record or undefined if not found
*/
async getOrder(args: GetOrdersArgs) {
return this.entityService.getSingle(args);
}
// TODO can this be a getOrders call?
/**
* Retrieves orders associated with specific token IDs.
*
* @param tokenIds - Array of token IDs to search for
* @param chainId - Chain ID to filter orders by
* @returns Promise resolving to matching orders
*/
async getOrdersByTokenIds(tokenIds: string[], chainId: number) {
return this.entityService.getMany({
where: {
itemIds: {
arrayOverlaps: tokenIds,
},
chainId: { eq: chainId },
},
sortBy: { createdAt: SortOrder.descending },
});
}
/**
* Creates a new nonce record for order validation.
*
* @param nonce - The nonce record to create
* @returns Promise resolving to the created nonce counter
* @throws {Error} If the database operation fails
*/
async createNonce(nonce: MarketplaceOrderNonceInsert) {
return this.dbService
.getConnection()
.insertInto("marketplace_order_nonces")
.values(nonce)
.returning("nonce_counter")
.executeTakeFirstOrThrow();
}
/**
* Retrieves a nonce record for a specific address and chain.
*
* @param nonce - Object containing address and chain_id
* @returns Promise resolving to the nonce record or undefined if not found
*/
async getNonce(
nonce: Pick<MarketplaceOrderNonceSelect, "address" | "chain_id">,
) {
if (!nonce.address || !nonce.chain_id) {
throw new Error("Address and chain ID are required");
}
return (
this.dbService
.getConnection()
.selectFrom("marketplace_order_nonces")
.selectAll()
.where("address", "=", nonce.address)
.where("chain_id", "=", nonce.chain_id)
.executeTakeFirst()
// TODO: Investigate why chain_id and nonce_counter are returned as strings
.then((res) => ({
...res,
chain_id: Number(res?.chain_id),
nonce_counter: Number(res?.nonce_counter),
}))
);
}
/**
* Updates a nonce record's counter.
*
* @param nonce - The nonce record to update
* @returns Promise resolving to the updated nonce record
* @throws {Error} If address or chain ID is missing
*/
async updateNonce(nonce: MarketplaceOrderNonceUpdate) {
if (!nonce.address || !nonce.chain_id) {
throw new Error("Address and chain ID are required");
}
return this.dbService
.getConnection()
.updateTable("marketplace_order_nonces")
.set({ nonce_counter: nonce.nonce_counter })
.where("address", "=", nonce.address)
.where("chain_id", "=", nonce.chain_id)
.returningAll()
.executeTakeFirstOrThrow();
}
/**
* Creates a new marketplace order.
*
* @param order - The order record to create
* @returns Promise resolving to the created order
* @throws {Error} If the database operation fails
*/
async storeOrder(order: MarketplaceOrderInsert) {
return this.dbService
.getConnection()
.insertInto("marketplace_orders")
.values(order)
.returningAll()
.executeTakeFirstOrThrow();
}
/**
* Updates an existing marketplace order.
*
* @param order - The order record to update
* @returns Promise resolving to the updated order
* @throws {Error} If order ID is missing or unknown
*/
async updateOrder(order: MarketplaceOrderUpdate) {
if (!order.id) {
throw new Error("Order ID is required");
}
return this.dbService
.getConnection()
.updateTable("marketplace_orders")
.set(order)
.where("id", "=", order.id)
.returningAll()
.executeTakeFirstOrThrow();
}
/**
* Updates multiple marketplace orders.
*
* @param orders - Array of order records to update
* @returns Promise resolving to array of updated orders
* @throws {Error} If any order ID is missing
*/
async updateOrders(orders: MarketplaceOrderUpdate[]) {
const results = [];
for (const order of orders) {
if (!order.id) {
throw new Error("Order ID is required for update");
}
const result = await this.dbService
.getConnection()
.updateTable("marketplace_orders")
.set(order)
.where("id", "=", order.id)
.returningAll()
.executeTakeFirstOrThrow();
results.push(result);
}
return results;
}
/**
* Upserts multiple marketplace orders.
*
* @param orders - Array of order records to upsert
* @returns Promise resolving to array of upserted orders
*/
async upsertOrders(orders: MarketplaceOrderInsert[]) {
return this.dbService
.getConnection()
.insertInto("marketplace_orders")
.values(orders)
.onConflict((oc) =>
oc.column("id").doUpdateSet((eb) => ({
invalidated: eb.ref("excluded.invalidated"),
validator_codes: eb.ref("excluded.validator_codes"),
})),
)
.returningAll()
.execute();
}
/**
* Deletes a marketplace order.
*
* @param orderId - ID of the order to delete
* @returns Promise resolving to the deleted order
* @throws {Error} If the database operation fails
*/
async deleteOrder(orderId: string) {
return this.dbService
.getConnection()
.deleteFrom("marketplace_orders")
.where("id", "=", orderId)
.returningAll()
.executeTakeFirstOrThrow();
}
/**
* Validates orders associated with specific token IDs.
* Uses the HypercertExchangeClient to check order validity.
*
* @param tokenIds - Array of token IDs to validate orders for
* @param chainId - Chain ID to filter orders by
* @returns Promise resolving to array of updated invalid orders
* @throws {Error} If validation or update fails
*/
async validateOrdersByTokenIds(tokenIds: string[], chainId: number) {
const ordersToUpdate: MarketplaceOrderUpdate[] = [];
for (const tokenId of tokenIds) {
const { data: matchingOrders } = await this.getOrdersByTokenIds(
[tokenId],
chainId,
);
if (!matchingOrders) {
console.warn(
`[SupabaseDataService::validateOrderByTokenId] No orders found for tokenId: ${tokenId}`,
);
continue;
}
const hec = new HypercertExchangeClient(
chainId,
// @ts-expect-error Typing issue with provider
EvmClientFactory.createEthersClient(chainId),
);
const validationResults = await hec.checkOrdersValidity(
matchingOrders.map((order) => ({
...order,
chainId: Number(order.chainId),
})),
);
// filter all orders that have changed validity or validator codes
const _changedOrders = validationResults
.filter((x) => {
const order = matchingOrders.find((y) => y.id === x.id);
return (
order?.invalidated !== x.valid ||
order?.validator_codes !== x.validatorCodes
);
})
.map((x) => ({
id: x.id,
invalidated: !x.valid,
validator_codes: x.validatorCodes,
}));
ordersToUpdate.push(..._changedOrders);
}
return await this.updateOrders(ordersToUpdate);
}
}