@prosopo/provider 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/api/captcha.cjs +115 -0
- package/dist/cjs/batch/commitments.cjs +155 -0
- package/dist/cjs/batch/index.cjs +4 -0
- package/dist/cjs/index.cjs +20 -0
- package/dist/cjs/tasks/calculateSolutions.cjs +67 -0
- package/dist/cjs/tasks/index.cjs +6 -0
- package/dist/cjs/tasks/tasks.cjs +396 -0
- package/dist/cjs/util.cjs +90 -0
- package/package.json +17 -10
- package/vite.cjs.config.ts +6 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const types = require("@prosopo/types");
|
|
4
|
+
const common = require("@prosopo/common");
|
|
5
|
+
const tasks = require("../tasks/tasks.cjs");
|
|
6
|
+
const util = require("../util.cjs");
|
|
7
|
+
const datasets = require("@prosopo/datasets");
|
|
8
|
+
const utilCrypto = require("@polkadot/util-crypto");
|
|
9
|
+
const express = require("express");
|
|
10
|
+
function prosopoRouter(env) {
|
|
11
|
+
const router = express.Router();
|
|
12
|
+
const tasks$1 = new tasks.Tasks(env);
|
|
13
|
+
router.get(
|
|
14
|
+
`${types.ApiPaths.GetCaptchaChallenge}/:${types.ApiParams.datasetId}/:${types.ApiParams.user}/:${types.ApiParams.dapp}/:${types.ApiParams.blockNumber}`,
|
|
15
|
+
async (req, res, next) => {
|
|
16
|
+
try {
|
|
17
|
+
const { blockNumber, datasetId, user, dapp } = types.CaptchaRequestBody.parse(req.params);
|
|
18
|
+
const api = env.api;
|
|
19
|
+
if (api === void 0) {
|
|
20
|
+
throw new Error("api not setup");
|
|
21
|
+
}
|
|
22
|
+
utilCrypto.validateAddress(user, false, api.registry.chainSS58);
|
|
23
|
+
const blockNumberParsed = util.parseBlockNumber(blockNumber);
|
|
24
|
+
await tasks$1.validateProviderWasRandomlyChosen(user, dapp, datasetId, blockNumberParsed);
|
|
25
|
+
const taskData = await tasks$1.getRandomCaptchasAndRequestHash(datasetId, user);
|
|
26
|
+
taskData.captchas = taskData.captchas.map((cwp) => ({
|
|
27
|
+
...cwp,
|
|
28
|
+
captcha: {
|
|
29
|
+
...cwp.captcha,
|
|
30
|
+
items: cwp.captcha.items.map((item) => datasets.parseCaptchaAssets(item, env.assetsResolver))
|
|
31
|
+
}
|
|
32
|
+
}));
|
|
33
|
+
return res.json(taskData);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return next(new common.ProsopoApiError(err, void 0, 400));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
router.post(types.ApiPaths.SubmitCaptchaSolution, async (req, res, next) => {
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = types.CaptchaSolutionBody.parse(req.body);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return next(new common.ProsopoApiError(err, void 0, 400));
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const result = await tasks$1.dappUserSolution(
|
|
48
|
+
parsed[types.ApiParams.user],
|
|
49
|
+
parsed[types.ApiParams.dapp],
|
|
50
|
+
parsed[types.ApiParams.requestHash],
|
|
51
|
+
parsed[types.ApiParams.captchas],
|
|
52
|
+
parsed[types.ApiParams.signature]
|
|
53
|
+
);
|
|
54
|
+
return res.json({
|
|
55
|
+
status: req.i18n.t(result.solutionApproved ? "API.CAPTCHA_PASSED" : "API.CAPTCHA_FAILED"),
|
|
56
|
+
...result
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return next(new common.ProsopoApiError(err, void 0, 400));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
router.post(types.ApiPaths.VerifyCaptchaSolution, async (req, res, next) => {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = types.VerifySolutionBody.parse(req.body);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return next(new common.ProsopoApiError(err, void 0, 400));
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
let solution;
|
|
71
|
+
let statusMessage = "API.USER_NOT_VERIFIED";
|
|
72
|
+
if (!parsed.commitmentId) {
|
|
73
|
+
solution = await tasks$1.getDappUserCommitmentByAccount(parsed.user);
|
|
74
|
+
} else {
|
|
75
|
+
solution = await tasks$1.getDappUserCommitmentById(parsed.commitmentId);
|
|
76
|
+
}
|
|
77
|
+
if (solution) {
|
|
78
|
+
let approved = false;
|
|
79
|
+
if (solution.status === types.CaptchaStatus.approved) {
|
|
80
|
+
statusMessage = "API.USER_VERIFIED";
|
|
81
|
+
approved = true;
|
|
82
|
+
}
|
|
83
|
+
return res.json({
|
|
84
|
+
status: req.t(statusMessage),
|
|
85
|
+
solutionApproved: approved,
|
|
86
|
+
commitmentId: solution.id
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return res.json({
|
|
90
|
+
status: req.t(statusMessage),
|
|
91
|
+
solutionApproved: false
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return next(new common.ProsopoApiError(err, void 0, 400));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
router.get(types.ApiPaths.GetProviderStatus, async (req, res, next) => {
|
|
98
|
+
try {
|
|
99
|
+
const status = await tasks$1.providerStatus();
|
|
100
|
+
return res.json({ status });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return next(new common.ProsopoApiError(err, void 0, 400));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
router.get(types.ApiPaths.GetProviderDetails, async (req, res, next) => {
|
|
106
|
+
try {
|
|
107
|
+
const details = await tasks$1.getProviderDetails();
|
|
108
|
+
return res.json(details);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return next(new common.ProsopoApiError(err, void 0, 400));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return router;
|
|
114
|
+
}
|
|
115
|
+
exports.prosopoRouter = prosopoRouter;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const types = require("@prosopo/types");
|
|
4
|
+
const util = require("@polkadot/util");
|
|
5
|
+
const contract = require("@prosopo/contract");
|
|
6
|
+
const util$1 = require("../util.cjs");
|
|
7
|
+
const utilCrypto = require("@polkadot/util-crypto");
|
|
8
|
+
const BN_TEN_THOUSAND = new util.BN(1e4);
|
|
9
|
+
const CONTRACT_METHOD_NAME = "providerCommitMany";
|
|
10
|
+
class BatchCommitmentsTask {
|
|
11
|
+
constructor(batchCommitConfig, contractApi, db, startNonce, logger) {
|
|
12
|
+
this.contract = contractApi;
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.batchCommitConfig = batchCommitConfig;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.nonce = startNonce;
|
|
17
|
+
}
|
|
18
|
+
async run() {
|
|
19
|
+
const taskId = utilCrypto.randomAsHex(32);
|
|
20
|
+
const taskRunning = await util$1.checkIfTaskIsRunning(types.ScheduledTaskNames.BatchCommitment, this.db);
|
|
21
|
+
if (!taskRunning) {
|
|
22
|
+
const intervalExceeded = await this.batchIntervalExceeded();
|
|
23
|
+
if (intervalExceeded) {
|
|
24
|
+
try {
|
|
25
|
+
await this.db.storeScheduledTaskStatus(
|
|
26
|
+
taskId,
|
|
27
|
+
types.ScheduledTaskNames.BatchCommitment,
|
|
28
|
+
types.ScheduledTaskStatus.Running
|
|
29
|
+
);
|
|
30
|
+
const commitments = await this.getCommitments();
|
|
31
|
+
if (commitments.length > 0) {
|
|
32
|
+
this.logger.info(`Found ${commitments.length} commitments to commit`);
|
|
33
|
+
const { extrinsics, ids: commitmentIds } = await this.createExtrinsics(commitments);
|
|
34
|
+
await contract.batch(this.contract.contract, this.contract.pair, extrinsics, this.logger);
|
|
35
|
+
await this.flagBatchedCommitments(commitmentIds);
|
|
36
|
+
await this.db.storeScheduledTaskStatus(
|
|
37
|
+
taskId,
|
|
38
|
+
types.ScheduledTaskNames.BatchCommitment,
|
|
39
|
+
types.ScheduledTaskStatus.Completed,
|
|
40
|
+
{
|
|
41
|
+
data: {
|
|
42
|
+
commitmentIds: commitments.filter((commitment) => commitmentIds.indexOf(commitment.id) > -1).map((c) => c.id)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
const err = e;
|
|
49
|
+
this.logger.error(e);
|
|
50
|
+
await this.db.storeScheduledTaskStatus(
|
|
51
|
+
taskId,
|
|
52
|
+
types.ScheduledTaskNames.BatchCommitment,
|
|
53
|
+
types.ScheduledTaskStatus.Failed,
|
|
54
|
+
{
|
|
55
|
+
error: JSON.stringify(e && err.message ? err.message : e)
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async createExtrinsics(commitments) {
|
|
63
|
+
const txs = [];
|
|
64
|
+
const fragment = this.contract.abi.findMessage(CONTRACT_METHOD_NAME);
|
|
65
|
+
const batchedCommitmentIds = [];
|
|
66
|
+
let totalRefTime = new util.BN(0);
|
|
67
|
+
let totalProofSize = new util.BN(0);
|
|
68
|
+
let totalFee = new util.BN(0);
|
|
69
|
+
const maxBlockWeight = this.contract.api.consts.system.blockWeights.maxBlock;
|
|
70
|
+
const commitmentArray = [];
|
|
71
|
+
let extrinsic;
|
|
72
|
+
for (const commitment of commitments) {
|
|
73
|
+
const commit = this.convertCommit(commitment);
|
|
74
|
+
commitmentArray.push(commit);
|
|
75
|
+
const encodedArgs = contract.encodeStringArgs(this.contract.abi, fragment, [commitmentArray]);
|
|
76
|
+
const buildExtrinsicResult = await this.contract.getExtrinsicAndGasEstimates(
|
|
77
|
+
"providerCommitMany",
|
|
78
|
+
encodedArgs
|
|
79
|
+
);
|
|
80
|
+
extrinsic = buildExtrinsicResult.extrinsic;
|
|
81
|
+
const { options, storageDeposit } = buildExtrinsicResult;
|
|
82
|
+
let paymentInfo;
|
|
83
|
+
try {
|
|
84
|
+
paymentInfo = (await extrinsic.paymentInfo(this.contract.pair)).partialFee.toBn();
|
|
85
|
+
this.logger.debug(`${CONTRACT_METHOD_NAME} paymentInfo:`, paymentInfo.toNumber());
|
|
86
|
+
} catch (e) {
|
|
87
|
+
paymentInfo = new util.BN(0);
|
|
88
|
+
}
|
|
89
|
+
totalRefTime = totalRefTime.add(
|
|
90
|
+
this.contract.api.registry.createType("WeightV2", options.gasLimit).refTime.toBn()
|
|
91
|
+
);
|
|
92
|
+
totalProofSize = totalProofSize.add(
|
|
93
|
+
this.contract.api.registry.createType("WeightV2", options.gasLimit).proofSize.toBn()
|
|
94
|
+
);
|
|
95
|
+
totalFee = totalFee.add(paymentInfo.add(storageDeposit.asCharge.toBn()));
|
|
96
|
+
const extrinsicTooHigh = this.extrinsicTooHigh(totalRefTime, totalProofSize, maxBlockWeight);
|
|
97
|
+
this.logger.debug(
|
|
98
|
+
"Free balance",
|
|
99
|
+
"`",
|
|
100
|
+
(await this.contract.api.query.system.account(this.contract.pair.address)).data.free.toBn().div(contract.oneUnit(this.contract.api)).toString(),
|
|
101
|
+
"`",
|
|
102
|
+
"UNIT"
|
|
103
|
+
);
|
|
104
|
+
this.logger.debug(
|
|
105
|
+
"Total Fee `",
|
|
106
|
+
totalFee.div(contract.oneUnit(this.contract.api)).toString(),
|
|
107
|
+
"`",
|
|
108
|
+
"UNIT"
|
|
109
|
+
);
|
|
110
|
+
const feeTooHigh = totalFee.gt(
|
|
111
|
+
(await this.contract.api.query.system.account(this.contract.pair.address)).data.free.toBn()
|
|
112
|
+
);
|
|
113
|
+
if (extrinsicTooHigh || feeTooHigh) {
|
|
114
|
+
const msg = extrinsicTooHigh ? "Max batch extrinsic percentage reached" : "Fee too high";
|
|
115
|
+
this.logger.warn(msg);
|
|
116
|
+
break;
|
|
117
|
+
} else {
|
|
118
|
+
batchedCommitmentIds.push(commitment.id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!extrinsic) {
|
|
122
|
+
throw new contract.ProsopoContractError("No extrinsics created");
|
|
123
|
+
}
|
|
124
|
+
txs.push(extrinsic);
|
|
125
|
+
this.logger.info(`${txs.length} transactions will be batched`);
|
|
126
|
+
this.logger.debug("totalRefTime:", totalRefTime.toString());
|
|
127
|
+
this.logger.debug("totalProofSize:", totalProofSize.toString());
|
|
128
|
+
return { extrinsics: txs, ids: batchedCommitmentIds, totalFee, totalRefTime, totalProofSize };
|
|
129
|
+
}
|
|
130
|
+
extrinsicTooHigh(totalRefTime, totalProofSize, maxBlockWeight) {
|
|
131
|
+
return totalRefTime.mul(BN_TEN_THOUSAND).div(maxBlockWeight.refTime.toBn()).toNumber() / 100 > this.batchCommitConfig.maxBatchExtrinsicPercentage;
|
|
132
|
+
}
|
|
133
|
+
async batchIntervalExceeded() {
|
|
134
|
+
const lastTime = await this.db.getLastBatchCommitTime();
|
|
135
|
+
return Date.now() - lastTime.getSeconds() > this.batchCommitConfig.interval;
|
|
136
|
+
}
|
|
137
|
+
async getCommitments() {
|
|
138
|
+
return await this.db.getUnbatchedDappUserCommitments();
|
|
139
|
+
}
|
|
140
|
+
async flagBatchedCommitments(commitmentIds) {
|
|
141
|
+
await this.db.flagBatchedDappUserCommitments(commitmentIds);
|
|
142
|
+
}
|
|
143
|
+
convertCommit(commitment) {
|
|
144
|
+
const { batched, processed, userSignature, requestedAt, completedAt, ...commit } = commitment;
|
|
145
|
+
return {
|
|
146
|
+
...commit,
|
|
147
|
+
userSignaturePart1: userSignature.slice(0, userSignature.length / 2),
|
|
148
|
+
userSignaturePart2: userSignature.slice(userSignature.length / 2),
|
|
149
|
+
// to satisfy typescript
|
|
150
|
+
requestedAt: new util.BN(requestedAt).toNumber(),
|
|
151
|
+
completedAt: new util.BN(completedAt).toNumber()
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.BatchCommitmentsTask = BatchCommitmentsTask;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
require("./tasks/index.cjs");
|
|
4
|
+
const util = require("./util.cjs");
|
|
5
|
+
require("./batch/index.cjs");
|
|
6
|
+
const captcha = require("./api/captcha.cjs");
|
|
7
|
+
const tasks = require("./tasks/tasks.cjs");
|
|
8
|
+
const calculateSolutions = require("./tasks/calculateSolutions.cjs");
|
|
9
|
+
const commitments = require("./batch/commitments.cjs");
|
|
10
|
+
exports.calculateNewSolutions = util.calculateNewSolutions;
|
|
11
|
+
exports.checkIfTaskIsRunning = util.checkIfTaskIsRunning;
|
|
12
|
+
exports.encodeStringAddress = util.encodeStringAddress;
|
|
13
|
+
exports.parseBlockNumber = util.parseBlockNumber;
|
|
14
|
+
exports.promiseQueue = util.promiseQueue;
|
|
15
|
+
exports.shuffleArray = util.shuffleArray;
|
|
16
|
+
exports.updateSolutions = util.updateSolutions;
|
|
17
|
+
exports.prosopoRouter = captcha.prosopoRouter;
|
|
18
|
+
exports.Tasks = tasks.Tasks;
|
|
19
|
+
exports.CalculateSolutionsTask = calculateSolutions.CalculateSolutionsTask;
|
|
20
|
+
exports.BatchCommitmentsTask = commitments.BatchCommitmentsTask;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const types = require("@prosopo/types");
|
|
4
|
+
const common = require("@prosopo/common");
|
|
5
|
+
const tasks = require("./tasks.cjs");
|
|
6
|
+
const util = require("../util.cjs");
|
|
7
|
+
const datasets = require("@prosopo/datasets");
|
|
8
|
+
class CalculateSolutionsTask extends tasks.Tasks {
|
|
9
|
+
constructor(env) {
|
|
10
|
+
super(env);
|
|
11
|
+
this.logger = common.getLogger(env.config.logLevel, "CalculateSolutionsTask");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Apply new captcha solutions to captcha dataset and recalculate merkle tree
|
|
15
|
+
*/
|
|
16
|
+
async run() {
|
|
17
|
+
try {
|
|
18
|
+
const taskRunning = await util.checkIfTaskIsRunning(types.ScheduledTaskNames.CalculateSolution, this.db);
|
|
19
|
+
if (!taskRunning) {
|
|
20
|
+
const provider = (await this.contract.methods.getProvider(this.contract.pair.address, {})).value.unwrap().unwrap();
|
|
21
|
+
const unsolvedCaptchas = await this.db.getAllCaptchasByDatasetId(
|
|
22
|
+
provider.datasetId.toString(),
|
|
23
|
+
types.CaptchaStates.Unsolved
|
|
24
|
+
);
|
|
25
|
+
if (!unsolvedCaptchas) {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
const unsolvedSorted = unsolvedCaptchas.sort(datasets.captchaSort);
|
|
29
|
+
this.logger.info(`There are ${unsolvedSorted.length} unsolved CAPTCHA challenges`);
|
|
30
|
+
const requiredNumberOfSolutions = this.captchaSolutionConfig.requiredNumberOfSolutions;
|
|
31
|
+
const winningPercentage = this.captchaSolutionConfig.solutionWinningPercentage;
|
|
32
|
+
const winningNumberOfSolutions = Math.round(requiredNumberOfSolutions * (winningPercentage / 100));
|
|
33
|
+
if (unsolvedSorted && unsolvedSorted.length > 0) {
|
|
34
|
+
const captchaIds = unsolvedSorted.map((captcha) => captcha.captchaId);
|
|
35
|
+
const solutions = await this.db.getAllDappUserSolutions(captchaIds) || [];
|
|
36
|
+
const solutionsToUpdate = util.calculateNewSolutions(solutions, winningNumberOfSolutions);
|
|
37
|
+
if (solutionsToUpdate.rows().length > 0) {
|
|
38
|
+
this.logger.info(
|
|
39
|
+
`There are ${solutionsToUpdate.rows().length} CAPTCHA challenges to update with solutions`
|
|
40
|
+
);
|
|
41
|
+
try {
|
|
42
|
+
const captchaIdsToUpdate = [...solutionsToUpdate["captchaId"].values()];
|
|
43
|
+
const commitmentIds = solutions.filter((s) => captchaIdsToUpdate.indexOf(s.captchaId) > -1).map((s) => s.commitmentId);
|
|
44
|
+
const dataset = await this.db.getDataset(provider.datasetId.toString());
|
|
45
|
+
dataset.captchas = util.updateSolutions(solutionsToUpdate, dataset.captchas, this.logger);
|
|
46
|
+
await this.providerSetDataset(dataset);
|
|
47
|
+
await this.db.flagProcessedDappUserSolutions(captchaIdsToUpdate);
|
|
48
|
+
await this.db.flagProcessedDappUserCommitments(commitmentIds);
|
|
49
|
+
await this.db.removeCaptchas(captchaIdsToUpdate);
|
|
50
|
+
return solutionsToUpdate.rows().length;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
this.logger.error(error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
} else {
|
|
57
|
+
this.logger.info(`There are no CAPTCHA challenges that require their solutions to be updated`);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new common.ProsopoEnvError(error, "GENERAL.CALCULATE_CAPTCHA_SOLUTION");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.CalculateSolutionsTask = CalculateSolutionsTask;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const tasks = require("./tasks.cjs");
|
|
4
|
+
const calculateSolutions = require("./calculateSolutions.cjs");
|
|
5
|
+
exports.Tasks = tasks.Tasks;
|
|
6
|
+
exports.CalculateSolutionsTask = calculateSolutions.CalculateSolutionsTask;
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const types = require("@prosopo/types");
|
|
4
|
+
const datasets = require("@prosopo/datasets");
|
|
5
|
+
const common = require("@prosopo/common");
|
|
6
|
+
const contract = require("@prosopo/contract");
|
|
7
|
+
const util$2 = require("@prosopo/util");
|
|
8
|
+
const util$1 = require("@polkadot/util");
|
|
9
|
+
const utilCrypto = require("@polkadot/util-crypto");
|
|
10
|
+
const util = require("../util.cjs");
|
|
11
|
+
class Tasks {
|
|
12
|
+
constructor(env) {
|
|
13
|
+
if (!env.contractInterface) {
|
|
14
|
+
throw new common.ProsopoEnvError(
|
|
15
|
+
"CONTRACT.CONTRACT_UNDEFINED",
|
|
16
|
+
this.constructor.name,
|
|
17
|
+
{},
|
|
18
|
+
{ contractAddress: env.contractAddress }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
this.config = env.config;
|
|
22
|
+
this.contract = env.contractInterface;
|
|
23
|
+
this.db = env.db;
|
|
24
|
+
this.captchaConfig = env.config.captchas;
|
|
25
|
+
this.captchaSolutionConfig = env.config.captchaSolutions;
|
|
26
|
+
this.logger = common.getLogger(env.config.logLevel, "Tasks");
|
|
27
|
+
}
|
|
28
|
+
async providerSetDatasetFromFile(file) {
|
|
29
|
+
const datasetRaw = datasets.parseCaptchaDataset(file);
|
|
30
|
+
this.logger.debug("Parsed raw data set");
|
|
31
|
+
return await this.providerSetDataset(datasetRaw);
|
|
32
|
+
}
|
|
33
|
+
async providerSetDataset(datasetRaw) {
|
|
34
|
+
var _a;
|
|
35
|
+
if (datasetRaw.captchas.length < this.config.captchas.solved.count + this.config.captchas.unsolved.count) {
|
|
36
|
+
throw new common.ProsopoEnvError("DATASET.CAPTCHAS_COUNT_LESS_THAN_CONFIGURED", this.providerSetDataset.name);
|
|
37
|
+
}
|
|
38
|
+
const solutions = datasetRaw.captchas.map((captcha) => captcha.solution ? 1 : 0).reduce((partialSum, b) => partialSum + b, 0);
|
|
39
|
+
if (solutions < this.config.captchas.solved.count) {
|
|
40
|
+
throw new common.ProsopoEnvError("DATASET.SOLUTIONS_COUNT_LESS_THAN_CONFIGURED", this.providerSetDataset.name);
|
|
41
|
+
}
|
|
42
|
+
const dataset = await datasets.buildDataset(datasetRaw);
|
|
43
|
+
if (!dataset.datasetId || !dataset.datasetContentId) {
|
|
44
|
+
throw new common.ProsopoEnvError("DATASET.DATASET_ID_UNDEFINED", this.providerSetDataset.name);
|
|
45
|
+
}
|
|
46
|
+
await ((_a = this.db) == null ? void 0 : _a.storeDataset(dataset));
|
|
47
|
+
await contract.wrapQuery(this.contract.query.providerSetDataset, this.contract.query)(
|
|
48
|
+
dataset.datasetId,
|
|
49
|
+
dataset.datasetContentId
|
|
50
|
+
);
|
|
51
|
+
const txResult = await this.contract.methods.providerSetDataset(dataset.datasetId, dataset.datasetContentId, {
|
|
52
|
+
value: 0
|
|
53
|
+
});
|
|
54
|
+
return txResult.result;
|
|
55
|
+
}
|
|
56
|
+
// Other tasks
|
|
57
|
+
/**
|
|
58
|
+
* @description Get random captchas that are solved or not solved, along with the merkle proof for each
|
|
59
|
+
* @param {string} datasetId the id of the data set
|
|
60
|
+
* @param {boolean} solved `true` when captcha is solved
|
|
61
|
+
* @param {number} size the number of records to be returned
|
|
62
|
+
*/
|
|
63
|
+
async getCaptchaWithProof(datasetId, solved, size) {
|
|
64
|
+
const captchaDocs = await this.db.getRandomCaptcha(solved, datasetId, size);
|
|
65
|
+
if (captchaDocs) {
|
|
66
|
+
const captchas = [];
|
|
67
|
+
for (const captcha of captchaDocs) {
|
|
68
|
+
const datasetDetails = await this.db.getDatasetDetails(datasetId);
|
|
69
|
+
const tree = new datasets.CaptchaMerkleTree();
|
|
70
|
+
if (datasetDetails.contentTree) {
|
|
71
|
+
tree.layers = datasetDetails.contentTree;
|
|
72
|
+
const proof = tree.proof(captcha.captchaContentId);
|
|
73
|
+
delete captcha.solution;
|
|
74
|
+
captcha.items = util.shuffleArray(captcha.items);
|
|
75
|
+
captchas.push({ captcha, proof });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return captchas;
|
|
79
|
+
}
|
|
80
|
+
throw new common.ProsopoEnvError(
|
|
81
|
+
"DATABASE.CAPTCHA_GET_FAILED",
|
|
82
|
+
this.getCaptchaWithProof.name,
|
|
83
|
+
{},
|
|
84
|
+
{ datasetId, solved, size }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate and store the text captcha solution(s) from the Dapp User in a web2 environment
|
|
89
|
+
* @param {string} userAccount
|
|
90
|
+
* @param {string} dappAccount
|
|
91
|
+
* @param {string} requestHash
|
|
92
|
+
* @param {JSON} captchas
|
|
93
|
+
* @param {string} signature
|
|
94
|
+
* @return {Promise<DappUserSolutionResult>} result containing the contract event
|
|
95
|
+
*/
|
|
96
|
+
async dappUserSolution(userAccount, dappAccount, requestHash, captchas, signature) {
|
|
97
|
+
if (!await this.dappIsActive(dappAccount)) {
|
|
98
|
+
throw new common.ProsopoEnvError("CONTRACT.DAPP_NOT_ACTIVE", this.getPaymentInfo.name, {}, { dappAccount });
|
|
99
|
+
}
|
|
100
|
+
const verification = utilCrypto.signatureVerify(util$1.stringToHex(requestHash), signature, userAccount);
|
|
101
|
+
if (!verification.isValid) {
|
|
102
|
+
throw new common.ProsopoEnvError("GENERAL.INVALID_SIGNATURE", this.dappUserSolution.name, {}, { userAccount });
|
|
103
|
+
}
|
|
104
|
+
let response = {
|
|
105
|
+
captchas: [],
|
|
106
|
+
solutionApproved: false
|
|
107
|
+
};
|
|
108
|
+
const { storedCaptchas, receivedCaptchas, captchaIds } = await this.validateReceivedCaptchasAgainstStoredCaptchas(captchas);
|
|
109
|
+
const { tree, commitmentId } = await this.buildTreeAndGetCommitmentId(receivedCaptchas);
|
|
110
|
+
const provider = (await this.contract.methods.getProvider(this.contract.pair.address, {})).value.unwrap().unwrap();
|
|
111
|
+
const pendingRecord = await this.db.getDappUserPending(requestHash);
|
|
112
|
+
const pendingRequest = await this.validateDappUserSolutionRequestIsPending(
|
|
113
|
+
requestHash,
|
|
114
|
+
pendingRecord,
|
|
115
|
+
userAccount,
|
|
116
|
+
captchaIds
|
|
117
|
+
);
|
|
118
|
+
const userSignature = util$1.hexToU8a(signature);
|
|
119
|
+
const blockNumber = (await contract.getBlockNumber(this.contract.api)).toNumber();
|
|
120
|
+
if (pendingRequest) {
|
|
121
|
+
const commit = {
|
|
122
|
+
id: commitmentId,
|
|
123
|
+
userAccount,
|
|
124
|
+
dappContract: dappAccount,
|
|
125
|
+
providerAccount: this.contract.pair.address,
|
|
126
|
+
datasetId: provider.datasetId.toString(),
|
|
127
|
+
status: types.CaptchaStatus.pending,
|
|
128
|
+
userSignature: Array.from(userSignature),
|
|
129
|
+
requestedAt: pendingRecord.requestedAtBlock,
|
|
130
|
+
// TODO is this correct or should it be block number?
|
|
131
|
+
completedAt: blockNumber,
|
|
132
|
+
processed: false,
|
|
133
|
+
batched: false
|
|
134
|
+
};
|
|
135
|
+
await this.db.storeDappUserSolution(receivedCaptchas, commit);
|
|
136
|
+
if (datasets.compareCaptchaSolutions(receivedCaptchas, storedCaptchas)) {
|
|
137
|
+
response = {
|
|
138
|
+
captchas: captchaIds.map((id) => ({
|
|
139
|
+
captchaId: id,
|
|
140
|
+
proof: tree.proof(id)
|
|
141
|
+
})),
|
|
142
|
+
solutionApproved: true
|
|
143
|
+
};
|
|
144
|
+
await this.db.approveDappUserCommitment(commitmentId);
|
|
145
|
+
} else {
|
|
146
|
+
response = {
|
|
147
|
+
captchas: captchaIds.map((id) => ({
|
|
148
|
+
captchaId: id,
|
|
149
|
+
proof: [[]]
|
|
150
|
+
})),
|
|
151
|
+
solutionApproved: false
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return response;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Validate that the dapp is active in the contract
|
|
159
|
+
*/
|
|
160
|
+
async dappIsActive(dappAccount) {
|
|
161
|
+
const dapp = await contract.wrapQuery(this.contract.query.getDapp, this.contract.query)(dappAccount);
|
|
162
|
+
return dapp.status.toString() === "Active";
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Gets provider status in contract
|
|
166
|
+
*/
|
|
167
|
+
async providerStatus() {
|
|
168
|
+
try {
|
|
169
|
+
const provider = await contract.wrapQuery(
|
|
170
|
+
this.contract.query.getProvider,
|
|
171
|
+
this.contract.query
|
|
172
|
+
)(this.contract.pair.address);
|
|
173
|
+
return { status: provider.status ? "Registered" : "Unregistered" };
|
|
174
|
+
} catch (e) {
|
|
175
|
+
return { status: "Unregistered" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Validate length of received captchas array matches length of captchas found in database
|
|
180
|
+
* Validate that the datasetId is the same for all captchas and is equal to the datasetId on the stored captchas
|
|
181
|
+
*/
|
|
182
|
+
async validateReceivedCaptchasAgainstStoredCaptchas(captchas) {
|
|
183
|
+
const receivedCaptchas = datasets.parseAndSortCaptchaSolutions(captchas);
|
|
184
|
+
const captchaIds = receivedCaptchas.map((captcha) => captcha.captchaId);
|
|
185
|
+
const storedCaptchas = await this.db.getCaptchaById(captchaIds);
|
|
186
|
+
if (!storedCaptchas || receivedCaptchas.length !== storedCaptchas.length) {
|
|
187
|
+
throw new common.ProsopoEnvError(
|
|
188
|
+
"CAPTCHA.INVALID_CAPTCHA_ID",
|
|
189
|
+
this.validateReceivedCaptchasAgainstStoredCaptchas.name,
|
|
190
|
+
{},
|
|
191
|
+
captchas
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (!storedCaptchas.every((captcha) => captcha.datasetId === util$2.at(storedCaptchas, 0).datasetId)) {
|
|
195
|
+
throw new common.ProsopoEnvError(
|
|
196
|
+
"CAPTCHA.DIFFERENT_DATASET_IDS",
|
|
197
|
+
this.validateReceivedCaptchasAgainstStoredCaptchas.name,
|
|
198
|
+
{},
|
|
199
|
+
captchas
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return { storedCaptchas, receivedCaptchas, captchaIds };
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Build merkle tree and get commitment from contract, returning the tree, commitment, and commitmentId
|
|
206
|
+
* @param {CaptchaSolution[]} captchaSolutions
|
|
207
|
+
* @returns {Promise<{ tree: CaptchaMerkleTree, commitment: CaptchaSolutionCommitment, commitmentId: string }>}
|
|
208
|
+
*/
|
|
209
|
+
async buildTreeAndGetCommitmentId(captchaSolutions) {
|
|
210
|
+
var _a;
|
|
211
|
+
const tree = new datasets.CaptchaMerkleTree();
|
|
212
|
+
const solutionsHashed = captchaSolutions.map((captcha) => datasets.computeCaptchaSolutionHash(captcha));
|
|
213
|
+
tree.build(solutionsHashed);
|
|
214
|
+
const commitmentId = (_a = tree.root) == null ? void 0 : _a.hash;
|
|
215
|
+
if (!commitmentId) {
|
|
216
|
+
throw new common.ProsopoEnvError(
|
|
217
|
+
"CONTRACT.CAPTCHA_SOLUTION_COMMITMENT_DOES_NOT_EXIST",
|
|
218
|
+
this.buildTreeAndGetCommitmentId.name,
|
|
219
|
+
{},
|
|
220
|
+
{ commitmentId }
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return { tree, commitmentId };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Validate that a Dapp User is responding to their own pending captcha request
|
|
227
|
+
* @param {string} requestHash
|
|
228
|
+
* @param {PendingCaptchaRequest} pendingRecord
|
|
229
|
+
* @param {string} userAccount
|
|
230
|
+
* @param {string[]} captchaIds
|
|
231
|
+
*/
|
|
232
|
+
async validateDappUserSolutionRequestIsPending(requestHash, pendingRecord, userAccount, captchaIds) {
|
|
233
|
+
const currentTime = Date.now();
|
|
234
|
+
if (pendingRecord.deadlineTimestamp < currentTime) {
|
|
235
|
+
this.logger.info("Deadline for responding to captcha has expired");
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if (pendingRecord) {
|
|
239
|
+
const pendingHashComputed = datasets.computePendingRequestHash(captchaIds, userAccount, pendingRecord.salt);
|
|
240
|
+
return requestHash === pendingHashComputed;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get two random captchas from specified dataset, create the response and store a hash of it, marked as pending
|
|
246
|
+
* @param {string} datasetId
|
|
247
|
+
* @param {string} userAccount
|
|
248
|
+
*/
|
|
249
|
+
async getRandomCaptchasAndRequestHash(datasetId, userAccount) {
|
|
250
|
+
const dataset = await this.db.getDatasetDetails(datasetId);
|
|
251
|
+
if (!dataset) {
|
|
252
|
+
throw new common.ProsopoEnvError("DATABASE.DATASET_GET_FAILED");
|
|
253
|
+
}
|
|
254
|
+
const unsolvedCount = Math.abs(Math.trunc(this.captchaConfig.unsolved.count));
|
|
255
|
+
const solvedCount = Math.abs(Math.trunc(this.captchaConfig.solved.count));
|
|
256
|
+
if (!solvedCount) {
|
|
257
|
+
throw new common.ProsopoEnvError("CONFIG.INVALID_CAPTCHA_NUMBER");
|
|
258
|
+
}
|
|
259
|
+
const solved = await this.getCaptchaWithProof(datasetId, true, solvedCount);
|
|
260
|
+
let unsolved = [];
|
|
261
|
+
if (unsolvedCount) {
|
|
262
|
+
unsolved = await this.getCaptchaWithProof(datasetId, false, unsolvedCount);
|
|
263
|
+
}
|
|
264
|
+
const captchas = util.shuffleArray([...solved, ...unsolved]);
|
|
265
|
+
const salt = utilCrypto.randomAsHex();
|
|
266
|
+
const requestHash = datasets.computePendingRequestHash(
|
|
267
|
+
captchas.map((c) => c.captcha.captchaId),
|
|
268
|
+
userAccount,
|
|
269
|
+
salt
|
|
270
|
+
);
|
|
271
|
+
const currentTime = Date.now();
|
|
272
|
+
const timeLimit = captchas.map((captcha) => captcha.captcha.timeLimitMs || 3e4).reduce((a, b) => a + b, 0);
|
|
273
|
+
const deadlineTs = timeLimit + currentTime;
|
|
274
|
+
const currentBlockNumber = await contract.getBlockNumber(this.contract.api);
|
|
275
|
+
await this.db.storeDappUserPending(userAccount, requestHash, salt, deadlineTs, currentBlockNumber.toNumber());
|
|
276
|
+
return { captchas, requestHash };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Block by block search for blockNo
|
|
280
|
+
*/
|
|
281
|
+
async isRecentBlock(contract2, header, blockNo, depth = this.captchaSolutionConfig.captchaBlockRecency) {
|
|
282
|
+
if (depth == 0) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const headerBlockNo = header.number.toPrimitive();
|
|
286
|
+
if (headerBlockNo === blockNo) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
const parent = await contract2.api.rpc.chain.getBlock(header.parentHash);
|
|
290
|
+
return this.isRecentBlock(contract2, parent.block.header, blockNo, depth - 1);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Validate that provided `datasetId` was a result of calling `get_random_provider` method
|
|
294
|
+
* @param {string} userAccount - Same user that called `get_random_provider`
|
|
295
|
+
* @param {string} dappContractAccount - account of dapp that is requesting captcha
|
|
296
|
+
* @param {string} datasetId - `captcha_dataset_id` from the result of `get_random_provider`
|
|
297
|
+
* @param {string} blockNumber - Block on which `get_random_provider` was called
|
|
298
|
+
*/
|
|
299
|
+
async validateProviderWasRandomlyChosen(userAccount, dappContractAccount, datasetId, blockNumber) {
|
|
300
|
+
const contract2 = await this.contract.contract;
|
|
301
|
+
if (!contract2) {
|
|
302
|
+
throw new common.ProsopoEnvError("CONTRACT.CONTRACT_UNDEFINED", this.validateProviderWasRandomlyChosen.name);
|
|
303
|
+
}
|
|
304
|
+
const header = await contract2.api.rpc.chain.getHeader();
|
|
305
|
+
const isBlockNoValid = await this.isRecentBlock(contract2, header, blockNumber);
|
|
306
|
+
if (!isBlockNoValid) {
|
|
307
|
+
throw new common.ProsopoEnvError(
|
|
308
|
+
"CAPTCHA.INVALID_BLOCK_NO",
|
|
309
|
+
this.validateProviderWasRandomlyChosen.name,
|
|
310
|
+
{},
|
|
311
|
+
{
|
|
312
|
+
userAccount,
|
|
313
|
+
dappContractAccount,
|
|
314
|
+
datasetId,
|
|
315
|
+
header,
|
|
316
|
+
blockNumber
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const block = await contract2.api.rpc.chain.getBlockHash(blockNumber);
|
|
321
|
+
const randomProviderAndBlockNo = await this.contract.queryAtBlock(
|
|
322
|
+
block,
|
|
323
|
+
"getRandomActiveProvider",
|
|
324
|
+
[userAccount, dappContractAccount]
|
|
325
|
+
);
|
|
326
|
+
if (datasetId.toString().localeCompare(randomProviderAndBlockNo.provider.datasetId.toString())) {
|
|
327
|
+
throw new common.ProsopoEnvError(
|
|
328
|
+
"DATASET.INVALID_DATASET_ID",
|
|
329
|
+
this.validateProviderWasRandomlyChosen.name,
|
|
330
|
+
{},
|
|
331
|
+
randomProviderAndBlockNo
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get payment info for a transaction
|
|
337
|
+
* @param {string} userAccount
|
|
338
|
+
* @param {string} blockHash
|
|
339
|
+
* @param {string} txHash
|
|
340
|
+
* @returns {Promise<RuntimeDispatchInfo|null>}
|
|
341
|
+
*/
|
|
342
|
+
async getPaymentInfo(userAccount, blockHash, txHash) {
|
|
343
|
+
const signedBlock = await this.contract.api.rpc.chain.getBlock(blockHash);
|
|
344
|
+
if (!signedBlock) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const extrinsic = signedBlock.block.extrinsics.find((extrinsic2) => extrinsic2.hash.toString() === txHash);
|
|
348
|
+
if (!extrinsic || extrinsic.signer.toString() !== userAccount) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const paymentInfo = await this.contract.api.rpc.payment.queryInfo(
|
|
352
|
+
extrinsic.toHex(),
|
|
353
|
+
blockHash
|
|
354
|
+
);
|
|
355
|
+
if (!paymentInfo) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return paymentInfo;
|
|
359
|
+
}
|
|
360
|
+
/*
|
|
361
|
+
* Get dapp user solution from database
|
|
362
|
+
*/
|
|
363
|
+
async getDappUserCommitmentById(commitmentId) {
|
|
364
|
+
const dappUserSolution = await this.db.getDappUserCommitmentById(commitmentId);
|
|
365
|
+
if (!dappUserSolution) {
|
|
366
|
+
throw new common.ProsopoEnvError(
|
|
367
|
+
"CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND",
|
|
368
|
+
this.getDappUserCommitmentById.name,
|
|
369
|
+
{},
|
|
370
|
+
{ commitmentId }
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return dappUserSolution;
|
|
374
|
+
}
|
|
375
|
+
/* Check if dapp user has verified solution in cache */
|
|
376
|
+
async getDappUserCommitmentByAccount(userAccount) {
|
|
377
|
+
const dappUserSolutions = await this.db.getDappUserCommitmentByAccount(userAccount);
|
|
378
|
+
if (dappUserSolutions.length > 0) {
|
|
379
|
+
for (const dappUserSolution of dappUserSolutions) {
|
|
380
|
+
if (dappUserSolution.status === types.ArgumentTypes.CaptchaStatus.approved) {
|
|
381
|
+
return dappUserSolution;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
/* Returns public details of provider */
|
|
388
|
+
async getProviderDetails() {
|
|
389
|
+
return await contract.wrapQuery(this.contract.query.getProvider, this.contract.query)(this.contract.pair.address);
|
|
390
|
+
}
|
|
391
|
+
/** Get the dataset from the databse */
|
|
392
|
+
async getProviderDataset(datasetId) {
|
|
393
|
+
return await this.db.getDataset(datasetId);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
exports.Tasks = Tasks;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const types = require("@prosopo/types");
|
|
4
|
+
const common = require("@prosopo/common");
|
|
5
|
+
const util$1 = require("@prosopo/util");
|
|
6
|
+
const keyring = require("@polkadot/keyring");
|
|
7
|
+
const util = require("@polkadot/util");
|
|
8
|
+
const pl = require("nodejs-polars");
|
|
9
|
+
function encodeStringAddress(address) {
|
|
10
|
+
try {
|
|
11
|
+
return keyring.encodeAddress(util.isHex(address) ? util.hexToU8a(address) : keyring.decodeAddress(address));
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw new common.ProsopoEnvError(error, "CONTRACT.INVALID_ADDRESS", {}, address);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function shuffleArray(array) {
|
|
17
|
+
for (let arrayIndex = array.length - 1; arrayIndex > 0; arrayIndex--) {
|
|
18
|
+
const randIndex = Math.floor(Math.random() * (arrayIndex + 1));
|
|
19
|
+
const tmp = util$1.at(array, randIndex, { required: false });
|
|
20
|
+
array[randIndex] = util$1.at(array, arrayIndex, { required: false });
|
|
21
|
+
array[arrayIndex] = tmp;
|
|
22
|
+
}
|
|
23
|
+
return array;
|
|
24
|
+
}
|
|
25
|
+
async function promiseQueue(array) {
|
|
26
|
+
const ret = [];
|
|
27
|
+
await [...array, () => Promise.resolve(void 0)].reduce((promise, curr, i) => {
|
|
28
|
+
return promise.then((res) => {
|
|
29
|
+
if (res) {
|
|
30
|
+
ret.push({ data: res });
|
|
31
|
+
}
|
|
32
|
+
return curr();
|
|
33
|
+
}).catch((err) => {
|
|
34
|
+
ret.push({ data: err });
|
|
35
|
+
return curr();
|
|
36
|
+
});
|
|
37
|
+
}, Promise.resolve(void 0));
|
|
38
|
+
return ret;
|
|
39
|
+
}
|
|
40
|
+
function parseBlockNumber(blockNumberString) {
|
|
41
|
+
return parseInt(blockNumberString.replace(/,/g, ""));
|
|
42
|
+
}
|
|
43
|
+
function calculateNewSolutions(solutions, winningNumberOfSolutions) {
|
|
44
|
+
if (solutions.length === 0) {
|
|
45
|
+
return pl.DataFrame([]);
|
|
46
|
+
}
|
|
47
|
+
const solutionsNoEmptyArrays = solutions.map(({ solution, ...otherAttrs }) => {
|
|
48
|
+
return { solutionKey: common.arrayJoin(solution, ","), ...otherAttrs };
|
|
49
|
+
});
|
|
50
|
+
let df = pl.readRecords(solutionsNoEmptyArrays);
|
|
51
|
+
df = df.drop("salt");
|
|
52
|
+
const group = df.groupBy(["captchaId", "solutionKey"]).agg(pl.count("captchaContentId").alias("count"));
|
|
53
|
+
const filtered = group.filter(pl.col("count").gt(winningNumberOfSolutions));
|
|
54
|
+
const key = filtered["solutionKey"];
|
|
55
|
+
return filtered.withColumn(key.str.split(",").rename("solution"));
|
|
56
|
+
}
|
|
57
|
+
function updateSolutions(solutions, captchas, logger) {
|
|
58
|
+
return captchas.map((captcha) => {
|
|
59
|
+
if (!captcha.solution) {
|
|
60
|
+
try {
|
|
61
|
+
const captchaSolutions = [
|
|
62
|
+
// TODO is below correct? 'solution' is not in the type
|
|
63
|
+
...solutions.filter(pl.col("captchaId").eq(pl.lit(captcha.captchaId)))["solution"].values()
|
|
64
|
+
];
|
|
65
|
+
if (captchaSolutions.length > 0) {
|
|
66
|
+
captcha.solution = captchaSolutions[0];
|
|
67
|
+
captcha.solved = true;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
logger.debug("No solution found for captchaId", captcha.captchaId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return captcha;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function checkIfTaskIsRunning(taskName, db) {
|
|
77
|
+
const runningTask = await db.getLastScheduledTaskStatus(taskName, types.ScheduledTaskStatus.Running);
|
|
78
|
+
if (runningTask) {
|
|
79
|
+
const completedTask = await db.getScheduledTaskStatus(runningTask.taskId, types.ScheduledTaskStatus.Completed);
|
|
80
|
+
return !completedTask;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
exports.calculateNewSolutions = calculateNewSolutions;
|
|
85
|
+
exports.checkIfTaskIsRunning = checkIfTaskIsRunning;
|
|
86
|
+
exports.encodeStringAddress = encodeStringAddress;
|
|
87
|
+
exports.parseBlockNumber = parseBlockNumber;
|
|
88
|
+
exports.promiseQueue = promiseQueue;
|
|
89
|
+
exports.shuffleArray = shuffleArray;
|
|
90
|
+
exports.updateSolutions = updateSolutions;
|
package/package.json
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosopo/provider",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"author": "PROSOPO LIMITED <info@prosopo.io>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/cjs/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
8
14
|
"scripts": {
|
|
9
15
|
"clean": "tsc --build --clean",
|
|
10
|
-
"build": "tsc --build --verbose",
|
|
16
|
+
"build": "tsc --build --verbose tsconfig.json",
|
|
17
|
+
"build:cjs": "npx vite --config vite.cjs.config.ts build",
|
|
11
18
|
"build:debug": "tsc --build --verbose",
|
|
12
19
|
"build:test": "tsc --build --verbose tsconfig.test.json",
|
|
13
20
|
"build:config": "tsc --project",
|
|
@@ -23,14 +30,14 @@
|
|
|
23
30
|
"@polkadot/types": "10.9.1",
|
|
24
31
|
"@polkadot/util": "12.3.2",
|
|
25
32
|
"@polkadot/util-crypto": "12.3.2",
|
|
26
|
-
"@prosopo/common": "0.2.
|
|
27
|
-
"@prosopo/contract": "0.2.
|
|
28
|
-
"@prosopo/database": "0.2.
|
|
29
|
-
"@prosopo/datasets": "0.2.
|
|
30
|
-
"@prosopo/env": "0.2.
|
|
31
|
-
"@prosopo/types": "0.2.
|
|
32
|
-
"@prosopo/types-database": "0.2.
|
|
33
|
-
"@prosopo/types-env": "0.2.
|
|
33
|
+
"@prosopo/common": "0.2.1",
|
|
34
|
+
"@prosopo/contract": "0.2.1",
|
|
35
|
+
"@prosopo/database": "0.2.1",
|
|
36
|
+
"@prosopo/datasets": "0.2.1",
|
|
37
|
+
"@prosopo/env": "0.2.1",
|
|
38
|
+
"@prosopo/types": "0.2.1",
|
|
39
|
+
"@prosopo/types-database": "0.2.1",
|
|
40
|
+
"@prosopo/types-env": "0.2.1",
|
|
34
41
|
"cron": "^2.1.0",
|
|
35
42
|
"cron-parser": "^4.5.0",
|
|
36
43
|
"express": "^4.18.1",
|