@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.
- package/dist/api/handlers.d.ts +2 -3
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +23 -1
- package/dist/api/handlers.js.map +1 -1
- package/dist/api/index.d.ts +0 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +0 -1
- package/dist/api/index.js.map +1 -1
- package/dist/cjs/api/handlers.cjs +10 -1
- package/dist/cjs/api/index.cjs +0 -3
- package/dist/cjs/contracts/captcha/dist/contract-info/captcha.cjs +2 -2
- package/dist/cjs/index.cjs +0 -2
- package/dist/cjs/modules/Manager.cjs +209 -210
- package/dist/cjs/packages/datasets/dist/captcha/util.cjs +6 -2
- package/dist/modules/Manager.d.ts.map +1 -1
- package/dist/modules/Manager.js +265 -249
- package/dist/modules/Manager.js.map +1 -1
- package/dist/modules/ProsopoCaptchaApi.d.ts.map +1 -1
- package/dist/modules/ProsopoCaptchaApi.js +13 -0
- package/dist/modules/ProsopoCaptchaApi.js.map +1 -1
- package/package.json +23 -15
- package/dist/api/HttpClientBase.d.ts +0 -9
- package/dist/api/HttpClientBase.d.ts.map +0 -1
- package/dist/api/HttpClientBase.js +0 -28
- package/dist/api/HttpClientBase.js.map +0 -1
- package/dist/cjs/api/HttpClientBase.cjs +0 -17
package/dist/modules/Manager.js
CHANGED
|
@@ -1,43 +1,31 @@
|
|
|
1
1
|
import { AccountNotFoundError } from '../api/errors.js';
|
|
2
|
-
import { ApiPromise, Keyring } from '@polkadot/api';
|
|
3
|
-
import { ProcaptchaConfigSchema
|
|
4
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
+
console.info('onOpen event triggered');
|
|
78
65
|
},
|
|
79
66
|
onClose: () => {
|
|
80
|
-
console.
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
252
|
-
updateState({ showModal: false });
|
|
253
|
-
const challenge = state.challenge;
|
|
159
|
+
const datasetId = getDatasetId();
|
|
254
160
|
const salt = randomAsHex();
|
|
255
|
-
//
|
|
256
|
-
const
|
|
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 (
|
|
288
|
-
const trimmedUrl = trimProviderUrl(
|
|
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:
|
|
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
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
423
|
-
return dappAccount;
|
|
439
|
+
return state.dappAccount;
|
|
424
440
|
};
|
|
425
|
-
const
|
|
441
|
+
const getBlockNumberFromState = () => {
|
|
426
442
|
if (!state.blockNumber) {
|
|
427
443
|
throw new Error('Account not loaded');
|
|
428
444
|
}
|
|
429
|
-
|
|
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,
|