diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml
index 640ff811..d2fbc3b8 100644
--- a/.devcontainer/docker-compose.yaml
+++ b/.devcontainer/docker-compose.yaml
@@ -20,11 +20,7 @@ services:
dockerfile: .devcontainer/Dockerfile
volumes:
- ../:/workspace/anubis:cached
- - cargo-target:/workspace/anubis/target:cached
environment:
VALKEY_URL: redis://valkey:6379/0
#entrypoint: ["/usr/bin/sleep", "infinity"]
user: vscode
-
-volumes:
- cargo-target:
diff --git a/lib/anubis.go b/lib/anubis.go
index 6a44e1fc..744b9d22 100644
--- a/lib/anubis.go
+++ b/lib/anubis.go
@@ -37,6 +37,7 @@ import (
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
_ "github.com/TecharoHQ/anubis/lib/challenge/preact"
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
+ _ "github.com/TecharoHQ/anubis/lib/challenge/wasm"
)
var (
diff --git a/lib/challenge/preact/js/app.tsx b/lib/challenge/preact/js/app.tsx
index efa90bdb..9b74ca64 100644
--- a/lib/challenge/preact/js/app.tsx
+++ b/lib/challenge/preact/js/app.tsx
@@ -1,6 +1,6 @@
import { render, h, Fragment } from "preact";
import { useState, useEffect } from "preact/hooks";
-import { g, j, r, u, x } from "./xeact.js";
+import { g, j, r, u, x } from "../../../../web/lib/xeact";
import { Sha256 } from "@aws-crypto/sha256-js";
/** @jsx h */
diff --git a/lib/challenge/wasm/proofofwork.go b/lib/challenge/wasm/proofofwork.go
new file mode 100644
index 00000000..843097f0
--- /dev/null
+++ b/lib/challenge/wasm/proofofwork.go
@@ -0,0 +1,81 @@
+package wasm
+
+import (
+ "crypto/subtle"
+ "fmt"
+ "log/slog"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/TecharoHQ/anubis/internal"
+ chall "github.com/TecharoHQ/anubis/lib/challenge"
+ "github.com/TecharoHQ/anubis/lib/localization"
+ "github.com/a-h/templ"
+)
+
+//go:generate go tool github.com/a-h/templ/cmd/templ generate
+
+func init() {
+ chall.Register("argon2id", &Impl{Algorithm: "argon2id"})
+ chall.Register("sha256", &Impl{Algorithm: "sha256"})
+}
+
+type Impl struct {
+ Algorithm string
+}
+
+func (i *Impl) Setup(mux *http.ServeMux) {}
+
+func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
+ loc := localization.GetLocalizer(r)
+ return page(loc), nil
+}
+
+func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
+ rule := in.Rule
+ challenge := in.Challenge.RandomData
+
+ nonceStr := r.FormValue("nonce")
+ if nonceStr == "" {
+ return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
+ }
+
+ nonce, err := strconv.Atoi(nonceStr)
+ if err != nil {
+ return chall.NewError("validate", "invalid response", fmt.Errorf("%w: nonce: %w", chall.ErrInvalidFormat, err))
+
+ }
+
+ elapsedTimeStr := r.FormValue("elapsedTime")
+ if elapsedTimeStr == "" {
+ return chall.NewError("validate", "invalid response", fmt.Errorf("%w elapsedTime", chall.ErrMissingField))
+ }
+
+ elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
+ if err != nil {
+ return chall.NewError("validate", "invalid response", fmt.Errorf("%w: elapsedTime: %w", chall.ErrInvalidFormat, err))
+ }
+
+ response := r.FormValue("response")
+ if response == "" {
+ return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
+ }
+
+ calcString := fmt.Sprintf("%s%d", challenge, nonce)
+ calculated := internal.SHA256sum(calcString)
+
+ if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
+ return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
+ }
+
+ // compare the leading zeroes
+ if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
+ return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted %d leading zeros but got %s", chall.ErrFailed, rule.Challenge.Difficulty, response))
+ }
+
+ lg.Debug("challenge took", "elapsedTime", elapsedTime)
+ chall.TimeTaken.WithLabelValues(i.Algorithm).Observe(elapsedTime)
+
+ return nil
+}
diff --git a/lib/challenge/wasm/proofofwork.templ b/lib/challenge/wasm/proofofwork.templ
new file mode 100644
index 00000000..48c90f4d
--- /dev/null
+++ b/lib/challenge/wasm/proofofwork.templ
@@ -0,0 +1,44 @@
+package wasm
+
+import (
+ "github.com/TecharoHQ/anubis"
+ "github.com/TecharoHQ/anubis/lib/localization"
+)
+
+templ page(localizer *localization.SimpleLocalizer) {
+
+

+

+
{ localizer.T("loading") }
+
+
+
+ if anubis.UseSimplifiedExplanation {
+
+ { localizer.T("simplified_explanation") }
+
+ } else {
+
+ { localizer.T("ai_companies_explanation") }
+
+
+ { localizer.T("anubis_compromise") }
+
+
+ { localizer.T("hack_purpose") }
+
+
+ { localizer.T("jshelter_note") }
+
+ }
+
+
+
+
+}
diff --git a/lib/challenge/wasm/proofofwork_templ.go b/lib/challenge/wasm/proofofwork_templ.go
new file mode 100644
index 00000000..147e2b7a
--- /dev/null
+++ b/lib/challenge/wasm/proofofwork_templ.go
@@ -0,0 +1,190 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.924
+package wasm
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "github.com/TecharoHQ/anubis"
+ "github.com/TecharoHQ/anubis/lib/localization"
+)
+
+func page(localizer *localization.SimpleLocalizer) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
)
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 12, Col: 41}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if anubis.UseSimplifiedExplanation {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("simplified_explanation"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 20, Col: 44}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("ai_companies_explanation"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 24, Col: 46}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("anubis_compromise"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 27, Col: 39}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("hack_purpose"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 30, Col: 34}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("jshelter_note"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 33, Col: 35}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/lib/challenge/wasm/proofofwork_test.go b/lib/challenge/wasm/proofofwork_test.go
new file mode 100644
index 00000000..dd31f163
--- /dev/null
+++ b/lib/challenge/wasm/proofofwork_test.go
@@ -0,0 +1,151 @@
+package wasm
+
+import (
+ "errors"
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/TecharoHQ/anubis/lib/challenge"
+ "github.com/TecharoHQ/anubis/lib/policy"
+ "github.com/TecharoHQ/anubis/lib/policy/config"
+)
+
+func mkRequest(t *testing.T, values map[string]string) *http.Request {
+ t.Helper()
+ req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ q := req.URL.Query()
+
+ for k, v := range values {
+ q.Set(k, v)
+ }
+
+ req.URL.RawQuery = q.Encode()
+
+ return req
+}
+
+func TestBasic(t *testing.T) {
+ i := &Impl{Algorithm: "fast"}
+ bot := &policy.Bot{
+ Challenge: &config.ChallengeRules{
+ Algorithm: "fast",
+ Difficulty: 0,
+ ReportAs: 0,
+ },
+ }
+ const challengeStr = "hunter"
+ const response = "2652bdba8fb4d2ab39ef28d8534d7694c557a4ae146c1e9237bd8d950280500e"
+
+ for _, cs := range []struct {
+ name string
+ req *http.Request
+ err error
+ challengeStr string
+ }{
+ {
+ name: "allgood",
+ req: mkRequest(t, map[string]string{
+ "nonce": "0",
+ "elapsedTime": "69",
+ "response": response,
+ }),
+ err: nil,
+ challengeStr: challengeStr,
+ },
+ {
+ name: "no-params",
+ req: mkRequest(t, map[string]string{}),
+ err: challenge.ErrMissingField,
+ challengeStr: challengeStr,
+ },
+ {
+ name: "missing-nonce",
+ req: mkRequest(t, map[string]string{
+ "elapsedTime": "69",
+ "response": response,
+ }),
+ err: challenge.ErrMissingField,
+ challengeStr: challengeStr,
+ },
+ {
+ name: "missing-elapsedTime",
+ req: mkRequest(t, map[string]string{
+ "nonce": "0",
+ "response": response,
+ }),
+ err: challenge.ErrMissingField,
+ challengeStr: challengeStr,
+ },
+ {
+ name: "missing-response",
+ req: mkRequest(t, map[string]string{
+ "nonce": "0",
+ "elapsedTime": "69",
+ }),
+ err: challenge.ErrMissingField,
+ challengeStr: challengeStr,
+ },
+ {
+ name: "wrong-nonce-format",
+ req: mkRequest(t, map[string]string{
+ "nonce": "taco",
+ "elapsedTime": "69",
+ "response": response,
+ }),
+ err: challenge.ErrInvalidFormat,
+ challengeStr: challengeStr,
+ },
+ {
+ name: "wrong-elapsedTime-format",
+ req: mkRequest(t, map[string]string{
+ "nonce": "0",
+ "elapsedTime": "taco",
+ "response": response,
+ }),
+ err: challenge.ErrInvalidFormat,
+ challengeStr: challengeStr,
+ },
+ {
+ name: "invalid-response",
+ req: mkRequest(t, map[string]string{
+ "nonce": "0",
+ "elapsedTime": "69",
+ "response": response,
+ }),
+ err: challenge.ErrFailed,
+ challengeStr: "Tacos are tasty",
+ },
+ } {
+ t.Run(cs.name, func(t *testing.T) {
+ lg := slog.With()
+
+ i.Setup(http.NewServeMux())
+
+ inp := &challenge.IssueInput{
+ Rule: bot,
+ Challenge: &challenge.Challenge{
+ RandomData: cs.challengeStr,
+ },
+ }
+
+ if _, err := i.Issue(httptest.NewRecorder(), cs.req, lg, inp); err != nil {
+ t.Errorf("can't issue challenge: %v", err)
+ }
+
+ if err := i.Validate(cs.req, lg, &challenge.ValidateInput{
+ Rule: bot,
+ Challenge: &challenge.Challenge{
+ RandomData: cs.challengeStr,
+ },
+ }); !errors.Is(err, cs.err) {
+ t.Errorf("got wrong error from Validate, got %v but wanted %v", err, cs.err)
+ }
+ })
+ }
+}
diff --git a/package.json b/package.json
index 37407134..619eb520 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,9 @@
"package": "yeet",
"lint": "make lint"
},
+ "imports": {
+ "lib/*": "./web/lib/*"
+ },
"author": "",
"license": "ISC",
"devDependencies": {
diff --git a/web/build.sh b/web/build.sh
index d0818e0c..364fdca9 100755
--- a/web/build.sh
+++ b/web/build.sh
@@ -49,7 +49,7 @@ for file in js/**/*.ts js/**/*.mjs; do
mkdir -p "$(dirname "$out")"
- esbuild "$file" --sourcemap --bundle --minify --outfile="$out" --banner:js="$LICENSE"
+ esbuild "$file" --sourcemap --bundle --outfile="$out" --banner:js="$LICENSE"
gzip -f -k -n "$out"
zstd -f -k --ultra -22 "$out"
brotli -fZk "$out"
diff --git a/web/js/algorithms/index.ts b/web/js/algorithms/index.ts
index 5b571837..e31a024f 100644
--- a/web/js/algorithms/index.ts
+++ b/web/js/algorithms/index.ts
@@ -1,6 +1,10 @@
import fast from "./fast";
+import wasm from "./wasm";
export default {
fast: fast,
slow: fast, // XXX(Xe): slow is deprecated, but keep this around in case anything goes bad
+
+ argon2id: wasm,
+ sha256: wasm,
}
\ No newline at end of file
diff --git a/web/js/algorithms/wasm.ts b/web/js/algorithms/wasm.ts
new file mode 100644
index 00000000..83032797
--- /dev/null
+++ b/web/js/algorithms/wasm.ts
@@ -0,0 +1,87 @@
+import { u } from "../../lib/xeact";
+
+type ProgressCallback = (nonce: number) => void;
+
+interface ProcessOptions {
+ basePrefix: string;
+ version: string;
+ algorithm: string;
+}
+
+const getHardwareConcurrency = () =>
+ navigator.hardwareConcurrency !== undefined ? navigator.hardwareConcurrency : 1;
+
+export default function process(
+ options: ProcessOptions,
+ data: string,
+ difficulty: number = 5,
+ signal: AbortSignal | null = null,
+ progressCallback?: ProgressCallback,
+ threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)),
+): Promise {
+ console.debug(options);
+ const { basePrefix, version, algorithm } = options;
+
+ let wasmFeatures = "baseline";
+
+ return new Promise(async (resolve, reject) => {
+ const module = await fetch(u(`${basePrefix}/.within.website/x/cmd/anubis/static/wasm/${wasmFeatures}/${algorithm}.wasm?cacheBuster=${version}`))
+ .then(x => WebAssembly.compileStreaming(x));
+
+ const webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/wasm.mjs?cacheBuster=${version}`;
+
+ const workers: Worker[] = [];
+ let settled = false;
+
+ const onAbort = () => {
+ console.log("PoW aborted");
+ cleanup();
+ reject(new DOMException("Aborted", "AbortError"));
+ };
+
+ const cleanup = () => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ workers.forEach((w) => w.terminate());
+ if (signal != null) {
+ signal.removeEventListener("abort", onAbort);
+ }
+ };
+
+ if (signal != null) {
+ if (signal.aborted) {
+ return onAbort();
+ }
+ signal.addEventListener("abort", onAbort, { once: true });
+ }
+
+ for (let i = 0; i < threads; i++) {
+ let worker = new Worker(webWorkerURL);
+
+ worker.onmessage = (event) => {
+ if (typeof event.data === "number") {
+ progressCallback?.(event.data);
+ } else {
+ cleanup();
+ resolve(event.data);
+ }
+ }
+
+ worker.onerror = (event) => {
+ cleanup();
+ reject(event);
+ }
+
+ worker.postMessage({
+ data,
+ difficulty,
+ nonce: i,
+ threads,
+ module,
+ });
+ }
+ });
+};
+
diff --git a/web/js/main.ts b/web/js/main.ts
index e37c536c..a5df154d 100644
--- a/web/js/main.ts
+++ b/web/js/main.ts
@@ -171,7 +171,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
try {
const t0 = Date.now();
const { hash, nonce } = await process(
- { basePrefix, version: anubisVersion },
+ { basePrefix, version: anubisVersion, algorithm: rules.algorithm },
challenge.randomData,
rules.difficulty,
null,
diff --git a/web/js/worker/wasm.ts b/web/js/worker/wasm.ts
new file mode 100644
index 00000000..cddbe427
--- /dev/null
+++ b/web/js/worker/wasm.ts
@@ -0,0 +1,106 @@
+export interface Args {
+ data: string;
+ difficulty: number;
+ nonce: number;
+ threads: number;
+ module: BufferSource;
+}
+
+interface AnubisExports {
+ anubis_work: (difficulty: number, initialNonce: number, threads: number) => number;
+ data_ptr: () => number;
+ result_hash_ptr: () => number;
+ result_hash_size: () => number;
+ set_data_length: (len: number) => void;
+ memory: WebAssembly.Memory;
+}
+
+addEventListener("message", async (event: MessageEvent) => {
+ console.log(event.data);
+ const { data, difficulty, threads, module } = event.data;
+ let { nonce } = event.data;
+
+ const importObject = {
+ anubis: {
+ anubis_update_nonce: (nonce: number) => postMessage(nonce),
+ }
+ };
+
+ if (nonce !== 0) {
+ importObject.anubis.anubis_update_nonce = (_) => { };
+ }
+
+ const obj = await WebAssembly.instantiate(module, importObject);
+
+ const {
+ anubis_work,
+ data_ptr,
+ result_hash_ptr,
+ result_hash_size,
+ set_data_length,
+ memory
+ } = (obj as unknown as any).exports as unknown as AnubisExports;
+ function uint8ArrayToHex(arr: Uint8Array) {
+ return Array.from(arr)
+ .map((c) => c.toString(16).padStart(2, "0"))
+ .join("");
+ }
+
+ function hexToUint8Array(hexString: string): Uint8Array {
+ // Remove whitespace and optional '0x' prefix
+ hexString = hexString.replace(/\s+/g, '').replace(/^0x/, '');
+
+ // Check for valid length
+ if (hexString.length % 2 !== 0) {
+ throw new Error('Invalid hex string length');
+ }
+
+ // Check for valid characters
+ if (!/^[0-9a-fA-F]+$/.test(hexString)) {
+ throw new Error('Invalid hex characters');
+ }
+
+ // Convert to Uint8Array
+ const byteArray = new Uint8Array(hexString.length / 2);
+ for (let i = 0; i < byteArray.length; i++) {
+ const byteValue = parseInt(hexString.substr(i * 2, 2), 16);
+ byteArray[i] = byteValue;
+ }
+
+ return byteArray;
+ }
+
+ // Write data to buffer
+ function writeToBuffer(data: Uint8Array) {
+ if (data.length > 1024) throw new Error("Data exceeds buffer size");
+
+ // Get pointer and create view
+ const offset = data_ptr();
+ const buffer = new Uint8Array(memory.buffer, offset, data.length);
+
+ // Copy data
+ buffer.set(data);
+
+ // Set data length
+ set_data_length(data.length);
+ }
+
+ function readFromChallenge() {
+ const offset = result_hash_ptr();
+ const buffer = new Uint8Array(memory.buffer, offset, result_hash_size());
+
+ return buffer;
+ }
+
+ writeToBuffer(hexToUint8Array(data));
+
+ nonce = anubis_work(difficulty, nonce, threads);
+ const challenge = readFromChallenge();
+ const result = uint8ArrayToHex(challenge);
+
+ postMessage({
+ hash: result,
+ difficulty,
+ nonce,
+ });
+});
\ No newline at end of file
diff --git a/lib/challenge/preact/js/xeact.js b/web/lib/xeact.js
similarity index 100%
rename from lib/challenge/preact/js/xeact.js
rename to web/lib/xeact.js