@prosopo/procaptcha 0.2.13 → 0.2.14

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