@human-protocol/sdk 1.0.0 → 1.0.1

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/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # Human Protocol Node.js SDK
2
+
3
+ Node.js SDK to launch/manage jobs on [Human Protocol](https://www.humanprotocol.org/)
4
+
5
+ ## Installation
6
+
7
+ This SDK is available on [NPM](https://www.npmjs.com/package/@human-protocol/sdk).
8
+
9
+ yarn @human-protocol/sdk
@@ -0,0 +1,86 @@
1
+ /* eslint-disable no-console */
2
+ import { Job } from '../src';
3
+ import {
4
+ DEFAULT_GAS_PAYER_PRIVKEY,
5
+ DEFAULT_HMTOKEN_ADDR,
6
+ REPUTATION_ORACLE_PRIVKEY,
7
+ WORKER1_ADDR,
8
+ WORKER2_ADDR,
9
+ } from '../test/utils/constants';
10
+ import { manifest } from '../test/utils/manifest';
11
+ import * as dotenv from 'dotenv';
12
+
13
+ dotenv.config();
14
+
15
+ const main = async () => {
16
+ // Create job object
17
+ const newJob = new Job({
18
+ gasPayer: DEFAULT_GAS_PAYER_PRIVKEY,
19
+ reputationOracle: REPUTATION_ORACLE_PRIVKEY,
20
+ manifest: manifest,
21
+ hmTokenAddr: DEFAULT_HMTOKEN_ADDR,
22
+ logLevel: 'debug',
23
+ });
24
+
25
+ // Initialize new job object
26
+ await newJob.initialize();
27
+
28
+ // Launch the job
29
+ await newJob.launch();
30
+
31
+ // Access the existing job
32
+ const job = new Job({
33
+ gasPayer: DEFAULT_GAS_PAYER_PRIVKEY,
34
+ reputationOracle: REPUTATION_ORACLE_PRIVKEY,
35
+ manifest: manifest,
36
+ hmTokenAddr: DEFAULT_HMTOKEN_ADDR,
37
+ factoryAddr: newJob.contractData?.factoryAddr,
38
+ escrowAddr: newJob.contractData?.escrowAddr,
39
+ storageAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
40
+ storageSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
41
+ storageEndpoint: process.env.AWS_ENDPOINT,
42
+ storageBucket: process.env.AWS_BUCKET,
43
+ storagePublicBucket: process.env.AWS_PUBLIC_BUCKET,
44
+ logLevel: 'debug',
45
+ });
46
+
47
+ // Initialize the job object
48
+ await job.initialize();
49
+
50
+ // Setup the job
51
+ await job.setup();
52
+
53
+ console.log(
54
+ `Status: ${await job.status()}, Balance: ${(
55
+ await job.balance()
56
+ )?.toString()}`
57
+ );
58
+
59
+ // Bulk payout workers
60
+ await job.bulkPayout(
61
+ [
62
+ {
63
+ address: WORKER1_ADDR,
64
+ amount: 70,
65
+ },
66
+ {
67
+ address: WORKER2_ADDR,
68
+ amount: 30,
69
+ },
70
+ ],
71
+ {
72
+ result: 'result',
73
+ }
74
+ );
75
+
76
+ // Complete the job
77
+ await job.complete();
78
+
79
+ console.log(
80
+ `Status: ${await job.status()}, Balance: ${(
81
+ await job.balance()
82
+ )?.toString()}`
83
+ );
84
+ };
85
+
86
+ main();
@@ -0,0 +1,74 @@
1
+ /* eslint-disable no-console */
2
+ import { Job } from '../src';
3
+ import {
4
+ DEFAULT_GAS_PAYER_PRIVKEY,
5
+ DEFAULT_HMTOKEN_ADDR,
6
+ REPUTATION_ORACLE_PRIVKEY,
7
+ WORKER1_ADDR,
8
+ WORKER2_ADDR,
9
+ } from '../test/utils/constants';
10
+ import { manifest } from '../test/utils/manifest';
11
+ import * as dotenv from 'dotenv';
12
+
13
+ dotenv.config();
14
+
15
+ const main = async () => {
16
+ // Create job object
17
+ const job = new Job({
18
+ gasPayer: DEFAULT_GAS_PAYER_PRIVKEY,
19
+ reputationOracle: REPUTATION_ORACLE_PRIVKEY,
20
+ manifest: manifest,
21
+ hmTokenAddr: DEFAULT_HMTOKEN_ADDR,
22
+ storageAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
23
+ storageSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
24
+ storageEndpoint: process.env.AWS_ENDPOINT,
25
+ storageBucket: process.env.AWS_BUCKET,
26
+ storagePublicBucket: process.env.AWS_PUBLIC_BUCKET,
27
+ logLevel: 'debug',
28
+ });
29
+
30
+ // Initialize new job
31
+ await job.initialize();
32
+
33
+ // Launch the job
34
+ await job.launch();
35
+
36
+ // Setup the job
37
+ await job.setup();
38
+
39
+ console.log(
40
+ `Status: ${await job.status()}, Balance: ${(
41
+ await job.balance()
42
+ )?.toString()}`
43
+ );
44
+
45
+ // Bulk payout workers
46
+ await job.bulkPayout(
47
+ [
48
+ {
49
+ address: WORKER1_ADDR,
50
+ amount: 70,
51
+ },
52
+ {
53
+ address: WORKER2_ADDR,
54
+ amount: 30,
55
+ },
56
+ ],
57
+ {
58
+ result: 'result',
59
+ },
60
+ false,
61
+ true
62
+ );
63
+
64
+ // Complete the job
65
+ await job.complete();
66
+
67
+ console.log(
68
+ `Status: ${await job.status()}, Balance: ${(
69
+ await job.balance()
70
+ )?.toString()}`
71
+ );
72
+ };
73
+
74
+ main();
@@ -0,0 +1,72 @@
1
+ /* eslint-disable no-console */
2
+ import { Job } from '../src';
3
+ import {
4
+ DEFAULT_GAS_PAYER_PRIVKEY,
5
+ DEFAULT_HMTOKEN_ADDR,
6
+ REPUTATION_ORACLE_PRIVKEY,
7
+ WORKER1_ADDR,
8
+ WORKER2_ADDR,
9
+ } from '../test/utils/constants';
10
+ import { manifest } from '../test/utils/manifest';
11
+ import * as dotenv from 'dotenv';
12
+
13
+ dotenv.config();
14
+
15
+ const main = async () => {
16
+ // Create job object
17
+ const job = new Job({
18
+ gasPayer: DEFAULT_GAS_PAYER_PRIVKEY,
19
+ reputationOracle: REPUTATION_ORACLE_PRIVKEY,
20
+ manifest: manifest,
21
+ hmTokenAddr: DEFAULT_HMTOKEN_ADDR,
22
+ storageAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
23
+ storageSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
24
+ storageEndpoint: process.env.AWS_ENDPOINT,
25
+ storageBucket: process.env.AWS_BUCKET,
26
+ storagePublicBucket: process.env.AWS_PUBLIC_BUCKET,
27
+ logLevel: 'debug',
28
+ });
29
+
30
+ // Initialize new job
31
+ await job.initialize();
32
+
33
+ // Launch the job
34
+ await job.launch();
35
+
36
+ // Setup the job
37
+ await job.setup();
38
+
39
+ console.log(
40
+ `Status: ${await job.status()}, Balance: ${(
41
+ await job.balance()
42
+ )?.toString()}`
43
+ );
44
+
45
+ // Bulk payout workers
46
+ await job.bulkPayout(
47
+ [
48
+ {
49
+ address: WORKER1_ADDR,
50
+ amount: 70,
51
+ },
52
+ {
53
+ address: WORKER2_ADDR,
54
+ amount: 30,
55
+ },
56
+ ],
57
+ {
58
+ result: 'result',
59
+ }
60
+ );
61
+
62
+ // Complete the job
63
+ await job.complete();
64
+
65
+ console.log(
66
+ `Status: ${await job.status()}, Balance: ${(
67
+ await job.balance()
68
+ )?.toString()}`
69
+ );
70
+ };
71
+
72
+ main();
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@human-protocol/sdk",
3
3
  "description": "Human Protocol SDK",
4
- "version": "1.0.0",
4
+ "version": "1.0.1",
5
5
  "files": [
6
6
  "src",
7
- "dist"
7
+ "dist",
8
+ "example",
9
+ "test"
8
10
  ],
9
11
  "main": "dist/index.js",
10
12
  "types": "dist/index.d.ts",
@@ -36,12 +38,15 @@
36
38
  ]
37
39
  },
38
40
  "dependencies": {
39
- "@human-protocol/core": "workspace:*",
41
+ "@human-protocol/core": "^1.0.9",
40
42
  "aws-sdk": "^2.1255.0",
41
43
  "crypto": "^1.0.1",
42
44
  "dotenv": "^16.0.3",
43
45
  "ethers": "^5.7.2",
44
46
  "secp256k1": "^4.0.3",
45
47
  "winston": "^3.8.2"
48
+ },
49
+ "peerDependencies": {
50
+ "@human-protocol/core": "^1.0.9"
46
51
  }
47
52
  }
package/src/job.ts CHANGED
@@ -142,8 +142,8 @@ export class Job {
142
142
  accessKeyId: storageAccessKeyId || '',
143
143
  secretAccessKey: storageSecretAccessKey || '',
144
144
  endpoint: storageEndpoint,
145
- publicBucket: storagePublicBucket || DEFAULT_BUCKET,
146
- bucket: storageBucket || DEFAULT_PUBLIC_BUCKET,
145
+ publicBucket: storagePublicBucket || DEFAULT_PUBLIC_BUCKET,
146
+ bucket: storageBucket || DEFAULT_BUCKET,
147
147
  };
148
148
 
149
149
  this._logger = createLogger(logLevel);
@@ -204,6 +204,8 @@ export class Job {
204
204
 
205
205
  if (!hasEscrow) {
206
206
  this._logError(new Error('Factory does not contain the escrow'));
207
+ this.contractData.factory = undefined;
208
+
207
209
  return false;
208
210
  }
209
211
 
@@ -214,17 +216,34 @@ export class Job {
214
216
  );
215
217
  this._logger.info('Accessed the escrow successfully.');
216
218
 
217
- this.manifestData = {
218
- ...this.manifestData,
219
- manifestlink: {
220
- url: await this.contractData?.escrow.manifestUrl(),
221
- hash: await this.contractData?.escrow.manifestHash(),
222
- },
223
- };
219
+ const manifestUrl = await this.contractData?.escrow.manifestUrl();
220
+ const manifestHash = await this.contractData?.escrow.manifestHash();
221
+
222
+ if (
223
+ (!manifestUrl.length || !manifestHash.length) &&
224
+ !this.manifestData?.manifest
225
+ ) {
226
+ this._logError(ErrorManifestMissing);
227
+
228
+ this.contractData.factory = undefined;
229
+ this.contractData.escrow = undefined;
230
+
231
+ return false;
232
+ }
224
233
 
225
- this.manifestData.manifest = (await this._download(
226
- this.manifestData.manifestlink?.url
227
- )) as Manifest;
234
+ if (manifestUrl.length && manifestHash.length) {
235
+ this.manifestData = {
236
+ ...this.manifestData,
237
+ manifestlink: {
238
+ url: manifestUrl,
239
+ hash: manifestHash,
240
+ },
241
+ };
242
+
243
+ this.manifestData.manifest = (await this._download(
244
+ manifestUrl
245
+ )) as Manifest;
246
+ }
228
247
  }
229
248
 
230
249
  return true;
@@ -238,18 +257,13 @@ export class Job {
238
257
  * @returns {Promise<boolean>} - True if the escrow is launched successfully.
239
258
  */
240
259
  async launch(): Promise<boolean> {
241
- if (!this.contractData || this.contractData.escrow) {
242
- this._logError(ErrorJobAlreadyLaunched);
243
- return false;
244
- }
245
-
246
260
  if (!this.contractData || !this.contractData.factory) {
247
261
  this._logError(ErrorJobNotInitialized);
248
262
  return false;
249
263
  }
250
264
 
251
- if (!this.manifestData || !this.manifestData.manifest) {
252
- this._logError(ErrorManifestMissing);
265
+ if (!this.contractData || this.contractData.escrow) {
266
+ this._logError(ErrorJobAlreadyLaunched);
253
267
  return false;
254
268
  }
255
269
 
@@ -281,21 +295,6 @@ export class Job {
281
295
  this.providerData?.gasPayer
282
296
  );
283
297
 
284
- this._logger.info('Uploading manifest...');
285
- const uploadResult = await this._upload(this.manifestData.manifest);
286
- if (!uploadResult) {
287
- this._logError(new Error('Error uploading manifest'));
288
- return false;
289
- }
290
-
291
- this.manifestData.manifestlink = {
292
- url: uploadResult.key,
293
- hash: uploadResult.hash,
294
- };
295
- this._logger.info(
296
- `Uploaded manifest.\n\tKey: ${uploadResult.key}\n\tHash: ${uploadResult.hash}`
297
- );
298
-
299
298
  return (
300
299
  (await this.status()) == EscrowStatus.Launched &&
301
300
  (await this.balance())?.toNumber() === 0
@@ -316,6 +315,16 @@ export class Job {
316
315
  return false;
317
316
  }
318
317
 
318
+ if (!this.manifestData || !this.manifestData.manifest) {
319
+ this._logError(ErrorManifestMissing);
320
+ return false;
321
+ }
322
+
323
+ if (this.manifestData.manifestlink) {
324
+ this._logError(new Error('Job is already setup'));
325
+ return false;
326
+ }
327
+
319
328
  if (!this.contractData.hmToken) {
320
329
  this._logError(ErrorHMTokenMissing);
321
330
  return false;
@@ -358,6 +367,21 @@ export class Job {
358
367
  }
359
368
  this._logger.info('HMT transferred.');
360
369
 
370
+ this._logger.info('Uploading manifest...');
371
+ const uploadResult = await this._upload(this.manifestData.manifest);
372
+ if (!uploadResult) {
373
+ this._logError(new Error('Error uploading manifest'));
374
+ return false;
375
+ }
376
+
377
+ this.manifestData.manifestlink = {
378
+ url: uploadResult.key,
379
+ hash: uploadResult.hash,
380
+ };
381
+ this._logger.info(
382
+ `Uploaded manifest.\n\tKey: ${uploadResult.key}\n\tHash: ${uploadResult.hash}`
383
+ );
384
+
361
385
  this._logger.info('Setting up the escrow...');
362
386
  const contractSetup = await this._raffleExecute(
363
387
  this.contractData.escrow,
package/src/storage.ts CHANGED
@@ -128,7 +128,7 @@ export const upload = async (
128
128
  Body: content,
129
129
  };
130
130
 
131
- await s3.putObject(params);
131
+ await s3.putObject(params).promise();
132
132
 
133
133
  return { key, hash };
134
134
  };
@@ -0,0 +1,753 @@
1
+ import { getPublicURL } from './../src/storage';
2
+ import { EscrowStatus, Job } from '../src';
3
+ import { upload } from '../src/storage';
4
+ import { toFullDigit } from '../src/utils';
5
+ import {
6
+ DEFAULT_GAS_PAYER_ADDR,
7
+ DEFAULT_GAS_PAYER_PRIVKEY,
8
+ DEFAULT_HMTOKEN_ADDR,
9
+ NOT_TRUSTED_OPERATOR_PRIVKEY,
10
+ REPUTATION_ORACLE_PRIVKEY,
11
+ TRUSTED_OPERATOR1_ADDR,
12
+ TRUSTED_OPERATOR1_PRIVKEY,
13
+ TRUSTED_OPERATOR2_ADDR,
14
+ WORKER1_ADDR,
15
+ WORKER2_ADDR,
16
+ WORKER3_ADDR,
17
+ } from './utils/constants';
18
+ import { manifest } from './utils/manifest';
19
+
20
+ jest.mock('../src/storage', () => ({
21
+ ...jest.requireActual('../src/storage'),
22
+ upload: jest.fn().mockResolvedValue({
23
+ key: 'uploaded-key',
24
+ hash: 'uploaded-hash',
25
+ }),
26
+ download: jest.fn().mockResolvedValue({
27
+ results: 0,
28
+ }),
29
+ getPublicURL: jest.fn().mockResolvedValue('public-url'),
30
+ }));
31
+
32
+ describe('Test Job', () => {
33
+ describe('New job', () => {
34
+ let job: Job;
35
+
36
+ beforeEach(() => {
37
+ job = new Job({
38
+ gasPayer: DEFAULT_GAS_PAYER_PRIVKEY,
39
+ reputationOracle: REPUTATION_ORACLE_PRIVKEY,
40
+ manifest: manifest,
41
+ hmTokenAddr: DEFAULT_HMTOKEN_ADDR,
42
+ logLevel: 'debug',
43
+ });
44
+ });
45
+
46
+ afterEach(() => {
47
+ jest.clearAllMocks();
48
+ });
49
+
50
+ it('Should be able to initializes the job by deploying escrow factory', async () => {
51
+ const initialized = await job.initialize();
52
+ expect(initialized).toBe(true);
53
+
54
+ expect(await job.contractData?.factory?.address).not.toBeNull();
55
+ });
56
+
57
+ it('Should be able to launch the job', async () => {
58
+ // Fail to launch the job before initialization
59
+ expect(await job.launch()).toBe(false);
60
+
61
+ await job.initialize();
62
+
63
+ expect(await job.launch()).toBe(true);
64
+ expect(await job.status()).toBe(EscrowStatus.Launched);
65
+ });
66
+
67
+ it('Should be able to setup the job', async () => {
68
+ // Fail to setup the job before launch
69
+ expect(await job.setup()).toBe(false);
70
+
71
+ await job.initialize();
72
+ await job.launch();
73
+
74
+ expect(await job.setup()).toBe(true);
75
+ });
76
+
77
+ it('Should be able to add trusted handlers', async () => {
78
+ await job.initialize();
79
+ await job.launch();
80
+
81
+ expect(await job.isTrustedHandler(DEFAULT_GAS_PAYER_ADDR)).toBe(true);
82
+
83
+ expect(
84
+ await job.addTrustedHandlers([
85
+ TRUSTED_OPERATOR1_ADDR,
86
+ TRUSTED_OPERATOR2_ADDR,
87
+ ])
88
+ ).toBe(true);
89
+
90
+ expect(await job.isTrustedHandler(TRUSTED_OPERATOR1_ADDR)).toBe(true);
91
+ expect(await job.isTrustedHandler(TRUSTED_OPERATOR2_ADDR)).toBe(true);
92
+ });
93
+
94
+ it('Should be able to bulk payout workers', async () => {
95
+ await job.initialize();
96
+ await job.launch();
97
+ await job.setup();
98
+
99
+ expect(
100
+ await job.bulkPayout(
101
+ [
102
+ {
103
+ address: WORKER1_ADDR,
104
+ amount: 20,
105
+ },
106
+ {
107
+ address: WORKER2_ADDR,
108
+ amount: 50,
109
+ },
110
+ ],
111
+ {}
112
+ )
113
+ ).toBe(true);
114
+
115
+ // The escrow contract is still in Partial state as there's still balance left.
116
+ expect((await job.balance())?.toString()).toBe(
117
+ toFullDigit(30).toString()
118
+ );
119
+ expect(await job.status()).toBe(EscrowStatus.Partial);
120
+
121
+ // Trying to pay more than the contract balance results in failure.
122
+ expect(
123
+ await job.bulkPayout(
124
+ [
125
+ {
126
+ address: WORKER3_ADDR,
127
+ amount: 50,
128
+ },
129
+ ],
130
+ {}
131
+ )
132
+ ).toBe(false);
133
+
134
+ // Paying the remaining amount empties the escrow and updates the status correctly.
135
+ expect(
136
+ await job.bulkPayout(
137
+ [
138
+ {
139
+ address: WORKER3_ADDR,
140
+ amount: 30,
141
+ },
142
+ ],
143
+ {}
144
+ )
145
+ ).toBe(true);
146
+ expect((await job.balance())?.toString()).toBe(toFullDigit(0).toString());
147
+ expect(await job.status()).toBe(EscrowStatus.Paid);
148
+ });
149
+
150
+ it('Should encrypt result, when bulk paying out workers', async () => {
151
+ await job.initialize();
152
+ await job.launch();
153
+ await job.setup();
154
+
155
+ jest.clearAllMocks();
156
+ const finalResults = { results: 0 };
157
+ await job.bulkPayout(
158
+ [
159
+ {
160
+ address: WORKER1_ADDR,
161
+ amount: 100,
162
+ },
163
+ ],
164
+ finalResults,
165
+ true
166
+ );
167
+
168
+ expect(upload).toHaveBeenCalledWith(
169
+ job.storageAccessData,
170
+ finalResults,
171
+ job.providerData?.reputationOracle?.publicKey,
172
+ true,
173
+ false
174
+ );
175
+ expect(upload).toHaveBeenCalledTimes(1);
176
+ });
177
+
178
+ it('Should not encrypt result, when bulk paying out workers', async () => {
179
+ await job.initialize();
180
+ await job.launch();
181
+ await job.setup();
182
+
183
+ jest.clearAllMocks();
184
+ const finalResults = { results: 0 };
185
+ await job.bulkPayout(
186
+ [
187
+ {
188
+ address: WORKER1_ADDR,
189
+ amount: 100,
190
+ },
191
+ ],
192
+ finalResults,
193
+ false
194
+ );
195
+
196
+ expect(upload).toHaveBeenCalledWith(
197
+ job.storageAccessData,
198
+ finalResults,
199
+ job.providerData?.reputationOracle?.publicKey,
200
+ false,
201
+ false
202
+ );
203
+ expect(upload).toHaveBeenCalledTimes(1);
204
+ });
205
+
206
+ it('Should store result in private storage, when bulk paying out workers', async () => {
207
+ await job.initialize();
208
+ await job.launch();
209
+ await job.setup();
210
+
211
+ jest.clearAllMocks();
212
+ const finalResults = { results: 0 };
213
+ await job.bulkPayout(
214
+ [
215
+ {
216
+ address: WORKER1_ADDR,
217
+ amount: 100,
218
+ },
219
+ ],
220
+ finalResults,
221
+ false,
222
+ false
223
+ );
224
+
225
+ expect(upload).toHaveBeenCalledWith(
226
+ job.storageAccessData,
227
+ finalResults,
228
+ job.providerData?.reputationOracle?.publicKey,
229
+ false,
230
+ false
231
+ );
232
+ expect(upload).toHaveBeenCalledTimes(1);
233
+ });
234
+
235
+ it('Should store result in public storage, when bulk paying out workers', async () => {
236
+ await job.initialize();
237
+ await job.launch();
238
+ await job.setup();
239
+
240
+ jest.clearAllMocks();
241
+ const finalResults = { results: 0 };
242
+ await job.bulkPayout(
243
+ [
244
+ {
245
+ address: WORKER1_ADDR,
246
+ amount: 50,
247
+ },
248
+ ],
249
+ finalResults,
250
+ false,
251
+ true
252
+ );
253
+
254
+ expect(upload).toHaveBeenCalledWith(
255
+ job.storageAccessData,
256
+ finalResults,
257
+ job.providerData?.reputationOracle?.publicKey,
258
+ false,
259
+ true
260
+ );
261
+ expect(upload).toHaveBeenCalledTimes(1);
262
+ expect(getPublicURL).toHaveBeenCalledTimes(1);
263
+ });
264
+
265
+ it('Should return final result', async () => {
266
+ await job.initialize();
267
+ await job.launch();
268
+ await job.setup();
269
+
270
+ const finalResults = { results: 0 };
271
+ await job.bulkPayout(
272
+ [
273
+ {
274
+ address: WORKER1_ADDR,
275
+ amount: 100,
276
+ },
277
+ ],
278
+ finalResults,
279
+ true
280
+ );
281
+
282
+ expect(JSON.stringify(await job.finalResults())).toBe(
283
+ JSON.stringify(finalResults)
284
+ );
285
+ });
286
+
287
+ it('Should be able to abort the job', async () => {
288
+ await job.initialize();
289
+ await job.launch();
290
+ await job.setup();
291
+
292
+ expect(await job.abort()).toBe(true);
293
+ });
294
+
295
+ it('Should be able to abort partially paid job', async () => {
296
+ await job.initialize();
297
+ await job.launch();
298
+ await job.setup();
299
+
300
+ const finalResults = { results: 0 };
301
+ await job.bulkPayout(
302
+ [
303
+ {
304
+ address: WORKER1_ADDR,
305
+ amount: 50,
306
+ },
307
+ ],
308
+ finalResults,
309
+ true
310
+ );
311
+
312
+ expect(await job.abort()).toBe(true);
313
+ });
314
+
315
+ it('Should not be able to abort fully paid job', async () => {
316
+ await job.initialize();
317
+ await job.launch();
318
+ await job.setup();
319
+
320
+ const finalResults = { results: 0 };
321
+ await job.bulkPayout(
322
+ [
323
+ {
324
+ address: WORKER1_ADDR,
325
+ amount: 100,
326
+ },
327
+ ],
328
+ finalResults,
329
+ true
330
+ );
331
+
332
+ expect(await job.abort()).toBe(false);
333
+ });
334
+
335
+ it('Should be able to cancel the job', async () => {
336
+ await job.initialize();
337
+ await job.launch();
338
+ await job.setup();
339
+
340
+ expect(await job.cancel()).toBe(true);
341
+ expect((await job.balance())?.toString()).toBe(toFullDigit(0).toString());
342
+ });
343
+
344
+ it('Should be able to cancel partially paid job', async () => {
345
+ await job.initialize();
346
+ await job.launch();
347
+ await job.setup();
348
+
349
+ const finalResults = { results: 0 };
350
+ await job.bulkPayout(
351
+ [
352
+ {
353
+ address: WORKER1_ADDR,
354
+ amount: 50,
355
+ },
356
+ ],
357
+ finalResults,
358
+ true
359
+ );
360
+
361
+ expect(await job.cancel()).toBe(true);
362
+ expect((await job.balance())?.toString()).toBe(toFullDigit(0).toString());
363
+ });
364
+
365
+ it('Should not be able to cancel paid job', async () => {
366
+ await job.initialize();
367
+ await job.launch();
368
+ await job.setup();
369
+
370
+ const finalResults = { results: 0 };
371
+ await job.bulkPayout(
372
+ [
373
+ {
374
+ address: WORKER1_ADDR,
375
+ amount: 100,
376
+ },
377
+ ],
378
+ finalResults,
379
+ true
380
+ );
381
+
382
+ expect(await job.cancel()).toBe(false);
383
+ });
384
+ });
385
+
386
+ describe('Access existing job from trusted handler', () => {
387
+ let job: Job;
388
+
389
+ beforeEach(async () => {
390
+ const originalJob = new Job({
391
+ gasPayer: DEFAULT_GAS_PAYER_PRIVKEY,
392
+ reputationOracle: REPUTATION_ORACLE_PRIVKEY,
393
+ manifest: manifest,
394
+ hmTokenAddr: DEFAULT_HMTOKEN_ADDR,
395
+ trustedHandlers: [TRUSTED_OPERATOR1_PRIVKEY],
396
+ logLevel: 'error',
397
+ });
398
+
399
+ await originalJob.initialize();
400
+ await originalJob.launch();
401
+ await originalJob.setup();
402
+
403
+ job = new Job({
404
+ gasPayer: NOT_TRUSTED_OPERATOR_PRIVKEY,
405
+ hmTokenAddr: DEFAULT_HMTOKEN_ADDR,
406
+ reputationOracle: REPUTATION_ORACLE_PRIVKEY,
407
+ escrowAddr: originalJob.contractData?.escrowAddr,
408
+ factoryAddr: originalJob.contractData?.factoryAddr,
409
+ trustedHandlers: [TRUSTED_OPERATOR1_PRIVKEY],
410
+ logLevel: 'debug',
411
+ });
412
+ });
413
+
414
+ afterEach(() => {
415
+ jest.clearAllMocks();
416
+ });
417
+
418
+ it('Should be able to initializes the job by accessing existing escrow', async () => {
419
+ const initialized = await job.initialize();
420
+ expect(initialized).toBe(true);
421
+
422
+ expect(await job.manifestData?.manifestlink?.url).toBe('uploaded-key');
423
+ expect(await job.manifestData?.manifestlink?.hash).toBe('uploaded-hash');
424
+ });
425
+
426
+ it('Should not be able to launch the job again', async () => {
427
+ await job.initialize();
428
+
429
+ expect(await job.launch()).toBe(false);
430
+ expect(await job.status()).toBe(EscrowStatus.Pending);
431
+ });
432
+
433
+ it('Should be able to setup the job again', async () => {
434
+ await job.initialize();
435
+
436
+ expect(await job.setup()).toBe(false);
437
+
438
+ expect((await job.balance())?.toString()).toBe(
439
+ toFullDigit(100).toString()
440
+ );
441
+ expect(await job.manifestData?.manifestlink?.url).toBe('uploaded-key');
442
+ expect(await job.manifestData?.manifestlink?.hash).toBe('uploaded-hash');
443
+ });
444
+
445
+ it('Should be able to add trusted handlers', async () => {
446
+ await job.initialize();
447
+ await job.launch();
448
+
449
+ expect(await job.isTrustedHandler(DEFAULT_GAS_PAYER_ADDR)).toBe(true);
450
+
451
+ expect(
452
+ await job.addTrustedHandlers([
453
+ TRUSTED_OPERATOR1_ADDR,
454
+ TRUSTED_OPERATOR2_ADDR,
455
+ ])
456
+ ).toBe(true);
457
+
458
+ expect(await job.isTrustedHandler(TRUSTED_OPERATOR1_ADDR)).toBe(true);
459
+ expect(await job.isTrustedHandler(TRUSTED_OPERATOR2_ADDR)).toBe(true);
460
+ });
461
+
462
+ it('Should be able to bulk payout workers', async () => {
463
+ await job.initialize();
464
+ await job.launch();
465
+ await job.setup();
466
+
467
+ expect(
468
+ await job.bulkPayout(
469
+ [
470
+ {
471
+ address: WORKER1_ADDR,
472
+ amount: 20,
473
+ },
474
+ {
475
+ address: WORKER2_ADDR,
476
+ amount: 50,
477
+ },
478
+ ],
479
+ {}
480
+ )
481
+ ).toBe(true);
482
+
483
+ // The escrow contract is still in Partial state as there's still balance left.
484
+ expect((await job.balance())?.toString()).toBe(
485
+ toFullDigit(30).toString()
486
+ );
487
+ expect(await job.status()).toBe(EscrowStatus.Partial);
488
+
489
+ // Trying to pay more than the contract balance results in failure.
490
+ expect(
491
+ await job.bulkPayout(
492
+ [
493
+ {
494
+ address: WORKER3_ADDR,
495
+ amount: 50,
496
+ },
497
+ ],
498
+ {}
499
+ )
500
+ ).toBe(false);
501
+
502
+ // Paying the remaining amount empties the escrow and updates the status correctly.
503
+ expect(
504
+ await job.bulkPayout(
505
+ [
506
+ {
507
+ address: WORKER3_ADDR,
508
+ amount: 30,
509
+ },
510
+ ],
511
+ {}
512
+ )
513
+ ).toBe(true);
514
+ expect((await job.balance())?.toString()).toBe(toFullDigit(0).toString());
515
+ expect(await job.status()).toBe(EscrowStatus.Paid);
516
+ });
517
+
518
+ it('Should encrypt result, when bulk paying out workers', async () => {
519
+ await job.initialize();
520
+ await job.launch();
521
+ await job.setup();
522
+
523
+ jest.clearAllMocks();
524
+ const finalResults = { results: 0 };
525
+ await job.bulkPayout(
526
+ [
527
+ {
528
+ address: WORKER1_ADDR,
529
+ amount: 100,
530
+ },
531
+ ],
532
+ finalResults,
533
+ true
534
+ );
535
+
536
+ expect(upload).toHaveBeenCalledWith(
537
+ job.storageAccessData,
538
+ finalResults,
539
+ job.providerData?.reputationOracle?.publicKey,
540
+ true,
541
+ false
542
+ );
543
+ expect(upload).toHaveBeenCalledTimes(1);
544
+ });
545
+
546
+ it('Should not encrypt result, when bulk paying out workers', async () => {
547
+ await job.initialize();
548
+ await job.launch();
549
+ await job.setup();
550
+
551
+ jest.clearAllMocks();
552
+ const finalResults = { results: 0 };
553
+ await job.bulkPayout(
554
+ [
555
+ {
556
+ address: WORKER1_ADDR,
557
+ amount: 100,
558
+ },
559
+ ],
560
+ finalResults,
561
+ false
562
+ );
563
+
564
+ expect(upload).toHaveBeenCalledWith(
565
+ job.storageAccessData,
566
+ finalResults,
567
+ job.providerData?.reputationOracle?.publicKey,
568
+ false,
569
+ false
570
+ );
571
+ expect(upload).toHaveBeenCalledTimes(1);
572
+ });
573
+
574
+ it('Should store result in private storage, when bulk paying out workers', async () => {
575
+ await job.initialize();
576
+ await job.launch();
577
+ await job.setup();
578
+
579
+ jest.clearAllMocks();
580
+ const finalResults = { results: 0 };
581
+ await job.bulkPayout(
582
+ [
583
+ {
584
+ address: WORKER1_ADDR,
585
+ amount: 100,
586
+ },
587
+ ],
588
+ finalResults,
589
+ false,
590
+ false
591
+ );
592
+
593
+ expect(upload).toHaveBeenCalledWith(
594
+ job.storageAccessData,
595
+ finalResults,
596
+ job.providerData?.reputationOracle?.publicKey,
597
+ false,
598
+ false
599
+ );
600
+ expect(upload).toHaveBeenCalledTimes(1);
601
+ });
602
+
603
+ it('Should store result in public storage, when bulk paying out workers', async () => {
604
+ await job.initialize();
605
+ await job.launch();
606
+ await job.setup();
607
+
608
+ jest.clearAllMocks();
609
+ const finalResults = { results: 0 };
610
+ await job.bulkPayout(
611
+ [
612
+ {
613
+ address: WORKER1_ADDR,
614
+ amount: 50,
615
+ },
616
+ ],
617
+ finalResults,
618
+ false,
619
+ true
620
+ );
621
+
622
+ expect(upload).toHaveBeenCalledWith(
623
+ job.storageAccessData,
624
+ finalResults,
625
+ job.providerData?.reputationOracle?.publicKey,
626
+ false,
627
+ true
628
+ );
629
+ expect(upload).toHaveBeenCalledTimes(1);
630
+ expect(getPublicURL).toHaveBeenCalledTimes(1);
631
+ });
632
+
633
+ it('Should return final result', async () => {
634
+ await job.initialize();
635
+ await job.launch();
636
+ await job.setup();
637
+
638
+ const finalResults = { results: 0 };
639
+ await job.bulkPayout(
640
+ [
641
+ {
642
+ address: WORKER1_ADDR,
643
+ amount: 100,
644
+ },
645
+ ],
646
+ finalResults,
647
+ true
648
+ );
649
+
650
+ expect(JSON.stringify(await job.finalResults())).toBe(
651
+ JSON.stringify(finalResults)
652
+ );
653
+ });
654
+
655
+ it('Should be able to abort the job', async () => {
656
+ await job.initialize();
657
+ await job.launch();
658
+ await job.setup();
659
+
660
+ expect(await job.abort()).toBe(true);
661
+ });
662
+
663
+ it('Should be able to abort partially paid job', async () => {
664
+ await job.initialize();
665
+ await job.launch();
666
+ await job.setup();
667
+
668
+ const finalResults = { results: 0 };
669
+ await job.bulkPayout(
670
+ [
671
+ {
672
+ address: WORKER1_ADDR,
673
+ amount: 50,
674
+ },
675
+ ],
676
+ finalResults,
677
+ true
678
+ );
679
+
680
+ expect(await job.abort()).toBe(true);
681
+ });
682
+
683
+ it('Should not be able to abort fully paid job', async () => {
684
+ await job.initialize();
685
+ await job.launch();
686
+ await job.setup();
687
+
688
+ const finalResults = { results: 0 };
689
+ await job.bulkPayout(
690
+ [
691
+ {
692
+ address: WORKER1_ADDR,
693
+ amount: 100,
694
+ },
695
+ ],
696
+ finalResults,
697
+ true
698
+ );
699
+
700
+ expect(await job.abort()).toBe(false);
701
+ });
702
+
703
+ it('Should be able to cancel the job', async () => {
704
+ await job.initialize();
705
+ await job.launch();
706
+ await job.setup();
707
+
708
+ expect(await job.cancel()).toBe(true);
709
+ expect((await job.balance())?.toString()).toBe(toFullDigit(0).toString());
710
+ });
711
+
712
+ it('Should be able to cancel partially paid job', async () => {
713
+ await job.initialize();
714
+ await job.launch();
715
+ await job.setup();
716
+
717
+ const finalResults = { results: 0 };
718
+ await job.bulkPayout(
719
+ [
720
+ {
721
+ address: WORKER1_ADDR,
722
+ amount: 50,
723
+ },
724
+ ],
725
+ finalResults,
726
+ true
727
+ );
728
+
729
+ expect(await job.cancel()).toBe(true);
730
+ expect((await job.balance())?.toString()).toBe(toFullDigit(0).toString());
731
+ });
732
+
733
+ it('Should not be able to cancel paid job', async () => {
734
+ await job.initialize();
735
+ await job.launch();
736
+ await job.setup();
737
+
738
+ const finalResults = { results: 0 };
739
+ await job.bulkPayout(
740
+ [
741
+ {
742
+ address: WORKER1_ADDR,
743
+ amount: 100,
744
+ },
745
+ ],
746
+ finalResults,
747
+ true
748
+ );
749
+
750
+ expect(await job.cancel()).toBe(false);
751
+ });
752
+ });
753
+ });
@@ -0,0 +1,27 @@
1
+ export const DEFAULT_HMTOKEN_ADDR =
2
+ '0x5FbDB2315678afecb367f032d93F642f64180aa3';
3
+
4
+ export const DEFAULT_GAS_PAYER_ADDR =
5
+ '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
6
+ export const DEFAULT_GAS_PAYER_PRIVKEY =
7
+ 'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
8
+
9
+ export const REPUTATION_ORACLE_ADDR =
10
+ '0x70997970C51812dc3A010C7d01b50e0d17dc79C8';
11
+ export const REPUTATION_ORACLE_PRIVKEY =
12
+ '59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d';
13
+
14
+ export const TRUSTED_OPERATOR1_ADDR =
15
+ '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC';
16
+ export const TRUSTED_OPERATOR1_PRIVKEY =
17
+ '5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a';
18
+
19
+ export const TRUSTED_OPERATOR2_ADDR =
20
+ '0x90F79bf6EB2c4f870365E785982E1f101E93b906';
21
+
22
+ export const WORKER1_ADDR = '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65';
23
+ export const WORKER2_ADDR = '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc';
24
+ export const WORKER3_ADDR = '0x976EA74026E726554dB657fA54763abd0C3a0aa9';
25
+
26
+ export const NOT_TRUSTED_OPERATOR_PRIVKEY =
27
+ '5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365b';
@@ -0,0 +1,33 @@
1
+ import { Manifest } from '../../src';
2
+
3
+ const CALLBACK_URL = 'http://google.com/webback';
4
+ const GAS_PAYER = '0x1413862c2b7054cdbfdc181b83962cb0fc11fd92';
5
+ const FAKE_URL = 'http://google.com/fake';
6
+ const IMAGE_LABEL_BINARY = 'image_label_binary';
7
+
8
+ export const manifest: Manifest = {
9
+ requester_restricted_answer_set: {
10
+ '0': { en: 'English Answer 1' },
11
+ '1': {
12
+ en: 'English Answer 2',
13
+ answer_example_uri: 'https://hcaptcha.com/example_answer2.jpg',
14
+ },
15
+ },
16
+ job_mode: 'batch',
17
+ request_type: IMAGE_LABEL_BINARY,
18
+ unsafe_content: false,
19
+ task_bid_price: 1,
20
+ oracle_stake: 0.05,
21
+ expiration_date: 0,
22
+ minimum_trust_server: 0.1,
23
+ minimum_trust_client: 0.1,
24
+ requester_accuracy_target: 0.1,
25
+ recording_oracle_addr: GAS_PAYER,
26
+ reputation_oracle_addr: GAS_PAYER,
27
+ reputation_agent_addr: GAS_PAYER,
28
+ instant_result_delivery_webhook: CALLBACK_URL,
29
+ requester_question: { en: 'How much money are we to make' },
30
+ requester_question_example: FAKE_URL,
31
+ job_total_tasks: 100,
32
+ taskdata_uri: FAKE_URL,
33
+ };