@prosopo/procaptcha 0.2.10 → 0.2.13

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.
@@ -1,43 +1,31 @@
1
1
  import { AccountNotFoundError } from '../api/errors.js';
2
- import { ApiPromise, Keyring } from '@polkadot/api';
3
- import { ProcaptchaConfigSchema, } from '@prosopo/types';
4
- import { ProviderApi } from '@prosopo/api';
2
+ import { ApiPromise, Keyring, WsProvider } from '@polkadot/api';
3
+ import { ProcaptchaConfigSchema } from '@prosopo/types';
4
+ import { Observable, from, lastValueFrom } from 'rxjs';
5
5
  import { ProsopoCaptchaContract, wrapQuery } from '@prosopo/contract';
6
6
  import { ProsopoEnvError, trimProviderUrl } from '@prosopo/common';
7
+ import { ProviderApi } from '@prosopo/api';
7
8
  import { ContractAbi as abiJson } from '@prosopo/captcha-contract';
8
- import { WsProvider } from '@polkadot/rpc-provider';
9
9
  import { at } from '@prosopo/util';
10
10
  import { randomAsHex } from '@polkadot/util-crypto';
11
- import { sleep } from '../utils/utils.js';
12
- import { stringToU8a } from '@polkadot/util';
11
+ import { retry, take } from 'rxjs/operators';
13
12
  import ExtensionWeb2 from '../api/ExtensionWeb2.js';
14
13
  import ExtensionWeb3 from '../api/ExtensionWeb3.js';
15
14
  import ProsopoCaptchaApi from './ProsopoCaptchaApi.js';
16
15
  import storage from './storage.js';
17
- export const defaultState = () => {
18
- return {
19
- // note order matters! see buildUpdateState. These fields are set in order, so disable modal first, then set loading to false, etc.
20
- showModal: false,
21
- loading: false,
22
- index: 0,
23
- challenge: undefined,
24
- solutions: undefined,
25
- isHuman: false,
26
- captchaApi: undefined,
27
- account: undefined,
28
- // don't handle timeout here, this should be handled by the state management
29
- };
30
- };
31
- const buildUpdateState = (state, onStateUpdate) => {
32
- const updateCurrentState = (nextState) => {
33
- // mutate the current state. Note that this is in order of properties in the nextState object.
34
- // e.g. given {b: 2, c: 3, a: 1}, b will be set, then c, then a. This is because JS stores fields in insertion order by default, unless you override it with a class or such by changing the key enumeration order.
35
- Object.assign(state, nextState);
36
- // then call the update function for the frontend to do the same
37
- onStateUpdate(nextState);
38
- console.log('Procaptcha state update:', nextState, '\nResult:', state);
39
- };
40
- return updateCurrentState;
16
+ export const defaultState = () => ({
17
+ showModal: false,
18
+ loading: false,
19
+ index: 0,
20
+ challenge: undefined,
21
+ solutions: undefined,
22
+ isHuman: false,
23
+ captchaApi: undefined,
24
+ account: undefined,
25
+ });
26
+ const buildUpdateState = (state, onStateUpdate) => (nextState) => {
27
+ Object.assign(state, nextState);
28
+ onStateUpdate(nextState);
41
29
  };
42
30
  export const getNetwork = (config) => {
43
31
  const network = config.networks[config.defaultNetwork];
@@ -50,16 +38,15 @@ export const getNetwork = (config) => {
50
38
  * The state operator. This is used to mutate the state of Procaptcha during the captcha process. State updates are published via the onStateUpdate callback. This should be used by frontends, e.g. react, to maintain the state of Procaptcha across renders.
51
39
  */
52
40
  export function Manager(configOptional, state, onStateUpdate, callbacks) {
53
- // events are emitted at various points during the captcha process. These each have default behaviours below which can be overridden by the frontend using callbacks.
54
41
  const alertError = (error) => {
55
- console.log(error);
42
+ console.error(error);
56
43
  alert(error.message);
57
44
  };
58
45
  const events = Object.assign({
59
46
  onAccountNotFound: alertError,
60
47
  onError: alertError,
61
48
  onHuman: (output) => {
62
- console.log('onHuman event triggered', output);
49
+ console.info('onHuman event triggered', output);
63
50
  },
64
51
  onExtensionNotFound: () => {
65
52
  alert('No extension found');
@@ -74,10 +61,10 @@ export function Manager(configOptional, state, onStateUpdate, callbacks) {
74
61
  alert('Uncompleted challenge has expired, please try again');
75
62
  },
76
63
  onOpen: () => {
77
- console.log('onOpen event triggered');
64
+ console.info('onOpen event triggered');
78
65
  },
79
66
  onClose: () => {
80
- console.log('onClose event triggered');
67
+ console.info('onClose event triggered');
81
68
  },
82
69
  }, callbacks);
83
70
  const dispatchErrorEvent = (err) => {
@@ -89,7 +76,6 @@ export function Manager(configOptional, state, onStateUpdate, callbacks) {
89
76
  events.onError(error);
90
77
  }
91
78
  };
92
- // get the state update mechanism
93
79
  const updateState = buildUpdateState(state, onStateUpdate);
94
80
  /**
95
81
  * Build the config on demand, using the optional config passed in from the outside. State may override various
@@ -98,27 +84,17 @@ export function Manager(configOptional, state, onStateUpdate, callbacks) {
98
84
  * This is because the captcha process has already been started using account "ABC".
99
85
  * @returns the config for procaptcha
100
86
  */
101
- const getConfig = () => {
102
- const config = {
103
- userAccountAddress: '',
104
- ...configOptional,
105
- };
106
- // overwrite the account in use with the one in state if it exists. Reduces likelihood of bugs where the user
107
- // changes account in the middle of the captcha process.
108
- if (state.account) {
109
- config.userAccountAddress = state.account.account.address;
110
- }
111
- return ProcaptchaConfigSchema.parse(config);
112
- };
87
+ const getConfig = () => ProcaptchaConfigSchema.parse({
88
+ ...configOptional,
89
+ userAccountAddress: state.account ? state.account.account.address : configOptional.userAccountAddress || '',
90
+ });
113
91
  const fallable = async (fn) => {
114
92
  try {
115
93
  await fn();
116
94
  }
117
95
  catch (err) {
118
96
  console.error(err);
119
- // dispatch relevant error event
120
97
  dispatchErrorEvent(err);
121
- // hit an error, disallow user's claim to be human
122
98
  updateState({ isHuman: false, showModal: false, loading: false });
123
99
  }
124
100
  };
@@ -126,188 +102,87 @@ export function Manager(configOptional, state, onStateUpdate, callbacks) {
126
102
  * Called on start of user verification. This is when the user ticks the box to claim they are human.
127
103
  */
128
104
  const start = async () => {
129
- console.log('Starting procaptcha');
130
105
  events.onOpen();
131
106
  await fallable(async () => {
132
- if (state.loading) {
133
- console.log('Procaptcha already loading');
134
- return;
135
- }
136
- if (state.isHuman) {
137
- console.log('already human');
107
+ if (state.loading || state.isHuman)
138
108
  return;
139
- }
140
109
  resetState();
141
- // set the loading flag to true (allow UI to show some sort of loading / pending indicator while we get the captcha process going)
142
- updateState({ loading: true });
143
- // snapshot the config into the state
144
- const config = getConfig();
145
- updateState({ dappAccount: config.account.address });
146
- // allow UI to catch up with the loading state
147
- await sleep(100);
148
- // check accounts / setup accounts
110
+ updateState({ dappAccount: getConfig().account.address, loading: true });
149
111
  const account = await loadAccount();
150
- // account has been found, check if account is already marked as human
151
- // first, ask the smart contract
152
112
  const contract = await loadContract();
153
- // We don't need to show CAPTCHA challenges if the user is determined as human by the contract
154
- let contractIsHuman = false;
155
- try {
156
- contractIsHuman = (await contract.query.dappOperatorIsHumanUser(account.account.address, config.solutionThreshold)).value
157
- .unwrap()
158
- .unwrap();
159
- }
160
- catch (error) {
161
- console.warn(error);
162
- }
163
- if (contractIsHuman) {
164
- updateState({ isHuman: true, loading: false });
165
- events.onHuman({
166
- user: account.account.address,
167
- dapp: getDappAccount(),
168
- });
169
- setValidChallengeTimeout();
113
+ if (await checkHumanInContract(contract, account)) {
114
+ handleHumanInContract(account);
170
115
  return;
171
116
  }
172
- // Check if there is a provider in local storage or get a random one from the contract
117
+ // Check if a provider is cached in local storage
173
118
  const providerUrlFromStorage = storage.getProviderUrl();
174
- let providerApi;
175
119
  if (providerUrlFromStorage) {
176
- providerApi = await loadProviderApi(providerUrlFromStorage);
177
- // if the provider was already in storage, the user may have already solved some captchas but they have not been put on chain yet
178
- // so contact the provider to check if this is the case
179
120
  try {
180
- const verifyDappUserResponse = await providerApi.verifyDappUser(account.account.address);
121
+ // Verify cached provider is legitimate
122
+ const verifyDappUserResponse = await getVerifyDappUserFunction(providerUrlFromStorage, account);
123
+ // If legitimate cached provider, check if human in cached provider
181
124
  if (verifyDappUserResponse.solutionApproved) {
182
- updateState({ isHuman: true, loading: false });
183
- events.onHuman({
184
- providerUrl: providerUrlFromStorage,
185
- user: account.account.address,
186
- dapp: getDappAccount(),
187
- commitmentId: verifyDappUserResponse.commitmentId,
188
- });
189
- setValidChallengeTimeout();
125
+ handleHumanInCachedProvider(providerUrlFromStorage, account, verifyDappUserResponse);
190
126
  return;
191
127
  }
192
128
  }
193
129
  catch (err) {
194
- // if the provider is down, we should continue with the process of selecting a random provider
195
130
  console.error('Error contacting provider from storage', providerUrlFromStorage);
196
- // continue as if the provider was not in storage
197
131
  }
198
132
  }
199
- const payload = {
200
- address: account.account.address,
201
- data: stringToU8a('message'),
202
- type: 'bytes',
203
- };
204
- const signed = await account.extension.signer.signRaw(payload);
205
- console.log('Signature:', signed);
206
- // get a random provider
207
- const getRandomProviderResponse = await wrapQuery(contract.query.getRandomActiveProvider, contract.query)(account.account.address, getDappAccount());
208
- const blockNumber = parseInt(getRandomProviderResponse.blockNumber.toString());
209
- console.log('provider', getRandomProviderResponse);
210
- const providerUrl = trimProviderUrl(getRandomProviderResponse.provider.url.toString());
211
- // get the provider api inst
212
- providerApi = await loadProviderApi(providerUrl);
213
- console.log('providerApi', providerApi);
214
- // get the captcha challenge and begin the challenge
215
- const captchaApi = await loadCaptchaApi(contract, getRandomProviderResponse, providerApi);
216
- console.log('captchaApi', captchaApi);
217
- const challenge = await captchaApi.getCaptchaChallenge();
218
- console.log('challenge', challenge);
133
+ // If not human in contract or cached provider, get new captcha from a random provider
134
+ const randomProviderResponse = await getRandomProviderResponse(contract, account);
135
+ const challenge = await getChallenge(randomProviderResponse, contract);
219
136
  if (challenge.captchas.length <= 0) {
220
137
  throw new Error('No captchas returned from provider');
221
138
  }
222
- // setup timeout
223
- const timeMillis = challenge.captchas
224
- .map((captcha) => captcha.captcha.timeLimitMs || 30 * 1000)
225
- .reduce((a, b) => a + b);
226
- const timeout = setTimeout(() => {
227
- console.log('challenge expired after ' + timeMillis + 'ms');
228
- events.onChallengeExpired();
229
- // expired, disallow user's claim to be human
230
- updateState({ isHuman: false, showModal: false, loading: false });
231
- }, timeMillis);
232
- // update state with new challenge
233
139
  updateState({
140
+ challenge,
234
141
  index: 0,
235
142
  solutions: challenge.captchas.map(() => []),
236
- challenge,
237
143
  showModal: true,
238
- timeout,
239
- blockNumber,
144
+ timeout: setTimeToComplete(challenge),
145
+ blockNumber: getBlockNumberFromProvider(randomProviderResponse),
240
146
  });
241
147
  });
242
148
  };
149
+ /**
150
+ * Submit the captcha solution.
151
+ */
243
152
  const submit = async () => {
244
153
  await fallable(async () => {
245
- console.log('submitting solutions');
246
- // disable the time limit, user has submitted their solution in time
247
154
  clearTimeout();
155
+ updateState({ showModal: false });
248
156
  if (!state.challenge) {
249
157
  throw new Error('cannot submit, no challenge found');
250
158
  }
251
- // hide the modal, no further input required from user
252
- updateState({ showModal: false });
253
- const challenge = state.challenge;
159
+ const datasetId = getDatasetId();
254
160
  const salt = randomAsHex();
255
- // append solution to each captcha in the challenge
256
- const captchaSolution = state.challenge.captchas.map((captcha, index) => {
257
- const solution = at(state.solutions, index);
258
- return {
259
- captchaId: captcha.captcha.captchaId,
260
- captchaContentId: captcha.captcha.captchaContentId,
261
- salt,
262
- solution,
263
- };
264
- });
265
- const account = getAccount();
266
- const blockNumber = getBlockNumber();
267
- const signer = account.extension.signer;
268
- const first = at(challenge.captchas, 0);
269
- if (!first.captcha.datasetId) {
270
- throw new Error('No datasetId set for challenge');
271
- }
272
- const captchaApi = getCaptchaApi();
273
- // send the commitment to the provider
274
- const submission = await captchaApi.submitCaptchaSolution(signer, challenge.requestHash, first.captcha.datasetId, captchaSolution, salt);
275
- // mark as is human if solution has been approved
161
+ // Build the captcha solution
162
+ const submission = await getCaptchaApi().submitCaptchaSolution(getAccount().extension.signer, state.challenge.requestHash, datasetId, getSolutionsFromState(salt), salt);
276
163
  const isHuman = submission[0].solutionApproved;
277
164
  if (!isHuman) {
278
- // user failed the captcha for some reason according to the provider
279
165
  events.onFailed();
280
166
  }
281
- // update the state with the result of the submission
282
167
  updateState({
283
168
  submission,
284
169
  isHuman,
285
170
  loading: false,
286
171
  });
287
- if (state.isHuman) {
288
- const trimmedUrl = trimProviderUrl(captchaApi.provider.provider.url.toString());
289
- // cache this provider for future use
172
+ if (isHuman) {
173
+ const trimmedUrl = trimProviderUrl(getCaptchaApi().provider.provider.url.toString());
290
174
  storage.setProviderUrl(trimmedUrl);
291
175
  events.onHuman({
292
176
  providerUrl: trimmedUrl,
293
- user: account.account.address,
177
+ user: getAccount().account.address,
294
178
  dapp: getDappAccount(),
295
179
  commitmentId: submission[1],
296
- blockNumber,
180
+ blockNumber: getBlockNumberFromState(),
297
181
  });
298
182
  setValidChallengeTimeout();
299
183
  }
300
184
  });
301
185
  };
302
- const cancel = async () => {
303
- console.log('cancel');
304
- // disable the time limit
305
- clearTimeout();
306
- // abandon the captcha process
307
- resetState();
308
- // trigger the onClose event
309
- events.onClose();
310
- };
311
186
  /**
312
187
  * (De)Select an image from the solution for the current round. If the hash is already in the solutions list, it will be removed (deselected) and if not it will be added (selected).
313
188
  * @param hash the hash of the image
@@ -319,20 +194,9 @@ export function Manager(configOptional, state, onStateUpdate, callbacks) {
319
194
  if (state.index >= state.challenge.captchas.length || state.index < 0) {
320
195
  throw new Error('cannot select, round index out of range');
321
196
  }
322
- const index = state.index;
323
- const solutions = state.solutions;
324
- const solution = at(solutions, index);
325
- if (solution.includes(hash)) {
326
- console.log('deselecting', hash);
327
- // remove the hash from the solution
328
- solution.splice(solution.indexOf(hash), 1);
329
- }
330
- else {
331
- console.log('selecting', hash);
332
- // add the hash to the solution
333
- solution.push(hash);
334
- }
335
- updateState({ solutions });
197
+ const solution = at(state.solutions, state.index);
198
+ handleIsSelected(solution, hash);
199
+ updateState({ solutions: state.solutions });
336
200
  };
337
201
  /**
338
202
  * Proceed to the next round of the challenge.
@@ -344,43 +208,215 @@ export function Manager(configOptional, state, onStateUpdate, callbacks) {
344
208
  if (state.index + 1 >= state.challenge.captchas.length) {
345
209
  throw new Error('cannot proceed to next round, already at last round');
346
210
  }
347
- console.log('proceeding to next round');
348
211
  updateState({ index: state.index + 1 });
349
212
  };
213
+ /**
214
+ * Load the captcha api using the contract and provider.
215
+ * @param contract the contract instance
216
+ * @param provider the provider instance
217
+ * @param providerApi the provider api instance
218
+ */
350
219
  const loadCaptchaApi = async (contract, provider, providerApi) => {
351
- const config = getConfig();
352
- // setup the captcha api to carry out a challenge
353
- const captchaApi = new ProsopoCaptchaApi(getAccount().account.address, contract, provider, providerApi, config.web2, getDappAccount());
354
- updateState({ captchaApi });
220
+ updateState({
221
+ captchaApi: new ProsopoCaptchaApi(getAccount().account.address, contract, provider, providerApi, getConfig().web2, getDappAccount()),
222
+ });
355
223
  return getCaptchaApi();
356
224
  };
225
+ /**
226
+ * Create an observable that emits on every new block.
227
+ * Used for retrying random provider requests.
228
+ */
229
+ const createBlockObservable = () => new Observable((subscriber) => () => ApiPromise.create({ provider: new WsProvider(getNetwork(getConfig()).endpoint) })
230
+ .then((api) => {
231
+ api.rpc.chain.subscribeNewHeads((header) => {
232
+ subscriber.next(header);
233
+ });
234
+ })
235
+ .catch((error) => {
236
+ subscriber.error(error);
237
+ }));
238
+ /**
239
+ * Load the account using address specified in config, or generate new address if not found in local storage for web2 mode.
240
+ */
241
+ const loadAccount = async () => {
242
+ const config = getConfig();
243
+ if (!config.web2 && !config.userAccountAddress) {
244
+ throw new Error('Account address has not been set for web3 mode');
245
+ }
246
+ const ext = config.web2 ? new ExtensionWeb2() : new ExtensionWeb3();
247
+ const account = await ext.getAccount(config);
248
+ storage.setAccount(account.account.address);
249
+ updateState({ account });
250
+ return getAccount();
251
+ };
252
+ /**
253
+ * Load the provider api
254
+ * @param providerUrl
255
+ */
357
256
  const loadProviderApi = async (providerUrl) => {
358
257
  const config = getConfig();
359
- const network = getNetwork(config);
360
258
  if (!config.account.address) {
361
259
  throw new ProsopoEnvError('GENERAL.SITE_KEY_MISSING');
362
260
  }
363
- return new ProviderApi(network, providerUrl, config.account.address);
261
+ return new ProviderApi(getNetwork(config), providerUrl, config.account.address);
262
+ };
263
+ /**
264
+ * Load the contract instance using addresses from config.
265
+ */
266
+ const loadContract = async () => {
267
+ const network = getNetwork(getConfig());
268
+ const api = await ApiPromise.create({ provider: new WsProvider(network.endpoint) });
269
+ const type = 'sr25519';
270
+ return new ProsopoCaptchaContract(api, JSON.parse(abiJson), network.contract.address, 'prosopo', 0, new Keyring({ type, ss58Format: api.registry.chainSS58 }).addFromAddress(getAccount().account.address));
271
+ };
272
+ /**
273
+ * Handles whether clicking on an image should select or deselect it.
274
+ * @param solution
275
+ * @param hash
276
+ */
277
+ function handleIsSelected(solution, hash) {
278
+ if (solution.includes(hash)) {
279
+ solution.splice(solution.indexOf(hash), 1);
280
+ }
281
+ else {
282
+ solution.push(hash);
283
+ }
284
+ }
285
+ /**
286
+ * Get the solutions from the state, with the salt added.
287
+ * @param salt
288
+ * @returns
289
+ */
290
+ function getSolutionsFromState(salt) {
291
+ if (!state.challenge) {
292
+ throw new Error('cannot get solutions, no challenge found');
293
+ }
294
+ return state.challenge.captchas.map((captcha, index) => ({
295
+ captchaId: captcha.captcha.captchaId,
296
+ captchaContentId: captcha.captcha.captchaContentId,
297
+ salt,
298
+ solution: at(state.solutions, index),
299
+ }));
300
+ }
301
+ /**
302
+ * Handle the case where the user is human and the provider is cached in local storage
303
+ * @param providerUrlFromStorage
304
+ * @param account
305
+ * @param verifyDappUserResponse
306
+ */
307
+ function handleHumanInCachedProvider(providerUrlFromStorage, account, verifyDappUserResponse) {
308
+ updateState({ isHuman: true, loading: false });
309
+ events.onHuman({
310
+ providerUrl: providerUrlFromStorage,
311
+ user: account.account.address,
312
+ dapp: getDappAccount(),
313
+ commitmentId: verifyDappUserResponse.commitmentId,
314
+ });
315
+ setValidChallengeTimeout();
316
+ }
317
+ /**
318
+ * Get the verifyDappUser function from the provider api
319
+ * @param providerUrlFromStorage
320
+ * @param account
321
+ * @returns
322
+ */
323
+ function getVerifyDappUserFunction(providerUrlFromStorage, account) {
324
+ return loadProviderApi(providerUrlFromStorage).then((providerApi) => providerApi.verifyDappUser(account.account.address));
325
+ }
326
+ /**
327
+ * Handle the case where the user is human and the provider is cached in the contract
328
+ * @param account
329
+ */
330
+ function handleHumanInContract(account) {
331
+ updateState({ isHuman: true, loading: false });
332
+ events.onHuman({
333
+ user: account.account.address,
334
+ dapp: getDappAccount(),
335
+ });
336
+ setValidChallengeTimeout();
337
+ }
338
+ /**
339
+ * Check if the user is human in the contract
340
+ * @param contract
341
+ * @param account
342
+ * @returns
343
+ */
344
+ async function checkHumanInContract(contract, account) {
345
+ try {
346
+ return await contract.query
347
+ .dappOperatorIsHumanUser(account.account.address, getConfig().solutionThreshold)
348
+ .then((res) => res.value.unwrap().unwrap());
349
+ }
350
+ catch (err) {
351
+ console.error(err);
352
+ return false;
353
+ }
354
+ }
355
+ /**
356
+ * Get the captcha challenge from the provider api
357
+ * @param getRandomProviderResponse
358
+ * @param contract
359
+ * @returns
360
+ */
361
+ function getChallenge(getRandomProviderResponse, contract) {
362
+ return loadProviderApi(trimProviderUrl(getRandomProviderResponse.provider.url.toString()))
363
+ .then((api) => loadCaptchaApi(contract, getRandomProviderResponse, api))
364
+ .then((captchaApi) => captchaApi.getCaptchaChallenge());
365
+ }
366
+ /**
367
+ * Get a random provider from the contract
368
+ * Uses retry to handle the case where the provider is not available on the first attempt
369
+ * Waits for block rollover to ensure new provider selected
370
+ * Returns promise
371
+ *
372
+ * @param contract
373
+ * @param account
374
+ * @returns
375
+ */
376
+ function getRandomProviderResponse(contract, account) {
377
+ return lastValueFrom(from(wrapQuery(contract.query.getRandomActiveProvider, contract.query)(account.account.address, getDappAccount())).pipe(retry({
378
+ count: 3,
379
+ delay: (error, retryCount) => {
380
+ console.error(`Attempt ${retryCount} failed. Retrying on next block. Error: ${error}`);
381
+ return createBlockObservable().pipe(take(1));
382
+ },
383
+ resetOnSuccess: true,
384
+ })));
385
+ }
386
+ /**
387
+ * Verify the captcha data
388
+ * @param challenge
389
+ * @returns
390
+ */
391
+ function setTimeToComplete(challenge) {
392
+ return setTimeout(() => {
393
+ events.onChallengeExpired();
394
+ updateState({ isHuman: false, showModal: false, loading: false });
395
+ }, challenge.captchas.map((captcha) => captcha.captcha.timeLimitMs || 30 * 1000).reduce((a, b) => a + b));
396
+ }
397
+ /**
398
+ * The timeout for the challenge to be completed
399
+ * Defaults to 2 minutes
400
+ * @returns
401
+ */
402
+ const setValidChallengeTimeout = () => {
403
+ updateState({
404
+ successfullChallengeTimeout: setTimeout(() => {
405
+ updateState({ isHuman: false });
406
+ events.onExpired();
407
+ }, configOptional.challengeValidLength || 120 * 1000),
408
+ });
409
+ };
410
+ const cancel = async () => {
411
+ clearTimeout();
412
+ resetState();
413
+ events.onClose();
364
414
  };
365
415
  const clearTimeout = () => {
366
- // clear the timeout
367
416
  window.clearTimeout(state.timeout);
368
- // then clear the timeout from the state
369
417
  updateState({ timeout: undefined });
370
418
  };
371
- const setValidChallengeTimeout = () => {
372
- console.log('setting valid challenge timeout');
373
- const timeMillis = configOptional.challengeValidLength || 120 * 1000; // default to 2 minutes
374
- const successfullChallengeTimeout = setTimeout(() => {
375
- console.log('valid challenge expired after ' + timeMillis + 'ms');
376
- // Human state expired, disallow user's claim to be human
377
- updateState({ isHuman: false });
378
- events.onExpired();
379
- }, timeMillis);
380
- updateState({ successfullChallengeTimeout });
381
- };
382
419
  const resetState = () => {
383
- // clear timeout just in case a timer is still active (shouldn't be)
384
420
  clearTimeout();
385
421
  updateState(defaultState());
386
422
  };
@@ -390,57 +426,37 @@ export function Manager(configOptional, state, onStateUpdate, callbacks) {
390
426
  }
391
427
  return state.captchaApi;
392
428
  };
393
- /**
394
- * Load the account using address specified in config, or generate new address if not found in local storage for web2 mode.
395
- */
396
- const loadAccount = async () => {
397
- const config = getConfig();
398
- // check if account has been provided in config (doesn't matter in web2 mode)
399
- if (!config.web2 && !config.userAccountAddress) {
400
- throw new Error('Account address has not been set for web3 mode');
401
- }
402
- // check if account exists in extension
403
- const ext = config.web2 ? new ExtensionWeb2() : new ExtensionWeb3();
404
- const account = await ext.getAccount(config);
405
- // Store the account in local storage
406
- storage.setAccount(account.account.address);
407
- console.log('Using account:', account);
408
- updateState({ account });
409
- return getAccount();
410
- };
411
429
  const getAccount = () => {
412
430
  if (!state.account) {
413
431
  throw new Error('Account not loaded');
414
432
  }
415
- const account = state.account;
416
- return account;
433
+ return state.account;
417
434
  };
418
435
  const getDappAccount = () => {
419
436
  if (!state.dappAccount) {
420
437
  throw new ProsopoEnvError('GENERAL.SITE_KEY_MISSING');
421
438
  }
422
- const dappAccount = state.dappAccount;
423
- return dappAccount;
439
+ return state.dappAccount;
424
440
  };
425
- const getBlockNumber = () => {
441
+ const getBlockNumberFromState = () => {
426
442
  if (!state.blockNumber) {
427
443
  throw new Error('Account not loaded');
428
444
  }
429
- const blockNumber = state.blockNumber;
430
- return blockNumber;
431
- };
432
- /**
433
- * Load the contract instance using addresses from config.
434
- */
435
- const loadContract = async () => {
436
- const config = getConfig();
437
- const network = getNetwork(config);
438
- const api = await ApiPromise.create({ provider: new WsProvider(network.endpoint) });
439
- // TODO create a shared keyring that's stored somewhere
440
- const type = 'sr25519';
441
- const keyring = new Keyring({ type, ss58Format: api.registry.chainSS58 });
442
- return new ProsopoCaptchaContract(api, JSON.parse(abiJson), network.contract.address, 'prosopo', 0, keyring.addFromAddress(getAccount().account.address));
445
+ return state.blockNumber;
443
446
  };
447
+ function getBlockNumberFromProvider(getRandomProviderResponse) {
448
+ return parseInt(getRandomProviderResponse.blockNumber.toString());
449
+ }
450
+ function getDatasetId() {
451
+ if (!state.challenge) {
452
+ throw new Error('cannot get datasetId, no challenge found');
453
+ }
454
+ const datasetId = at(state.challenge.captchas, 0).captcha.datasetId;
455
+ if (!datasetId) {
456
+ throw new Error('No datasetId set for challenge');
457
+ }
458
+ return datasetId;
459
+ }
444
460
  return {
445
461
  start,
446
462
  cancel,