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' ;
2
3
import { Abi } from 'abitype' ;
3
4
import { marketABI } from '@windingtree/contracts' ;
4
5
import { Client , createCheckInOutSignature } from '../index.js' ;
@@ -78,6 +79,36 @@ export interface ContractClientConfig {
78
79
abi : Abi ;
79
80
}
80
81
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
+
81
112
/**
82
113
* Creates an instance of DealsRegistry.
83
114
*
@@ -87,12 +118,14 @@ export interface ContractClientConfig {
87
118
export class DealsRegistry <
88
119
CustomRequestQuery extends GenericQuery ,
89
120
CustomOfferOptions extends GenericOfferOptions ,
90
- > {
121
+ > extends EventEmitter < DealEvents < CustomRequestQuery , CustomOfferOptions > > {
91
122
private client : Client < CustomRequestQuery , CustomOfferOptions > ;
92
123
/** Mapping of an offer id => Deal */
93
124
private deals : Map < string , DealRecord < CustomRequestQuery , CustomOfferOptions > > ; // id => Deal
94
125
private storage ?: Storage ;
95
126
private storageKey : string ;
127
+ private checkInterval ?: NodeJS . Timer ;
128
+ private ongoingCheck = false ;
96
129
97
130
/**
98
131
* Creates an instance of DealsRegistry.
@@ -101,13 +134,21 @@ export class DealsRegistry<
101
134
* @memberof DealsRegistry
102
135
*/
103
136
constructor ( options : DealsRegistryOptions < CustomRequestQuery , CustomOfferOptions > ) {
137
+ super ( ) ;
138
+
104
139
const { client, storage, prefix } = options ;
105
140
106
141
this . client = client ;
107
142
this . deals = new Map < string , DealRecord < CustomRequestQuery , CustomOfferOptions > > ( ) ;
108
143
this . storageKey = `${ prefix } _deals_records` ;
109
144
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 ) ;
111
152
}
112
153
113
154
/**
@@ -171,18 +212,54 @@ export class DealsRegistry<
171
212
return domain ;
172
213
}
173
214
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
+
174
253
/**
175
254
* Builds and saves the deal record based on the offer
176
255
*
177
256
* @private
178
257
* @param {OfferData<CustomRequestQuery, CustomOfferOptions> } offer Offer data object
179
- * @param {PublicClient } publicClient Ethereum public client
180
258
* @returns {Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> }
181
259
* @memberof DealsRegistry
182
260
*/
183
261
private async _buildDealRecord (
184
262
offer : OfferData < CustomRequestQuery , CustomOfferOptions > ,
185
- publicClient : PublicClient ,
186
263
) : Promise < DealRecord < CustomRequestQuery , CustomOfferOptions > > {
187
264
const market = this . _getMarketConfig ( offer ) ;
188
265
const marketContract = {
@@ -193,15 +270,16 @@ export class DealsRegistry<
193
270
194
271
// Fetching deal information from smart contract
195
272
// 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
+ } ) ;
201
279
202
280
// Preparing deal record for registry
203
281
const dealRecord : DealRecord < CustomRequestQuery , CustomOfferOptions > = {
204
- chainId : await publicClient . getChainId ( ) ,
282
+ chainId : await this . client . publicClient . getChainId ( ) ,
205
283
created : created ,
206
284
offer,
207
285
retailerId : retailerId ,
@@ -218,13 +296,19 @@ export class DealsRegistry<
218
296
return dealRecord ;
219
297
}
220
298
299
+ /**
300
+ * Graceful deals registry stop
301
+ */
302
+ stop ( ) {
303
+ clearInterval ( this . checkInterval ) ;
304
+ }
305
+
221
306
/**
222
307
* Creates a deal from offer
223
308
*
224
309
* @param {OfferData<CustomRequestQuery, CustomOfferOptions> } offer
225
310
* @param {Hash } paymentId Chosen payment Id (from offer.payment)
226
311
* @param {Hash } retailerId Retailer Id
227
- * @param {PublicClient } publicClient Ethereum public client
228
312
* @param {WalletClient } walletClient Ethereum wallet client
229
313
* @param {TxCallback } [txCallback] Optional transaction hash callback
230
314
* @returns {Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> } Deal record
@@ -234,10 +318,21 @@ export class DealsRegistry<
234
318
offer : OfferData < CustomRequestQuery , CustomOfferOptions > ,
235
319
paymentId : Hash ,
236
320
retailerId : Hash ,
237
- publicClient : PublicClient ,
238
321
walletClient : WalletClient ,
239
322
txCallback ?: TxCallback ,
240
323
) : 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
+
241
336
const market = this . _getMarketConfig ( offer ) ;
242
337
243
338
// Extracting the proper payment method by Id
@@ -250,7 +345,7 @@ export class DealsRegistry<
250
345
paymentOption . asset ,
251
346
market . address ,
252
347
BigInt ( paymentOption . price ) ,
253
- publicClient ,
348
+ this . client . publicClient ,
254
349
walletClient ,
255
350
txCallback ,
256
351
) ;
@@ -260,26 +355,22 @@ export class DealsRegistry<
260
355
marketABI ,
261
356
'deal' ,
262
357
[ offer . payload , offer . payment , paymentId , retailerId , [ offer . signature ] ] ,
263
- publicClient ,
358
+ this . client . publicClient ,
264
359
walletClient ,
265
360
txCallback ,
266
361
) ;
267
362
268
- return await this . _buildDealRecord ( offer , publicClient ) ;
363
+ return await this . _buildDealRecord ( offer ) ;
269
364
}
270
365
271
366
/**
272
367
* Returns an up-to-date deal record
273
368
*
274
369
* @param {Hash } offerId Offer Id
275
- * @param {PublicClient } publicClient Ethereum public client
276
370
* @returns {Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>> }
277
371
* @memberof DealsRegistry
278
372
*/
279
- async get (
280
- offerId : Hash ,
281
- publicClient : PublicClient ,
282
- ) : Promise < DealRecord < CustomRequestQuery , CustomOfferOptions > > {
373
+ async get ( offerId : Hash ) : Promise < DealRecord < CustomRequestQuery , CustomOfferOptions > > {
283
374
const dealRecord = this . deals . get ( offerId ) ;
284
375
285
376
if ( ! dealRecord ) {
@@ -290,24 +381,21 @@ export class DealsRegistry<
290
381
return dealRecord ;
291
382
}
292
383
293
- return await this . _buildDealRecord ( dealRecord . offer , publicClient ) ;
384
+ return await this . _buildDealRecord ( dealRecord . offer ) ;
294
385
}
295
386
296
387
/**
297
388
* Returns all an up-to-date deal records
298
389
*
299
- * @param {PublicClient } publicClient Ethereum public client
300
390
* @returns {Promise<DealRecord<CustomRequestQuery, CustomOfferOptions>[]> }
301
391
* @memberof DealsRegistry
302
392
*/
303
- async getAll (
304
- publicClient : PublicClient ,
305
- ) : Promise < DealRecord < CustomRequestQuery , CustomOfferOptions > [ ] > {
393
+ async getAll ( ) : Promise < DealRecord < CustomRequestQuery , CustomOfferOptions > [ ] > {
306
394
const records : DealRecord < CustomRequestQuery , CustomOfferOptions > [ ] = [ ] ;
307
395
308
396
for ( const record of this . deals . values ( ) ) {
309
397
try {
310
- records . push ( await this . get ( record . offer . payload . id , publicClient ) ) ;
398
+ records . push ( await this . get ( record . offer . payload . id ) ) ;
311
399
} catch ( error ) {
312
400
logger . error ( error ) ;
313
401
}
@@ -320,19 +408,17 @@ export class DealsRegistry<
320
408
* Cancels the deal
321
409
*
322
410
* @param {Hash } offerId Offer Id
323
- * @param {PublicClient } publicClient Ethereum public client
324
411
* @param {WalletClient } walletClient Ethereum wallet client
325
412
* @param {TxCallback } [txCallback] Optional tx hash callback
326
413
* @returns {Promise<void> }
327
414
* @memberof DealsRegistry
328
415
*/
329
416
async cancel (
330
417
offerId : Hash ,
331
- publicClient : PublicClient ,
332
418
walletClient : WalletClient ,
333
419
txCallback ?: TxCallback ,
334
420
) : Promise < DealRecord < CustomRequestQuery , CustomOfferOptions > > {
335
- const dealRecord = await this . get ( offerId , publicClient ) ;
421
+ const dealRecord = await this . get ( offerId ) ;
336
422
337
423
if ( ! [ DealStatus . Created , DealStatus . Claimed ] . includes ( dealRecord . status ) ) {
338
424
throw new Error ( `Cancellation not allowed in the status ${ DealStatus [ dealRecord . status ] } ` ) ;
@@ -345,20 +431,19 @@ export class DealsRegistry<
345
431
marketABI ,
346
432
'cancel' ,
347
433
[ dealRecord . offer . payload . id , dealRecord . offer . cancel ] ,
348
- publicClient ,
434
+ this . client . publicClient ,
349
435
walletClient ,
350
436
txCallback ,
351
437
) ;
352
438
353
- return await this . _buildDealRecord ( dealRecord . offer , publicClient ) ;
439
+ return await this . _buildDealRecord ( dealRecord . offer ) ;
354
440
}
355
441
356
442
/**
357
443
* Transfers the deal to another address
358
444
*
359
445
* @param {Hash } offerId Offer Id
360
446
* @param {string } to New owner address
361
- * @param {PublicClient } publicClient Ethereum public client
362
447
* @param {WalletClient } walletClient Ethereum wallet client
363
448
* @param {TxCallback } [txCallback] Optional tx hash callback
364
449
* @returns {Promise<void> }
@@ -367,11 +452,10 @@ export class DealsRegistry<
367
452
async transfer (
368
453
offerId : Hash ,
369
454
to : Address ,
370
- publicClient : PublicClient ,
371
455
walletClient : WalletClient ,
372
456
txCallback ?: TxCallback ,
373
457
) : Promise < DealRecord < CustomRequestQuery , CustomOfferOptions > > {
374
- let dealRecord = await this . get ( offerId , publicClient ) ;
458
+ let dealRecord = await this . get ( offerId ) ;
375
459
376
460
const signerAddress = await getSignerAddress ( walletClient ) ;
377
461
@@ -384,7 +468,7 @@ export class DealsRegistry<
384
468
}
385
469
386
470
// Updating the record
387
- dealRecord = await this . _buildDealRecord ( dealRecord . offer , publicClient ) ;
471
+ dealRecord = await this . _buildDealRecord ( dealRecord . offer ) ;
388
472
389
473
if ( ! [ DealStatus . Created , DealStatus . Claimed ] . includes ( dealRecord . status ) ) {
390
474
throw new Error ( `Transfer not allowed in the status ${ DealStatus [ dealRecord . status ] } ` ) ;
@@ -398,28 +482,27 @@ export class DealsRegistry<
398
482
marketABI ,
399
483
'offerTokens' ,
400
484
[ dealRecord . offer . payload . id ] ,
401
- publicClient ,
485
+ this . client . publicClient ,
402
486
) ;
403
487
404
488
await sendHelper (
405
489
market . address ,
406
490
marketABI ,
407
491
'safeTransferFrom' ,
408
492
[ signerAddress , to , tokenId ] ,
409
- publicClient ,
493
+ this . client . publicClient ,
410
494
walletClient ,
411
495
txCallback ,
412
496
) ;
413
497
414
- return await this . _buildDealRecord ( dealRecord . offer , publicClient ) ;
498
+ return await this . _buildDealRecord ( dealRecord . offer ) ;
415
499
}
416
500
417
501
/**
418
502
* Makes the deal check-in
419
503
*
420
504
* @param {Hash } offerId
421
505
* @param {Hash } supplierSignature
422
- * @param {PublicClient } publicClient Ethereum public client
423
506
* @param {WalletClient } walletClient Ethereum wallet client
424
507
* @param {TxCallback } [txCallback] Optional tx hash callback
425
508
* @returns {Promise<void> }
@@ -428,11 +511,10 @@ export class DealsRegistry<
428
511
async checkIn (
429
512
offerId : Hash ,
430
513
supplierSignature : Hash ,
431
- publicClient : PublicClient ,
432
514
walletClient : WalletClient ,
433
515
txCallback ?: TxCallback ,
434
516
) : Promise < DealRecord < CustomRequestQuery , CustomOfferOptions > > {
435
- const dealRecord = await this . get ( offerId , publicClient ) ;
517
+ const dealRecord = await this . get ( offerId ) ;
436
518
437
519
if ( ! [ DealStatus . Created , DealStatus . Claimed ] . includes ( dealRecord . status ) ) {
438
520
throw new Error ( `CheckIn not allowed in the status ${ DealStatus [ dealRecord . status ] } ` ) ;
@@ -460,11 +542,11 @@ export class DealsRegistry<
460
542
marketABI ,
461
543
'checkIn' ,
462
544
[ dealRecord . offer . payload . id , [ buyerSignature , supplierSignature ] ] ,
463
- publicClient ,
545
+ this . client . publicClient ,
464
546
walletClient ,
465
547
txCallback ,
466
548
) ;
467
549
468
- return await this . _buildDealRecord ( dealRecord . offer , publicClient ) ;
550
+ return await this . _buildDealRecord ( dealRecord . offer ) ;
469
551
}
470
552
}
0 commit comments