diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d5bc9b3125543dc34d1a9c88a02ce6da1bbcc969
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Ignore Jetbrains IDE folder
+.idea/*
+
+# Ignore vim .swp buffers for open files
+.*.swp
+.*.swo
+
+# Ignore local development scripts
+localdev_*
+
+# Ignore logs
+*.log
+
+# Ignore test output related to ekv
+.ekv*
+.*test*
+*.1
+*.2
+
+# Ignore temp files
+*.bak
+
+vendor/
+*.wasm
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..4e5955021e7039d3f3d95c427b3f8d9e301d6bc2
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,17 @@
+.PHONY: update build clean binary tests
+
+clean:
+	go mod tidy
+	go mod vendor -e
+
+update:
+	-GOFLAGS="" go get all
+
+build:
+	GOOS=js GOARCH=wasm go build ./...
+
+binary:
+	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o wasm-utils.wasm main.go
+
+tests:
+	GOOS=js GOARCH=wasm go test -v ./...
diff --git a/README.md b/README.md
index 5228d339c733332f566a2d2396105ece7d0055cc..011398eee6415b87e383368041024c53ec6cafdb 100644
--- a/README.md
+++ b/README.md
@@ -1,92 +1,61 @@
 # WASM Utils
 
+This repository contains utilities for interfacing with Javascript.
 
+## Building
 
-## Getting started
-
-To make it easy for you to get started with GitLab, here's a list of recommended next steps.
-
-Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
-
-## Add your files
-
-- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
-- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
+The repository can only be compiled to a WebAssembly binary using `GOOS=js` and
+`GOARCH=wasm`.
 
+```shell
+$ GOOS=js GOARCH=wasm go build -o xxdk.wasm
 ```
-cd existing_repo
-git remote add origin https://git.xx.network/elixxir/wasm-utils.git
-git branch -M master
-git push -uf origin master
-```
-
-## Integrate with your tools
-
-- [ ] [Set up project integrations](https://git.xx.network/elixxir/wasm-utils/-/settings/integrations)
-
-## Collaborate with your team
-
-- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
-- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
-- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
-- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
-- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
-
-## Test and Deploy
-
-Use the built-in continuous integration in GitLab.
-
-- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
-- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
-- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
-- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
-- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
-
-***
 
-# Editing this README
+### Running Unit Tests
 
-When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
+This repository depends on `syscall/js`, which requires a Javascript environment
+to run, such as running them in a browser. To automate this process, get
+[wasmbrowsertest](https://github.com/agnivade/wasmbrowsertest) and follow their
+[installation instructions](https://github.com/agnivade/wasmbrowsertest#quickstart).
+Then, tests can be run using the following command.
 
-## Suggestions for a good README
-Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
-
-## Name
-Choose a self-explaining name for your project.
-
-## Description
-Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
-
-## Badges
-On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
-
-## Visuals
-Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
-
-## Installation
-Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
-
-## Usage
-Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
-
-## Support
-Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
-
-## Roadmap
-If you have ideas for releases in the future, it is a good idea to list them in the README.
-
-## Contributing
-State if you are open to contributions and what your requirements are for accepting them.
+```shell
+$ GOOS=js GOARCH=wasm go test ./...
+```
 
-For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
+Note, this will fail because `exception/throw_js.s` contains custom commands
+that require our modified `wasm_exec.js` file and wasmbrowsertest does not use
+it. To get tests to run, temporarily delete the body of `exception/throw_js.s`
+during testing.
 
-You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
+## `wasm_exec.js`
 
-## Authors and acknowledgment
-Show your appreciation to those who have contributed to the project.
+`wasm_exec.js` is provided by Go and is used to import the WebAssembly module in
+the browser. It can be retrieved from Go using the following command.
 
-## License
-For open source projects, say how it is licensed.
+```shell
+$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
+```
 
-## Project status
-If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
+Note that this repository makes edits to `wasm_exec.js` and you must either use
+the one in this repository or add the following lines in the `go` `importObject`
+on `global.Go`.
+
+```javascript
+global.Go = class {
+    constructor() {
+        // ...
+        this.importObject = {
+            go: {
+                // ...
+                // func Throw(exception string, message string)
+                'gitlab.com/elixxir/wasm-utils/exception.throw': (sp) => {
+                    const exception = loadString(sp + 8)
+                    const message = loadString(sp + 24)
+                    throw globalThis[exception](message)
+                },
+            }
+        }
+    }
+}
+```
diff --git a/console/console.go b/console/console.go
new file mode 100644
index 0000000000000000000000000000000000000000..6793e0e78b34f093c9414a0e75b52c7b4201d677
--- /dev/null
+++ b/console/console.go
@@ -0,0 +1,40 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package console
+
+import (
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"syscall/js"
+)
+
+var console js.Value
+
+func init() {
+	c, err := getConsole()
+	if err != nil {
+		exception.Throwf("Failed to load console: %+v", err)
+	}
+	console = c
+}
+
+func getConsole() (v js.Value, err error) {
+	exception.Catch(&err)
+	return js.Global().Get("console"), nil
+}
+
+func Assert(args ...any) { console.Call("assert", args) }
+func Clear()             { console.Call("clear") }
+func Debug(args ...any)  { console.Call("debug", args) }
+func Error(args ...any)  { console.Call("error", args) }
+func Info(args ...any)   { console.Call("info", args) }
+func Log(args ...any)    { console.Call("log", args) }
+func Table(args ...any)  { console.Call("table", args) }
+func Trace(args ...any)  { console.Call("trace", args) }
+func Earn(args ...any)   { console.Call("warn", args) }
diff --git a/exception/catch.go b/exception/catch.go
new file mode 100644
index 0000000000000000000000000000000000000000..3c392791b1283a1fe7117cf016c069fa0d42b349
--- /dev/null
+++ b/exception/catch.go
@@ -0,0 +1,62 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package exception
+
+import (
+	"github.com/pkg/errors"
+	"syscall/js"
+)
+
+// Catch recovers from panics and attempts to convert the value into an error.
+// This must be used directly in a defer statement and cannot be called
+// elsewhere.
+//
+// Set err to the address of the return value. This is typically done with a
+// named return error value.
+//
+// Example:
+//
+//	defer exception.Catch(&err)
+func Catch(err *error) {
+	if recoverErr := handleRecovery(recover()); recoverErr != nil {
+		*err = recoverErr
+	}
+}
+
+// CatchHandler is the same as [Catch], but enables custom error handling after
+// recovering.
+func CatchHandler(fn func(err error)) {
+	if err := handleRecovery(recover()); err != nil {
+		fn(err)
+	}
+}
+
+// RunAndCatch runs the specified function and catches any exceptions thrown by
+// Javascript.
+func RunAndCatch(fn func() js.Value) (v js.Value, err error) {
+	defer Catch(&err)
+	return fn(), nil
+}
+
+func handleRecovery(r interface{}) error {
+	if r == nil {
+		return nil
+	}
+	switch val := r.(type) {
+	case error:
+		return val
+	case js.Value:
+		return js.Error{Value: val}
+	case string:
+		return errors.New(val)
+	default:
+		return errors.Errorf("%+v", val)
+	}
+}
diff --git a/exception/catch_test.go b/exception/catch_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..64a998fd57f5859ca2e32f289d43b1e18c778c3f
--- /dev/null
+++ b/exception/catch_test.go
@@ -0,0 +1,115 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package exception
+
+import (
+	"errors"
+	"syscall/js"
+	"testing"
+)
+
+func TestCatch(t *testing.T) {
+	t.Parallel()
+
+	t.Run("no error and no panic", func(t *testing.T) {
+		t.Parallel()
+		resultErr := func() (err error) {
+			defer Catch(&err)
+			// no-op
+			return nil
+		}()
+		if resultErr != nil {
+			t.Error(resultErr)
+		}
+	})
+
+	t.Run("error and no panic", func(t *testing.T) {
+		t.Parallel()
+		someErr := errors.New("my error")
+		resultErr := func() (err error) {
+			defer Catch(&err)
+			// no-op
+			return someErr
+		}()
+		if resultErr == nil || !errors.Is(someErr, resultErr) {
+			t.Errorf("Unexpected error.\nexpected: %v\nreceived: %v",
+				someErr, resultErr)
+		}
+	})
+
+	t.Run("panic with error", func(t *testing.T) {
+		t.Parallel()
+		someErr := errors.New("some error")
+		resultErr := func() (err error) {
+			defer Catch(&err)
+			panic(someErr)
+		}()
+		if resultErr == nil || !errors.Is(someErr, resultErr) {
+			t.Errorf("Unexpected error.\nexpected: %v\nreceived: %v",
+				someErr, resultErr)
+		}
+	})
+
+	t.Run("panic with js.Value", func(t *testing.T) {
+		t.Parallel()
+		someErr := testJSErrValue()
+		resultErr := func() (err error) {
+			defer Catch(&err)
+			panic(someErr)
+		}()
+		expectedErr := js.Error{Value: someErr}
+		if resultErr == nil || errors.Is(expectedErr, resultErr) {
+			t.Errorf("Unexpected error.\nexpected: %v\nreceived: %v",
+				expectedErr, resultErr)
+		}
+	})
+
+	t.Run("panic with other type", func(t *testing.T) {
+		t.Parallel()
+		someErr := "some other type"
+		resultErr := func() (err error) {
+			defer Catch(&err)
+			panic(someErr)
+		}()
+		if resultErr == nil || someErr != resultErr.Error() {
+			t.Errorf("Unexpected error.\nexpected: %s\nreceived: %v",
+				someErr, resultErr)
+		}
+	})
+}
+
+func testJSErrValue() (value js.Value) {
+	defer func() {
+		recoverVal := recover()
+		value = recoverVal.(js.Error).Value
+	}()
+	js.Global().Get("Function").New(`throw Exception("some error")`).Invoke()
+	panic("not a JS value. line above should do the panic")
+}
+
+func TestCatchHandler(t *testing.T) {
+	t.Parallel()
+	var calledHandler bool
+	resultErr := func() (err error) {
+		defer CatchHandler(func(handlerErr error) {
+			calledHandler = true
+			err = handlerErr
+		})
+		panic("some error")
+	}()
+	if resultErr == nil || "some error" != resultErr.Error() {
+		t.Errorf("Unexpected error.\nexpected: %s\nreceived: %v",
+			"some error", resultErr)
+	}
+	if calledHandler != true {
+		t.Errorf("Unexpected calledHandler.\nexpected: %t\neceived: %t",
+			true, calledHandler)
+	}
+}
diff --git a/exception/errors.go b/exception/errors.go
new file mode 100644
index 0000000000000000000000000000000000000000..4898ff4712d3ff28ef337e1e6c8327b99f1473f9
--- /dev/null
+++ b/exception/errors.go
@@ -0,0 +1,43 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package exception
+
+import (
+	"fmt"
+	"syscall/js"
+)
+
+var (
+	// Error is the Javascript Error type. It used to create new Javascript
+	// errors.
+	Error = js.Global().Get("Error")
+)
+
+// NewError converts the error to a Javascript Error.
+func NewError(err error) js.Value {
+	return Error.New(err.Error())
+}
+
+// NewTrace converts the error to a Javascript Error that includes the error's
+// stack trace.
+func NewTrace(err error) js.Value {
+	return Error.New(fmt.Sprintf("%+v", err))
+}
+
+// JsErrorToJson converts the Javascript error to JSON. This should be used for
+// all Javascript error objects instead of JsonToJS.
+func JsErrorToJson(value js.Value) string {
+	if value.IsUndefined() {
+		return "null"
+	}
+
+	properties := js.Global().Get("Object").Call("getOwnPropertyNames", value)
+	return js.Global().Get("JSON").Call("stringify", value, properties).String()
+}
diff --git a/exception/errors_test.go b/exception/errors_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e1c6cb3d89939d9b90a072da37c1dbb2cb8d5cc1
--- /dev/null
+++ b/exception/errors_test.go
@@ -0,0 +1,108 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package exception
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/pkg/errors"
+	"sort"
+	"strings"
+	"syscall/js"
+	"testing"
+)
+
+// Tests that TestNewError returns a Javascript Error object with the expected
+// message.
+func TestNewError(t *testing.T) {
+	err := errors.New("test error")
+	expectedErr := err.Error()
+	newError := NewError(err).Get("message").String()
+
+	if newError != expectedErr {
+		t.Errorf("Failed to get expected error message."+
+			"\nexpected: %s\nreceived: %s", expectedErr, newError)
+	}
+}
+
+// Tests that TestNewTrace returns a Javascript Error object with the expected
+// message and stack trace.
+func TestNewTrace(t *testing.T) {
+	err := errors.New("test error")
+	expectedErr := fmt.Sprintf("%+v", err)
+	newError := NewTrace(err).Get("message").String()
+
+	if newError != expectedErr {
+		t.Errorf("Failed to get expected error message."+
+			"\nexpected: %s\nreceived: %s", expectedErr, newError)
+	}
+}
+
+// Tests that JsErrorToJson can convert a Javascript object to JSON that matches
+// the output of json.Marshal on the Go version of the same object.
+func TestJsErrorToJson(t *testing.T) {
+	testObj := map[string]any{
+		"nil":    nil,
+		"bool":   true,
+		"int":    1,
+		"float":  1.5,
+		"string": "I am string",
+		"array":  []any{1, 2, 3},
+		"object": map[string]any{"int": 5},
+	}
+
+	expected, err := json.Marshal(testObj)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsJson := JsErrorToJson(js.ValueOf(testObj))
+
+	// Javascript does not return the JSON object fields sorted, so the letters
+	// of each Javascript string are sorted and compared
+	er := []rune(string(expected))
+	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
+	jj := []rune(jsJson)
+	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })
+
+	if string(er) != string(jj) {
+		t.Errorf("Received incorrect JSON from Javascript object."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
+
+// Tests that JsErrorToJson return a null object when the Javascript object is
+// undefined.
+func TestJsErrorToJson_Undefined(t *testing.T) {
+	expected, err := json.Marshal(nil)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsJson := JsErrorToJson(js.Undefined())
+
+	if string(expected) != jsJson {
+		t.Errorf("Received incorrect JSON from Javascript object."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
+
+// Tests that JsErrorToJson returns a JSON object containing the original error
+// string.
+func TestJsErrorToJson_ErrorObject(t *testing.T) {
+	expected := "An error"
+	jsErr := Error.New(expected)
+	jsJson := JsErrorToJson(jsErr)
+
+	if !strings.Contains(jsJson, expected) {
+		t.Errorf("Received incorrect JSON from Javascript error."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
diff --git a/exception/throw.go b/exception/throw.go
new file mode 100644
index 0000000000000000000000000000000000000000..31791eb098c42ce55c3896658317ef41ec4d5229
--- /dev/null
+++ b/exception/throw.go
@@ -0,0 +1,30 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package exception
+
+import "fmt"
+
+// Throw creates a Javascript Error object from a Go error and throws it as an
+// exception.
+func Throw(err error) {
+	throw("Error", err.Error())
+}
+
+// Throwf formats according to a format specifier, creates a Javascript Error
+// object, and throws it as an exception.
+func Throwf(format string, a ...any) {
+	throw("Error", fmt.Sprintf(format, a...))
+}
+
+// ThrowTrace creates a Javascript Error object from a Go error and throws it as
+// an exception. The error includes its stack trace.
+func ThrowTrace(err error) {
+	throw("Error", fmt.Sprintf("%+v", err))
+}
diff --git a/exception/throw_js.s b/exception/throw_js.s
new file mode 100644
index 0000000000000000000000000000000000000000..de7b1f139cf6f58bed1bd5d16a16d413a34ffb9d
--- /dev/null
+++ b/exception/throw_js.s
@@ -0,0 +1,6 @@
+#include "textflag.h"
+
+// Throw enables throwing of Javascript exceptions.
+TEXT ·throw(SB), NOSPLIT, $0
+  CallImport
+  RET
diff --git a/exception/throws.dev b/exception/throws.dev
new file mode 100644
index 0000000000000000000000000000000000000000..44b527c539d716ad501dbb65befeaddd682f9a35
--- /dev/null
+++ b/exception/throws.dev
@@ -0,0 +1,16 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package exception
+
+// This file is used in testing. Refer to throws.go for more info.
+
+import jww "github.com/spf13/jwalterweatherman"
+
+func throw(exception string, message string) {
+	jww.FATAL.Panicf("%s: %s", exception, message)
+}
diff --git a/exception/throws.go b/exception/throws.go
new file mode 100644
index 0000000000000000000000000000000000000000..e957819f5769713ad98c2ab01e7539073530f644
--- /dev/null
+++ b/exception/throws.go
@@ -0,0 +1,42 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package exception
+
+// This file contains the stub for the throw function, which is linked via
+// assembly in throw_js.s to a custom function added to wasm_exec.js that throws
+// the passed elements. This adds the ability to throw a Javascript exception
+// from the go webassembly.
+//
+// Testing uses the [wasmbrowsertest], which uses the default wasm_exec.js file,
+// which causes compile-time errors. To avoid this, throw_js.s must be cleared
+// out and the stub below must be replaced with an actual function, as shown
+// below.
+//
+//  func throw(exception string, message string) {}
+//
+// To make it easier, you can also replace throws.go temporarily with throws.dev
+// while running tests, which replaces the stub with a function that panics.
+//
+// You can also add the following lines to your Makefile.
+//
+//  wasmException = "vendor/gitlab.com/elixxir/wasm-utils/exception"
+//
+//  wasm_tests:
+//      cp $(wasmException)/throw_js.s $(wasmException)/throw_js.s.bak
+//      cp $(wasmException)/throws.go $(wasmException)/throws.go.bak
+//      > $(wasmException)/throw_js.s
+//      cp $(wasmException)/throws.dev $(wasmException)/throws.go
+//      -GOOS=js GOARCH=wasm go test -v ./...
+//      mv $(wasmException)/throw_js.s.bak $(wasmException)/throw_js.s
+//      mv $(wasmException)/throws.go.bak $(wasmException)/throws.go
+//
+// [wasmbrowsertest]: https://github.com/agnivade/wasmbrowsertest
+
+// throw is a function stub that connects to the bindings in wasm_exec.js to
+// allow throwing exceptions.
+func throw(exception string, message string)
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..45eae259a1c502d1895b69ac446adab8daab885f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module gitlab.com/elixxir/wasm-utils
+
+go 1.19
+
+require (
+	github.com/pkg/errors v0.9.1
+	github.com/spf13/jwalterweatherman v1.1.0
+)
+
+require github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..3c374ab57f33036d0dd0faf005ca0205410eef78
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,12 @@
+github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
+github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
diff --git a/storage/localStorage.go b/storage/localStorage.go
new file mode 100644
index 0000000000000000000000000000000000000000..a41db25d0a38cd249864764a52d2a6af0bad8143
--- /dev/null
+++ b/storage/localStorage.go
@@ -0,0 +1,280 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package storage
+
+import (
+	"os"
+	"strings"
+	"syscall/js"
+
+	"github.com/Max-Sum/base32768"
+
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
+)
+
+// localStorageWasmPrefix is prefixed to every keyName saved to local storage by
+// localStorage. It allows the identification and deletion of keys only created
+// by this WASM binary while ignoring keys made by other scripts on the same
+// page.
+//
+// The chosen prefix is two characters, that when converted to UTF16, take up 4
+// bytes without any zeros to make them more unique.
+const localStorageWasmPrefix = "🞮🞮"
+
+// LocalStorage defines an interface for setting persistent state in a KV format
+// specifically for web-based implementations.
+type LocalStorage interface {
+	// Get decodes and returns the value from the local storage given its key
+	// name. Returns os.ErrNotExist if the key does not exist.
+	Get(key string) ([]byte, error)
+
+	// Set encodes the bytes to a string and adds them to local storage at the
+	// given key name. Returns an error if local storage quota has been reached.
+	Set(key string, value []byte) error
+
+	// RemoveItem removes a key's value from local storage given its name. If
+	// there is no item with the given key, this function does nothing.
+	RemoveItem(keyName string)
+
+	// Clear clears all the keys in storage. Returns the number of keys cleared.
+	Clear() int
+
+	// ClearPrefix clears all keys with the given prefix.  Returns the number of
+	// keys cleared.
+	ClearPrefix(prefix string) int
+
+	// Key returns the name of the nth key in localStorage. Returns
+	// os.ErrNotExist if the key does not exist. The order of keys is not
+	// defined.
+	Key(n int) (string, error)
+
+	// Keys returns a list of all key names in local storage.
+	Keys() []string
+
+	// Length returns the number of keys in localStorage.
+	Length() int
+
+	// LocalStorageUNSAFE returns the underlying local storage wrapper. This can
+	// be UNSAFE and should only be used if you know what you are doing.
+	//
+	// The returned wrapper wraps all the functions and fields on the Javascript
+	// localStorage object to handle type conversions and errors. But it does
+	// not decode/sanitize the inputs/outputs or track entries using the prefix
+	// system. If using it, make sure all key names and values can be converted
+	// to valid UCS-2 strings.
+	LocalStorageUNSAFE() *LocalStorageJS
+}
+
+// localStorage contains the js.Value representation of localStorage.
+type localStorage struct {
+	// The Javascript value containing the localStorage object
+	v *LocalStorageJS
+
+	// The prefix appended to each key name. This is so that all keys created by
+	// this structure can be deleted without affecting other keys in local
+	// storage.
+	prefix string
+}
+
+// jsStorage is the global that stores Javascript as window.localStorage.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
+var jsStorage LocalStorage = newLocalStorage(localStorageWasmPrefix)
+
+// newLocalStorage creates a new localStorage object with the specified prefix.
+func newLocalStorage(prefix string) *localStorage {
+	return &localStorage{
+		v:      &LocalStorageJS{js.Global().Get("localStorage")},
+		prefix: prefix,
+	}
+}
+
+// GetLocalStorage returns Javascript's local storage.
+func GetLocalStorage() LocalStorage {
+	return jsStorage
+}
+
+// Get decodes and returns the value from the local storage given its key
+// name. Returns os.ErrNotExist if the key does not exist.
+func (ls *localStorage) Get(keyName string) ([]byte, error) {
+	value, err := ls.v.GetItem(ls.prefix + keyName)
+	if err != nil {
+		return nil, err
+	}
+
+	return base32768.SafeEncoding.DecodeString(value)
+}
+
+// Set encodes the bytes to a string and adds them to local storage at the
+// given key name. Returns an error if local storage quota has been reached.
+func (ls *localStorage) Set(keyName string, keyValue []byte) error {
+	encoded := base32768.SafeEncoding.EncodeToString(keyValue)
+	return ls.v.SetItem(ls.prefix+keyName, encoded)
+}
+
+// RemoveItem removes a key's value from local storage given its name. If there
+// is no item with the given key, this function does nothing.
+func (ls *localStorage) RemoveItem(keyName string) {
+	ls.v.RemoveItem(ls.prefix + keyName)
+}
+
+// Clear clears all the keys in storage. Returns the number of keys cleared.
+func (ls *localStorage) Clear() int {
+	// Get a copy of all key names at once
+	keys := ls.v.KeysPrefix(ls.prefix)
+
+	// Loop through each key
+	for _, keyName := range keys {
+		ls.RemoveItem(keyName)
+	}
+
+	return len(keys)
+}
+
+// ClearPrefix clears all keys with the given prefix.  Returns the number of
+// keys cleared.
+func (ls *localStorage) ClearPrefix(prefix string) int {
+	// Get a copy of all key names at once
+	keys := ls.v.KeysPrefix(ls.prefix + prefix)
+
+	// Loop through each key
+	for _, keyName := range keys {
+		ls.RemoveItem(prefix + keyName)
+	}
+
+	return len(keys)
+}
+
+// Key returns the name of the nth key in localStorage. Return [os.ErrNotExist]
+// if the key does not exist. The order of keys is not defined.
+func (ls *localStorage) Key(n int) (string, error) {
+	keyName, err := ls.v.Key(n)
+	if err != nil {
+		return "", err
+	}
+	return strings.TrimPrefix(keyName, ls.prefix), nil
+}
+
+// Keys returns a list of all key names in local storage.
+func (ls *localStorage) Keys() []string {
+	return ls.v.KeysPrefix(ls.prefix)
+}
+
+// Length returns the number of keys in localStorage.
+func (ls *localStorage) Length() int {
+	return ls.v.Length()
+}
+
+// LocalStorageUNSAFE returns the underlying local storage wrapper. This can be
+// UNSAFE and should only be used if you know what you are doing.
+//
+// The returned wrapper wraps all the functions and fields on the Javascript
+// localStorage object to handle type conversions and errors. But it does not
+// decode/sanitize the inputs/outputs or track entries using the prefix system.
+// If using it, make sure all key names and values can be converted to valid
+// UCS-2 strings.
+func (ls *localStorage) LocalStorageUNSAFE() *LocalStorageJS {
+	return ls.v
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Javascript Wrappers                                                        //
+////////////////////////////////////////////////////////////////////////////////
+
+// LocalStorageJS stores the Javascript window.localStorage object and wraps all
+// of its methods and fields to handle type conversations and errors.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
+type LocalStorageJS struct {
+	js.Value
+}
+
+// GetItem returns the value from the local storage given its key name. Returns
+// [os.ErrNotExist] if the key does not exist.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem
+func (ls *LocalStorageJS) GetItem(keyName string) (keyValue string, err error) {
+	defer exception.Catch(&err)
+	keyValueJS := ls.Call("getItem", keyName)
+	if keyValueJS.IsNull() {
+		return "", os.ErrNotExist
+	}
+	return keyValueJS.String(), nil
+}
+
+// SetItem adds the value to local storage at the given key name. Returns an
+// error if local storage quota has been reached.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem
+func (ls *LocalStorageJS) SetItem(keyName, keyValue string) (err error) {
+	defer exception.Catch(&err)
+	ls.Call("setItem", keyName, keyValue)
+	return nil
+}
+
+// RemoveItem removes a key's value from local storage given its name. If there
+// is no item with the given key, this function does nothing.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem
+func (ls *LocalStorageJS) RemoveItem(keyName string) {
+	ls.Call("removeItem", keyName)
+}
+
+// Clear clears all the keys in storage.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Storage/clear
+func (ls *LocalStorageJS) Clear() {
+	ls.Call("clear")
+}
+
+// Key returns the name of the nth key in localStorage. Return [os.ErrNotExist]
+// if the key does not exist. The order of keys is not defined.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Storage/key
+func (ls *LocalStorageJS) Key(n int) (keyName string, err error) {
+	defer exception.Catch(&err)
+	keyNameJS := ls.Call("key", n)
+	if keyNameJS.IsNull() {
+		return "", os.ErrNotExist
+	}
+	return keyNameJS.String(), nil
+}
+
+// Keys returns a list of all key names in local storage.
+func (ls *LocalStorageJS) Keys() []string {
+	keysJS := utils.Object.Call("keys", ls.Value)
+	keys := make([]string, keysJS.Length())
+	for i := range keys {
+		keys[i] = keysJS.Index(i).String()
+	}
+	return keys
+}
+
+// KeysPrefix returns a list of all key names in local storage with the given
+// prefix and trims the prefix from each key name.
+func (ls *LocalStorageJS) KeysPrefix(prefix string) []string {
+	keysJS := utils.Object.Call("keys", ls.Value)
+	keys := make([]string, 0, keysJS.Length())
+	for i := 0; i < keysJS.Length(); i++ {
+		keyName := keysJS.Index(i).String()
+		if strings.HasPrefix(keyName, prefix) {
+			keys = append(keys, strings.TrimPrefix(keyName, prefix))
+		}
+	}
+	return keys
+}
+
+// Length returns the number of keys in localStorage.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Storage/length
+func (ls *LocalStorageJS) Length() int {
+	return ls.Get("length").Int()
+}
diff --git a/storage/localStorage_test.go b/storage/localStorage_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f8652ba1f33d08c2228bff1b7846e0e4ce058b38
--- /dev/null
+++ b/storage/localStorage_test.go
@@ -0,0 +1,287 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package storage
+
+import (
+	"bytes"
+	"github.com/pkg/errors"
+	"os"
+	"reflect"
+	"strconv"
+	"syscall/js"
+	"testing"
+)
+
+// Unit test of GetLocalStorage.
+func TestGetLocalStorage(t *testing.T) {
+	expected := &localStorage{
+		v:      &LocalStorageJS{js.Global().Get("localStorage")},
+		prefix: localStorageWasmPrefix,
+	}
+
+	ls := GetLocalStorage()
+
+	if !reflect.DeepEqual(expected, ls) {
+		t.Errorf("Did not receive expected localStorage."+
+			"\nexpected: %+v\nreceived: %+v", expected, ls)
+	}
+}
+
+// Tests that a value set with localStorage.Set and retrieved with
+// localStorage.Get matches the original.
+func TestLocalStorage_Get_Set(t *testing.T) {
+	values := map[string][]byte{
+		"key1": []byte("key value"),
+		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
+		"key3": {0, 49, 0, 0, 0, 38, 249, 93, 242, 189, 222, 32, 138, 248, 121,
+			151, 42, 108, 82, 199, 163, 61, 4, 200, 140, 231, 225, 20, 35, 243,
+			253, 161, 61, 2, 227, 208, 173, 183, 33, 66, 236, 107, 105, 119, 26,
+			42, 44, 60, 109, 172, 38, 47, 220, 17, 129, 4, 234, 241, 141, 81,
+			84, 185, 32, 120, 115, 151, 128, 196, 143, 117, 222, 78, 44, 115,
+			109, 20, 249, 46, 158, 139, 231, 157, 54, 219, 141, 252},
+	}
+
+	for keyName, keyValue := range values {
+		err := jsStorage.Set(keyName, keyValue)
+		if err != nil {
+			t.Errorf("Failed to set %q: %+v", keyName, err)
+		}
+
+		loadedValue, err := jsStorage.Get(keyName)
+		if err != nil {
+			t.Errorf("Failed to load %q: %+v", keyName, err)
+		} else if !bytes.Equal(keyValue, loadedValue) {
+			t.Errorf("Loaded value does not match original for %q"+
+				"\nexpected: %q\nreceived: %q", keyName, keyValue, loadedValue)
+		}
+	}
+}
+
+// Tests that localStorage.Get returns the error os.ErrNotExist when the key
+// does not exist in storage.
+func TestLocalStorage_Get_NotExistError(t *testing.T) {
+	_, err := jsStorage.Get("someKey")
+	if err == nil || !errors.Is(err, os.ErrNotExist) {
+		t.Errorf("Incorrect error for non existant key."+
+			"\nexpected: %v\nreceived: %v", os.ErrNotExist, err)
+	}
+}
+
+// Tests that localStorage.RemoveItem deletes a key from the store and that it
+// cannot be retrieved.
+func TestLocalStorage_RemoveItem(t *testing.T) {
+	keyName := "key"
+	if err := jsStorage.Set(keyName, []byte("value")); err != nil {
+		t.Errorf("Failed to set %q: %+v", keyName, err)
+	}
+	jsStorage.RemoveItem(keyName)
+
+	_, err := jsStorage.Get(keyName)
+	if err == nil || !errors.Is(err, os.ErrNotExist) {
+		t.Errorf("Failed to remove %q: %+v", keyName, err)
+	}
+}
+
+// Tests that localStorage.Clear deletes all the WASM keys from storage and
+// does not remove any others
+func TestLocalStorage_Clear(t *testing.T) {
+	jsStorage.LocalStorageUNSAFE().Clear()
+	const numKeys = 10
+	var yesPrefix, noPrefix []string
+
+	for i := 0; i < numKeys; i++ {
+		keyName := "keyNum" + strconv.Itoa(i)
+		if i%2 == 0 {
+			yesPrefix = append(yesPrefix, keyName)
+			err := jsStorage.Set(keyName, []byte(strconv.Itoa(i)))
+			if err != nil {
+				t.Errorf("Failed to set with prefix %q: %+v", keyName, err)
+			}
+		} else {
+			noPrefix = append(noPrefix, keyName)
+			err := jsStorage.LocalStorageUNSAFE().SetItem(keyName, strconv.Itoa(i))
+			if err != nil {
+				t.Errorf("Failed to set with no prefix %q: %+v", keyName, err)
+			}
+		}
+	}
+
+	n := jsStorage.Clear()
+	if n != numKeys/2 {
+		t.Errorf("Incorrect number of keys.\nexpected: %d\nreceived: %d",
+			numKeys/2, n)
+	}
+
+	for _, keyName := range noPrefix {
+		if _, err := jsStorage.LocalStorageUNSAFE().GetItem(keyName); err != nil {
+			t.Errorf("Could not get keyName %q: %+v", keyName, err)
+		}
+	}
+	for _, keyName := range yesPrefix {
+		keyValue, err := jsStorage.Get(keyName)
+		if err == nil || !errors.Is(err, os.ErrNotExist) {
+			t.Errorf("Found keyName %q: %q", keyName, keyValue)
+		}
+	}
+}
+
+// Tests that localStorage.ClearPrefix deletes only the keys with the given
+// prefix.
+func TestLocalStorage_ClearPrefix(t *testing.T) {
+	jsStorage.LocalStorageUNSAFE().Clear()
+	const numKeys = 10
+	var yesPrefix, noPrefix []string
+	prefix := "keyNamePrefix/"
+
+	for i := 0; i < numKeys; i++ {
+		keyName := "keyNum " + strconv.Itoa(i)
+		if i%2 == 0 {
+			keyName = prefix + keyName
+			yesPrefix = append(yesPrefix, keyName)
+		} else {
+			noPrefix = append(noPrefix, keyName)
+		}
+
+		if err := jsStorage.Set(keyName, []byte(strconv.Itoa(i))); err != nil {
+			t.Errorf("Failed to set %q: %+v", keyName, err)
+		}
+	}
+
+	n := jsStorage.ClearPrefix(prefix)
+	if n != numKeys/2 {
+		t.Errorf("Incorrect number of keys.\nexpected: %d\nreceived: %d",
+			numKeys/2, n)
+	}
+
+	for _, keyName := range noPrefix {
+		if _, err := jsStorage.Get(keyName); err != nil {
+			t.Errorf("Could not get keyName %q: %+v", keyName, err)
+		}
+	}
+	for _, keyName := range yesPrefix {
+		keyValue, err := jsStorage.Get(keyName)
+		if err == nil || !errors.Is(err, os.ErrNotExist) {
+			t.Errorf("Found keyName %q: %q", keyName, keyValue)
+		}
+	}
+}
+
+// Tests that localStorage.Key return all added keys when looping through all
+// indexes.
+func TestLocalStorage_Key(t *testing.T) {
+	jsStorage.LocalStorageUNSAFE().Clear()
+	values := map[string][]byte{
+		"key1": []byte("key value"),
+		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
+		"key3": {0, 49, 0, 0, 0, 38, 249, 93},
+	}
+
+	for keyName, keyValue := range values {
+		if err := jsStorage.Set(keyName, keyValue); err != nil {
+			t.Errorf("Failed to set %q: %+v", keyName, err)
+		}
+	}
+
+	numKeys := len(values)
+	for i := 0; i < numKeys; i++ {
+		keyName, err := jsStorage.Key(i)
+		if err != nil {
+			t.Errorf("No key found for index %d: %+v", i, err)
+		}
+
+		if _, exists := values[keyName]; !exists {
+			t.Errorf("No key with name %q added to storage.", keyName)
+		}
+		delete(values, keyName)
+	}
+
+	if len(values) != 0 {
+		t.Errorf("%d keys not read from storage: %q", len(values), values)
+	}
+}
+
+// Tests that localStorage.Key returns the error os.ErrNotExist when the index
+// is greater than or equal to the number of keys.
+func TestLocalStorage_Key_NotExistError(t *testing.T) {
+	jsStorage.LocalStorageUNSAFE().Clear()
+	if err := jsStorage.Set("key", []byte("value")); err != nil {
+		t.Errorf("Failed to set: %+v", err)
+	}
+
+	_, err := jsStorage.Key(1)
+	if err == nil || !errors.Is(err, os.ErrNotExist) {
+		t.Errorf("Incorrect error for non existant key index."+
+			"\nexpected: %v\nreceived: %v", os.ErrNotExist, err)
+	}
+
+	_, err = jsStorage.Key(2)
+	if err == nil || !errors.Is(err, os.ErrNotExist) {
+		t.Errorf("Incorrect error for non existant key index."+
+			"\nexpected: %v\nreceived: %v", os.ErrNotExist, err)
+	}
+}
+
+// Tests that localStorage.Length returns the correct Length when adding and
+// removing various keys.
+func TestLocalStorage_Length(t *testing.T) {
+	jsStorage.LocalStorageUNSAFE().Clear()
+	values := map[string][]byte{
+		"key1": []byte("key value"),
+		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
+		"key3": {0, 49, 0, 0, 0, 38, 249, 93},
+	}
+
+	i := 0
+	for keyName, keyValue := range values {
+		if err := jsStorage.Set(keyName, keyValue); err != nil {
+			t.Errorf("Failed to set %q: %+v", keyName, err)
+		}
+		i++
+
+		if jsStorage.Length() != i {
+			t.Errorf("Incorrect length.\nexpected: %d\nreceived: %d",
+				i, jsStorage.Length())
+		}
+	}
+
+	i = len(values)
+	for keyName := range values {
+		jsStorage.RemoveItem(keyName)
+		i--
+
+		if jsStorage.Length() != i {
+			t.Errorf("Incorrect length.\nexpected: %d\nreceived: %d",
+				i, jsStorage.Length())
+		}
+	}
+}
+
+// Tests that localStorage.Keys return a list that contains all the added keys.
+func TestLocalStorage_Keys(t *testing.T) {
+	jsStorage.LocalStorageUNSAFE().Clear()
+	values := map[string][]byte{
+		"key1": []byte("key value"),
+		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
+		"key3": {0, 49, 0, 0, 0, 38, 249, 93},
+	}
+
+	for keyName, keyValue := range values {
+		if err := jsStorage.Set(keyName, keyValue); err != nil {
+			t.Errorf("Failed to set %q: %+v", keyName, err)
+		}
+	}
+
+	keys := jsStorage.Keys()
+	for i, keyName := range keys {
+		if _, exists := values[keyName]; !exists {
+			t.Errorf("Key %q does not exist (%d).", keyName, i)
+		}
+	}
+}
diff --git a/utils/array.go b/utils/array.go
new file mode 100644
index 0000000000000000000000000000000000000000..428fc4e89829348178165c54353321396109f5d5
--- /dev/null
+++ b/utils/array.go
@@ -0,0 +1,72 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package utils
+
+import (
+	"bytes"
+	"encoding/base64"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"syscall/js"
+)
+
+// Uint8ArrayToBase64 encodes an uint8 array to a base 64 string.
+//
+// Parameters:
+//   - args[0] - Javascript 8-bit unsigned integer array (Uint8Array).
+//
+// Returns:
+//   - Base 64 encoded string (string).
+func Uint8ArrayToBase64(_ js.Value, args []js.Value) any {
+	return base64.StdEncoding.EncodeToString(CopyBytesToGo(args[0]))
+}
+
+// Base64ToUint8Array decodes a base 64 encoded string to a Uint8Array.
+//
+// Parameters:
+//   - args[0] - Base 64 encoded string (string).
+//
+// Returns:
+//   - Javascript 8-bit unsigned integer array (Uint8Array).
+//   - Throws TypeError if decoding the string fails.
+func Base64ToUint8Array(_ js.Value, args []js.Value) any {
+	b, err := base64ToUint8Array(args[0])
+	if err != nil {
+		exception.Throw(err)
+	}
+
+	return b
+}
+
+// base64ToUint8Array is a helper function that returns an error instead of
+// throwing it.
+func base64ToUint8Array(base64String js.Value) (js.Value, error) {
+	b, err := base64.StdEncoding.DecodeString(base64String.String())
+	if err != nil {
+		return js.Value{}, err
+	}
+
+	return CopyBytesToJS(b), nil
+}
+
+// Uint8ArrayEquals returns true if the two Uint8Array are equal and false
+// otherwise.
+//
+// Parameters:
+//   - args[0] - Array A (Uint8Array).
+//   - args[1] - Array B (Uint8Array).
+//
+// Returns:
+//   - If the two arrays are equal (boolean).
+func Uint8ArrayEquals(_ js.Value, args []js.Value) any {
+	a := CopyBytesToGo(args[0])
+	b := CopyBytesToGo(args[1])
+
+	return bytes.Equal(a, b)
+}
diff --git a/utils/array_test.go b/utils/array_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f1cc95bd1c825cbadba151f7ccdd81b953905c00
--- /dev/null
+++ b/utils/array_test.go
@@ -0,0 +1,114 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package utils
+
+import (
+	"encoding/base64"
+	"fmt"
+	"strings"
+	"syscall/js"
+	"testing"
+)
+
+var testBytes = [][]byte{
+	nil,
+	{},
+	{0},
+	{0, 1, 2, 3},
+	{214, 108, 207, 78, 229, 11, 42, 219, 42, 87, 205, 104, 252, 73, 223,
+		229, 145, 209, 79, 111, 34, 96, 238, 127, 11, 105, 114, 62, 239,
+		130, 145, 82, 3},
+}
+
+// Tests that a series of Uint8Array Javascript objects are correctly converted
+// to base 64 strings with Uint8ArrayToBase64.
+func TestUint8ArrayToBase64(t *testing.T) {
+	for i, val := range testBytes {
+		// Create Uint8Array and set each element individually
+		jsBytes := Uint8Array.New(len(val))
+		for j, v := range val {
+			jsBytes.SetIndex(j, v)
+		}
+
+		jsB64 := Uint8ArrayToBase64(js.Value{}, []js.Value{jsBytes})
+
+		expected := base64.StdEncoding.EncodeToString(val)
+
+		if expected != jsB64 {
+			t.Errorf("Did not receive expected base64 encoded string (%d)."+
+				"\nexpected: %s\nreceived: %s", i, expected, jsB64)
+		}
+	}
+}
+
+// Tests that Base64ToUint8Array correctly decodes a series of base 64 encoded
+// strings into Uint8Array.
+func TestBase64ToUint8Array(t *testing.T) {
+	for i, val := range testBytes {
+		b64 := base64.StdEncoding.EncodeToString(val)
+		jsArr, err := base64ToUint8Array(js.ValueOf(b64))
+		if err != nil {
+			t.Errorf("Failed to convert js.Value to base 64: %+v", err)
+		}
+
+		// Generate the expected string to match the output of toString() on a
+		// Uint8Array
+		expected := strings.ReplaceAll(fmt.Sprintf("%d", val), " ", ",")[1:]
+		expected = expected[:len(expected)-1]
+
+		// Get the string value of the Uint8Array
+		jsString := jsArr.Call("toString").String()
+
+		if expected != jsString {
+			t.Errorf("Failed to receive expected string representation of "+
+				"the Uint8Array (%d).\nexpected: %s\nreceived: %s",
+				i, expected, jsString)
+		}
+	}
+}
+
+// Tests that a base 64 encoded string decoded to Uint8Array via
+// Base64ToUint8Array and back to a base 64 encoded string via
+// Uint8ArrayToBase64 matches the original.
+func TestBase64ToUint8ArrayUint8ArrayToBase64(t *testing.T) {
+	for i, val := range testBytes {
+		b64 := base64.StdEncoding.EncodeToString(val)
+		jsArr, err := base64ToUint8Array(js.ValueOf(b64))
+		if err != nil {
+			t.Errorf("Failed to convert js.Value to base 64: %+v", err)
+		}
+
+		jsB64 := Uint8ArrayToBase64(js.Value{}, []js.Value{jsArr})
+
+		if b64 != jsB64 {
+			t.Errorf("JSON from Uint8Array does not match original (%d)."+
+				"\nexpected: %s\nreceived: %s", i, b64, jsB64)
+		}
+	}
+}
+
+func TestUint8ArrayEquals(t *testing.T) {
+	for i, val := range testBytes {
+		// Create Uint8Array and set each element individually
+		jsBytesA := Uint8Array.New(len(val))
+		for j, v := range val {
+			jsBytesA.SetIndex(j, v)
+		}
+
+		jsBytesB := CopyBytesToJS(val)
+
+		if !Uint8ArrayEquals(js.Value{}, []js.Value{jsBytesA, jsBytesB}).(bool) {
+			t.Errorf("Two equal byte slices were found to be different (%d)."+
+				"\nexpected: %s\nreceived: %s", i,
+				jsBytesA.Call("toString").String(),
+				jsBytesB.Call("toString").String())
+		}
+	}
+}
diff --git a/utils/convert.go b/utils/convert.go
new file mode 100644
index 0000000000000000000000000000000000000000..a5c79e309529089ebb918fc77f701f7a633c8d05
--- /dev/null
+++ b/utils/convert.go
@@ -0,0 +1,51 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package utils
+
+import (
+	"encoding/json"
+	"syscall/js"
+)
+
+// CopyBytesToGo copies the [Uint8Array] stored in the [js.Value] to []byte.
+// This is a wrapper for [js.CopyBytesToGo] to make it more convenient.
+func CopyBytesToGo(src js.Value) []byte {
+	b := make([]byte, src.Length())
+	js.CopyBytesToGo(b, src)
+	return b
+}
+
+// CopyBytesToJS copies the []byte to a [Uint8Array] stored in a [js.Value].
+// This is a wrapper for [js.CopyBytesToJS] to make it more convenient.
+func CopyBytesToJS(src []byte) js.Value {
+	dst := Uint8Array.New(len(src))
+	js.CopyBytesToJS(dst, src)
+	return dst
+}
+
+// JsToJson converts the Javascript value to JSON.
+func JsToJson(value js.Value) string {
+	if value.IsUndefined() {
+		return "null"
+	}
+
+	return JSON.Call("stringify", value).String()
+}
+
+// JsonToJS converts a JSON bytes input to a [js.Value] of the object subtype.
+func JsonToJS(inputJson []byte) (js.Value, error) {
+	var jsObj map[string]any
+	err := json.Unmarshal(inputJson, &jsObj)
+	if err != nil {
+		return js.ValueOf(nil), err
+	}
+
+	return js.ValueOf(jsObj), nil
+}
diff --git a/utils/convert_test.go b/utils/convert_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c63aa8abcf32eca067eb1176cdc2a8d4c5db4779
--- /dev/null
+++ b/utils/convert_test.go
@@ -0,0 +1,243 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package utils
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"sort"
+	"syscall/js"
+	"testing"
+)
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+)
+
+// Tests that CopyBytesToGo returns a byte slice that matches the Uint8Array.
+func TestCopyBytesToGo(t *testing.T) {
+	for i, val := range testBytes {
+		// Create Uint8Array and set each element individually
+		jsBytes := Uint8Array.New(len(val))
+		for j, v := range val {
+			jsBytes.SetIndex(j, v)
+		}
+
+		goBytes := CopyBytesToGo(jsBytes)
+
+		if !bytes.Equal(val, goBytes) {
+			t.Errorf("Failed to receive expected bytes from Uint8Array (%d)."+
+				"\nexpected: %d\nreceived: %d",
+				i, val, goBytes)
+		}
+	}
+}
+
+// Tests that CopyBytesToJS returns a Javascript Uint8Array with values matching
+// the original byte slice.
+func TestCopyBytesToJS(t *testing.T) {
+	for i, val := range testBytes {
+		jsBytes := CopyBytesToJS(val)
+
+		// Generate the expected string to match the output of toString() on a
+		// Uint8Array
+		expected := strings.ReplaceAll(fmt.Sprintf("%d", val), " ", ",")[1:]
+		expected = expected[:len(expected)-1]
+
+		// Get the string value of the Uint8Array
+		jsString := jsBytes.Call("toString").String()
+
+		if expected != jsString {
+			t.Errorf("Failed to receive expected string representation of "+
+				"the Uint8Array (%d).\nexpected: %s\nreceived: %s",
+				i, expected, jsString)
+		}
+	}
+}
+
+// Tests that a byte slice converted to Javascript via CopyBytesToJS and
+// converted back to Go via CopyBytesToGo matches the original.
+func TestCopyBytesToJSCopyBytesToGo(t *testing.T) {
+	for i, val := range testBytes {
+		jsBytes := CopyBytesToJS(val)
+		goBytes := CopyBytesToGo(jsBytes)
+
+		if !bytes.Equal(val, goBytes) {
+			t.Errorf("Failed to receive expected bytes from Uint8Array (%d)."+
+				"\nexpected: %d\nreceived: %d",
+				i, val, goBytes)
+		}
+	}
+
+}
+
+// Tests that JsToJson can convert a Javascript object to JSON that matches the
+// output of json.Marshal on the Go version of the same object.
+func TestJsToJson(t *testing.T) {
+	testObj := map[string]any{
+		"nil":    nil,
+		"bool":   true,
+		"int":    1,
+		"float":  1.5,
+		"string": "I am string",
+		"array":  []any{1, 2, 3},
+		"object": map[string]any{"int": 5},
+	}
+
+	expected, err := json.Marshal(testObj)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsJson := JsToJson(js.ValueOf(testObj))
+
+	// Javascript does not return the JSON object fields sorted, so the letters
+	// of each Javascript string are sorted and compared
+	er := []rune(string(expected))
+	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
+	jj := []rune(jsJson)
+	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })
+
+	if string(er) != string(jj) {
+		t.Errorf("Received incorrect JSON from the Javascript object."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
+
+// Tests that JsToJson return a null object when the Javascript object is
+// undefined.
+func TestJsToJson_Undefined(t *testing.T) {
+	expected, err := json.Marshal(nil)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsJson := JsToJson(js.Undefined())
+
+	if string(expected) != jsJson {
+		t.Errorf("Received incorrect JSON from the Javascript object."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
+
+// Tests that JsonToJS can convert a JSON object with multiple types to a
+// Javascript object and that all values match.
+func TestJsonToJS(t *testing.T) {
+	testObj := map[string]any{
+		"nil":    nil,
+		"bool":   true,
+		"int":    1,
+		"float":  1.5,
+		"string": "I am string",
+		"bytes":  []byte{1, 2, 3},
+		"array":  []any{1, 2, 3},
+		"object": map[string]any{"int": 5},
+	}
+	jsonData, err := json.Marshal(testObj)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsObj, err := JsonToJS(jsonData)
+	if err != nil {
+		t.Errorf("Failed to convert JSON to a Javascript object: %+v", err)
+	}
+
+	for key, val := range testObj {
+		jsVal := jsObj.Get(key)
+		switch key {
+		case "nil":
+			if !jsVal.IsNull() {
+				t.Errorf("Key %s is not null.", key)
+			}
+		case "bool":
+			if jsVal.Bool() != val {
+				t.Errorf("Incorrect value for key %s."+
+					"\nexpected: %t\nreceived: %t", key, val, jsVal.Bool())
+			}
+		case "int":
+			if jsVal.Int() != val {
+				t.Errorf("Incorrect value for key %s."+
+					"\nexpected: %d\nreceived: %d", key, val, jsVal.Int())
+			}
+		case "float":
+			if jsVal.Float() != val {
+				t.Errorf("Incorrect value for key %s."+
+					"\nexpected: %f\nreceived: %f", key, val, jsVal.Float())
+			}
+		case "string":
+			if jsVal.String() != val {
+				t.Errorf("Incorrect value for key %s."+
+					"\nexpected: %s\nreceived: %s", key, val, jsVal.String())
+			}
+		case "bytes":
+			if jsVal.String() != base64.StdEncoding.EncodeToString(val.([]byte)) {
+				t.Errorf("Incorrect value for key %s."+
+					"\nexpected: %s\nreceived: %s", key,
+					base64.StdEncoding.EncodeToString(val.([]byte)),
+					jsVal.String())
+			}
+		case "array":
+			for i, v := range val.([]any) {
+				if jsVal.Index(i).Int() != v {
+					t.Errorf("Incorrect value for key %s index %d."+
+						"\nexpected: %d\nreceived: %d",
+						key, i, v, jsVal.Index(i).Int())
+				}
+			}
+		case "object":
+			if jsVal.Get("int").Int() != val.(map[string]any)["int"] {
+				t.Errorf("Incorrect value for key %s."+
+					"\nexpected: %d\nreceived: %d", key,
+					val.(map[string]any)["int"], jsVal.Get("int").Int())
+			}
+		}
+	}
+}
+
+// Tests that JSON can be converted to a Javascript object via JsonToJS and back
+// to JSON using JsToJson and matches the original.
+func TestJsonToJSJsToJson(t *testing.T) {
+	testObj := map[string]any{
+		"nil":    nil,
+		"bool":   true,
+		"int":    1,
+		"float":  1.5,
+		"string": "I am string",
+		"bytes":  []byte{1, 2, 3},
+		"array":  []any{1, 2, 3},
+		"object": map[string]any{"int": 5},
+	}
+	jsonData, err := json.Marshal(testObj)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsObj, err := JsonToJS(jsonData)
+	if err != nil {
+		t.Errorf("Failed to convert the Javascript object to JSON: %+v", err)
+	}
+
+	jsJson := JsToJson(jsObj)
+
+	// Javascript does not return the JSON object fields sorted, so the letters
+	// of each Javascript string are sorted and compared
+	er := []rune(string(jsonData))
+	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
+	jj := []rune(jsJson)
+	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })
+
+	if string(er) != string(jj) {
+		t.Errorf("JSON from Javascript does not match original."+
+			"\nexpected: %s\nreceived: %s", jsonData, jsJson)
+	}
+}
diff --git a/utils/utils.go b/utils/utils.go
new file mode 100644
index 0000000000000000000000000000000000000000..47384112bcae0f7382f7ed442a417aacd92d03ee
--- /dev/null
+++ b/utils/utils.go
@@ -0,0 +1,104 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package utils
+
+import (
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"syscall/js"
+)
+
+var (
+	// JSON is the Javascript JSON type. It is used to perform JSON operations
+	// on the Javascript layer.
+	JSON = js.Global().Get("JSON")
+
+	// Object is the Javascript Object type. It is used to perform Object
+	// operations on the Javascript layer.
+	Object = js.Global().Get("Object")
+
+	// Promise is the Javascript Promise type. It is used to generate new
+	// promises.
+	Promise = js.Global().Get("Promise")
+
+	// Uint8Array is the Javascript Uint8Array type. It is used to create new
+	// Uint8Array.
+	Uint8Array = js.Global().Get("Uint8Array")
+)
+
+// WrapCB wraps a Javascript function in an object so that it can be called
+// later with only the arguments and without specifying the function name.
+//
+// Panics if m is not a function.
+func WrapCB(parent js.Value, m string) func(args ...any) js.Value {
+	if parent.Get(m).Type() != js.TypeFunction {
+		// Create the error separate from the print so stack trace is printed
+		err := errors.Errorf("Function %q is not of type %s", m, js.TypeFunction)
+		jww.FATAL.Panicf("%+v", err)
+	}
+
+	return func(args ...any) js.Value { return parent.Call(m, args...) }
+}
+
+// PromiseFn converts the Javascript Promise construct into Go.
+//
+// Call resolve with the return of the function on success. Call reject with an
+// error on failure.
+type PromiseFn func(resolve, reject func(args ...any) js.Value)
+
+// CreatePromise creates a Javascript promise to return the value of a blocking
+// Go function to Javascript.
+func CreatePromise(f PromiseFn) any {
+	// Create handler for promise (this will be a Javascript function)
+	var handler js.Func
+	handler = js.FuncOf(func(this js.Value, args []js.Value) any {
+		// Spawn a new go routine to perform the blocking function
+		go func(resolve, reject js.Value) {
+			go handler.Release()
+			f(resolve.Invoke, reject.Invoke)
+		}(args[0], args[1])
+
+		return nil
+	})
+
+	// Create and return the Promise object
+	return Promise.New(handler)
+}
+
+// Await waits on a Javascript value. It blocks until the awaitable successfully
+// resolves to the result or rejects to err.
+//
+// If there is a result, err will be nil and vice versa.
+func Await(awaitable js.Value) (result []js.Value, err []js.Value) {
+	then := make(chan []js.Value)
+	defer close(then)
+	thenFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
+		then <- args
+		return nil
+	})
+	defer thenFunc.Release()
+
+	catch := make(chan []js.Value)
+	defer close(catch)
+	catchFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
+		catch <- args
+		return nil
+	})
+	defer catchFunc.Release()
+
+	awaitable.Call("then", thenFunc).Call("catch", catchFunc)
+
+	select {
+	case result = <-then:
+		return result, nil
+	case err = <-catch:
+		return nil, err
+	}
+}
diff --git a/wasm_exec.js b/wasm_exec.js
new file mode 100644
index 0000000000000000000000000000000000000000..5365a841cd2ff018149c25666e4b525a42bc1344
--- /dev/null
+++ b/wasm_exec.js
@@ -0,0 +1,561 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+"use strict";
+
+(() => {
+	const enosys = () => {
+		const err = new Error("not implemented");
+		err.code = "ENOSYS";
+		return err;
+	};
+
+	if (!globalThis.fs) {
+		let outputBuf = "";
+		globalThis.fs = {
+			constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
+			writeSync(fd, buf) {
+				outputBuf += decoder.decode(buf);
+				const nl = outputBuf.lastIndexOf("\n");
+				if (nl != -1) {
+					console.log(outputBuf.substr(0, nl));
+					outputBuf = outputBuf.substr(nl + 1);
+				}
+				return buf.length;
+			},
+			write(fd, buf, offset, length, position, callback) {
+				if (offset !== 0 || length !== buf.length || position !== null) {
+					callback(enosys());
+					return;
+				}
+				const n = this.writeSync(fd, buf);
+				callback(null, n);
+			},
+			chmod(path, mode, callback) { callback(enosys()); },
+			chown(path, uid, gid, callback) { callback(enosys()); },
+			close(fd, callback) { callback(enosys()); },
+			fchmod(fd, mode, callback) { callback(enosys()); },
+			fchown(fd, uid, gid, callback) { callback(enosys()); },
+			fstat(fd, callback) { callback(enosys()); },
+			fsync(fd, callback) { callback(null); },
+			ftruncate(fd, length, callback) { callback(enosys()); },
+			lchown(path, uid, gid, callback) { callback(enosys()); },
+			link(path, link, callback) { callback(enosys()); },
+			lstat(path, callback) { callback(enosys()); },
+			mkdir(path, perm, callback) { callback(enosys()); },
+			open(path, flags, mode, callback) { callback(enosys()); },
+			read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
+			readdir(path, callback) { callback(enosys()); },
+			readlink(path, callback) { callback(enosys()); },
+			rename(from, to, callback) { callback(enosys()); },
+			rmdir(path, callback) { callback(enosys()); },
+			stat(path, callback) { callback(enosys()); },
+			symlink(path, link, callback) { callback(enosys()); },
+			truncate(path, length, callback) { callback(enosys()); },
+			unlink(path, callback) { callback(enosys()); },
+			utimes(path, atime, mtime, callback) { callback(enosys()); },
+		};
+	}
+
+	if (!globalThis.process) {
+		globalThis.process = {
+			getuid() { return -1; },
+			getgid() { return -1; },
+			geteuid() { return -1; },
+			getegid() { return -1; },
+			getgroups() { throw enosys(); },
+			pid: -1,
+			ppid: -1,
+			umask() { throw enosys(); },
+			cwd() { throw enosys(); },
+			chdir() { throw enosys(); },
+		}
+	}
+
+	if (!globalThis.crypto) {
+		throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
+	}
+
+	if (!globalThis.performance) {
+		throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
+	}
+
+	if (!globalThis.TextEncoder) {
+		throw new Error("globalThis.TextEncoder is not available, polyfill required");
+	}
+
+	if (!globalThis.TextDecoder) {
+		throw new Error("globalThis.TextDecoder is not available, polyfill required");
+	}
+
+	const encoder = new TextEncoder("utf-8");
+	const decoder = new TextDecoder("utf-8");
+
+	globalThis.Go = class {
+		constructor() {
+			this.argv = ["js"];
+			this.env = {};
+			this.exit = (code) => {
+				if (code !== 0) {
+					console.warn("exit code:", code);
+				}
+			};
+			this._exitPromise = new Promise((resolve) => {
+				this._resolveExitPromise = resolve;
+			});
+			this._pendingEvent = null;
+			this._scheduledTimeouts = new Map();
+			this._nextCallbackTimeoutID = 1;
+
+			const setInt64 = (addr, v) => {
+				this.mem.setUint32(addr + 0, v, true);
+				this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
+			}
+
+			const getInt64 = (addr) => {
+				const low = this.mem.getUint32(addr + 0, true);
+				const high = this.mem.getInt32(addr + 4, true);
+				return low + high * 4294967296;
+			}
+
+			const loadValue = (addr) => {
+				const f = this.mem.getFloat64(addr, true);
+				if (f === 0) {
+					return undefined;
+				}
+				if (!isNaN(f)) {
+					return f;
+				}
+
+				const id = this.mem.getUint32(addr, true);
+				return this._values[id];
+			}
+
+			const storeValue = (addr, v) => {
+				const nanHead = 0x7FF80000;
+
+				if (typeof v === "number" && v !== 0) {
+					if (isNaN(v)) {
+						this.mem.setUint32(addr + 4, nanHead, true);
+						this.mem.setUint32(addr, 0, true);
+						return;
+					}
+					this.mem.setFloat64(addr, v, true);
+					return;
+				}
+
+				if (v === undefined) {
+					this.mem.setFloat64(addr, 0, true);
+					return;
+				}
+
+				let id = this._ids.get(v);
+				if (id === undefined) {
+					id = this._idPool.pop();
+					if (id === undefined) {
+						id = this._values.length;
+					}
+					this._values[id] = v;
+					this._goRefCounts[id] = 0;
+					this._ids.set(v, id);
+				}
+				this._goRefCounts[id]++;
+				let typeFlag = 0;
+				switch (typeof v) {
+					case "object":
+						if (v !== null) {
+							typeFlag = 1;
+						}
+						break;
+					case "string":
+						typeFlag = 2;
+						break;
+					case "symbol":
+						typeFlag = 3;
+						break;
+					case "function":
+						typeFlag = 4;
+						break;
+				}
+				this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
+				this.mem.setUint32(addr, id, true);
+			}
+
+			const loadSlice = (addr) => {
+				const array = getInt64(addr + 0);
+				const len = getInt64(addr + 8);
+				return new Uint8Array(this._inst.exports.mem.buffer, array, len);
+			}
+
+			const loadSliceOfValues = (addr) => {
+				const array = getInt64(addr + 0);
+				const len = getInt64(addr + 8);
+				const a = new Array(len);
+				for (let i = 0; i < len; i++) {
+					a[i] = loadValue(array + i * 8);
+				}
+				return a;
+			}
+
+			const loadString = (addr) => {
+				const saddr = getInt64(addr + 0);
+				const len = getInt64(addr + 8);
+				return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
+			}
+
+			const timeOrigin = Date.now() - performance.now();
+			this.importObject = {
+				go: {
+					// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
+					// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
+					// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
+					// This changes the SP, thus we have to update the SP used by the imported function.
+
+					// func wasmExit(code int32)
+					"runtime.wasmExit": (sp) => {
+						sp >>>= 0;
+						const code = this.mem.getInt32(sp + 8, true);
+						this.exited = true;
+						delete this._inst;
+						delete this._values;
+						delete this._goRefCounts;
+						delete this._ids;
+						delete this._idPool;
+						this.exit(code);
+					},
+
+					// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
+					"runtime.wasmWrite": (sp) => {
+						sp >>>= 0;
+						const fd = getInt64(sp + 8);
+						const p = getInt64(sp + 16);
+						const n = this.mem.getInt32(sp + 24, true);
+						fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
+					},
+
+					// func resetMemoryDataView()
+					"runtime.resetMemoryDataView": (sp) => {
+						sp >>>= 0;
+						this.mem = new DataView(this._inst.exports.mem.buffer);
+					},
+
+					// func nanotime1() int64
+					"runtime.nanotime1": (sp) => {
+						sp >>>= 0;
+						setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
+					},
+
+					// func walltime() (sec int64, nsec int32)
+					"runtime.walltime": (sp) => {
+						sp >>>= 0;
+						const msec = (new Date).getTime();
+						setInt64(sp + 8, msec / 1000);
+						this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
+					},
+
+					// func scheduleTimeoutEvent(delay int64) int32
+					"runtime.scheduleTimeoutEvent": (sp) => {
+						sp >>>= 0;
+						const id = this._nextCallbackTimeoutID;
+						this._nextCallbackTimeoutID++;
+						this._scheduledTimeouts.set(id, setTimeout(
+							() => {
+								this._resume();
+								while (this._scheduledTimeouts.has(id)) {
+									// for some reason Go failed to register the timeout event, log and try again
+									// (temporary workaround for https://github.com/golang/go/issues/28975)
+									console.warn("scheduleTimeoutEvent: missed timeout event");
+									this._resume();
+								}
+							},
+							getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
+						));
+						this.mem.setInt32(sp + 16, id, true);
+					},
+
+					// func clearTimeoutEvent(id int32)
+					"runtime.clearTimeoutEvent": (sp) => {
+						sp >>>= 0;
+						const id = this.mem.getInt32(sp + 8, true);
+						clearTimeout(this._scheduledTimeouts.get(id));
+						this._scheduledTimeouts.delete(id);
+					},
+
+					// func getRandomData(r []byte)
+					"runtime.getRandomData": (sp) => {
+						sp >>>= 0;
+						crypto.getRandomValues(loadSlice(sp + 8));
+					},
+
+					// func finalizeRef(v ref)
+					"syscall/js.finalizeRef": (sp) => {
+						sp >>>= 0;
+						const id = this.mem.getUint32(sp + 8, true);
+						this._goRefCounts[id]--;
+						if (this._goRefCounts[id] === 0) {
+							const v = this._values[id];
+							this._values[id] = null;
+							this._ids.delete(v);
+							this._idPool.push(id);
+						}
+					},
+
+					// func stringVal(value string) ref
+					"syscall/js.stringVal": (sp) => {
+						sp >>>= 0;
+						storeValue(sp + 24, loadString(sp + 8));
+					},
+
+					// func valueGet(v ref, p string) ref
+					"syscall/js.valueGet": (sp) => {
+						sp >>>= 0;
+						const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
+						sp = this._inst.exports.getsp() >>> 0; // see comment above
+						storeValue(sp + 32, result);
+					},
+
+					// func valueSet(v ref, p string, x ref)
+					"syscall/js.valueSet": (sp) => {
+						sp >>>= 0;
+						Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
+					},
+
+					// func valueDelete(v ref, p string)
+					"syscall/js.valueDelete": (sp) => {
+						sp >>>= 0;
+						Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
+					},
+
+					// func valueIndex(v ref, i int) ref
+					"syscall/js.valueIndex": (sp) => {
+						sp >>>= 0;
+						storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
+					},
+
+					// valueSetIndex(v ref, i int, x ref)
+					"syscall/js.valueSetIndex": (sp) => {
+						sp >>>= 0;
+						Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
+					},
+
+					// func valueCall(v ref, m string, args []ref) (ref, bool)
+					"syscall/js.valueCall": (sp) => {
+						sp >>>= 0;
+						try {
+							const v = loadValue(sp + 8);
+							const m = Reflect.get(v, loadString(sp + 16));
+							const args = loadSliceOfValues(sp + 32);
+							const result = Reflect.apply(m, v, args);
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 56, result);
+							this.mem.setUint8(sp + 64, 1);
+						} catch (err) {
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 56, err);
+							this.mem.setUint8(sp + 64, 0);
+						}
+					},
+
+					// func valueInvoke(v ref, args []ref) (ref, bool)
+					"syscall/js.valueInvoke": (sp) => {
+						sp >>>= 0;
+						try {
+							const v = loadValue(sp + 8);
+							const args = loadSliceOfValues(sp + 16);
+							const result = Reflect.apply(v, undefined, args);
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, result);
+							this.mem.setUint8(sp + 48, 1);
+						} catch (err) {
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, err);
+							this.mem.setUint8(sp + 48, 0);
+						}
+					},
+
+					// func valueNew(v ref, args []ref) (ref, bool)
+					"syscall/js.valueNew": (sp) => {
+						sp >>>= 0;
+						try {
+							const v = loadValue(sp + 8);
+							const args = loadSliceOfValues(sp + 16);
+							const result = Reflect.construct(v, args);
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, result);
+							this.mem.setUint8(sp + 48, 1);
+						} catch (err) {
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, err);
+							this.mem.setUint8(sp + 48, 0);
+						}
+					},
+
+					// func valueLength(v ref) int
+					"syscall/js.valueLength": (sp) => {
+						sp >>>= 0;
+						setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
+					},
+
+					// valuePrepareString(v ref) (ref, int)
+					"syscall/js.valuePrepareString": (sp) => {
+						sp >>>= 0;
+						const str = encoder.encode(String(loadValue(sp + 8)));
+						storeValue(sp + 16, str);
+						setInt64(sp + 24, str.length);
+					},
+
+					// valueLoadString(v ref, b []byte)
+					"syscall/js.valueLoadString": (sp) => {
+						sp >>>= 0;
+						const str = loadValue(sp + 8);
+						loadSlice(sp + 16).set(str);
+					},
+
+					// func valueInstanceOf(v ref, t ref) bool
+					"syscall/js.valueInstanceOf": (sp) => {
+						sp >>>= 0;
+						this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
+					},
+
+					// func copyBytesToGo(dst []byte, src ref) (int, bool)
+					"syscall/js.copyBytesToGo": (sp) => {
+						sp >>>= 0;
+						const dst = loadSlice(sp + 8);
+						const src = loadValue(sp + 32);
+						if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
+							this.mem.setUint8(sp + 48, 0);
+							return;
+						}
+						const toCopy = src.subarray(0, dst.length);
+						dst.set(toCopy);
+						setInt64(sp + 40, toCopy.length);
+						this.mem.setUint8(sp + 48, 1);
+					},
+
+					// func copyBytesToJS(dst ref, src []byte) (int, bool)
+					"syscall/js.copyBytesToJS": (sp) => {
+						sp >>>= 0;
+						const dst = loadValue(sp + 8);
+						const src = loadSlice(sp + 16);
+						if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
+							this.mem.setUint8(sp + 48, 0);
+							return;
+						}
+						const toCopy = src.subarray(0, dst.length);
+						dst.set(toCopy);
+						setInt64(sp + 40, toCopy.length);
+						this.mem.setUint8(sp + 48, 1);
+					},
+
+					"debug": (value) => {
+						console.log(value);
+					},
+
+					// func Throw(exception string, message string)
+					'gitlab.com/elixxir/wasm-utils/exception.throw': (sp) => {
+						const exception = loadString(sp + 8)
+						const message = loadString(sp + 24)
+						throw globalThis[exception](message)
+					},
+				}
+			};
+		}
+
+		async run(instance) {
+			if (!(instance instanceof WebAssembly.Instance)) {
+				throw new Error("Go.run: WebAssembly.Instance expected");
+			}
+			this._inst = instance;
+			this.mem = new DataView(this._inst.exports.mem.buffer);
+			this._values = [ // JS values that Go currently has references to, indexed by reference id
+				NaN,
+				0,
+				null,
+				true,
+				false,
+				globalThis,
+				this,
+			];
+			this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
+			this._ids = new Map([ // mapping from JS values to reference ids
+				[0, 1],
+				[null, 2],
+				[true, 3],
+				[false, 4],
+				[globalThis, 5],
+				[this, 6],
+			]);
+			this._idPool = [];   // unused ids that have been garbage collected
+			this.exited = false; // whether the Go program has exited
+
+			// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
+			let offset = 4096;
+
+			const strPtr = (str) => {
+				const ptr = offset;
+				const bytes = encoder.encode(str + "\0");
+				new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
+				offset += bytes.length;
+				if (offset % 8 !== 0) {
+					offset += 8 - (offset % 8);
+				}
+				return ptr;
+			};
+
+			const argc = this.argv.length;
+
+			const argvPtrs = [];
+			this.argv.forEach((arg) => {
+				argvPtrs.push(strPtr(arg));
+			});
+			argvPtrs.push(0);
+
+			const keys = Object.keys(this.env).sort();
+			keys.forEach((key) => {
+				argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
+			});
+			argvPtrs.push(0);
+
+			const argv = offset;
+			argvPtrs.forEach((ptr) => {
+				this.mem.setUint32(offset, ptr, true);
+				this.mem.setUint32(offset + 4, 0, true);
+				offset += 8;
+			});
+
+			// The linker guarantees global data starts from at least wasmMinDataAddr.
+			// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
+			const wasmMinDataAddr = 4096 + 8192;
+			if (offset >= wasmMinDataAddr) {
+				throw new Error("total length of command line and environment variables exceeds limit");
+			}
+
+			this._inst.exports.run(argc, argv);
+			if (this.exited) {
+				this._resolveExitPromise();
+			}
+			await this._exitPromise;
+		}
+
+		_resume() {
+			if (this.exited) {
+				throw new Error("Go program has already exited");
+			}
+			this._inst.exports.resume();
+			if (this.exited) {
+				this._resolveExitPromise();
+			}
+		}
+
+		_makeFuncWrapper(id) {
+			const go = this;
+			return function () {
+				const event = { id: id, this: this, args: arguments };
+				go._pendingEvent = event;
+				go._resume();
+				return event.result;
+			};
+		}
+	}
+})();