@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.
- package/dist/api/ExtensionWeb2.d.ts.map +1 -1
- package/dist/api/ExtensionWeb2.js +9 -8
- package/dist/api/ExtensionWeb2.js.map +1 -1
- package/dist/api/sign.js +2 -2
- package/dist/api/sign.js.map +1 -1
- package/dist/cjs/api/ExtensionWeb2.cjs +17 -16
- package/dist/cjs/modules/Manager.cjs +213 -212
- package/dist/cjs/modules/ProsopoCaptchaApi.cjs +10 -12
- package/dist/modules/Manager.d.ts.map +1 -1
- package/dist/modules/Manager.js +252 -267
- package/dist/modules/Manager.js.map +1 -1
- package/dist/modules/ProsopoCaptchaApi.d.ts +1 -1
- package/dist/modules/ProsopoCaptchaApi.d.ts.map +1 -1
- package/dist/modules/ProsopoCaptchaApi.js +1 -1
- package/dist/modules/ProsopoCaptchaApi.js.map +1 -1
- package/package.json +7 -7
- package/dist/cjs/contracts/captcha/dist/build-extrinsic/captcha.cjs +0 -339
- package/dist/cjs/contracts/captcha/dist/contract-info/captcha.cjs +0 -6
- package/dist/cjs/contracts/captcha/dist/contracts/captcha.cjs +0 -79
- package/dist/cjs/contracts/captcha/dist/data/captcha.json.cjs +0 -3374
- package/dist/cjs/contracts/captcha/dist/event-data/captcha.json.cjs +0 -3
- package/dist/cjs/contracts/captcha/dist/events/captcha.cjs +0 -23
- package/dist/cjs/contracts/captcha/dist/index.cjs +0 -44
- package/dist/cjs/contracts/captcha/dist/mixed-methods/captcha.cjs +0 -470
- package/dist/cjs/contracts/captcha/dist/query/captcha.cjs +0 -468
- package/dist/cjs/contracts/captcha/dist/shared/utils.cjs +0 -31
- package/dist/cjs/contracts/captcha/dist/tx-sign-and-send/captcha.cjs +0 -426
- package/dist/cjs/contracts/captcha/dist/types-arguments/captcha.cjs +0 -62
- package/dist/cjs/packages/datasets/dist/captcha/captcha.cjs +0 -143
- package/dist/cjs/packages/datasets/dist/captcha/dataset.cjs +0 -87
- package/dist/cjs/packages/datasets/dist/captcha/index.cjs +0 -27
- package/dist/cjs/packages/datasets/dist/captcha/merkle.cjs +0 -106
- package/dist/cjs/packages/datasets/dist/captcha/util.cjs +0 -16
- package/dist/cjs/packages/datasets/dist/index.cjs +0 -2
package/dist/modules/Manager.js
CHANGED
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
import { AccountNotFoundError } from '../api/errors.js';
|
|
2
|
-
import { ApiPromise
|
|
3
|
-
import { ProcaptchaConfigSchema } from '@prosopo/types';
|
|
4
|
-
import {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
78
|
+
console.log('onOpen event triggered');
|
|
65
79
|
},
|
|
66
80
|
onClose: () => {
|
|
67
|
-
console.
|
|
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 = () =>
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
114
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
145
|
-
blockNumber
|
|
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
|
-
|
|
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
|
-
//
|
|
162
|
-
const
|
|
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(
|
|
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:
|
|
294
|
+
user: account.account.address,
|
|
178
295
|
dapp: getDappAccount(),
|
|
179
296
|
commitmentId: submission[1],
|
|
180
|
-
blockNumber
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
423
|
+
const dappAccount = state.dappAccount;
|
|
424
|
+
return dappAccount;
|
|
440
425
|
};
|
|
441
|
-
const
|
|
426
|
+
const getBlockNumber = () => {
|
|
442
427
|
if (!state.blockNumber) {
|
|
443
428
|
throw new Error('Account not loaded');
|
|
444
429
|
}
|
|
445
|
-
|
|
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,
|