diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index 6843799136..b52f204e80 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -107,6 +107,9 @@ jobs: # rebuild typechain npm run build:typechain + # lint + npm run lint:fix + # set the author in git git config user.name "prosoponator[bot]" git config user.email "dev@prosopo.io" diff --git a/demos/client-example/src/App.tsx b/demos/client-example/src/App.tsx index 6d7b57b0e6..1ef161f03d 100644 --- a/demos/client-example/src/App.tsx +++ b/demos/client-example/src/App.tsx @@ -54,7 +54,7 @@ function App(props: AppProps) { dappName: 'client-example', defaultEnvironment: (process.env.PROSOPO_DEFAULT_ENVIRONMENT as EnvironmentTypes) || EnvironmentTypesSchema.enum.development, - serverUrl: process.env.PROSOPO_SERVER_URL || '', + serverUrl: process.env.PROSOPO_SERVER_URL || 'localhost:9228', mongoAtlasUri: process.env.PROSOPO_MONGO_EVENTS_URI || '', devOnlyWatchEvents: process.env._DEV_ONLY_WATCH_EVENTS === 'true' || false, }) @@ -63,8 +63,9 @@ function App(props: AppProps) { const urlPath = isLogin ? 'login' : 'signup' const onLoggedIn = (token: string) => { - console.log('getting private resource with token ', token) - fetch(new URL('/private', config.serverUrl).href, { + const url = new URL('/private', config.serverUrl).href + console.log('getting private resource with token ', token, 'at', url) + fetch(url, { method: 'GET', headers: { Origin: 'http://localhost:9230', // TODO: change this to env var @@ -98,7 +99,9 @@ function App(props: AppProps) { password, [ApiParams.procaptchaResponse]: procaptchaOutput, } - fetch(new URL(urlPath, config.serverUrl).href, { + const url = new URL(urlPath, config.serverUrl).href + console.log('posting to', url, 'with payload', payload) + fetch(url, { method: 'POST', headers: { ...corsHeaders, diff --git a/packages/account/src/extension/ExtensionWeb2.ts b/packages/account/src/extension/ExtensionWeb2.ts index d8b03364dc..20adae7b59 100644 --- a/packages/account/src/extension/ExtensionWeb2.ts +++ b/packages/account/src/extension/ExtensionWeb2.ts @@ -76,7 +76,7 @@ export class ExtensionWeb2 extends Extension { }, }, name: 'procaptcha-web2', - version: '0.1.11', + version: '0.3.x', signer, } } diff --git a/packages/api/src/api/ProviderApi.ts b/packages/api/src/api/ProviderApi.ts index 995cb82ff0..9b5c766117 100644 --- a/packages/api/src/api/ProviderApi.ts +++ b/packages/api/src/api/ProviderApi.ts @@ -13,6 +13,7 @@ // limitations under the License. import { AccountId } from '@prosopo/captcha-contract' import { + ApiParams, ApiPaths, CaptchaResponseBody, CaptchaSolution, @@ -25,6 +26,7 @@ import { PowCaptchaSolutionResponse, ProviderRegistered, StoredEvents, + SubmitPowCaptchaSolutionBodyType, VerificationResponse, VerifySolutionBodyType, } from '@prosopo/types' @@ -63,8 +65,8 @@ export default class ProviderApi extends HttpClientBase implements ProviderApi { const captchaSolutionBody: CaptchaSolutionBodyType = CaptchaSolutionBody.parse({ captchas, requestHash, - user: userAccount, - dapp: this.account, + [ApiParams.user]: userAccount, + [ApiParams.dapp]: this.account, salt, signature, }) @@ -78,10 +80,10 @@ export default class ProviderApi extends HttpClientBase implements ProviderApi { maxVerifiedTime?: number ): Promise { const payload: { - dapp: AccountId - user: AccountId - commitmentId?: string - maxVerifiedTime?: number + [ApiParams.dapp]: AccountId + [ApiParams.user]: AccountId + [ApiParams.commitmentId]?: string + [ApiParams.maxVerifiedTime]?: number } = { dapp: dapp, user: userAccount } if (commitmentId) { payload['commitmentId'] = commitmentId @@ -103,17 +105,18 @@ export default class ProviderApi extends HttpClientBase implements ProviderApi { randomProvider: RandomProvider, nonce: number ): Promise { - const { provider, blockNumber } = randomProvider - return this.post(ApiPaths.SubmitPowCaptchaSolution, { - blockNumber, - challenge: challenge.challenge, - difficulty: challenge.difficulty, - signature: challenge.signature, - userAccount, - dappAccount, - provider, - nonce, - }) + const { blockNumber } = randomProvider + const body: SubmitPowCaptchaSolutionBodyType = { + [ApiParams.blockNumber]: blockNumber, + [ApiParams.challenge]: challenge.challenge, + [ApiParams.difficulty]: challenge.difficulty, + [ApiParams.signature]: challenge.signature, + // TODO add utility to convert `AccountId` to string + [ApiParams.user]: userAccount.toString(), + [ApiParams.dapp]: dappAccount.toString(), + [ApiParams.nonce]: nonce, + } + return this.post(ApiPaths.SubmitPowCaptchaSolution, body) } public submitUserEvents(events: StoredEvents, accountId: AccountId) { @@ -128,7 +131,7 @@ export default class ProviderApi extends HttpClientBase implements ProviderApi { return this.fetch(ApiPaths.GetProviderDetails) } - public getPowCaptchaVerify(challengeId: string, dappAccount: string): Promise { - return this.post(ApiPaths.ServerPowCaptchaVerify, { challengeId, dappAccount }) + public submitPowCaptchaVerify(challenge: string, dapp: string): Promise { + return this.post(ApiPaths.ServerPowCaptchaVerify, { [ApiParams.challenge]: challenge, [ApiParams.dapp]: dapp }) } } diff --git a/packages/common/src/locales/en.json b/packages/common/src/locales/en.json index 505eb3b9df..a4171f36c2 100644 --- a/packages/common/src/locales/en.json +++ b/packages/common/src/locales/en.json @@ -1,4 +1,7 @@ { + "ACCOUNT": { + "NO_POLKADOT_EXTENSION": "Polkadot extension not found" + }, "WIDGET": { "SELECT_ALL": "Select all images containing a", "NEXT": "Next", @@ -158,7 +161,6 @@ "INVALID_DIR_FORMAT": "Invalid directory format" }, "PROGUI": { - "NO_STATE_PROVIDER": "useGlobalState must be used within a GlobalStateProvider", - "NO_POLKADOT_EXTENSION": "Polkadot extension not found" + "NO_STATE_PROVIDER": "useGlobalState must be used within a GlobalStateProvider" } } diff --git a/packages/procaptcha-pow/src/Services/Manager.ts b/packages/procaptcha-pow/src/Services/Manager.ts index 671d3ecf1c..fb27094856 100644 --- a/packages/procaptcha-pow/src/Services/Manager.ts +++ b/packages/procaptcha-pow/src/Services/Manager.ts @@ -142,11 +142,19 @@ export const Manager = ( } resetState() + + const config = getConfig() + + // use the passed in account (could be web3) or the zero account + const userAccount = config.userAccountAddress || '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM' + // set the loading flag to true (allow UI to show some sort of loading / pending indicator while we get the captcha process going) - updateState({ loading: true }) + updateState({ + loading: true, + account: { account: { address: userAccount } }, + }) // snapshot the config into the state - const config = getConfig() updateState({ dappAccount: config.account.address }) // allow UI to catch up with the loading state @@ -180,20 +188,24 @@ export const Manager = ( const challenge = await providerApi.getPowCaptchaChallenge(account.account.address, getDappAccount()) const solution = solvePoW(challenge.challenge, challenge.difficulty) - await providerApi.submitPowCaptchaSolution( + const verifiedSolution = await providerApi.submitPowCaptchaSolution( challenge, getAccount().account.address, getDappAccount(), getRandomProviderResponse, solution ) - if (state.isHuman) { + if (verifiedSolution[ApiParams.verified]) { + updateState({ + isHuman: true, + loading: false, + }) events.onHuman({ providerUrl, [ApiParams.user]: getAccount().account.address, [ApiParams.dapp]: getDappAccount(), - [ApiParams.challengeId]: challenge.challenge, - [ApiParams.blockNumber]: getBlockNumber(), + [ApiParams.challenge]: challenge.challenge, + [ApiParams.blockNumber]: getRandomProviderResponse.blockNumber, }) } } diff --git a/packages/procaptcha-pow/src/components/Captcha.tsx b/packages/procaptcha-pow/src/components/Captcha.tsx index d831d4bb5b..ad02c4e3ae 100644 --- a/packages/procaptcha-pow/src/components/Captcha.tsx +++ b/packages/procaptcha-pow/src/components/Captcha.tsx @@ -16,8 +16,11 @@ import { ContainerDiv, LoadingSpinner, Logo, + WIDGET_BORDER, + WIDGET_BORDER_RADIUS, WIDGET_DIMENSIONS, WIDGET_INNER_HEIGHT, + WIDGET_PADDING, WIDGET_URL, WIDGET_URL_TEXT, WidthBasedStylesDiv, @@ -48,11 +51,11 @@ const Procaptcha = (props: ProcaptchaProps) => { {' '}
{ {' '}
(challenge.captchas, 0) if (!first.captcha.datasetId) { @@ -516,6 +516,14 @@ export function Manager( return blockNumber } + const getExtension = (account?: Account) => { + account = account || getAccount() + if (!account.extension) { + throw new ProsopoEnvError('ACCOUNT.NO_POLKADOT_EXTENSION', { context: { error: 'Extension not loaded' } }) + } + return account.extension + } + /** * Load the contract instance using addresses from config. */ diff --git a/packages/provider/src/api/captcha.ts b/packages/provider/src/api/captcha.ts index 438dd44f37..07e679a83d 100644 --- a/packages/provider/src/api/captcha.ts +++ b/packages/provider/src/api/captcha.ts @@ -22,7 +22,9 @@ import { CaptchaWithProof, DappUserSolutionResult, ImageVerificationResponse, + PowCaptchaSolutionResponse, ServerPowCaptchaVerifyRequestBody, + SubmitPowCaptchaSolutionBody, VerificationResponse, VerifySolutionBody, VerifySolutionBodyType, @@ -82,6 +84,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router { } return res.json(captchaResponse) } catch (err) { + tasks.logger.error(err) return next(new ProsopoApiError('API.BAD_REQUEST', { context: { error: err, errorCode: 400 } })) } } @@ -117,6 +120,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router { } return res.json(returnValue) } catch (err) { + tasks.logger.error(err) return next(new ProsopoApiError('API.UNKNOWN', { context: { errorCode: 400, error: err } })) } }) @@ -176,11 +180,11 @@ export function prosopoRouter(env: ProviderEnvironment): Router { * @param {string} dappAccount - Dapp User id * @param {string} challenge - The captcha solution to look up */ - router.get(ApiPaths.ServerPowCaptchaVerify, async (req, res, next) => { + router.post(ApiPaths.ServerPowCaptchaVerify, async (req, res, next) => { try { - const { challengeId, dapp } = ServerPowCaptchaVerifyRequestBody.parse(req.body) + const { challenge, dapp } = ServerPowCaptchaVerifyRequestBody.parse(req.body) - const approved = await tasks.serverVerifyPowCaptchaSolution(dapp, challengeId) + const approved = await tasks.serverVerifyPowCaptchaSolution(dapp, challenge) const verificationResponse: VerificationResponse = { status: req.t(approved ? 'API.USER_VERIFIED' : 'API.USER_NOT_VERIFIED'), @@ -189,6 +193,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router { return res.json(verificationResponse) } catch (err) { + tasks.logger.error(err) return next(new ProsopoApiError('API.BAD_REQUEST', { context: { errorCode: 400, error: err } })) } }) @@ -196,13 +201,12 @@ export function prosopoRouter(env: ProviderEnvironment): Router { /** * Supplies a PoW challenge to a Dapp User * - * @param {string} userAccount - Dapp User address - * @param {string} dappAccount - Dapp User address + * @param {string} userAccount - User address + * @param {string} dappAccount - Dapp address */ router.post(ApiPaths.GetPowCaptchaChallenge, async (req, res, next) => { try { const { userAccount, dappAccount } = req.body - console.log(userAccount, dappAccount) // Assert that the user and dapp accounts are strings if (typeof userAccount !== 'string' || typeof dappAccount !== 'string') { throw new ProsopoApiError('API.BAD_REQUEST', { @@ -220,7 +224,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router { const challenge = await tasks.getPowCaptchaChallenge(userAccount, dappAccount, origin) return res.json(challenge) } catch (err) { - console.log(err) + tasks.logger.error(err) return next(new ProsopoApiError('API.BAD_REQUEST', { context: { errorCode: 400, error: err } })) } }) @@ -228,18 +232,22 @@ export function prosopoRouter(env: ProviderEnvironment): Router { /** * Verifies a user's PoW solution as being approved or not * - * @param {string} blocknumber - Dapp User address - * @param {string} challenge - Dapp User address - * @param {number} difficulty - Dapp User address - * @param {string} signature - Dapp User address - * @param {string} nonce - Dapp User address + * @param {string} blocknumber - the block number at which the captcha was requested + * @param {string} challenge - the challenge string + * @param {number} difficulty - the difficulty of the challenge + * @param {string} signature - the signature of the challenge + * @param {string} nonce - the nonce of the challenge */ router.post(ApiPaths.SubmitPowCaptchaSolution, async (req, res, next) => { try { - const { blocknumber, challenge, difficulty, signature, nonce } = req.body - const verified = await tasks.verifyPowCaptchaSolution(blocknumber, challenge, difficulty, signature, nonce) - return res.json({ verified }) + const { blockNumber, challenge, difficulty, signature, nonce } = SubmitPowCaptchaSolutionBody.parse( + req.body + ) + const verified = await tasks.verifyPowCaptchaSolution(blockNumber, challenge, difficulty, signature, nonce) + const response: PowCaptchaSolutionResponse = { verified } + return res.json(response) } catch (err) { + tasks.logger.error(err) return next(new ProsopoApiError('API.BAD_REQUEST', { context: { errorCode: 400, error: err } })) } }) @@ -256,6 +264,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router { await tasks.saveCaptchaEvent(events, accountId) return res.json({ status: 'success' }) } catch (err) { + tasks.logger.error(err) return next(new ProsopoApiError('API.BAD_REQUEST', { context: { errorCode: 400, error: err } })) } }) @@ -268,6 +277,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router { const status = await tasks.providerStatus() return res.json({ status }) } catch (err) { + tasks.logger.error(err) return next(new ProsopoApiError('API.BAD_REQUEST', { context: { errorCode: 400, error: err } })) } }) @@ -280,6 +290,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router { const details = await tasks.getProviderDetails() return res.json(details) } catch (err) { + tasks.logger.error(err) return next(new ProsopoApiError('API.BAD_REQUEST', { context: { errorCode: 400, error: err } })) } }) diff --git a/packages/provider/src/tasks/tasks.ts b/packages/provider/src/tasks/tasks.ts index d32fc89fbc..8f035fb681 100644 --- a/packages/provider/src/tasks/tasks.ts +++ b/packages/provider/src/tasks/tasks.ts @@ -189,14 +189,14 @@ export class Tasks { /** * @description Verifies a PoW Captcha for a given user and dapp * - * @param {string} blocknumber - the block at which the Provider was selected + * @param {string} blockNumber - the block at which the Provider was selected * @param {string} challenge - the starting string for the PoW challenge * @param {string} difficulty - how many leading zeroes the solution must have * @param {string} signature - proof that the Provider provided the challenge * @param {string} nonce - the string that the user has found that satisfies the PoW challenge */ async verifyPowCaptchaSolution( - blocknumber: number, + blockNumber: number, challenge: string, difficulty: number, signature: string, @@ -205,12 +205,13 @@ export class Tasks { const latestHeader = await this.contract.api.rpc.chain.getHeader() const latestBlockNumber = latestHeader.number.toNumber() - if (latestBlockNumber > blocknumber - 5) { + if (blockNumber < latestBlockNumber - 5) { throw new ProsopoContractError('CONTRACT.INVALID_BLOCKHASH', { context: { ERROR: 'Blockhash must be from within last 5 blocks', failedFuncName: this.verifyPowCaptchaSolution.name, - blocknumber, + blockNumber, + latestBlockNumber, }, }) } diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index ebadc6148d..3f364c9ba2 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -171,7 +171,7 @@ export class ProsopoServer { * @returns */ public async isVerified(payload: ProcaptchaOutput, maxVerifiedTime?: number): Promise { - const { user, dapp, providerUrl, commitmentId, blockNumber, challengeId } = payload + const { user, dapp, providerUrl, commitmentId, blockNumber, challenge } = payload const contractApi = await this.getContractApi() const randomProvider = await this.checkRandomProvider(user, dapp, providerUrl, blockNumber, maxVerifiedTime) @@ -186,8 +186,8 @@ export class ProsopoServer { this.logger.info('Random provider is valid. Verifying with provider.') // We can now trust the provider URL as it has been shown to have been randomly selected const providerApi = await this.getProviderApi(providerUrl) - if (challengeId) { - const result = await providerApi.getPowCaptchaVerify(dapp, challengeId) + if (challenge) { + const result = await providerApi.submitPowCaptchaVerify(challenge, dapp) return result.verified } const result = await providerApi.verifyDappUser(dapp, user, commitmentId, maxVerifiedTime) diff --git a/packages/types/src/procaptcha/manager.ts b/packages/types/src/procaptcha/manager.ts index edddcd8389..3aefba5934 100644 --- a/packages/types/src/procaptcha/manager.ts +++ b/packages/types/src/procaptcha/manager.ts @@ -22,7 +22,7 @@ import { number, object, string, infer as zInfer } from 'zod' */ export interface Account { account: InjectedAccount - extension: InjectedExtension + extension?: InjectedExtension } export const ProcaptchaOutputSchema = object({ @@ -31,7 +31,7 @@ export const ProcaptchaOutputSchema = object({ [ApiParams.dapp]: string(), [ApiParams.user]: string(), [ApiParams.blockNumber]: number().optional(), - [ApiParams.challengeId]: string().optional(), + [ApiParams.challenge]: string().optional(), }) /** diff --git a/packages/types/src/provider/api.ts b/packages/types/src/provider/api.ts index 2ccfb66670..5745998080 100644 --- a/packages/types/src/provider/api.ts +++ b/packages/types/src/provider/api.ts @@ -38,6 +38,7 @@ export enum ApiParams { datasetId = 'datasetId', user = 'user', dapp = 'dapp', + provider = 'provider', blockNumber = 'blockNumber', signature = 'signature', requestHash = 'requestHash', @@ -49,7 +50,9 @@ export enum ApiParams { maxVerifiedTime = 'maxVerifiedTime', verified = 'verified', status = 'status', - challengeId = 'challengeId', + challenge = 'challenge', + difficulty = 'difficulty', + nonce = 'nonce', } export interface DappUserSolutionResult { @@ -130,9 +133,9 @@ export interface ImageVerificationResponse extends VerificationResponse { } export interface GetPowCaptchaResponse { - challenge: string - difficulty: number - signature: string + [ApiParams.challenge]: string + [ApiParams.difficulty]: number + [ApiParams.signature]: string } export interface PowCaptchaSolutionResponse { @@ -140,8 +143,20 @@ export interface PowCaptchaSolutionResponse { } export const ServerPowCaptchaVerifyRequestBody = object({ - [ApiParams.challengeId]: string(), + [ApiParams.challenge]: string(), [ApiParams.dapp]: string(), }) export type ServerPowCaptchaVerifyRequestBodyType = zInfer + +export const SubmitPowCaptchaSolutionBody = object({ + [ApiParams.blockNumber]: number(), + [ApiParams.challenge]: string(), + [ApiParams.difficulty]: number(), + [ApiParams.signature]: string(), + [ApiParams.user]: string(), + [ApiParams.dapp]: string(), + [ApiParams.nonce]: number(), +}) + +export type SubmitPowCaptchaSolutionBodyType = zInfer diff --git a/packages/web-components/src/CaptchaPlaceholder.tsx b/packages/web-components/src/CaptchaPlaceholder.tsx index 525b41f508..8c05dd68ff 100644 --- a/packages/web-components/src/CaptchaPlaceholder.tsx +++ b/packages/web-components/src/CaptchaPlaceholder.tsx @@ -14,7 +14,15 @@ /** @jsxImportSource @emotion/react */ import { ContainerDiv, WidthBasedStylesDiv } from './Containers.js' import { LoadingSpinner } from './LoadingSpinner.js' -import { WIDGET_DIMENSIONS, WIDGET_INNER_HEIGHT, WIDGET_URL, WIDGET_URL_TEXT } from './WidgetConstants.js' +import { + WIDGET_BORDER, + WIDGET_BORDER_RADIUS, + WIDGET_DIMENSIONS, + WIDGET_INNER_HEIGHT, + WIDGET_PADDING, + WIDGET_URL, + WIDGET_URL_TEXT, +} from './WidgetConstants.js' import { darkTheme, lightTheme } from './theme.js' import Logo from './Logo.js' @@ -32,11 +40,11 @@ export const ProcaptchaPlaceholder = (props: PlaceholderProps) => { {' '}
{ const signRaw = injector?.signer?.signRaw if (!signRaw) { - throw new ProsopoEnvError('PROGUI.NO_POLKADOT_EXTENSION') + throw new ProsopoEnvError('ACCOUNT.NO_POLKADOT_EXTENSION') } const blockNumberString = (await getCurrentBlockNumber()).toString()