Skip to content

Commit caa1dab

Browse files
committed
feat: 🎸 Deals state chacker added to deals registry
1 parent 42afdac commit caa1dab

File tree

1 file changed

+126
-44
lines changed

1 file changed

+126
-44
lines changed

‎src/client/dealsRegistry.ts

Lines changed: 126 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Address, Hash, HDAccount, getAddress, PublicClient, WalletClient } from 'viem';
1+
import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events';
2+
import { Address, Hash, HDAccount, getAddress, WalletClient } from 'viem';
23
import { Abi } from 'abitype';
34
import { marketABI } from '@windingtree/contracts';
45
import { Client, createCheckInOutSignature } from '../index.js';
@@ -78,6 +79,36 @@ export interface ContractClientConfig {
7879
abi: Abi;
7980
}
8081

82+
/**
83+
* Deals manager events interface
84+
*/
85+
export interface DealEvents<
86+
CustomRequestQuery extends GenericQuery,
87+
CustomOfferOptions extends GenericOfferOptions,
88+
> {
89+
/**
90+
* @example
91+
*
92+
* ```js
93+
* registry.addEventListener('status', () => {
94+
* // ... deal status changed
95+
* })
96+
* ```
97+
*/
98+
status: CustomEvent<DealRecord<CustomRequestQuery, CustomOfferOptions>>;
99+
100+
/**
101+
* @example
102+
*
103+
* ```js
104+
* registry.addEventListener('changed', () => {
105+
* // ... deals store changed
106+
* })
107+
* ```
108+
*/
109+
changed: CustomEvent<void>;
110+
}
111+
81112
/**
82113
* Creates an instance of DealsRegistry.
83114
*
@@ -87,12 +118,14 @@ export interface ContractClientConfig {
87118
export class DealsRegistry<
88119
CustomRequestQuery extends GenericQuery,
89120
CustomOfferOptions extends GenericOfferOptions,
90-
> {
121+
> extends EventEmitter<DealEvents<CustomRequestQuery, CustomOfferOptions>> {
91122
private client: Client<CustomRequestQuery, CustomOfferOptions>;
92123
/** Mapping of an offer id => Deal */
93124
private deals: Map<string, DealRecord<CustomRequestQuery, CustomOfferOptions>>; // id => Deal
94125
private storage?: Storage;
95126
private storageKey: string;
127+
private checkInterval?: NodeJS.Timer;
128+
private ongoingCheck = false;
96129

97130
/**
98131
* Creates an instance of DealsRegistry.
@@ -101,13 +134,21 @@ export class DealsRegistry<
101134
* @memberof DealsRegistry
102135
*/
103136
constructor(options: DealsRegistryOptions<CustomRequestQuery, CustomOfferOptions>) {
137+
super();
138+
104139
const { client, storage, prefix } = options;
105140

106141
this.client = client;
107142
this.deals = new Map<string, DealRecord<CustomRequestQuery, CustomOfferOptions>>();
108143
this.storageKey = `${prefix}_deals_records`;
109144
this.storage = storage;
110-
this._storageUp().catch(logger.error);
145+
this._storageUp()
146+
.then(() => {
147+
this.checkInterval = setInterval(() => {
148+
this._checkDealsStates().catch(logger.error);
149+
}, 2000);
150+
})
151+
.catch(logger.error);
111152
}
112153

113154
/**
@@ -171,18 +212,54 @@ export class DealsRegistry<
171212
return domain;
172213
}
173214

215+
/**
216+
* Checks and updates state of all deals records
217+
*
218+
* @private
219+
* @memberof DealsRegistry
220+
*/
221+
private async _checkDealsStates(): Promise<void> {
222+
if (this.ongoingCheck) {
223+
return;
224+
}
225+
226+
this.ongoingCheck = true;
227+
const records = await this.getAll();
228+
const recordsToCheck = records.filter(({ status }) =>
229+
[DealStatus.Created, DealStatus.Claimed, DealStatus.CheckedIn, DealStatus.Disputed].includes(
230+
status,
231+
),
232+
);
233+
const checkedRecords = await Promise.all(
234+
recordsToCheck.map((r) => this._buildDealRecord(r.offer)),
235+
);
236+
let shouldEmitChanged = false;
237+
checkedRecords.forEach((r, index) => {
238+
if (r.status !== recordsToCheck[index].status) {
239+
shouldEmitChanged = true;
240+
this.dispatchEvent(
241+
new CustomEvent<DealRecord<CustomRequestQuery, CustomOfferOptions>>('status', {
242+
detail: r,
243+
}),
244+
);
245+
}
246+
});
247+
if (shouldEmitChanged) {
248+
this.dispatchEvent(new CustomEvent<void>('changed'));
249+
}
250+
this.ongoingCheck = false;
251+
}
252+
174253
/**
175254
* Builds and saves the deal record based on the offer
176255
*
177256
* @private
178257
* @param {OfferData<CustomRequestQuery, CustomOfferOptions>} offer Offer data object
179-
* @param {PublicClient} publicClient Ethereum public client
180258
* @returns {Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>>}
181259
* @memberof DealsRegistry
182260
*/
183261
private async _buildDealRecord(
184262
offer: OfferData<CustomRequestQuery, CustomOfferOptions>,
185-
publicClient: PublicClient,
186263
): Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> {
187264
const market = this._getMarketConfig(offer);
188265
const marketContract = {
@@ -193,15 +270,16 @@ export class DealsRegistry<
193270

194271
// Fetching deal information from smart contract
195272
// eslint-disable-next-line @typescript-eslint/no-unused-vars
196-
const [created, _, retailerId, buyer, price, asset, status] = await publicClient.readContract({
197-
...marketContract,
198-
functionName: 'deals',
199-
args: [offer.payload.id],
200-
});
273+
const [created, _, retailerId, buyer, price, asset, status] =
274+
await this.client.publicClient.readContract({
275+
...marketContract,
276+
functionName: 'deals',
277+
args: [offer.payload.id],
278+
});
201279

202280
// Preparing deal record for registry
203281
const dealRecord: DealRecord<CustomRequestQuery, CustomOfferOptions> = {
204-
chainId: await publicClient.getChainId(),
282+
chainId: await this.client.publicClient.getChainId(),
205283
created: created,
206284
offer,
207285
retailerId: retailerId,
@@ -218,13 +296,19 @@ export class DealsRegistry<
218296
return dealRecord;
219297
}
220298

299+
/**
300+
* Graceful deals registry stop
301+
*/
302+
stop() {
303+
clearInterval(this.checkInterval);
304+
}
305+
221306
/**
222307
* Creates a deal from offer
223308
*
224309
* @param {OfferData<CustomRequestQuery, CustomOfferOptions>} offer
225310
* @param {Hash} paymentId Chosen payment Id (from offer.payment)
226311
* @param {Hash} retailerId Retailer Id
227-
* @param {PublicClient} publicClient Ethereum public client
228312
* @param {WalletClient} walletClient Ethereum wallet client
229313
* @param {TxCallback} [txCallback] Optional transaction hash callback
230314
* @returns {Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>>} Deal record
@@ -234,10 +318,21 @@ export class DealsRegistry<
234318
offer: OfferData<CustomRequestQuery, CustomOfferOptions>,
235319
paymentId: Hash,
236320
retailerId: Hash,
237-
publicClient: PublicClient,
238321
walletClient: WalletClient,
239322
txCallback?: TxCallback,
240323
): Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> {
324+
let deal: DealRecord<CustomRequestQuery, CustomOfferOptions> | undefined;
325+
326+
try {
327+
deal = await this.get(offer.payload.id);
328+
} catch (error) {
329+
logger.error(error);
330+
}
331+
332+
if (deal) {
333+
throw new Error(`Deal ${offer.payload.id} already created!`);
334+
}
335+
241336
const market = this._getMarketConfig(offer);
242337

243338
// Extracting the proper payment method by Id
@@ -250,7 +345,7 @@ export class DealsRegistry<
250345
paymentOption.asset,
251346
market.address,
252347
BigInt(paymentOption.price),
253-
publicClient,
348+
this.client.publicClient,
254349
walletClient,
255350
txCallback,
256351
);
@@ -260,26 +355,22 @@ export class DealsRegistry<
260355
marketABI,
261356
'deal',
262357
[offer.payload, offer.payment, paymentId, retailerId, [offer.signature]],
263-
publicClient,
358+
this.client.publicClient,
264359
walletClient,
265360
txCallback,
266361
);
267362

268-
return await this._buildDealRecord(offer, publicClient);
363+
return await this._buildDealRecord(offer);
269364
}
270365

271366
/**
272367
* Returns an up-to-date deal record
273368
*
274369
* @param {Hash} offerId Offer Id
275-
* @param {PublicClient} publicClient Ethereum public client
276370
* @returns {Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>>}
277371
* @memberof DealsRegistry
278372
*/
279-
async get(
280-
offerId: Hash,
281-
publicClient: PublicClient,
282-
): Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> {
373+
async get(offerId: Hash): Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> {
283374
const dealRecord = this.deals.get(offerId);
284375

285376
if (!dealRecord) {
@@ -290,24 +381,21 @@ export class DealsRegistry<
290381
return dealRecord;
291382
}
292383

293-
return await this._buildDealRecord(dealRecord.offer, publicClient);
384+
return await this._buildDealRecord(dealRecord.offer);
294385
}
295386

296387
/**
297388
* Returns all an up-to-date deal records
298389
*
299-
* @param {PublicClient} publicClient Ethereum public client
300390
* @returns {Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>[]>}
301391
* @memberof DealsRegistry
302392
*/
303-
async getAll(
304-
publicClient: PublicClient,
305-
): Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>[]> {
393+
async getAll(): Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>[]> {
306394
const records: DealRecord<CustomRequestQuery, CustomOfferOptions>[] = [];
307395

308396
for (const record of this.deals.values()) {
309397
try {
310-
records.push(await this.get(record.offer.payload.id, publicClient));
398+
records.push(await this.get(record.offer.payload.id));
311399
} catch (error) {
312400
logger.error(error);
313401
}
@@ -320,19 +408,17 @@ export class DealsRegistry<
320408
* Cancels the deal
321409
*
322410
* @param {Hash} offerId Offer Id
323-
* @param {PublicClient} publicClient Ethereum public client
324411
* @param {WalletClient} walletClient Ethereum wallet client
325412
* @param {TxCallback} [txCallback] Optional tx hash callback
326413
* @returns {Promise<void>}
327414
* @memberof DealsRegistry
328415
*/
329416
async cancel(
330417
offerId: Hash,
331-
publicClient: PublicClient,
332418
walletClient: WalletClient,
333419
txCallback?: TxCallback,
334420
): Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> {
335-
const dealRecord = await this.get(offerId, publicClient);
421+
const dealRecord = await this.get(offerId);
336422

337423
if (![DealStatus.Created, DealStatus.Claimed].includes(dealRecord.status)) {
338424
throw new Error(`Cancellation not allowed in the status ${DealStatus[dealRecord.status]}`);
@@ -345,20 +431,19 @@ export class DealsRegistry<
345431
marketABI,
346432
'cancel',
347433
[dealRecord.offer.payload.id, dealRecord.offer.cancel],
348-
publicClient,
434+
this.client.publicClient,
349435
walletClient,
350436
txCallback,
351437
);
352438

353-
return await this._buildDealRecord(dealRecord.offer, publicClient);
439+
return await this._buildDealRecord(dealRecord.offer);
354440
}
355441

356442
/**
357443
* Transfers the deal to another address
358444
*
359445
* @param {Hash} offerId Offer Id
360446
* @param {string} to New owner address
361-
* @param {PublicClient} publicClient Ethereum public client
362447
* @param {WalletClient} walletClient Ethereum wallet client
363448
* @param {TxCallback} [txCallback] Optional tx hash callback
364449
* @returns {Promise<void>}
@@ -367,11 +452,10 @@ export class DealsRegistry<
367452
async transfer(
368453
offerId: Hash,
369454
to: Address,
370-
publicClient: PublicClient,
371455
walletClient: WalletClient,
372456
txCallback?: TxCallback,
373457
): Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> {
374-
let dealRecord = await this.get(offerId, publicClient);
458+
let dealRecord = await this.get(offerId);
375459

376460
const signerAddress = await getSignerAddress(walletClient);
377461

@@ -384,7 +468,7 @@ export class DealsRegistry<
384468
}
385469

386470
// Updating the record
387-
dealRecord = await this._buildDealRecord(dealRecord.offer, publicClient);
471+
dealRecord = await this._buildDealRecord(dealRecord.offer);
388472

389473
if (![DealStatus.Created, DealStatus.Claimed].includes(dealRecord.status)) {
390474
throw new Error(`Transfer not allowed in the status ${DealStatus[dealRecord.status]}`);
@@ -398,28 +482,27 @@ export class DealsRegistry<
398482
marketABI,
399483
'offerTokens',
400484
[dealRecord.offer.payload.id],
401-
publicClient,
485+
this.client.publicClient,
402486
);
403487

404488
await sendHelper(
405489
market.address,
406490
marketABI,
407491
'safeTransferFrom',
408492
[signerAddress, to, tokenId],
409-
publicClient,
493+
this.client.publicClient,
410494
walletClient,
411495
txCallback,
412496
);
413497

414-
return await this._buildDealRecord(dealRecord.offer, publicClient);
498+
return await this._buildDealRecord(dealRecord.offer);
415499
}
416500

417501
/**
418502
* Makes the deal check-in
419503
*
420504
* @param {Hash} offerId
421505
* @param {Hash} supplierSignature
422-
* @param {PublicClient} publicClient Ethereum public client
423506
* @param {WalletClient} walletClient Ethereum wallet client
424507
* @param {TxCallback} [txCallback] Optional tx hash callback
425508
* @returns {Promise<void>}
@@ -428,11 +511,10 @@ export class DealsRegistry<
428511
async checkIn(
429512
offerId: Hash,
430513
supplierSignature: Hash,
431-
publicClient: PublicClient,
432514
walletClient: WalletClient,
433515
txCallback?: TxCallback,
434516
): Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> {
435-
const dealRecord = await this.get(offerId, publicClient);
517+
const dealRecord = await this.get(offerId);
436518

437519
if (![DealStatus.Created, DealStatus.Claimed].includes(dealRecord.status)) {
438520
throw new Error(`CheckIn not allowed in the status ${DealStatus[dealRecord.status]}`);
@@ -460,11 +542,11 @@ export class DealsRegistry<
460542
marketABI,
461543
'checkIn',
462544
[dealRecord.offer.payload.id, [buyerSignature, supplierSignature]],
463-
publicClient,
545+
this.client.publicClient,
464546
walletClient,
465547
txCallback,
466548
);
467549

468-
return await this._buildDealRecord(dealRecord.offer, publicClient);
550+
return await this._buildDealRecord(dealRecord.offer);
469551
}
470552
}

0 commit comments

Comments
 (0)