From d7beef5093a5e7bc5513131869a6c103535b3840 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 27 Aug 2024 15:54:10 +0100 Subject: [PATCH 1/4] Fix captcha storage mechanism. Add tests --- packages/cli/src/argv.ts | 45 ++++--- packages/cli/src/commands/index.ts | 2 +- .../cli/src/commands/providerDeregister.ts | 71 +++++------ .../cli/src/commands/providerSetDataset.ts | 77 ++++++------ .../src/commands/storeCaptchasExternally.ts | 46 +++++++ packages/cli/src/commands/validators.ts | 98 +++++---------- .../src/captchaDatabase/captchaDatabase.ts | 27 +++- packages/database/src/databases/mongo.ts | 24 ++-- .../src/tasks/dataset/datasetTasks.ts | 82 ++++++++----- .../src/tasks/imgCaptcha/imgCaptchaTasks.ts | 1 - .../provider/src/tasks/powCaptcha/powTasks.ts | 21 ++-- .../unit/tasks/dataset/datasetTasks.test.ts | 115 ++++++++++++++++-- .../tasks/imgCaptcha/imgCaptchaTasks.test.ts | 2 - .../unit/tasks/powCaptcha/powTasks.test.ts | 2 - packages/types-database/src/types/mongo.ts | 19 +-- packages/types/src/provider/api.ts | 7 ++ 16 files changed, 384 insertions(+), 255 deletions(-) create mode 100644 packages/cli/src/commands/storeCaptchasExternally.ts diff --git a/packages/cli/src/argv.ts b/packages/cli/src/argv.ts index 0282d9b9a2..d5b9182a83 100644 --- a/packages/cli/src/argv.ts +++ b/packages/cli/src/argv.ts @@ -16,30 +16,35 @@ import { LogLevel, getLogger } from "@prosopo/common"; import type { ProsopoConfigOutput } from "@prosopo/types"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import { commandProviderSetDataset, commandVersion } from "./commands/index.js"; +import { + commandProviderSetDataset, + commandStoreCaptchasExternally, + commandVersion, +} from "./commands/index.js"; export type AwaitedProcessedArgs = { - [x: string]: unknown; - api: boolean; - _: (string | number)[]; - $0: string; + [x: string]: unknown; + api: boolean; + _: (string | number)[]; + $0: string; }; export function processArgs( - args: string[], - pair: KeyringPair, - config: ProsopoConfigOutput, + args: string[], + pair: KeyringPair, + config: ProsopoConfigOutput, ) { - const logger = getLogger(LogLevel.enum.info, "CLI"); - return yargs(hideBin(args)) - .usage("Usage: $0 [global options] [options]") - .option("api", { demand: false, default: false, type: "boolean" } as const) - .option("adminApi", { - demand: false, - default: false, - type: "boolean", - } as const) - .command(commandProviderSetDataset(pair, config, { logger })) - .command(commandVersion(pair, config, { logger })) - .parse(); + const logger = getLogger(LogLevel.enum.info, "CLI"); + return yargs(hideBin(args)) + .usage("Usage: $0 [global options] [options]") + .option("api", { demand: false, default: false, type: "boolean" } as const) + .option("adminApi", { + demand: false, + default: false, + type: "boolean", + } as const) + .command(commandProviderSetDataset(pair, config, { logger })) + .command(commandStoreCaptchasExternally(pair, config, { logger })) + .command(commandVersion(pair, config, { logger })) + .parse(); } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 0b6a690999..46b5439001 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -11,6 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -export { default as commandProviderDeregister } from "./providerDeregister.js"; export { default as commandProviderSetDataset } from "./providerSetDataset.js"; +export { default as commandStoreCaptchasExternally } from "./storeCaptchasExternally.js"; export { default as commandVersion } from "./version.js"; diff --git a/packages/cli/src/commands/providerDeregister.ts b/packages/cli/src/commands/providerDeregister.ts index 0f7902e20d..ba4ede2ed5 100644 --- a/packages/cli/src/commands/providerDeregister.ts +++ b/packages/cli/src/commands/providerDeregister.ts @@ -1,9 +1,3 @@ -import type { KeyringPair } from "@polkadot/keyring/types"; -import { LogLevel, type Logger, getLogger } from "@prosopo/common"; -import { ProsopoEnvError } from "@prosopo/common"; -import { ProviderEnvironment } from "@prosopo/env"; -import { Tasks } from "@prosopo/provider"; -import type { ProsopoConfigOutput } from "@prosopo/types"; // Copyright 2021-2024 Prosopo (UK) Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,42 +11,49 @@ import type { ProsopoConfigOutput } from "@prosopo/types"; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +import type { KeyringPair } from "@polkadot/keyring/types"; +import { LogLevel, type Logger, getLogger } from "@prosopo/common"; +import { ProsopoEnvError } from "@prosopo/common"; +import { ProviderEnvironment } from "@prosopo/env"; +import { Tasks } from "@prosopo/provider"; +import type { ProsopoConfigOutput } from "@prosopo/types"; import type { ArgumentsCamelCase, Argv } from "yargs"; import { validateAddress } from "./validators.js"; export default ( - pair: KeyringPair, - config: ProsopoConfigOutput, - cmdArgs?: { logger?: Logger }, + pair: KeyringPair, + config: ProsopoConfigOutput, + cmdArgs?: { logger?: Logger }, ) => { - const logger = - cmdArgs?.logger || getLogger(LogLevel.enum.info, "cli.provider_deregister"); + const logger = + cmdArgs?.logger || getLogger(LogLevel.enum.info, "cli.provider_deregister"); - return { - command: "provider_deregister", - describe: "Deregister a Provider", - builder: (yargs: Argv) => - yargs.option("address", { - type: "string" as const, - demand: true, - desc: "The AccountId of the Provider", - } as const), - handler: async (argv: ArgumentsCamelCase) => { - try { - const env = new ProviderEnvironment(config, pair); - await env.isReady(); - const tasks = new Tasks(env); + return { + command: "provider_deregister", + describe: "Deregister a Provider", + builder: (yargs: Argv) => + yargs.option("address", { + type: "string" as const, + demand: true, + desc: "The AccountId of the Provider", + } as const), + handler: async (argv: ArgumentsCamelCase) => { + try { + const env = new ProviderEnvironment(config, pair); + await env.isReady(); + const tasks = new Tasks(env); - // TODO provider deregister does not accept params... it should? - // await tasks.contract.tx.providerDeregister(argv.address) + // TODO provider deregister does not accept params... it should? + // await tasks.contract.tx.providerDeregister(argv.address) - // logger.info('Provider registered') + // logger.info('Provider registered') - throw new ProsopoEnvError("GENERAL.NOT_IMPLEMENTED"); - } catch (err) { - logger.error(err); - } - }, - middlewares: [validateAddress], - }; + throw new ProsopoEnvError("GENERAL.NOT_IMPLEMENTED"); + } catch (err) { + logger.error(err); + } + }, + middlewares: [validateAddress], + }; }; diff --git a/packages/cli/src/commands/providerSetDataset.ts b/packages/cli/src/commands/providerSetDataset.ts index 0de6dc071d..854ff3f7ae 100644 --- a/packages/cli/src/commands/providerSetDataset.ts +++ b/packages/cli/src/commands/providerSetDataset.ts @@ -1,9 +1,3 @@ -import type { KeyringPair } from "@polkadot/keyring/types"; -import { LogLevel, type Logger, getLogger } from "@prosopo/common"; -import { ProviderEnvironment } from "@prosopo/env"; -import { Tasks } from "@prosopo/provider"; -import type { ProsopoConfigOutput } from "@prosopo/types"; -import type { ArgumentsCamelCase, Argv } from "yargs"; // Copyright 2021-2024 Prosopo (UK) Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,42 +11,49 @@ import type { ArgumentsCamelCase, Argv } from "yargs"; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +import type { KeyringPair } from "@polkadot/keyring/types"; +import { LogLevel, type Logger, getLogger } from "@prosopo/common"; +import { ProviderEnvironment } from "@prosopo/env"; +import { Tasks } from "@prosopo/provider"; +import type { ProsopoConfigOutput } from "@prosopo/types"; +import type { ArgumentsCamelCase, Argv } from "yargs"; import * as z from "zod"; import { loadJSONFile } from "../files.js"; export default ( - pair: KeyringPair, - config: ProsopoConfigOutput, - cmdArgs?: { logger?: Logger }, + pair: KeyringPair, + config: ProsopoConfigOutput, + cmdArgs?: { logger?: Logger }, ) => { - const logger = - cmdArgs?.logger || - getLogger(LogLevel.enum.info, "cli.provider_set_data_set"); + const logger = + cmdArgs?.logger || + getLogger(LogLevel.enum.info, "cli.provider_set_data_set"); - return { - command: "provider_set_data_set", - describe: "Add a dataset as a Provider", - builder: (yargs: Argv) => - yargs.option("file", { - type: "string" as const, - demand: true, - desc: "The file path of a JSON dataset file", - } as const), - handler: async (argv: ArgumentsCamelCase) => { - try { - const env = new ProviderEnvironment(config, pair); - await env.isReady(); - const tasks = new Tasks(env); - const file = z.string().parse(argv.file); - const jsonFile = loadJSONFile(file) as JSON; - logger.info(`Loaded JSON from ${file}`); - const result = - await tasks.datasetManager.providerSetDatasetFromFile(jsonFile); - logger.info(JSON.stringify(result, null, 2)); - } catch (err) { - logger.error(err); - } - }, - middlewares: [], - }; + return { + command: "provider_set_data_set", + describe: "Add a dataset as a Provider", + builder: (yargs: Argv) => + yargs.option("file", { + type: "string" as const, + demand: true, + desc: "The file path of a JSON dataset file", + } as const), + handler: async (argv: ArgumentsCamelCase) => { + try { + const env = new ProviderEnvironment(config, pair); + await env.isReady(); + const tasks = new Tasks(env); + const file = z.string().parse(argv.file); + const jsonFile = loadJSONFile(file) as JSON; + logger.info(`Loaded JSON from ${file}`); + const result = + await tasks.datasetManager.providerSetDatasetFromFile(jsonFile); + logger.info(JSON.stringify(result, null, 2)); + } catch (err) { + logger.error(err); + } + }, + middlewares: [], + }; }; diff --git a/packages/cli/src/commands/storeCaptchasExternally.ts b/packages/cli/src/commands/storeCaptchasExternally.ts new file mode 100644 index 0000000000..794638c529 --- /dev/null +++ b/packages/cli/src/commands/storeCaptchasExternally.ts @@ -0,0 +1,46 @@ +// Copyright 2021-2024 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { KeyringPair } from "@polkadot/keyring/types"; +import { LogLevel, type Logger, getLogger } from "@prosopo/common"; +import { ProviderEnvironment } from "@prosopo/env"; +import { Tasks } from "@prosopo/provider"; +import type { ProsopoConfigOutput } from "@prosopo/types"; + +export default ( + pair: KeyringPair, + config: ProsopoConfigOutput, + cmdArgs?: { logger?: Logger }, +) => { + const logger = + cmdArgs?.logger || getLogger(LogLevel.enum.info, "cli.store_captchas"); + + return { + command: "store_captchas", + describe: "Store captcha records externally for billing purposes", + handler: async () => { + try { + const env = new ProviderEnvironment(config, pair); + await env.isReady(); + const tasks = new Tasks(env); + await tasks.datasetManager.storeCommitmentsExternal().catch((err) => { + env.logger.error(err); + }); + } catch (err) { + logger.error(err); + } + }, + middlewares: [], + }; +}; diff --git a/packages/cli/src/commands/validators.ts b/packages/cli/src/commands/validators.ts index 0716791320..66a9ee14ed 100644 --- a/packages/cli/src/commands/validators.ts +++ b/packages/cli/src/commands/validators.ts @@ -1,11 +1,3 @@ -import type { Compact } from "@polkadot/types-codec/base"; -import type { u128 } from "@polkadot/types-codec/primitive"; -import { ProsopoEnvError } from "@prosopo/common"; -import { encodeStringAddress } from "@prosopo/provider"; -import { PayeeSchema } from "@prosopo/types"; -import { lodash } from "@prosopo/util/lodash"; -import parser from "cron-parser"; -import type { ArgumentsCamelCase } from "yargs"; // Copyright 2021-2024 Prosopo (UK) Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,71 +11,47 @@ import type { ArgumentsCamelCase } from "yargs"; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import * as z from "zod"; -export const validateAddress = ( - argv: ArgumentsCamelCase, -): { address: string } => { - const address = encodeStringAddress(argv.address as string); - - return { address }; -}; - -export const validateContract = (argv: ArgumentsCamelCase) => { - const address = encodeStringAddress(argv.contract as string); +import type { Compact } from "@polkadot/types-codec/base"; +import type { u128 } from "@polkadot/types-codec/primitive"; +import { ProsopoEnvError } from "@prosopo/common"; +import { encodeStringAddress } from "@prosopo/provider"; +import parser from "cron-parser"; - return { address }; -}; +import type { ArgumentsCamelCase } from "yargs"; -export const validatePayee = (argv: ArgumentsCamelCase) => { - try { - if (!argv.payee) return; - const _ = lodash(); - const payeeArg: string = _.capitalize(z.string().parse(argv.payee)); - const payee = PayeeSchema.parse(payeeArg); +export const validateAddress = ( + argv: ArgumentsCamelCase, +): { address: string } => { + const address = encodeStringAddress(argv.address as string); - return { payee }; - } catch (error) { - throw new ProsopoEnvError("CLI.PARAMETER_ERROR", { - context: { payee: [argv.payee], error }, - }); - } + return { address }; }; export const validateValue = (argv: ArgumentsCamelCase) => { - if (typeof argv.value !== "number") { - throw new ProsopoEnvError("CLI.PARAMETER_ERROR", { - context: { value: [argv.value] }, - }); - } - const value: Compact = argv.value as unknown as Compact; - return { value }; -}; - -export const validateFee = (argv: ArgumentsCamelCase) => { - if (typeof argv.fee !== "number") { - throw new ProsopoEnvError("CLI.PARAMETER_ERROR", { - context: { name: validateValue.name, fee: argv.fee }, - }); - } - const fee: Compact = argv.fee as unknown as Compact; - return { fee }; + if (typeof argv.value !== "number") { + throw new ProsopoEnvError("CLI.PARAMETER_ERROR", { + context: { value: [argv.value] }, + }); + } + const value: Compact = argv.value as unknown as Compact; + return { value }; }; export const validateScheduleExpression = (argv: ArgumentsCamelCase) => { - if (typeof argv.schedule === "string") { - const result = parser.parseString(argv.schedule as string); - - if (argv.schedule in result.errors) { - throw new ProsopoEnvError("CLI.PARAMETER_ERROR", { - context: { - payee: [argv.shedule], - failedFuncName: validateScheduleExpression.name, - }, - }); - } - - return { schedule: argv.schedule as string }; - } - return { schedule: null }; + if (typeof argv.schedule === "string") { + const result = parser.parseString(argv.schedule as string); + + if (argv.schedule in result.errors) { + throw new ProsopoEnvError("CLI.PARAMETER_ERROR", { + context: { + payee: [argv.shedule], + failedFuncName: validateScheduleExpression.name, + }, + }); + } + + return { schedule: argv.schedule as string }; + } + return { schedule: null }; }; diff --git a/packages/database/src/captchaDatabase/captchaDatabase.ts b/packages/database/src/captchaDatabase/captchaDatabase.ts index 99e121d6de..96ada208d9 100644 --- a/packages/database/src/captchaDatabase/captchaDatabase.ts +++ b/packages/database/src/captchaDatabase/captchaDatabase.ts @@ -16,6 +16,7 @@ import { getLoggerDefault } from "@prosopo/common"; import { PowCaptchaRecord, PowCaptchaRecordSchema, + PoWCaptchaStored, type UserCommitmentRecord, UserCommitmentRecordSchema, } from "@prosopo/types-database"; @@ -28,7 +29,7 @@ let StoredPoWCaptcha: mongoose.Model; export const saveCaptchas = async ( imageCaptchaEvents: UserCommitmentRecord[], - powCaptchaEvents: PoWCaptchaUser[], + powCaptchaEvents: PoWCaptchaStored[], atlasUri: string, ) => { const connection = mongoose.createConnection(atlasUri, { @@ -51,12 +52,28 @@ export const saveCaptchas = async ( .on("error", reject); }); if (imageCaptchaEvents.length) { - await StoredImageCaptcha.insertMany(imageCaptchaEvents); - logger.info("Mongo Saved Image Events"); + const result = await StoredImageCaptcha.bulkWrite( + imageCaptchaEvents.map((doc) => ({ + updateOne: { + filter: { id: doc.id }, + update: { $set: doc }, + upsert: true, + }, + })), + ); + logger.info("Mongo Saved Image Events", result); } if (powCaptchaEvents.length) { - await StoredPoWCaptcha.insertMany(powCaptchaEvents); - logger.info("Mongo Saved PoW Events"); + const result = await StoredPoWCaptcha.bulkWrite( + powCaptchaEvents.map((doc) => ({ + updateOne: { + filter: { challenge: doc.challenge }, + update: { $set: doc }, + upsert: true, + }, + })), + ); + logger.info("Mongo Saved PoW Events", result); } await connection.close(); diff --git a/packages/database/src/databases/mongo.ts b/packages/database/src/databases/mongo.ts index 24361309b6..6b6de864a6 100644 --- a/packages/database/src/databases/mongo.ts +++ b/packages/database/src/databases/mongo.ts @@ -542,7 +542,6 @@ export class ProsopoDatabase extends AsyncFactory implements Database { result: { status: CaptchaStatus.pending }, userSubmitted, serverChecked, - storedStatus, difficulty, providerSignature, userSignature, @@ -631,25 +630,24 @@ export class ProsopoDatabase extends AsyncFactory implements Database { result: CaptchaResult, serverChecked: boolean = false, userSubmitted: boolean = false, - storedStatus: StoredStatusNames = StoredStatusNames.notStored, userSignature?: string, ): Promise { const tables = this.getTables(); + const timestamp = Date.now(); const update: Pick< PoWCaptchaStored, | "result" | "serverChecked" | "userSubmitted" - | "storedStatus" + | "storedAtTimestamp" | "userSignature" | "lastUpdatedTimestamp" > = { result, serverChecked, userSubmitted, - storedStatus, userSignature, - lastUpdatedTimestamp: Date.now(), + lastUpdatedTimestamp: timestamp, }; try { const updateResult = await tables.powCaptcha.updateOne( @@ -737,12 +735,8 @@ export class ProsopoDatabase extends AsyncFactory implements Database { /** @description Mark a list of captcha commits as stored */ async markDappUserCommitmentsStored(commitmentIds: Hash[]): Promise { - const updateDoc: Pick< - StoredCaptcha, - "storedStatus" | "lastUpdatedTimestamp" - > = { - storedStatus: StoredStatusNames.stored, - lastUpdatedTimestamp: Date.now(), + const updateDoc: Pick = { + storedAtTimestamp: Date.now(), }; await this.tables?.commitment.updateMany( { id: { $in: commitmentIds } }, @@ -786,12 +780,8 @@ export class ProsopoDatabase extends AsyncFactory implements Database { /** @description Mark a list of PoW captcha commits as stored */ async markDappUserPoWCommitmentsStored(challenges: string[]): Promise { - const updateDoc: Pick< - StoredCaptcha, - "storedStatus" | "lastUpdatedTimestamp" - > = { - storedStatus: StoredStatusNames.stored, - lastUpdatedTimestamp: Date.now(), + const updateDoc: Pick = { + storedAtTimestamp: Date.now(), }; await this.tables?.powCaptcha.updateMany( diff --git a/packages/provider/src/tasks/dataset/datasetTasks.ts b/packages/provider/src/tasks/dataset/datasetTasks.ts index 993d372e02..b91783b6db 100644 --- a/packages/provider/src/tasks/dataset/datasetTasks.ts +++ b/packages/provider/src/tasks/dataset/datasetTasks.ts @@ -90,14 +90,14 @@ export class DatasetManager { return; } - const taskID = await this.db.createScheduledTaskStatus( + const lastTask = await this.db.getLastScheduledTaskStatus( ScheduledTaskNames.StoreCommitmentsExternal, - ScheduledTaskStatus.Running, + ScheduledTaskStatus.Completed, ); - const lastTask = await this.db.getLastScheduledTaskStatus( + const taskID = await this.db.createScheduledTaskStatus( ScheduledTaskNames.StoreCommitmentsExternal, - ScheduledTaskStatus.Completed, + ScheduledTaskStatus.Running, ); try { @@ -110,48 +110,66 @@ export class DatasetManager { this.logger.info( `Filtering records to only get updated records: ${JSON.stringify(lastTask)}`, ); + this.logger.info("Last task ran at ", new Date(lastTask.updated || 0)); commitments = commitments.filter( (commitment) => - lastTask.updated && lastTask.updated && commitment.lastUpdatedTimestamp && - commitment.lastUpdatedTimestamp > lastTask.updated && - !lastTask.result?.data.commitments.includes(commitment.id), + (commitment.lastUpdatedTimestamp > lastTask.updated || + !commitment.lastUpdatedTimestamp), ); - powRecords = powRecords.filter( - (commitment) => + this.logger.info( + "PoW Records to store: ", + powRecords.map((pr) => ({ + challenge: pr.challenge, + lastUpdatedTimestamp: new Date(pr.lastUpdatedTimestamp || 0), + })), + ); + powRecords = powRecords.filter((commitment) => { + return ( lastTask.updated && commitment.lastUpdatedTimestamp && - commitment.lastUpdatedTimestamp > lastTask.updated && - !lastTask.result?.data.powRecords.includes(commitment.challenge), - ); + // either the update stamp is more recent than the last time this task ran or there is no update stamp, + // so it is a new record + (commitment.lastUpdatedTimestamp > lastTask.updated || + !commitment.lastUpdatedTimestamp) + ); + }); } - this.logger.info(`Storing ${commitments.length} commitments externally`); + if (commitments.length || powRecords.length) { + this.logger.info( + `Storing ${commitments.length} commitments externally`, + ); - this.logger.info( - `Storing ${powRecords.length} pow challenges externally`, - ); + this.logger.info( + `Storing ${powRecords.length} pow challenges externally`, + ); - await saveCaptchas(commitments, powRecords, this.config.mongoCaptchaUri); + await saveCaptchas( + commitments, + powRecords, + this.config.mongoCaptchaUri, + ); - await this.db.markDappUserCommitmentsStored( - commitments.map((commitment) => commitment.id), - ); - await this.db.markDappUserPoWCommitmentsStored( - powRecords.map((powRecords) => powRecords.challenge), - ); + await this.db.markDappUserCommitmentsStored( + commitments.map((commitment) => commitment.id), + ); + await this.db.markDappUserPoWCommitmentsStored( + powRecords.map((powRecords) => powRecords.challenge), + ); - await this.db.updateScheduledTaskStatus( - taskID, - ScheduledTaskStatus.Completed, - { - data: { - commitments: commitments.map((c) => c.id), - powRecords: powRecords.map((pr) => pr.challenge), + await this.db.updateScheduledTaskStatus( + taskID, + ScheduledTaskStatus.Completed, + { + data: { + commitments: commitments.map((c) => c.id), + powRecords: powRecords.map((pr) => pr.challenge), + }, }, - }, - ); + ); + } } catch (e: any) { this.logger.error(e); await this.db.updateScheduledTaskStatus( diff --git a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts index aab5b89c64..616da6369e 100644 --- a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts +++ b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts @@ -252,7 +252,6 @@ export class ImgCaptchaManager { userSignature: userRequestHashSignature, userSubmitted: true, serverChecked: false, - storedStatus: StoredStatusNames.userSubmitted, requestedAtTimestamp: timestamp, ipAddress, }; diff --git a/packages/provider/src/tasks/powCaptcha/powTasks.ts b/packages/provider/src/tasks/powCaptcha/powTasks.ts index 66c041d431..b465517d6c 100644 --- a/packages/provider/src/tasks/powCaptcha/powTasks.ts +++ b/packages/provider/src/tasks/powCaptcha/powTasks.ts @@ -105,7 +105,15 @@ export class PowCaptchaManager { ApiParams.timestamp, ); - // Check recency before looking up the record to avoid unnecessary network connections + const challengeRecord = + await this.db.getPowCaptchaRecordByChallenge(challenge); + + if (!challengeRecord) { + logger.debug("No record of this challenge"); + // no record of this challenge + return false; + } + if (!verifyRecency(challenge, timeout)) { await this.db.updatePowCaptchaRecord( challenge, @@ -115,21 +123,11 @@ export class PowCaptchaManager { }, false, true, - StoredStatusNames.userSubmitted, userTimestampSignature, ); return false; } - const challengeRecord = - await this.db.getPowCaptchaRecordByChallenge(challenge); - - if (!challengeRecord) { - logger.debug("No record of this challenge"); - // no record of this challenge - return false; - } - const correct = validateSolution(nonce, challenge, difficulty); let result: CaptchaResult = { status: CaptchaStatus.approved }; @@ -145,7 +143,6 @@ export class PowCaptchaManager { result, false, true, - StoredStatusNames.userSubmitted, userTimestampSignature, ); return correct; diff --git a/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.test.ts b/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.test.ts index 058c60984f..21db903a5a 100644 --- a/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.test.ts +++ b/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.test.ts @@ -1,13 +1,3 @@ -import type { Logger } from "@prosopo/common"; -import { saveCaptchaEvent, saveCaptchas } from "@prosopo/database"; -import { parseCaptchaDataset } from "@prosopo/datasets"; -import type { - CaptchaConfig, - DatasetRaw, - ProsopoConfigOutput, - StoredEvents, -} from "@prosopo/types"; -import type { Database } from "@prosopo/types-database"; // Copyright 2021-2024 Prosopo (UK) Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +11,24 @@ import type { Database } from "@prosopo/types-database"; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +import type { Logger } from "@prosopo/common"; +import { saveCaptchaEvent, saveCaptchas } from "@prosopo/database"; +import { parseCaptchaDataset } from "@prosopo/datasets"; +import { + CaptchaConfig, + DatasetRaw, + ProsopoConfigOutput, + ScheduledTaskNames, + ScheduledTaskStatus, + StoredEvents, +} from "@prosopo/types"; +import type { + Database, + PoWCaptchaStored, + ScheduledTaskRecord, + UserCommitmentRecord, +} from "@prosopo/types-database"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { DatasetManager } from "../../../../tasks/dataset/datasetTasks.js"; @@ -137,12 +145,20 @@ describe("DatasetManager", () => { }); it("should store commitments externally if mongoCaptchaUri is set", async () => { - const mockCommitments = [{ id: "commitment1" }]; - const mockPoWCommitments = [{ challenge: "challengeId" }]; + const mockCommitments: Pick[] = [ + { id: "commitment1" }, + ]; + const mockPoWCommitments: Pick[] = [ + { + challenge: "1234567___userAccount___dappAccount", + }, + ]; + // biome-ignore lint/suspicious/noExplicitAny: TODO fix (db.getUnstoredDappUserCommitments as any).mockResolvedValue( mockCommitments, ); + // biome-ignore lint/suspicious/noExplicitAny: TODO fix (db.createScheduledTaskStatus as any).mockResolvedValue({}); @@ -170,4 +186,79 @@ describe("DatasetManager", () => { mockPoWCommitments.map((c) => c.challenge), ); }); + + it("should not store commitments externally if they have been stored", async () => { + const mockCommitments: Pick< + UserCommitmentRecord, + "id" | "lastUpdatedTimestamp" + >[] = [{ id: "commitment1", lastUpdatedTimestamp: 1 }]; + const mockPoWCommitments: Pick< + PoWCaptchaStored, + "challenge" | "lastUpdatedTimestamp" + >[] = [ + { + challenge: "1234567___userAccount___dappAccount", + lastUpdatedTimestamp: 3, + }, + ]; + const mockLastScheduledTask: Pick = { + updated: 2, + }; + const mockNewScheduledTask: Pick< + ScheduledTaskRecord, + "updated" | "processName" | "_id" + > = { + _id: "testID", + updated: 4, + processName: ScheduledTaskNames.StoreCommitmentsExternal, + }; + + // biome-ignore lint/suspicious/noExplicitAny: TODO fix + (db.getUnstoredDappUserCommitments as any).mockResolvedValue( + mockCommitments, + ); + + // biome-ignore lint/suspicious/noExplicitAny: TODO fix + (db.getUnstoredDappUserPoWCommitments as any).mockResolvedValue( + mockPoWCommitments, + ); + + // biome-ignore lint/suspicious/noExplicitAny: TODO fix + (db.getLastScheduledTaskStatus as any).mockResolvedValue( + mockLastScheduledTask, + ); + + // biome-ignore lint/suspicious/noExplicitAny: TODO fix + (db.createScheduledTaskStatus as any).mockResolvedValue( + mockNewScheduledTask._id, + ); + + // biome-ignore lint/suspicious/noExplicitAny: TODO fix + (db.updateScheduledTaskStatus as any).mockResolvedValue({}); + + await datasetManager.storeCommitmentsExternal(); + + expect(db.getUnstoredDappUserCommitments).toHaveBeenCalled(); + expect(db.getUnstoredDappUserPoWCommitments).toHaveBeenCalled(); + expect(saveCaptchas).toHaveBeenCalledWith( + [], + mockPoWCommitments, + config.mongoCaptchaUri, + ); + expect(db.markDappUserCommitmentsStored).toHaveBeenCalledWith([]); + expect(db.markDappUserPoWCommitmentsStored).toHaveBeenCalledWith( + mockPoWCommitments.map((c) => c.challenge), + ); + + expect(db.updateScheduledTaskStatus).toHaveBeenCalledWith( + mockNewScheduledTask._id, + ScheduledTaskStatus.Completed, + { + data: { + commitments: [], + powRecords: mockPoWCommitments.map((c) => c.challenge), + }, + }, + ); + }); }); diff --git a/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.test.ts b/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.test.ts index 0a3aed8f7c..2ee8713c9d 100644 --- a/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.test.ts +++ b/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.test.ts @@ -317,7 +317,6 @@ describe("ImgCaptchaManager", () => { userSignature: "", userSubmitted: true, serverChecked: false, - storedStatus: StoredStatusNames.notStored, requestedAtTimestamp: 0, ipAddress: "0.0.0.0", lastUpdatedTimestamp: Date.now(), @@ -362,7 +361,6 @@ describe("ImgCaptchaManager", () => { userSignature: "", userSubmitted: true, serverChecked: false, - storedStatus: StoredStatusNames.notStored, requestedAtTimestamp: 0, ipAddress: "0.0.0.0", lastUpdatedTimestamp: Date.now(), diff --git a/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts b/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts index a42b5c2c5c..98130a6aaa 100644 --- a/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts +++ b/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts @@ -122,7 +122,6 @@ describe("PowCaptchaManager", () => { result: { status: CaptchaStatus.pending }, userSubmitted: false, serverChecked: false, - storedStatus: StoredStatusNames.notStored, ipAddress, providerSignature, lastUpdatedTimestamp: Date.now(), @@ -201,7 +200,6 @@ describe("PowCaptchaManager", () => { { status: CaptchaStatus.approved }, false, true, - StoredStatusNames.userSubmitted, userSignature, ]; diff --git a/packages/types-database/src/types/mongo.ts b/packages/types-database/src/types/mongo.ts index 8e8bac70bd..e3e54a9d9d 100644 --- a/packages/types-database/src/types/mongo.ts +++ b/packages/types-database/src/types/mongo.ts @@ -83,7 +83,7 @@ export interface StoredCaptcha { ipAddress: string; userSubmitted: boolean; serverChecked: boolean; - storedStatus: StoredStatus; + storedAtTimestamp?: Timestamp; lastUpdatedTimestamp?: Timestamp; } @@ -110,14 +110,9 @@ export const UserCommitmentSchema = object({ ipAddress: string(), userSubmitted: boolean(), serverChecked: boolean(), - storedStatus: union([ - literal(StoredStatusNames.notStored), - literal(StoredStatusNames.userSubmitted), - literal(StoredStatusNames.serverChecked), - literal(StoredStatusNames.stored), - ]), - requestedAtTimestamp: number(), - lastUpdatedTimestamp: number().optional(), + storedAtTimestamp: TimestampSchema.optional(), + requestedAtTimestamp: TimestampSchema, + lastUpdatedTimestamp: TimestampSchema.optional(), }) satisfies ZodType; export interface SolutionRecord extends CaptchaSolution { @@ -184,7 +179,7 @@ export const PowCaptchaRecordSchema = new Schema({ userSignature: { type: String, required: false }, userSubmitted: { type: Boolean, required: true }, serverChecked: { type: Boolean, required: true }, - storedStatus: { type: String, enum: StoredStatusNames, required: true }, + storedAtTimestamp: { type: Number, required: false }, }); // Set an index on the captchaId field, ascending @@ -209,7 +204,7 @@ export const UserCommitmentRecordSchema = new Schema({ userSignature: { type: String, required: true }, userSubmitted: { type: Boolean, required: true }, serverChecked: { type: Boolean, required: true }, - storedStatus: { type: String, enum: StoredStatusNames, required: false }, + storedAtTimestamp: { type: Number, required: false }, requestedAtTimestamp: { type: Number, required: true }, lastUpdatedTimestamp: { type: Number, required: false }, }); @@ -463,7 +458,6 @@ export interface Database { ipAddress: string, serverChecked?: boolean, userSubmitted?: boolean, - storedStatus?: StoredStatus, userSignature?: string, ): Promise; @@ -476,7 +470,6 @@ export interface Database { result: CaptchaResult, serverChecked: boolean, userSubmitted: boolean, - storedStatus: StoredStatusNames, userSignature?: string, ): Promise; } diff --git a/packages/types/src/provider/api.ts b/packages/types/src/provider/api.ts index 2e851c2d1c..d6989ba5ee 100644 --- a/packages/types/src/provider/api.ts +++ b/packages/types/src/provider/api.ts @@ -57,9 +57,16 @@ export enum ApiPaths { export type TGetImageCaptchaChallengePathAndParams = `${ApiPaths.GetImageCaptchaChallenge}/${string}/${string}/${string}`; + export type TGetImageCaptchaChallengeURL = `${string}${TGetImageCaptchaChallengePathAndParams}`; +export type TGetPowCaptchaChallengeURL = + `${string}${ApiPaths.GetPowCaptchaChallenge}`; + +export type TSubmitPowCaptchaSolutionURL = + `${string}${ApiPaths.SubmitPowCaptchaSolution}`; + export enum AdminApiPaths { BatchCommit = "/v1/prosopo/provider/admin/batch", UpdateDataset = "/v1/prosopo/provider/admin/dataset", From 31a80913f5463c35d4be8783e4de1ecc7f9ab122 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Wed, 28 Aug 2024 10:05:15 +0100 Subject: [PATCH 2/4] Fix up tests. Move verifyRecency function to util --- package-lock.json | 2 - packages/contract/src/contract/block.ts | 18 ------ packages/provider/package.json | 1 - .../provider/src/tasks/powCaptcha/powTasks.ts | 3 +- .../src/tasks/powCaptcha/powTasksUtils.ts | 3 +- .../unit/tasks/powCaptcha/powTasks.test.ts | 56 +++++++++++-------- .../tasks/powCaptcha/powTasksUtils.test.ts | 4 +- packages/provider/src/util.ts | 3 +- packages/util/src/index.ts | 1 + packages/util/src/verifyRecency.ts | 17 ++++++ 10 files changed, 58 insertions(+), 50 deletions(-) create mode 100644 packages/util/src/verifyRecency.ts diff --git a/package-lock.json b/package-lock.json index bebcf40354..985135f802 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,7 +107,6 @@ "@polkadot/util": "12.6.2", "@polkadot/util-crypto": "12.6.2", "@prosopo/api": "2.0.1", - "@prosopo/contract": "2.0.1", "@prosopo/dotenv": "2.0.1", "@prosopo/procaptcha": "2.0.1", "@prosopo/server": "2.0.1", @@ -24149,7 +24148,6 @@ "@polkadot/util-crypto": "12.6.2", "@prosopo/common": "2.0.1", "@prosopo/config": "2.0.1", - "@prosopo/contract": "2.0.1", "@prosopo/database": "2.0.1", "@prosopo/datasets": "2.0.1", "@prosopo/env": "2.0.1", diff --git a/packages/contract/src/contract/block.ts b/packages/contract/src/contract/block.ts index 0facbdebd1..6f6ad67402 100644 --- a/packages/contract/src/contract/block.ts +++ b/packages/contract/src/contract/block.ts @@ -31,21 +31,3 @@ export const getCurrentBlockNumber = async ( ): Promise => { return (await api.rpc.chain.getBlock()).block.header.number.toNumber(); }; - -/** - * Verify the time since the blockNumber is equal to or less than the maxVerifiedTime. - * @param challenge - * @param maxVerifiedTime - */ -export const verifyRecency = (challenge: string, maxVerifiedTime: number) => { - // Get the timestamp from the challenge - const timestamp = challenge.split("___")[0]; - - if (!timestamp) { - throw new Error("Invalid challenge"); - } - - const currentTimestamp = Date.now(); - const challengeTimestamp = Number.parseInt(timestamp, 10); - return currentTimestamp - challengeTimestamp <= maxVerifiedTime; -}; diff --git a/packages/provider/package.json b/packages/provider/package.json index 3ad397ea32..9680f349ce 100644 --- a/packages/provider/package.json +++ b/packages/provider/package.json @@ -35,7 +35,6 @@ "@polkadot/util-crypto": "12.6.2", "@prosopo/common": "2.0.1", "@prosopo/config": "2.0.1", - "@prosopo/contract": "2.0.1", "@prosopo/database": "2.0.1", "@prosopo/datasets": "2.0.1", "@prosopo/env": "2.0.1", diff --git a/packages/provider/src/tasks/powCaptcha/powTasks.ts b/packages/provider/src/tasks/powCaptcha/powTasks.ts index b465517d6c..fb1fce7aae 100644 --- a/packages/provider/src/tasks/powCaptcha/powTasks.ts +++ b/packages/provider/src/tasks/powCaptcha/powTasks.ts @@ -23,9 +23,8 @@ import { PoWChallengeId, } from "@prosopo/types"; import { Database, StoredStatusNames } from "@prosopo/types-database"; -import { at } from "@prosopo/util"; +import { at, verifyRecency } from "@prosopo/util"; import { checkPowSignature, validateSolution } from "./powTasksUtils.js"; -import { verifyRecency } from "@prosopo/contract"; const logger = getLoggerDefault(); diff --git a/packages/provider/src/tasks/powCaptcha/powTasksUtils.ts b/packages/provider/src/tasks/powCaptcha/powTasksUtils.ts index cff57b88ef..10c8f90d45 100644 --- a/packages/provider/src/tasks/powCaptcha/powTasksUtils.ts +++ b/packages/provider/src/tasks/powCaptcha/powTasksUtils.ts @@ -15,7 +15,8 @@ import { sha256 } from "@noble/hashes/sha256"; import { stringToHex } from "@polkadot/util"; import { signatureVerify } from "@polkadot/util-crypto"; import { ProsopoContractError } from "@prosopo/common"; -import { verifyRecency } from "@prosopo/contract"; + +import { verifyRecency } from "@prosopo/util"; export const validateSolution = ( nonce: number, diff --git a/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts b/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts index 98130a6aaa..c285274ce0 100644 --- a/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts +++ b/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts @@ -14,6 +14,7 @@ import type { KeyringPair } from "@polkadot/keyring/types"; import { stringToHex, u8aToHex } from "@polkadot/util"; +import { verifyRecency } from "@prosopo/util"; import { ProsopoEnvError } from "@prosopo/common"; import { ApiParams, @@ -21,18 +22,13 @@ import { POW_SEPARATOR, PoWChallengeId, } from "@prosopo/types"; -import { - Database, - PoWCaptchaStored, - StoredStatusNames, -} from "@prosopo/types-database"; +import { Database, PoWCaptchaStored } from "@prosopo/types-database"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { PowCaptchaManager } from "../../../../tasks/powCaptcha/powTasks.js"; import { checkPowSignature, validateSolution, } from "../../../../tasks/powCaptcha/powTasksUtils.js"; -import { verifyRecency } from "@prosopo/contract"; vi.mock("@polkadot/util-crypto", () => ({ signatureVerify: vi.fn(), @@ -43,9 +39,13 @@ vi.mock("@polkadot/util", () => ({ stringToHex: vi.fn(), })); -vi.mock("@prosopo/contract", () => ({ - verifyRecency: vi.fn(), -})); +vi.mock("@prosopo/util", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + verifyRecency: vi.fn(), + }; +}); vi.mock("../../../../tasks/powCaptcha/powTasksUtils.js", () => ({ checkPowSignature: vi.fn(), @@ -216,17 +216,33 @@ describe("PowCaptchaManager", () => { const timeout = 1000; const timestampSignature = "testTimestampSignature"; const ipAddress = "ipAddress"; + const challengeRecord: PoWCaptchaStored = { + challenge, + dappAccount: pair.address, + userAccount: "testUserAccount", + requestedAtTimestamp: 12345, + result: { status: CaptchaStatus.pending }, + userSubmitted: false, + serverChecked: false, + ipAddress, + providerSignature: "testSignature", + difficulty, + lastUpdatedTimestamp: 0, + }; // biome-ignore lint/suspicious/noExplicitAny: TODO fix (verifyRecency as any).mockImplementation(() => { - throw new ProsopoEnvError("CAPTCHA.INVALID_CAPTCHA_CHALLENGE", { - context: { - failedFuncName: "verifyPowCaptchaSolution", - }, - }); + return true; }); - await expect( - powCaptchaManager.verifyPowCaptchaSolution( + (db.getPowCaptchaRecordByChallenge as any).mockResolvedValue( + challengeRecord, + ); + + // biome-ignore lint/suspicious/noExplicitAny: TODO fix + (validateSolution as any).mockImplementation(() => false); + + expect( + await powCaptchaManager.verifyPowCaptchaSolution( challenge, difficulty, signature, @@ -235,13 +251,7 @@ describe("PowCaptchaManager", () => { timestampSignature, ipAddress, ), - ).rejects.toThrow( - new ProsopoEnvError("CAPTCHA.INVALID_CAPTCHA_CHALLENGE", { - context: { - failedFuncName: "verifyPowCaptchaSolution", - }, - }), - ); + ).toBe(false); expect(verifyRecency).toHaveBeenCalledWith(challenge, timeout); }); diff --git a/packages/provider/src/tests/unit/tasks/powCaptcha/powTasksUtils.test.ts b/packages/provider/src/tests/unit/tasks/powCaptcha/powTasksUtils.test.ts index 5fcc45e16c..03cf2ffbd6 100644 --- a/packages/provider/src/tests/unit/tasks/powCaptcha/powTasksUtils.test.ts +++ b/packages/provider/src/tests/unit/tasks/powCaptcha/powTasksUtils.test.ts @@ -14,18 +14,18 @@ import { signatureVerify } from "@polkadot/util-crypto"; import { ProsopoContractError } from "@prosopo/common"; -import { verifyRecency } from "@prosopo/contract"; import { describe, expect, it, vi } from "vitest"; import { checkPowSignature, validateSolution, } from "../../../../tasks/powCaptcha/powTasksUtils.js"; +import { verifyRecency } from "@prosopo/util"; vi.mock("@polkadot/util-crypto", () => ({ signatureVerify: vi.fn(), })); -vi.mock("@prosopo/contract", () => ({ +vi.mock("@prosopo/util", () => ({ verifyRecency: vi.fn(), })); diff --git a/packages/provider/src/util.ts b/packages/provider/src/util.ts index 57b1a40b3e..c905fdd531 100644 --- a/packages/provider/src/util.ts +++ b/packages/provider/src/util.ts @@ -2,7 +2,6 @@ import { decodeAddress, encodeAddress } from "@polkadot/util-crypto/address"; import { hexToU8a } from "@polkadot/util/hex"; import { isHex } from "@polkadot/util/is"; import { ProsopoContractError } from "@prosopo/common"; -import { type ScheduledTaskNames, ScheduledTaskStatus } from "@prosopo/types"; // Copyright 2021-2024 Prosopo (UK) Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +15,8 @@ import { type ScheduledTaskNames, ScheduledTaskStatus } from "@prosopo/types"; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +import { type ScheduledTaskNames, ScheduledTaskStatus } from "@prosopo/types"; import type { Database } from "@prosopo/types-database"; import { at } from "@prosopo/util"; diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 10bf1db59f..dba620dc3c 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -27,3 +27,4 @@ export * from "./permutations.js"; export * from "./version.js"; export * from "./hex.js"; export * from "./checks.js"; +export { verifyRecency } from "./verifyRecency.js"; diff --git a/packages/util/src/verifyRecency.ts b/packages/util/src/verifyRecency.ts new file mode 100644 index 0000000000..6e6208e2fd --- /dev/null +++ b/packages/util/src/verifyRecency.ts @@ -0,0 +1,17 @@ +/** + * Verify the time since the blockNumber is equal to or less than the maxVerifiedTime. + * @param challenge + * @param maxVerifiedTime + */ +export const verifyRecency = (challenge: string, maxVerifiedTime: number) => { + // Get the timestamp from the challenge + const timestamp = challenge.split("___")[0]; + + if (!timestamp) { + return false; + } + + const currentTimestamp = Date.now(); + const challengeTimestamp = Number.parseInt(timestamp, 10); + return currentTimestamp - challengeTimestamp <= maxVerifiedTime; +}; From 9221990deca4a39ee9626cdf9cb2a1df1fd70cb4 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Wed, 28 Aug 2024 10:10:13 +0100 Subject: [PATCH 3/4] Update test env template --- dev/scripts/env.test | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/scripts/env.test b/dev/scripts/env.test index 7a3de6a797..3f0c868f18 100644 --- a/dev/scripts/env.test +++ b/dev/scripts/env.test @@ -9,3 +9,4 @@ PROSOPO_PROVIDER_MNEMONIC=puppy cream effort carbon despair leg pyramid cotton e NODE_ENV=test PROSOPO_LOG_LEVEL=info PROSOPO_DEFAULT_ENVIRONMENT=development +PROSOPO_MONGO_CAPTCHA_URI=mongodb://root:root@localhost:27017/captchastorage?authSource=admin From 0af7d23e8e260deadabc9c9abf2f32a6292392a4 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Wed, 28 Aug 2024 10:14:58 +0100 Subject: [PATCH 4/4] Fix build --- .../provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts b/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts index c285274ce0..fb039c14a1 100644 --- a/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts +++ b/packages/provider/src/tests/unit/tasks/powCaptcha/powTasks.test.ts @@ -40,7 +40,7 @@ vi.mock("@polkadot/util", () => ({ })); vi.mock("@prosopo/util", async (importOriginal) => { - const actual = await importOriginal(); + const actual = (await importOriginal()) as Record; return { ...actual, verifyRecency: vi.fn(),