Clean up code. Reorganize files. Port stuff from other branches. + more
This turns the project into a monorepo using pnpm workspaces, dramatically simplifying the build process. It also fixes a lot of bugs and just generally makes the codebase a lot cleaner.
This commit is contained in:
12
packages/widget/package-lock.json
generated
12
packages/widget/package-lock.json
generated
@@ -8,6 +8,9 @@
|
||||
"name": "@impost/widget",
|
||||
"version": "0.1.0",
|
||||
"license": "BSL-1.0",
|
||||
"dependencies": {
|
||||
"comlink": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.24",
|
||||
"lit": "^3.1.2",
|
||||
@@ -1433,7 +1436,6 @@
|
||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -1655,6 +1657,11 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/comlink": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
|
||||
"integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="
|
||||
},
|
||||
"node_modules/compare-versions": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
|
||||
@@ -2148,7 +2155,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2422,7 +2428,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -2461,7 +2466,6 @@
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"lit-element": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@impost/lib": "workspace:*",
|
||||
"@types/node": "^20.11.24",
|
||||
"lit": "^3.1.2",
|
||||
"lit-element": "^3.1.2",
|
||||
@@ -30,6 +31,7 @@
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-dts": "^4.5.4"
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"comlink": "^4.4.2"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import { LitElement, html, css, isServer, type PropertyValues } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { type ChallengeSolveRequest, type SolutionMessage, WorkerMessageType, type WorkerRequest, WorkerResponseType } from './types/worker';
|
||||
import { type Challenge, ChallengeStrategy } from '@impost/lib';
|
||||
import { type Challenge } from '@impost/lib';
|
||||
import { get_wasm_module } from '@impost/lib/solver';
|
||||
import * as Comlink from 'comlink';
|
||||
|
||||
import ChallengeWorker from './solver-worker?worker&inline';
|
||||
import SolverWorker from './solver-worker?worker&inline';
|
||||
|
||||
@customElement('pow-captcha')
|
||||
export class PowCaptcha extends LitElement {
|
||||
type SolverWorkerAPI = Comlink.Remote<typeof import("./solver-worker")>;
|
||||
|
||||
@customElement('impost-captcha')
|
||||
export class ImpostCaptcha extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -63,13 +65,13 @@ export class PowCaptcha extends LitElement {
|
||||
.impost-footer div a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
/// ================================================
|
||||
/// Configuration
|
||||
/// ================================================
|
||||
|
||||
|
||||
// one of: "load", "focus", "submit", "off"
|
||||
@property({ type: String })
|
||||
auto: "onload" | "onfocus" | "onsubmit" | "off" = "off";
|
||||
|
||||
@@ -79,24 +81,28 @@ export class PowCaptcha extends LitElement {
|
||||
@property({ type: String })
|
||||
challengejson: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
showHashrate: boolean = false;
|
||||
|
||||
/// ================================================
|
||||
/// Internals
|
||||
/// ================================================
|
||||
|
||||
// needed to allow for multiple widgets on the same page if you wanted to
|
||||
// do that for some reason
|
||||
uid: string = Math.floor(Math.random() * 100000).toString();
|
||||
private uid: string = Math.floor(Math.random() * 100000).toString();
|
||||
|
||||
private _internals: ElementInternals | null = null;
|
||||
static formAssociated = true;
|
||||
|
||||
@state()
|
||||
private solution: string = '';
|
||||
|
||||
@state()
|
||||
private errorMessage: string = '';
|
||||
private solution: string | null = null;
|
||||
|
||||
@state()
|
||||
private challengeData: Challenge | null = null;
|
||||
|
||||
@state()
|
||||
private solved: boolean = false;
|
||||
|
||||
@state()
|
||||
private isSolving: boolean = false;
|
||||
private status: 'unsolved' | 'solving' | 'solved' | 'error' = 'unsolved';
|
||||
|
||||
@state()
|
||||
private disabled: boolean = true;
|
||||
@@ -104,18 +110,23 @@ export class PowCaptcha extends LitElement {
|
||||
@state()
|
||||
private hashRate: number = 0;
|
||||
|
||||
|
||||
// stores the nonce and solution atomics
|
||||
private sab: SharedArrayBuffer = new SharedArrayBuffer(8);
|
||||
|
||||
private solverWorkers: Worker[] | null = null;
|
||||
private solverWorkers: SolverWorkerAPI[] | null = null;
|
||||
private nativeWorkers: Worker[] | null = null;
|
||||
|
||||
private solveStartTime: number | null = null;
|
||||
private hashRateInterval: number | null = null;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._internals = this.attachInternals();
|
||||
this.fetchChallenge();
|
||||
|
||||
console.log(this._internals.form);
|
||||
|
||||
this.initWorkers();
|
||||
|
||||
switch (this.auto) {
|
||||
@@ -127,7 +138,23 @@ export class PowCaptcha extends LitElement {
|
||||
break;
|
||||
case 'onsubmit':
|
||||
if (this.parentElement?.nodeName === 'FORM') {
|
||||
this.parentElement.addEventListener('submit', () => this.solveChallenge());
|
||||
this.parentElement.addEventListener('submit', async (ev) => {
|
||||
if (this.status === 'solved') {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
await this.solveChallenge();
|
||||
|
||||
const form = this.parentElement as HTMLFormElement;
|
||||
|
||||
if (form.requestSubmit) {
|
||||
form.requestSubmit();
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -146,9 +173,10 @@ export class PowCaptcha extends LitElement {
|
||||
this.hashRateInterval = null;
|
||||
}
|
||||
|
||||
for (const worker of this.solverWorkers || []) {
|
||||
for (const worker of this.nativeWorkers || []) {
|
||||
worker.terminate();
|
||||
this.solverWorkers = null;
|
||||
this.nativeWorkers = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +185,6 @@ export class PowCaptcha extends LitElement {
|
||||
}
|
||||
|
||||
async fetchChallenge() {
|
||||
this.errorMessage = '';
|
||||
if (this.challengeData !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -168,6 +195,7 @@ export class PowCaptcha extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// challenge data must be provided by the user when using SSR
|
||||
if (isServer) {
|
||||
return;
|
||||
}
|
||||
@@ -179,16 +207,21 @@ export class PowCaptcha extends LitElement {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching challenge:', error);
|
||||
this.errorMessage = 'Failed to fetch challenge. Please try again.';
|
||||
this.status = 'error';
|
||||
});
|
||||
}
|
||||
|
||||
async initWorkers() {
|
||||
this.solverWorkers = [];
|
||||
this.nativeWorkers = [];
|
||||
|
||||
const num_workers = navigator.hardwareConcurrency;
|
||||
for (let i = 0; i < num_workers; i++) {
|
||||
this.solverWorkers.push(new ChallengeWorker());
|
||||
const nativeWorker = new SolverWorker();
|
||||
const comlinkWorker = Comlink.wrap<SolverWorkerAPI>(nativeWorker);
|
||||
|
||||
this.nativeWorkers.push(nativeWorker);
|
||||
this.solverWorkers.push(comlinkWorker);
|
||||
}
|
||||
|
||||
const atomics_view = new Int32Array(this.sab);
|
||||
@@ -198,106 +231,53 @@ export class PowCaptcha extends LitElement {
|
||||
let wasm_module = await get_wasm_module();
|
||||
let worker_promises: Promise<void>[] = [];
|
||||
for (let i = 0; i < this.solverWorkers.length; i++) {
|
||||
console.log('Worker', i);
|
||||
const worker = this.solverWorkers[i]!;
|
||||
|
||||
worker_promises.push(new Promise<void>((resolve, reject) => {
|
||||
const message_handler = (event: MessageEvent<SolutionMessage>) => {
|
||||
console.log('Worker', i, 'got message', event.data);
|
||||
|
||||
if (event.data.type === WorkerResponseType.Error) {
|
||||
console.error("Worker error:", event.data.error);
|
||||
reject(event.data.error);
|
||||
}
|
||||
|
||||
if (event.data.type === WorkerResponseType.InitOk) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject(new Error("Unexpected message from worker"));
|
||||
};
|
||||
|
||||
const error_handler = (error: ErrorEvent) => {
|
||||
console.error("Worker error:", error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
worker.addEventListener('message', message_handler);
|
||||
worker.addEventListener('error', error_handler);
|
||||
|
||||
worker.postMessage({
|
||||
type: WorkerMessageType.Init,
|
||||
module: wasm_module,
|
||||
sab: this.sab,
|
||||
} as WorkerRequest);
|
||||
}));
|
||||
const solver = this.solverWorkers[i]!;
|
||||
worker_promises.push(solver.init(wasm_module, this.sab)); // Direct call to exposed `init` method
|
||||
}
|
||||
|
||||
const timeoutMs = 10 * 1000;
|
||||
let timeout: number;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
this.errorMessage = 'Failed to initialize workers in time. Please refresh the page.';
|
||||
reject(new Error(`Function timed out after ${timeoutMs}ms`));
|
||||
reject(new Error(`Function timeout after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
Promise.allSettled(worker_promises).then(() => {
|
||||
Promise.allSettled(worker_promises).then(results => {
|
||||
const failedInits = results.filter(r => r.status === 'rejected');
|
||||
if (failedInits.length > 0) {
|
||||
console.error('Some workers failed to initialize:', failedInits);
|
||||
// we might want to collect all errors, and if every
|
||||
// worker fails, we can throw, but carry on if only some
|
||||
// fail. For now, we'll just throw if any fail.
|
||||
|
||||
throw new Error("One or more workers failed to initialize.");
|
||||
}
|
||||
|
||||
console.log('All workers initialized');
|
||||
return;
|
||||
}),
|
||||
timeoutPromise,
|
||||
]).then(() => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
}
|
||||
|
||||
async issueChallengeToWorker(worker: Worker, request: ChallengeSolveRequest): Promise<SolutionMessage> {
|
||||
return new Promise<SolutionMessage>((resolve, reject) => {
|
||||
const message_handler = (event: MessageEvent<SolutionMessage>) => {
|
||||
worker.removeEventListener('message', message_handler);
|
||||
worker.removeEventListener('error', error_handler);
|
||||
|
||||
resolve(event.data);
|
||||
};
|
||||
|
||||
const error_handler = (error: ErrorEvent) => {
|
||||
worker.removeEventListener('error', error_handler);
|
||||
worker.removeEventListener('message', message_handler);
|
||||
console.error("Worker error:", error);
|
||||
|
||||
reject(error);
|
||||
};
|
||||
|
||||
worker.addEventListener('message', message_handler);
|
||||
worker.addEventListener('error', error_handler);
|
||||
|
||||
switch (request.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
worker.postMessage({
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
salt: request.salt,
|
||||
difficulty: request.difficulty,
|
||||
} as WorkerRequest);
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
worker.postMessage({
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
target: request.target,
|
||||
salt: request.salt,
|
||||
} as WorkerRequest);
|
||||
break;
|
||||
}
|
||||
}).catch(error => {
|
||||
clearTimeout(timeout);
|
||||
console.error("Worker initialization failed:", error);
|
||||
this.status = 'error';
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async solveChallenge() {
|
||||
if (!this.challengeData || this.solverWorkers === null) {
|
||||
this.errorMessage = 'Captcha is not ready. Please wait or refresh.';
|
||||
// in all normal cases, this should be impossible
|
||||
this.status = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.solution !== '') {
|
||||
if (this.solution !== null || this.status !== 'unsolved') {
|
||||
// do not solve twice
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,45 +286,23 @@ export class PowCaptcha extends LitElement {
|
||||
const nonce = this.getCurrentWorkingNonce();
|
||||
|
||||
this.hashRate = (nonce / ((performance.now() - this.solveStartTime!) / 1000));
|
||||
console.log(this.hashRate);
|
||||
}, 250);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solve', {
|
||||
detail: {
|
||||
solution: this.solution,
|
||||
},
|
||||
detail: { challenge: this.challengeData, }, // empty solution
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
}));
|
||||
|
||||
console.log(this.challengeData);
|
||||
|
||||
this.isSolving = true;
|
||||
this.errorMessage = '';
|
||||
this.solution = '';
|
||||
this.status = 'solving';
|
||||
this.solution = null;
|
||||
|
||||
const atomics_view = new Int32Array(this.sab);
|
||||
// reset atomics
|
||||
Atomics.store(atomics_view, 0, 0);
|
||||
Atomics.store(atomics_view, 1, -1);
|
||||
|
||||
let request: ChallengeSolveRequest;
|
||||
|
||||
switch (this.challengeData.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
request = {
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
salt: this.challengeData.salt,
|
||||
difficulty: this.challengeData.difficulty,
|
||||
};
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
request = {
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
target: this.challengeData.target,
|
||||
salt: this.challengeData.salt,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('Sending challenge to workers...');
|
||||
// TODO: the first response is not always the solution, due to cmpxchg
|
||||
// blocking, some workers may block on the read, and as soon as they
|
||||
@@ -352,40 +310,42 @@ export class PowCaptcha extends LitElement {
|
||||
//
|
||||
// We need to do a better job of tracking solvers, so if one worker
|
||||
// errors out, we only error out if all workers have errored out.
|
||||
let worker_promises: Promise<SolutionMessage>[] = [];
|
||||
for (let worker of this.solverWorkers) {
|
||||
// dispatch to all workers, func is async so it will not block
|
||||
worker_promises.push(this.issueChallengeToWorker(worker, request));
|
||||
let worker_promises: Promise<string>[] = [];
|
||||
for (let solver of this.solverWorkers) {
|
||||
worker_promises.push(solver.solve_challenge(this.challengeData as Challenge));
|
||||
}
|
||||
|
||||
let solution = await Promise.race(worker_promises);
|
||||
try {
|
||||
await Promise.race(worker_promises);
|
||||
|
||||
if (solution.type === WorkerResponseType.Error) {
|
||||
this.errorMessage = solution.error;
|
||||
return;
|
||||
// The true solution is stored in the SharedArrayBuffer.
|
||||
this.solution = String(Atomics.load(atomics_view, 1));
|
||||
this.status = 'solved';
|
||||
} catch (error: any) {
|
||||
console.error("Captcha solving failed:", error);
|
||||
this.status = 'error';
|
||||
} finally {
|
||||
if (this.hashRateInterval !== null) {
|
||||
clearInterval(this.hashRateInterval);
|
||||
this.hashRateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (solution.type !== WorkerResponseType.Solution) {
|
||||
this.errorMessage = "Something went wrong, please try again later.";
|
||||
return;
|
||||
}
|
||||
|
||||
this.solution = Atomics.load(atomics_view, 1).toString();
|
||||
this.isSolving = false;
|
||||
this.solved = true;
|
||||
|
||||
if (this.hashRateInterval !== null) {
|
||||
clearInterval(this.hashRateInterval);
|
||||
this.hashRateInterval = null;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solved', {
|
||||
detail: {
|
||||
if (this.status === 'solved') {
|
||||
this._internals!.setFormValue(JSON.stringify({
|
||||
challenge: this.challengeData.salt,
|
||||
solution: this.solution,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
}));
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solved', {
|
||||
detail: {
|
||||
challenge: this.challengeData,
|
||||
solution: this.solution,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
solvePreventDefault(event: Event) {
|
||||
@@ -400,12 +360,6 @@ export class PowCaptcha extends LitElement {
|
||||
this.challengejson = '';
|
||||
}
|
||||
|
||||
if (this.errorMessage) {
|
||||
return html`
|
||||
<div class="error-message">${this.errorMessage}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.challengeData === null) {
|
||||
return html`
|
||||
<div class="loading-message">Loading captcha challenge...</div>
|
||||
@@ -416,9 +370,9 @@ export class PowCaptcha extends LitElement {
|
||||
<div class="impost-widget">
|
||||
<div class="impost-main">
|
||||
<div class="impost-checkbox">
|
||||
${!this.isSolving ? html`
|
||||
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.solved}>
|
||||
` : html`
|
||||
${this.status !== 'solving' ? html`${this.status === 'error' ? html`<svg class="impost-error-icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 14q-.425 0-.712-.288T11 13V6q0-.425.288-.712T12 5t.713.288T13 6v7q0 .425-.288.713T12 14m0 5q-.425 0-.712-.288T11 18t.288-.712T12 17t.713.288T13 18t-.288.713T12 19"/></svg>` : html`
|
||||
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.status === 'solved'}>
|
||||
`}` : html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
|
||||
<!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
|
||||
<g stroke="currentColor">
|
||||
@@ -431,11 +385,11 @@ export class PowCaptcha extends LitElement {
|
||||
</svg>
|
||||
`}
|
||||
</div>
|
||||
<label for="impost-checkbox-${this.uid}">${this.solved ? 'Verified' : html`${this.isSolving ? 'Verifying...' : 'I am not a robot'}`}</label>
|
||||
<label for="impost-checkbox-${this.uid}">${this.status === 'error' ? 'Something went wrong' : this.status === 'solved' ? 'Verified' : html`${this.status === 'solving' ? 'Verifying...' : 'I am not a robot'}`}</label>
|
||||
</div>
|
||||
|
||||
<div class="impost-footer">
|
||||
${this.isSolving && this.hashRate > 0 ? html`
|
||||
${this.showHashrate && this.status === 'solving' && this.hashRate > 0 ? html`
|
||||
<div>
|
||||
<span>H/s:</span>
|
||||
<span>${this.hashRate.toFixed(2)}</span>
|
||||
@@ -449,8 +403,9 @@ export class PowCaptcha extends LitElement {
|
||||
|
||||
<input
|
||||
type = "text"
|
||||
id = "impost-solution"
|
||||
class="hidden"
|
||||
name = "impost-solution"
|
||||
id = "impost-solution-${this.uid}"
|
||||
style="display: none;"
|
||||
.value = ${this.solution}
|
||||
/>
|
||||
`;
|
||||
@@ -1 +1 @@
|
||||
export * from './pow-captcha';
|
||||
export * from './captcha';
|
||||
@@ -1,91 +1,64 @@
|
||||
// This worker just sits on another thread and waits for message to solve
|
||||
// challenges so that we dont block the render thread
|
||||
|
||||
import {
|
||||
type WorkerRequest,
|
||||
type SolutionMessage,
|
||||
WorkerMessageType,
|
||||
WorkerResponseType,
|
||||
} from "./types/worker";
|
||||
|
||||
import { ChallengeStrategy } from "@impost/lib";
|
||||
|
||||
import { type SolverModule, init_solver, solve_leaading_zeroes_challenge, solve_target_number_challenge } from '@impost/lib/solver';
|
||||
import { type Challenge } from "@impost/lib";
|
||||
import { type SolverModule, init_solver, solve_challenge as libimpost_solve_challenge } from '@impost/lib/solver';
|
||||
import * as Comlink from 'comlink';
|
||||
|
||||
let solver: SolverModule | null = null;
|
||||
|
||||
let atomic_nonce: Int32Array | null = null;
|
||||
let atomic_solution: Int32Array | null = null;
|
||||
|
||||
onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
||||
if (event.data.type === WorkerMessageType.Init) {
|
||||
atomic_nonce = new Int32Array(event.data.sab, 0, 1);
|
||||
atomic_solution = new Int32Array(event.data.sab, 4, 1);
|
||||
let module = event.data.module;
|
||||
/**
|
||||
* Initializes the worker.
|
||||
* Must be called before any solve functions.
|
||||
* @param module The solver web assembly module.
|
||||
* @param sab The SharedArrayBuffer for nonce and solution.
|
||||
*/
|
||||
export async function init(module: WebAssembly.Module, sab: SharedArrayBuffer): Promise<void> {
|
||||
atomic_nonce = new Int32Array(sab, 0, 1);
|
||||
atomic_solution = new Int32Array(sab, 4, 1);
|
||||
|
||||
try {
|
||||
solver = await init_solver({
|
||||
__get_solution: () => Atomics.load(atomic_solution!, 0),
|
||||
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
||||
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||
}, module);
|
||||
} catch (error: any) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: `Could not load WASM solver: ${error.message}`,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!solver) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "Failed to load WASM solver",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
postMessage({
|
||||
type: WorkerResponseType.InitOk,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
try {
|
||||
solver = await init_solver({
|
||||
__get_solution: () => Atomics.load(atomic_solution!, 0),
|
||||
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
||||
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||
__log: (str_ptr: number, str_len: number) => console.log(new TextDecoder().decode(new Uint8Array(solver!.exports.memory.buffer, str_ptr, str_len))),
|
||||
}, module);
|
||||
} catch (error: any) {
|
||||
console.error("Worker: Failed to initialize WASM solver", error);
|
||||
throw new Error(`Could not load WASM solver in worker: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!solver) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "WASM solver not loaded",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
throw new Error("Worker: Failed to load WASM solver.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves a given challenge.
|
||||
* @param challenge The challenge data.
|
||||
* @returns The nonce solution on success, or throws an error.
|
||||
*/
|
||||
export async function solve_challenge(challenge: Challenge): Promise<string> {
|
||||
if (!solver || !atomic_nonce || !atomic_solution) {
|
||||
throw new Error("WASM solver or atomics not initialized in worker. Call init() first.");
|
||||
}
|
||||
|
||||
if (atomic_nonce === null || atomic_solution === null) {
|
||||
throw new Error("Atomics not initialized");
|
||||
console.log("Worker: Solving challenge...");
|
||||
|
||||
const solutionNonce = libimpost_solve_challenge(solver, challenge);
|
||||
|
||||
if (solutionNonce < 0) {
|
||||
throw new Error(`Worker: Failed to solve challenge. Internal code: ${solutionNonce}`);
|
||||
}
|
||||
|
||||
const { strategy } = event.data;
|
||||
const finalSolution = Atomics.load(atomic_solution, 0);
|
||||
return finalSolution.toString();
|
||||
}
|
||||
|
||||
let solution: number;
|
||||
switch (strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
solution = solve_leaading_zeroes_challenge(solver, event.data)
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
solution = solve_target_number_challenge(solver, event.data)
|
||||
break;
|
||||
}
|
||||
|
||||
// we are just assuming that if its less than -1, its the min i32
|
||||
if (solution < 0) {
|
||||
return postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "failed to solve challenge",
|
||||
} as SolutionMessage);
|
||||
}
|
||||
|
||||
postMessage({
|
||||
type: WorkerResponseType.Solution,
|
||||
nonce: solution === -1 ? null : solution.toString()
|
||||
} as SolutionMessage);
|
||||
};
|
||||
// * Do not forget to expose functions we want to use on the main thread
|
||||
Comlink.expose({ solve_challenge, init });
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { ChallengeStrategy } from "@impost/lib";
|
||||
|
||||
export enum WorkerMessageType {
|
||||
Init = "init",
|
||||
Challenge = "challenge",
|
||||
}
|
||||
|
||||
interface WorkerInitRequest {
|
||||
type: WorkerMessageType.Init;
|
||||
module: WebAssembly.Module;
|
||||
|
||||
sab: SharedArrayBuffer;
|
||||
}
|
||||
|
||||
interface ChallengeLeadingZeroesSolveRequest {
|
||||
strategy: ChallengeStrategy.LeadingZeroes;
|
||||
salt: string;
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroesSolveRequest {
|
||||
type: WorkerMessageType.Challenge;
|
||||
}
|
||||
|
||||
interface ChallengeTargetNumberSolveRequest {
|
||||
strategy: ChallengeStrategy.TargetNumber;
|
||||
target: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
interface WorkerChallengeTargetNumberSolveRequest extends ChallengeTargetNumberSolveRequest {
|
||||
type: WorkerMessageType.Challenge;
|
||||
}
|
||||
|
||||
export type ChallengeSolveRequest = ChallengeLeadingZeroesSolveRequest | ChallengeTargetNumberSolveRequest;
|
||||
type WorkerChallengeSolveRequest = WorkerChallengeLeadingZeroesSolveRequest | WorkerChallengeTargetNumberSolveRequest;
|
||||
|
||||
export type WorkerRequest = WorkerInitRequest | WorkerChallengeSolveRequest;
|
||||
|
||||
export enum WorkerResponseType {
|
||||
Error = "error",
|
||||
InitOk = "init_ok",
|
||||
Solution = "solution",
|
||||
}
|
||||
|
||||
interface ErrorMessageResponse {
|
||||
type: WorkerResponseType.Error;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface SolutionMessageResponse {
|
||||
type: WorkerResponseType.Solution;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
interface InitOkMessageResponse {
|
||||
type: WorkerResponseType.InitOk;
|
||||
}
|
||||
|
||||
export type SolutionMessage = ErrorMessageResponse | SolutionMessageResponse | InitOkMessageResponse;
|
||||
2
packages/widget/src/vite-env.d.ts
vendored
2
packages/widget/src/vite-env.d.ts
vendored
@@ -1 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
@@ -17,13 +17,10 @@ export default defineConfig({
|
||||
dir: 'dist',
|
||||
},
|
||||
},
|
||||
// WARN: this setting has caused issues for me in the past, but now it
|
||||
// seems to work fine. I'm not sure why, but minifying the bundle saves
|
||||
// 5KB, and it doesnt *cause* issues, so I'm leaving it on.
|
||||
minify: true,
|
||||
minify: true
|
||||
},
|
||||
|
||||
plugins: [
|
||||
dts(),
|
||||
dts()
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user