@human-protocol/sdk 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/job.ts ADDED
@@ -0,0 +1,1064 @@
1
+ import { BigNumber, Contract, ethers } from 'ethers';
2
+ import winston from 'winston';
3
+
4
+ import { DEFAULT_BUCKET, DEFAULT_PUBLIC_BUCKET } from './constants';
5
+ import { download, getKeyFromURL, getPublicURL, upload } from './storage';
6
+ import {
7
+ EscrowStatus,
8
+ Manifest,
9
+ Payout,
10
+ ProviderData,
11
+ Result,
12
+ UploadResult,
13
+ ManifestData,
14
+ ContractData,
15
+ JobArguments,
16
+ StorageAccessData,
17
+ } from './types';
18
+ import {
19
+ deployEscrowFactory,
20
+ getEscrow,
21
+ getEscrowFactory,
22
+ getHmToken,
23
+ getStaking,
24
+ toFullDigit,
25
+ } from './utils';
26
+ import {
27
+ ErrorHMTokenMissing,
28
+ ErrorJobAlreadyLaunched,
29
+ ErrorJobNotInitialized,
30
+ ErrorJobNotLaunched,
31
+ ErrorManifestMissing,
32
+ ErrorReputationOracleMissing,
33
+ ErrorStakingMissing,
34
+ ErrorStorageAccessDataMissing,
35
+ } from './error';
36
+ import { createLogger } from './logger';
37
+
38
+ /**
39
+ * @class Human Protocol Job
40
+ */
41
+ export class Job {
42
+ /**
43
+ * Ethers provider data
44
+ */
45
+ providerData?: ProviderData;
46
+
47
+ /**
48
+ * Job manifest & result data
49
+ */
50
+ manifestData?: ManifestData;
51
+
52
+ /**
53
+ * Job smart contract data
54
+ */
55
+ contractData?: ContractData;
56
+
57
+ /**
58
+ * Cloud storage access data
59
+ */
60
+ storageAccessData?: StorageAccessData;
61
+
62
+ private _logger: winston.Logger;
63
+
64
+ /**
65
+ * Get the total cost of the job
66
+ * @return {number} The total cost of the job
67
+ */
68
+ get amount(): number {
69
+ return (
70
+ (this.manifestData?.manifest?.task_bid_price || 0) *
71
+ (this.manifestData?.manifest?.job_total_tasks || 0)
72
+ );
73
+ }
74
+
75
+ /**
76
+ * **Job constructor**
77
+ *
78
+ * If the network is not specified, it'll connect to http://localhost:8545.
79
+ * If the network other than hardhat is specified, either of Infura/Alchemy key is required to connect.
80
+ *
81
+ * @param {JobArguments} args - Job arguments
82
+ */
83
+ constructor({
84
+ network,
85
+ alchemyKey,
86
+ infuraKey,
87
+ gasPayer,
88
+ reputationOracle,
89
+ trustedHandlers,
90
+ hmTokenAddr,
91
+ factoryAddr,
92
+ escrowAddr,
93
+ manifest,
94
+ storageAccessKeyId,
95
+ storageSecretAccessKey,
96
+ storageEndpoint,
97
+ storagePublicBucket,
98
+ storageBucket,
99
+ stakingAddr,
100
+ logLevel = 'info',
101
+ }: JobArguments) {
102
+ const provider = network
103
+ ? ethers.getDefaultProvider(network, {
104
+ alchemy: alchemyKey,
105
+ infura: infuraKey,
106
+ })
107
+ : new ethers.providers.JsonRpcProvider();
108
+
109
+ this.providerData = {
110
+ provider,
111
+ };
112
+
113
+ if (typeof gasPayer === 'string') {
114
+ this.providerData.gasPayer = new ethers.Wallet(gasPayer, provider);
115
+ } else {
116
+ this.providerData.gasPayer = gasPayer;
117
+ }
118
+
119
+ if (typeof reputationOracle === 'string') {
120
+ this.providerData.reputationOracle = new ethers.Wallet(
121
+ reputationOracle,
122
+ provider
123
+ );
124
+ } else {
125
+ this.providerData.reputationOracle = reputationOracle;
126
+ }
127
+
128
+ this.providerData.trustedHandlers =
129
+ trustedHandlers?.map((trustedHandler) => {
130
+ if (typeof trustedHandler === 'string') {
131
+ return new ethers.Wallet(trustedHandler, provider);
132
+ }
133
+ return trustedHandler;
134
+ }) || [];
135
+
136
+ this.contractData = {
137
+ hmTokenAddr,
138
+ escrowAddr,
139
+ factoryAddr,
140
+ stakingAddr,
141
+ };
142
+
143
+ this.manifestData = { manifest };
144
+
145
+ this.storageAccessData = {
146
+ accessKeyId: storageAccessKeyId || '',
147
+ secretAccessKey: storageSecretAccessKey || '',
148
+ endpoint: storageEndpoint,
149
+ publicBucket: storagePublicBucket || DEFAULT_PUBLIC_BUCKET,
150
+ bucket: storageBucket || DEFAULT_BUCKET,
151
+ };
152
+
153
+ this._logger = createLogger(logLevel);
154
+ }
155
+
156
+ /**
157
+ * **Initialize the escrow**
158
+ *
159
+ * For existing escrows, access the escrow on-chain, and read manifest.
160
+ * For new escrows, deploy escrow factory to launch new escrow.
161
+ *
162
+ * @returns {Promise<boolean>} - True if escrow is initialized successfully.
163
+ */
164
+ async initialize(): Promise<boolean> {
165
+ if (!this.contractData) {
166
+ this._logError(new Error('Contract data is missing'));
167
+ return false;
168
+ }
169
+
170
+ this.contractData.hmToken = await getHmToken(
171
+ this.contractData.hmTokenAddr,
172
+ this.providerData?.gasPayer
173
+ );
174
+
175
+ if (!this.contractData?.escrowAddr) {
176
+ if (!this.manifestData?.manifest) {
177
+ this._logError(ErrorManifestMissing);
178
+ return false;
179
+ }
180
+
181
+ if (!this.contractData.stakingAddr) {
182
+ this._logError(new Error('Staking contract is missing'));
183
+ return false;
184
+ }
185
+
186
+ this._logger.info('Getting staking...');
187
+ this.contractData.staking = await getStaking(
188
+ this.contractData.stakingAddr,
189
+ this.providerData?.gasPayer
190
+ );
191
+
192
+ this._logger.info('Deploying escrow factory...');
193
+ this.contractData.factory = await deployEscrowFactory(
194
+ this.contractData.hmTokenAddr,
195
+ this.contractData.stakingAddr,
196
+ this.providerData?.gasPayer
197
+ );
198
+ this.contractData.factoryAddr = this.contractData.factory.address;
199
+ this._logger.info(
200
+ `Escrow factory is deployed at ${this.contractData.factory.address}.`
201
+ );
202
+ } else {
203
+ if (!this.contractData?.factoryAddr) {
204
+ this._logError(
205
+ new Error('Factory address is required for existing escrow')
206
+ );
207
+ return false;
208
+ }
209
+
210
+ this._logger.info('Getting escrow factory...');
211
+ this.contractData.factory = await getEscrowFactory(
212
+ this.contractData?.factoryAddr,
213
+ this.providerData?.gasPayer
214
+ );
215
+
216
+ this._logger.info('Checking if staking is configured...');
217
+ const stakingAddr = await this.contractData.factory.staking();
218
+ if (!stakingAddr) {
219
+ this._logError(new Error('Factory is not configured with staking'));
220
+ this.contractData.factory = undefined;
221
+
222
+ return false;
223
+ }
224
+ this._logger.info('Getting staking...');
225
+ this.contractData.staking = await getStaking(
226
+ stakingAddr,
227
+ this.providerData?.gasPayer
228
+ );
229
+ this.contractData.stakingAddr = stakingAddr;
230
+
231
+ this._logger.info('Checking if reward pool is configured...');
232
+ const rewardPoolAddr = await this.contractData.staking.rewardPool();
233
+ if (!rewardPoolAddr) {
234
+ this._logError(new Error('Staking is not configured with reward pool'));
235
+ this.contractData.staking = undefined;
236
+ this.contractData.factory = undefined;
237
+
238
+ return false;
239
+ }
240
+
241
+ this._logger.info('Checking if escrow exists in the factory...');
242
+ const hasEscrow = await this.contractData?.factory.hasEscrow(
243
+ this.contractData?.escrowAddr
244
+ );
245
+
246
+ if (!hasEscrow) {
247
+ this._logError(new Error('Factory does not contain the escrow'));
248
+ this.contractData.factory = undefined;
249
+
250
+ return false;
251
+ }
252
+
253
+ this._logger.info('Accessing the escrow...');
254
+ this.contractData.escrow = await getEscrow(
255
+ this.contractData?.escrowAddr,
256
+ this.providerData?.gasPayer
257
+ );
258
+ this._logger.info('Accessed the escrow successfully.');
259
+
260
+ const manifestUrl = await this.contractData?.escrow.manifestUrl();
261
+ const manifestHash = await this.contractData?.escrow.manifestHash();
262
+
263
+ if (
264
+ (!manifestUrl.length || !manifestHash.length) &&
265
+ !this.manifestData?.manifest
266
+ ) {
267
+ this._logError(ErrorManifestMissing);
268
+
269
+ this.contractData.factory = undefined;
270
+ this.contractData.escrow = undefined;
271
+
272
+ return false;
273
+ }
274
+
275
+ if (manifestUrl.length && manifestHash.length) {
276
+ this.manifestData = {
277
+ ...this.manifestData,
278
+ manifestlink: {
279
+ url: manifestUrl,
280
+ hash: manifestHash,
281
+ },
282
+ };
283
+
284
+ this.manifestData.manifest = (await this._download(
285
+ manifestUrl
286
+ )) as Manifest;
287
+ }
288
+ }
289
+
290
+ return true;
291
+ }
292
+
293
+ /**
294
+ * **Launch the escrow**
295
+ *
296
+ * Deploy new escrow contract, and uploads manifest.
297
+ *
298
+ * @returns {Promise<boolean>} - True if the escrow is launched successfully.
299
+ */
300
+ async launch(): Promise<boolean> {
301
+ if (!this.contractData || !this.contractData.factory) {
302
+ this._logError(ErrorJobNotInitialized);
303
+ return false;
304
+ }
305
+
306
+ if (!this.contractData || this.contractData.escrow) {
307
+ this._logError(ErrorJobAlreadyLaunched);
308
+ return false;
309
+ }
310
+
311
+ if (!this.providerData || !this.providerData.reputationOracle) {
312
+ this._logError(ErrorReputationOracleMissing);
313
+ return false;
314
+ }
315
+
316
+ this._logger.info('Launching escrow...');
317
+
318
+ try {
319
+ const txReceipt = await this.contractData?.factory?.createEscrow(
320
+ this.providerData?.trustedHandlers?.map(
321
+ (trustedHandler) => trustedHandler.address
322
+ ) || []
323
+ );
324
+
325
+ const txResponse = await txReceipt?.wait();
326
+
327
+ const event = txResponse?.events?.find(
328
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
329
+ (event: any) => event.event === 'Launched'
330
+ );
331
+
332
+ const escrowAddr = event?.args?.[1];
333
+ this._logger.info(`Escrow is deployed at ${escrowAddr}.`);
334
+
335
+ this.contractData.escrowAddr = escrowAddr;
336
+ this.contractData.escrow = await getEscrow(
337
+ escrowAddr,
338
+ this.providerData?.gasPayer
339
+ );
340
+ } catch {
341
+ this._logError(new Error('Error creating escrow...'));
342
+ return false;
343
+ }
344
+
345
+ return (
346
+ (await this.status()) == EscrowStatus.Launched &&
347
+ (await this.balance())?.toNumber() === 0
348
+ );
349
+ }
350
+
351
+ /**
352
+ * Setup escrow
353
+ *
354
+ * Sets the escrow contract to be ready to receive answers from the Recording Oracle.
355
+ *
356
+ * @param {string | undefined} senderAddr - Address of HMToken sender
357
+ * @returns {Promise<boolean>} True if the escrow is setup successfully.
358
+ */
359
+ async setup(senderAddr?: string): Promise<boolean> {
360
+ if (!this.contractData?.escrow) {
361
+ this._logError(ErrorJobNotLaunched);
362
+ return false;
363
+ }
364
+
365
+ if (!this.manifestData || !this.manifestData.manifest) {
366
+ this._logError(ErrorManifestMissing);
367
+ return false;
368
+ }
369
+
370
+ if (this.manifestData.manifestlink) {
371
+ this._logError(new Error('Job is already setup'));
372
+ return false;
373
+ }
374
+
375
+ if (!this.contractData.hmToken) {
376
+ this._logError(ErrorHMTokenMissing);
377
+ return false;
378
+ }
379
+
380
+ const reputationOracleStake =
381
+ (this.manifestData?.manifest?.oracle_stake || 0) * 100;
382
+ const recordingOracleStake =
383
+ (this.manifestData?.manifest?.oracle_stake || 0) * 100;
384
+ const repuationOracleAddr =
385
+ this.manifestData?.manifest?.reputation_oracle_addr || '';
386
+ const recordingOracleAddr =
387
+ this.manifestData?.manifest?.recording_oracle_addr || '';
388
+
389
+ this._logger.info(
390
+ `Transferring ${this.amount} HMT to ${this.contractData.escrow.address}...`
391
+ );
392
+ const transferred = await (senderAddr
393
+ ? this._raffleExecute(
394
+ this.contractData.hmToken,
395
+ 'transferFrom',
396
+ senderAddr,
397
+ this.contractData.escrow.address,
398
+ toFullDigit(this.amount)
399
+ )
400
+ : this._raffleExecute(
401
+ this.contractData.hmToken,
402
+ 'transfer',
403
+ this.contractData.escrow.address,
404
+ toFullDigit(this.amount)
405
+ ));
406
+
407
+ if (!transferred) {
408
+ this._logError(
409
+ new Error(
410
+ 'Failed to transfer HMT with all credentials, not continuing to setup'
411
+ )
412
+ );
413
+ return false;
414
+ }
415
+ this._logger.info('HMT transferred.');
416
+
417
+ this._logger.info('Uploading manifest...');
418
+ const uploadResult = await this._upload(this.manifestData.manifest);
419
+ if (!uploadResult) {
420
+ this._logError(new Error('Error uploading manifest'));
421
+ return false;
422
+ }
423
+
424
+ this.manifestData.manifestlink = {
425
+ url: uploadResult.key,
426
+ hash: uploadResult.hash,
427
+ };
428
+ this._logger.info(
429
+ `Uploaded manifest.\n\tKey: ${uploadResult.key}\n\tHash: ${uploadResult.hash}`
430
+ );
431
+
432
+ this._logger.info('Setting up the escrow...');
433
+ const contractSetup = await this._raffleExecute(
434
+ this.contractData.escrow,
435
+ 'setup',
436
+ repuationOracleAddr,
437
+ recordingOracleAddr,
438
+ reputationOracleStake,
439
+ recordingOracleStake,
440
+ this.manifestData?.manifestlink?.url,
441
+ this.manifestData?.manifestlink?.hash
442
+ );
443
+
444
+ if (!contractSetup) {
445
+ this._logError(new Error('Failed to setup contract'));
446
+ return false;
447
+ }
448
+
449
+ this._logger.info('Escrow is set up.');
450
+
451
+ return (
452
+ (await this.status()) === EscrowStatus.Pending &&
453
+ (await this.balance())?.toString() === toFullDigit(this.amount).toString()
454
+ );
455
+ }
456
+
457
+ /**
458
+ * Add trusted handlers
459
+ *
460
+ * Add trusted handlers that can freely transact with the contract and
461
+ * perform aborts and cancels for example.
462
+ *
463
+ * @param {string[]} handlers - Trusted handlers to add
464
+ * @returns {Promise<boolean>} - True if trusted handlers are added successfully.
465
+ */
466
+ async addTrustedHandlers(handlers: string[]): Promise<boolean> {
467
+ if (!this.contractData?.escrow) {
468
+ this._logError(ErrorJobNotLaunched);
469
+ return false;
470
+ }
471
+
472
+ const result = await this._raffleExecute(
473
+ this.contractData.escrow,
474
+ 'addTrustedHandlers',
475
+ handlers
476
+ );
477
+
478
+ if (!result) {
479
+ this._logError(new Error('Failed to add trusted handlers to the job'));
480
+ return false;
481
+ }
482
+
483
+ return result;
484
+ }
485
+
486
+ /**
487
+ * **Bulk payout**
488
+ *
489
+ * Payout the workers submitting the result.
490
+ *
491
+ * @param {Payout[]} payouts - Workers address & amount to payout
492
+ * @param {Result} result - Job result submitted
493
+ * @param {bool} encrypt - Whether to encrypt the result, or not
494
+ * @param {bool} isPublic - Whether to store data in public storage, or private.
495
+ * @returns {Promise<boolean>} - True if the workers are paid out successfully.
496
+ */
497
+ async bulkPayout(
498
+ payouts: Payout[],
499
+ result: Result,
500
+ encrypt = true,
501
+ isPublic = false
502
+ ): Promise<boolean> {
503
+ if (!this.providerData?.reputationOracle) {
504
+ this._logError(ErrorReputationOracleMissing);
505
+ return false;
506
+ }
507
+
508
+ if (!this.contractData?.escrow) {
509
+ this._logError(ErrorJobNotLaunched);
510
+ return false;
511
+ }
512
+
513
+ this._logger.info('Uploading result...');
514
+ const uploadResult = await this._upload(result, encrypt, isPublic);
515
+
516
+ if (!uploadResult) {
517
+ this._logError(new Error('Error uploading result'));
518
+ return false;
519
+ }
520
+
521
+ const { key, hash } = uploadResult;
522
+ this._logger.info(`Uploaded result.\n\tKey: ${key}\n\tHash: ${hash}`);
523
+
524
+ if (!this.storageAccessData) {
525
+ this._logError(ErrorStorageAccessDataMissing);
526
+ return false;
527
+ }
528
+
529
+ const url = isPublic ? getPublicURL(this.storageAccessData, key) : key;
530
+
531
+ this._logger.info('Bulk paying out the workers...');
532
+ await this._raffleExecute(
533
+ this.contractData.escrow,
534
+ 'bulkPayOut',
535
+ payouts.map(({ address }) => address),
536
+ payouts.map(({ amount }) => toFullDigit(amount)),
537
+ url,
538
+ hash,
539
+ 1
540
+ );
541
+
542
+ const bulkPaid = await this.contractData.escrow.bulkPaid();
543
+ if (!bulkPaid) {
544
+ this._logError(new Error('Failed to bulk payout users'));
545
+ return false;
546
+ }
547
+
548
+ this._logger.info('Workers are paid out.');
549
+
550
+ return bulkPaid;
551
+ }
552
+
553
+ /**
554
+ * **Abort the escrow**
555
+ *
556
+ * @returns {Promise<boolean>} - True if the escrow is aborted successfully.
557
+ */
558
+ async abort(): Promise<boolean> {
559
+ if (!this.contractData?.escrow) {
560
+ this._logError(ErrorJobNotLaunched);
561
+ return false;
562
+ }
563
+
564
+ this._logger.info('Aborting the job...');
565
+ const aborted = await this._raffleExecute(
566
+ this.contractData.escrow,
567
+ 'abort'
568
+ );
569
+
570
+ if (!aborted) {
571
+ this._logError(new Error('Failed to abort the job'));
572
+ return false;
573
+ }
574
+ this._logger.info('Job is aborted successfully.');
575
+
576
+ return aborted;
577
+ }
578
+
579
+ /**
580
+ * **Cancel the escrow**
581
+ *
582
+ * @returns {Promise<boolean>} - True if the escrow is cancelled successfully.
583
+ */
584
+ async cancel(): Promise<boolean> {
585
+ if (!this.contractData?.escrow) {
586
+ this._logError(ErrorJobNotLaunched);
587
+ return false;
588
+ }
589
+
590
+ this._logger.info('Cancelling the job...');
591
+ const cancelled = await this._raffleExecute(
592
+ this.contractData.escrow,
593
+ 'cancel'
594
+ );
595
+
596
+ if (!cancelled) {
597
+ this._logError(new Error('Failed to cancel the job'));
598
+ return false;
599
+ }
600
+ this._logger.info('Job is cancelled successfully.');
601
+
602
+ return (await this.status()) === EscrowStatus.Cancelled;
603
+ }
604
+
605
+ /**
606
+ * **Store intermediate result**
607
+ *
608
+ * Uploads intermediate result to the storage, and saves the URL/Hash on-chain.
609
+ *
610
+ * @param {Result} result - Intermediate result
611
+ * @returns {Promise<boolean>} - True if the intermediate result is stored successfully.
612
+ */
613
+ async storeIntermediateResults(result: Result): Promise<boolean> {
614
+ if (!this.providerData?.reputationOracle) {
615
+ this._logError(ErrorReputationOracleMissing);
616
+ return false;
617
+ }
618
+
619
+ if (!this.contractData?.escrow) {
620
+ this._logError(ErrorJobNotLaunched);
621
+ return false;
622
+ }
623
+
624
+ this._logger.info('Uploading intermediate result...');
625
+ const uploadResult = await this._upload(result);
626
+
627
+ if (!uploadResult) {
628
+ this._logError(new Error('Error uploading intermediate result'));
629
+ return false;
630
+ }
631
+
632
+ const { key, hash } = uploadResult;
633
+ this._logger.info(
634
+ `Uploaded intermediate result.\n\tKey: ${key}\n\tHash: ${hash}`
635
+ );
636
+
637
+ this._logger.info('Saving intermediate result on-chain...');
638
+ const resultStored = await this._raffleExecute(
639
+ this.contractData.escrow,
640
+ 'storeResults',
641
+ key,
642
+ hash
643
+ );
644
+
645
+ if (!resultStored) {
646
+ this._logError(new Error('Failed to store results'));
647
+ return false;
648
+ }
649
+ this._logger.info('Intermediate result is stored on-chain successfully.');
650
+
651
+ this.manifestData = {
652
+ ...this.manifestData,
653
+ intermediateResultLink: { url: key, hash },
654
+ };
655
+
656
+ return resultStored;
657
+ }
658
+
659
+ /**
660
+ * **Complete the escrow**
661
+ *
662
+ * @returns {Promise<boolean>} - True if the escrow if completed successfully.
663
+ */
664
+ async complete() {
665
+ if (!this.contractData?.escrow) {
666
+ this._logError(ErrorJobNotLaunched);
667
+ return false;
668
+ }
669
+
670
+ const completed = await this._raffleExecute(
671
+ this.contractData.escrow,
672
+ 'complete'
673
+ );
674
+
675
+ if (!completed) {
676
+ this._logError(new Error('Failed to complete the job'));
677
+ return false;
678
+ }
679
+
680
+ return (await this.status()) === EscrowStatus.Complete;
681
+ }
682
+
683
+ /**
684
+ * **Stake HMTokens**
685
+ *
686
+ * @param {number} amount - Amount to stake
687
+ * @param {string | undefined} from - Address to stake
688
+ * @returns {Promise<boolean>} - True if the token is staked
689
+ */
690
+ async stake(amount: number, from?: string) {
691
+ if (!this.contractData?.staking) {
692
+ this._logError(ErrorStakingMissing);
693
+ return false;
694
+ }
695
+ if (!this.contractData.hmToken) {
696
+ this._logError(ErrorHMTokenMissing);
697
+ return false;
698
+ }
699
+
700
+ const operator = this._findOperator(from);
701
+
702
+ if (!operator) {
703
+ this._logError(new Error('Unknown wallet'));
704
+ return false;
705
+ }
706
+
707
+ try {
708
+ const approved = await this.contractData.hmToken
709
+ .connect(operator)
710
+ .approve(this.contractData.staking.address, toFullDigit(amount));
711
+
712
+ if (!approved) {
713
+ throw new Error('Not approved');
714
+ }
715
+ } catch {
716
+ this._logError(new Error('Error approving HMTokens for staking'));
717
+ return false;
718
+ }
719
+
720
+ try {
721
+ await this.contractData.staking
722
+ .connect(operator)
723
+ .stake(toFullDigit(amount));
724
+ } catch {
725
+ this._logError(new Error(`Error executing transaction from ${from}`));
726
+ return false;
727
+ }
728
+ return true;
729
+ }
730
+
731
+ /**
732
+ * **Unstake HMTokens**
733
+ *
734
+ * @param {number} amount - Amount to unstake
735
+ * @param {string | undefined} from - Address to unstake
736
+ * @returns {Promise<boolean>} - True if the token is unstaked
737
+ */
738
+ async unstake(amount: number, from?: string) {
739
+ if (!this.contractData?.staking) {
740
+ this._logError(ErrorStakingMissing);
741
+ return false;
742
+ }
743
+
744
+ const operator = this._findOperator(from);
745
+
746
+ if (!operator) {
747
+ this._logError(new Error('Unknown wallet'));
748
+ return false;
749
+ }
750
+
751
+ try {
752
+ await this.contractData.staking
753
+ .connect(operator)
754
+ .unstake(toFullDigit(amount));
755
+ } catch {
756
+ this._logError(new Error(`Error executing transaction from ${from}`));
757
+ return false;
758
+ }
759
+ return true;
760
+ }
761
+
762
+ /**
763
+ * **Withdraw unstaked HMTokens**
764
+ *
765
+ * @param {string | undefined} from - Address to withdraw
766
+ * @returns {Promise<boolean>} - True if the token is withdrawn
767
+ */
768
+ async withdraw(from?: string) {
769
+ if (!this.contractData?.staking) {
770
+ this._logError(ErrorStakingMissing);
771
+ return false;
772
+ }
773
+
774
+ const operator = this._findOperator(from);
775
+
776
+ if (!operator) {
777
+ this._logError(new Error('Unknown wallet'));
778
+ return false;
779
+ }
780
+
781
+ try {
782
+ await this.contractData.staking.connect(operator).withdraw();
783
+ } catch {
784
+ this._logError(new Error(`Error executing transaction from ${from}`));
785
+ return false;
786
+ }
787
+ return true;
788
+ }
789
+
790
+ /**
791
+ * **Allocate HMTokens staked to the job**
792
+ *
793
+ * @param {number} amount - Amount to allocate
794
+ * @param {string | undefined} - Address to allocate with
795
+ * @returns {Promise<boolean>} - True if the token is allocated
796
+ */
797
+ async allocate(amount: number, from?: string) {
798
+ if (!this.contractData?.staking) {
799
+ this._logError(ErrorStakingMissing);
800
+ return false;
801
+ }
802
+
803
+ if (!this.contractData.escrowAddr) {
804
+ this._logError(ErrorJobNotLaunched);
805
+ return false;
806
+ }
807
+
808
+ const operator = this._findOperator(from);
809
+
810
+ if (!operator) {
811
+ this._logError(new Error('Unknown wallet'));
812
+ return false;
813
+ }
814
+
815
+ try {
816
+ await this.contractData.staking
817
+ .connect(operator)
818
+ .allocate(this.contractData.escrowAddr, toFullDigit(amount));
819
+ } catch {
820
+ this._logError(new Error(`Error executing transaction from ${from}`));
821
+ return false;
822
+ }
823
+ return true;
824
+ }
825
+
826
+ /**
827
+ * **Unallocate HMTokens from the job**
828
+ *
829
+ * @param {string | undefined} - Address to close allocation with
830
+ * @returns {Promise<boolean>} - True if the token is unallocated.
831
+ */
832
+ async closeAllocation(from?: string) {
833
+ if (!this.contractData?.staking) {
834
+ this._logError(ErrorStakingMissing);
835
+ return false;
836
+ }
837
+
838
+ if (!this.contractData.escrowAddr) {
839
+ this._logError(ErrorJobNotLaunched);
840
+ return false;
841
+ }
842
+
843
+ const operator = this._findOperator(from);
844
+
845
+ if (!operator) {
846
+ this._logError(new Error('Unknown wallet'));
847
+ return false;
848
+ }
849
+
850
+ try {
851
+ await this.contractData.staking
852
+ .connect(operator)
853
+ .closeAllocation(this.contractData.escrowAddr);
854
+ } catch {
855
+ this._logError(new Error(`Error executing transaction from ${from}`));
856
+ return false;
857
+ }
858
+ return true;
859
+ }
860
+
861
+ /**
862
+ * **Get current status of the escrow**
863
+ *
864
+ * @returns {Promise<EscrowStatus | undefined>} - Status of the escrow
865
+ */
866
+ async status(): Promise<EscrowStatus | undefined> {
867
+ if (!this.contractData?.escrow) {
868
+ this._logError(ErrorJobNotLaunched);
869
+ return undefined;
870
+ }
871
+
872
+ return (await this.contractData.escrow.status()) as EscrowStatus;
873
+ }
874
+
875
+ /**
876
+ * **Get current balance of the escrow**
877
+ *
878
+ * @returns {Promise<BigNumber | undefined>} - Balance of the escrow
879
+ */
880
+ async balance(): Promise<BigNumber | undefined> {
881
+ if (!this.contractData?.escrow) {
882
+ this._logError(ErrorJobNotLaunched);
883
+ return undefined;
884
+ }
885
+
886
+ return await this.contractData.escrow.getBalance();
887
+ }
888
+
889
+ /**
890
+ * **Get intermediate result stored**
891
+ *
892
+ * @returns {Promise<Result | undefined>} - Intermediate result
893
+ */
894
+ async intermediateResults(): Promise<Result | undefined> {
895
+ if (!this.manifestData?.intermediateResultLink) {
896
+ this._logError(new Error('Intermediate result is missing.'));
897
+ return undefined;
898
+ }
899
+
900
+ return this._download(this.manifestData.intermediateResultLink.url);
901
+ }
902
+
903
+ /**
904
+ * **Get final result stored**
905
+ *
906
+ * @returns {Promise<Result | undefined>} - Final result
907
+ */
908
+ async finalResults(): Promise<Result | undefined> {
909
+ if (!this.contractData?.escrow) {
910
+ this._logError(ErrorJobNotLaunched);
911
+ return undefined;
912
+ }
913
+
914
+ const finalResultsURL = await this.contractData?.escrow?.finalResultsUrl();
915
+
916
+ if (!finalResultsURL) {
917
+ return undefined;
918
+ }
919
+
920
+ const key = getKeyFromURL(finalResultsURL);
921
+
922
+ return await this._download(key);
923
+ }
924
+
925
+ /**
926
+ * **Check if handler is trusted**
927
+ *
928
+ * @param {string} handlerAddr Address of the handler
929
+ * @returns {Promise<boolean>} - True if the handler is trusted
930
+ */
931
+ async isTrustedHandler(handlerAddr: string): Promise<boolean> {
932
+ if (!this.contractData?.escrow) {
933
+ this._logError(ErrorJobNotLaunched);
934
+ return false;
935
+ }
936
+
937
+ return await this.contractData?.escrow?.areTrustedHandlers(handlerAddr);
938
+ }
939
+
940
+ /**
941
+ * **Download result from cloud storage**
942
+ *
943
+ * @param {string | undefined} url - Result URL to download
944
+ * @returns {Result | undefined} - Downloaded result
945
+ */
946
+ private async _download(url?: string): Promise<Result | undefined> {
947
+ if (!url || !this.providerData?.reputationOracle) {
948
+ return undefined;
949
+ }
950
+
951
+ if (!this.storageAccessData) {
952
+ this._logError(ErrorStorageAccessDataMissing);
953
+ return undefined;
954
+ }
955
+
956
+ return await download(
957
+ this.storageAccessData,
958
+ url,
959
+ this.providerData?.reputationOracle.privateKey
960
+ );
961
+ }
962
+
963
+ /**
964
+ * **Uploads result to cloud storage**
965
+ *
966
+ * @param {Result} result - Result to upload
967
+ * @param {boolean} encrypt - Whether to encrypt result, or not.
968
+ * @param {bool} isPublic - Whether to store data in public storage, or private.
969
+ * @returns {Promise<UploadResult | undefined>} - Uploaded result
970
+ */
971
+ private async _upload(
972
+ result: Result,
973
+ encrypt = true,
974
+ isPublic = false
975
+ ): Promise<UploadResult | undefined> {
976
+ if (!this.providerData?.reputationOracle) {
977
+ this._logError(ErrorReputationOracleMissing);
978
+ return undefined;
979
+ }
980
+
981
+ if (!this.storageAccessData) {
982
+ this._logError(ErrorStorageAccessDataMissing);
983
+ return undefined;
984
+ }
985
+
986
+ return await upload(
987
+ this.storageAccessData,
988
+ result,
989
+ this.providerData?.reputationOracle?.publicKey,
990
+ encrypt,
991
+ isPublic
992
+ );
993
+ }
994
+
995
+ /**
996
+ * **Raffle executes on-chain call**
997
+ *
998
+ * Try to execute the on-chain call from all possible trusted handlers
999
+ *
1000
+ * @param {Function} txn - On-chain call to execute
1001
+ * @param {any} args - On-chain call arguments
1002
+ * @returns {Promise<boolean>} - True if one of handler succeed to execute the on-chain call
1003
+ */
1004
+ private async _raffleExecute(
1005
+ contract: Contract,
1006
+ functionName: string,
1007
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1008
+ ...args: any
1009
+ ): Promise<boolean> {
1010
+ try {
1011
+ if (this.providerData?.gasPayer) {
1012
+ await contract
1013
+ .connect(this.providerData.gasPayer)
1014
+ .functions[functionName](...args);
1015
+ return true;
1016
+ }
1017
+ this._logger.info(
1018
+ 'Default gas payer is missing, trying with other trusted handlers...'
1019
+ );
1020
+ } catch (err) {
1021
+ this._logger.info(
1022
+ 'Error executing the transaction from default gas payer, trying with other trusted handlers...'
1023
+ );
1024
+ }
1025
+
1026
+ for (const trustedHandler of this.providerData?.trustedHandlers || []) {
1027
+ try {
1028
+ await contract.connect(trustedHandler).functions[functionName](...args);
1029
+ return true;
1030
+ } catch (err) {
1031
+ this._logError(
1032
+ new Error(
1033
+ 'Error executing the transaction from all of the trusted handlers. Stop continue executing...'
1034
+ )
1035
+ );
1036
+ }
1037
+ }
1038
+ return false;
1039
+ }
1040
+
1041
+ /**
1042
+ * **Error log**
1043
+ *
1044
+ * @param {Error} error - Occured error
1045
+ */
1046
+ private _logError(error: Error) {
1047
+ this._logger.error(error.message);
1048
+ }
1049
+
1050
+ /**
1051
+ * **Find operator to execute tx**
1052
+ *
1053
+ * @param {string} addr - Address of the operator
1054
+ * @returns {ethers.Wallet | undefined} - Operator wallet
1055
+ */
1056
+ private _findOperator(addr?: string): ethers.Wallet | undefined {
1057
+ return addr
1058
+ ? [
1059
+ this.providerData?.gasPayer,
1060
+ ...(this.providerData?.trustedHandlers || []),
1061
+ ].find((account?: ethers.Wallet) => account?.address === addr)
1062
+ : this.providerData?.gasPayer;
1063
+ }
1064
+ }