diff --git a/README.md b/README.md
index a96835a5c0a7a4c83a3939c16d6f94f0569cb788..2447a9ea598e036b56435984c15af482f6396da8 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # xxdk-WASM
 
-This repository contains the WebAssembly bindings for xxDK. It also includes a
-test server to serve the compiled WebAssembly module.
+This repository contains the WebAssembly bindings for xxDK. It also includes
+examples and a test server to serve the compiled WebAssembly module.
 
 ## Building
 
@@ -15,34 +15,35 @@ $ GOOS=js GOARCH=wasm go build -o xxdk.wasm
 ### Running Unit Tests
 
 Because the bindings use `syscall/js`, tests cannot only be run in a browser. To
-automate this process first install
-[wasmbrowsertest](https://github.com/agnivade/wasmbrowsertest). Then, tests can
-be run using the following command.
+automate this process first 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.
 
 ```shell
 $ GOOS=js GOARCH=wasm go test ./...
 ```
 
-Note, this will fail because `utils/utils_js.s` contains commands only recognized
-by the Go WebAssembly compiler and for some reason not recognized by the test
-runner. To get tests to run, temporarily delete the body of `utils/utils_js.s`
-during testing.
+Note, this will fail because `utils/utils_js.s` contains commands only
+recognized by the Go WebAssembly compiler and for some reason are not recognized
+by the test runner. To get tests to run, temporarily delete the body of
+`utils/utils_js.s` during testing.
 
-## Testing
+## Testing and Examples
 
-The `test` directory contains `assets`, a simple web page to run the Javascript,
-and `server`, which runs a simple Go HTTP server to deliver the webpage.
+The `test` directory contains a basic HTTP server that serves an HTML file
+running the WebAssembly binary. This is used for basic testing of the binary.
 
-To run the server, first compile the bindings and save them to the `assets`
+To run the server, first compile the bindings and save them to the `test`
 directory. Then run the server
 
 ```shell
-$ GOOS=js GOARCH=wasm go build -o test/assets/xxdk.wasm
-$ cd test/server/
+$ GOOS=js GOARCH=wasm go build -o test/xxdk.wasm
+$ cd test/
 $ go run main.go
 ```
 
-Navigate to http://localhost:9090 to see the web page.
+Navigate to http://localhost:9090 to see the webpage.
 
 ## `wasm_exec.js`
 
diff --git a/examples/ndf.json b/examples/ndf.json
deleted file mode 100644
index 0ca92937841d565ce0ed0243f5f032e86cfbbe55..0000000000000000000000000000000000000000
--- a/examples/ndf.json
+++ /dev/null
@@ -1 +0,0 @@
-{"Timestamp":"2022-09-02T10:27:19.6661079-07:00","Gateways":[{"Id":"uKOO90b6GDwXcLr/hGU3sjX6O0AmDA4nvC4bDEO7wFMB","Address":"0.0.0.0:8440","Tls_certificate":"-----BEGIN CERTIFICATE-----\r\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\r\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\r\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\r\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\r\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\r\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\r\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\r\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\r\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\r\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\r\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\r\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\r\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\r\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\r\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\r\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\r\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\r\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\r\n6m52PyzMNV+2N21IPppKwA==\r\n-----END CERTIFICATE-----\r\n","Bin":"SouthAndCentralAmerica"},{"Id":"KDzk21/pQ01RtoOHdh7207dkUqx5Mq7MIJsRPsOKXcMB","Address":"0.0.0.0:8442","Tls_certificate":"-----BEGIN CERTIFICATE-----\r\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\r\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\r\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\r\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\r\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\r\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\r\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\r\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\r\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\r\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\r\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\r\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\r\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\r\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\r\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\r\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\r\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\r\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\r\n6m52PyzMNV+2N21IPppKwA==\r\n-----END CERTIFICATE-----\r\n","Bin":"CentralEurope"},{"Id":"jgZt6uZkK6tTGRnk0tAmVO9Akv11y0yBPolNkw/e+bAB","Address":"0.0.0.0:8443","Tls_certificate":"-----BEGIN CERTIFICATE-----\r\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\r\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\r\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\r\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\r\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\r\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\r\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\r\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\r\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\r\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\r\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\r\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\r\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\r\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\r\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\r\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\r\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\r\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\r\n6m52PyzMNV+2N21IPppKwA==\r\n-----END CERTIFICATE-----\r\n","Bin":"EasternEurope"},{"Id":"AFARX39tIj246msuyhutEWlFQv7LSaAe8cR0i+aB59gB","Address":"0.0.0.0:8444","Tls_certificate":"-----BEGIN CERTIFICATE-----\r\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\r\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\r\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\r\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\r\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\r\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\r\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\r\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\r\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\r\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\r\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\r\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\r\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\r\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\r\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\r\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\r\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\r\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\r\n6m52PyzMNV+2N21IPppKwA==\r\n-----END CERTIFICATE-----\r\n","Bin":"MiddleEast"},{"Id":"EyVHEz37phB8XZVl2N6Lu3p/bxnVIBvoAPhbPXIcpZsB","Address":"0.0.0.0:8441","Tls_certificate":"-----BEGIN CERTIFICATE-----\r\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\r\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\r\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\r\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\r\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\r\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\r\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\r\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\r\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\r\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\r\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\r\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\r\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\r\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\r\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\r\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\r\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\r\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\r\n6m52PyzMNV+2N21IPppKwA==\r\n-----END CERTIFICATE-----\r\n","Bin":"WesternEurope"}],"Nodes":[{"Id":"uKOO90b6GDwXcLr/hGU3sjX6O0AmDA4nvC4bDEO7wFMC","Address":"","Tls_certificate":"","Status":0},{"Id":"KDzk21/pQ01RtoOHdh7207dkUqx5Mq7MIJsRPsOKXcMC","Address":"","Tls_certificate":"","Status":0},{"Id":"jgZt6uZkK6tTGRnk0tAmVO9Akv11y0yBPolNkw/e+bAC","Address":"","Tls_certificate":"","Status":0},{"Id":"AFARX39tIj246msuyhutEWlFQv7LSaAe8cR0i+aB59gC","Address":"","Tls_certificate":"","Status":0},{"Id":"EyVHEz37phB8XZVl2N6Lu3p/bxnVIBvoAPhbPXIcpZsC","Address":"","Tls_certificate":"","Status":0}],"Registration":{"Address":":18000","ClientRegistrationAddress":"0.0.0.0:11421","Tls_certificate":"-----BEGIN CERTIFICATE-----\r\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\r\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\r\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\r\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\r\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\r\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\r\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\r\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\r\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\r\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\r\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\r\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\r\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\r\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\r\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\r\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\r\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\r\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\r\n6m52PyzMNV+2N21IPppKwA==\r\n-----END CERTIFICATE-----\r\n","EllipticPubKey":"AWjTOI++PCwzIVLMiXcBHzKtxWShdbBLluGOtbnqd10="},"Notification":{"Address":"","Tls_certificate":""},"Udb":{"Id":"uu49Kr9myQUG0gG8VKwalxmem65pfPu4D2Lk1u8pi/UD","Cert":"-----BEGIN CERTIFICATE-----\r\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\r\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\r\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\r\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\r\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\r\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\r\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\r\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\r\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\r\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\r\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\r\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\r\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\r\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\r\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\r\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\r\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\r\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\r\n6m52PyzMNV+2N21IPppKwA==\r\n-----END CERTIFICATE-----\r\n","Address":"127.0.0.1:18001","DhPubKey":"eyJWYWx1ZSI6MTQ3MTM5NzkxNDA3Njk5MjM0OTk3MTkyOTEwNTk1MDc1NTI3NDM1NDYwMDYxNTI0NzcyOTQ0MzQzNzQ4NjgxMjA2MjY1ODY5ODQ2NjQyMzMyMzQ1NzA3OTkwNjc3NDA2MDQwMTQ1MTc3NTM0MzIxNTM4MTgyNDQwNzE3ODkwMzU5OTkzNDI4Njg2MTA5NTI1MjAyMTE4MjA1MjIzODkxNDMxODk5MTY4MjYxOTU5MTIzOTc3MDcxMzYzMDIzMTYxOTgzNTIwNjIwNDA1MjM1NzQ3NTkwMDg0NjkzNjYxODA0MTE1NjkxNjc3NDg4MDQ5MzY3NTIxMTk1Nzk1NjAyOTcxNjI1MzYzODY0NDkyOTk5NDk2NTI3MTkwNjA2MzgzNTkzMjg5NjA3MDk3MTg1MjEwNzkyOTk5OTgyNjY3Mjc5MDk1Njc4ODI2NTE1OTA5NTA2MDQyNTA4MDgxNDU2NDkzNDU2OTk3MzU0MDQwNTA0NDM2NTMwMTI2OTQwNDM0MzkxNjEyMTMyNTcyMjEwNDY5OTExMDY4NjAyMzQ2MTExNjAyOTUwNzQyNzk2NzUyNDQ2MDQ0MTk3OTM4OTQ2ODg1ODQ1MTE5NzMzNjU0NzQzOTc1OTY4NTIwNzYyNDgyODI3MzY1NDM0MDMxNDc5NjEzOTE3MTQ5ODgzNjQwMzQ3ODU1NTEyODY0ODc5NTU2NjY5ODcxNjI1NzAyMzczNTU1MTkxMTE5NjM2NDE0MDg4NDc2NjUyODc4NTYzNzAwNzE2NTAxNzk5NDA5NjA3NDI3Njg5OTI5MTcxMjY2MTQ3MDQ3NzU2MjA0Njg5NDM5NTE5MDkyNjgwNDk0ODg2MTMxNDc0NjY1MzMxNzE4MDY0MjQ3NzQ0MTE5MTQ2Mjg4MDY0NjIwOTMzMjQ4OTg3MjAzNzc4MjQxNTEzNTQ2MTc1NDQ1NjM4ODU4MjU0MTk0NTQ4MjY3NjIwOTYwNTEwMjc0Nzc2NjU4Mzc3MDM4OTcxNzkzMDUxOTAxNTk1NDkyNDQ1OTQzNjk3NTE5NTExMjAzOTkzMzgxOTAxNjI3NzY2Mjk1MDc1NDQ0NTM3NDIyMTc5ODgxMDI3NjcwMTE4MzM2NzgzODA5MjcxODAxNTMwNjk4MzE5ODIxNjg2Mzc1ODc5NTY1NzgwMDcxMjE0MzMwOTA5ODk5MTU1MTExNjQ3Mjc5NTg5NDc0ODIyMzkzMDQ3OTU1NzMyMTY3NzI4ODI1NTMyNDQ1ODg4MDk2MTY1NzIxMzExNDcxNTIyODUxLCJGaW5nZXJwcmludCI6MTY4MDE1NDE1MTEyMzMwOTgzNjN9"},"E2e":{"Prime":"E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D49413394C049B7A8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688B55B3DD2AEDF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E7861575E745D31F8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC6ADC718DD2A3E041023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C4A530E8FFB1BC51DADDF453B0B2717C2BC6669ED76B4BDD5C9FF558E88F26E5785302BEDBCA23EAC5ACE92096EE8A60642FB61E8F3D24990B8CB12EE448EEF78E184C7242DD161C7738F32BF29A841698978825B4111B4BC3E1E198455095958333D776D8B2BEEED3A1A1A221A6E37E664A64B83981C46FFDDC1A45E3D5211AAF8BFBC072768C4F50D7D7803D2D4F278DE8014A47323631D7E064DE81C0C6BFA43EF0E6998860F1390B5D3FEACAF1696015CB79C3F9C2D93D961120CD0E5F12CBB687EAB045241F96789C38E89D796138E6319BE62E35D87B1048CA28BE389B575E994DCA755471584A09EC723742DC35873847AEF49F66E43873","Small_prime":"","Generator":"2"},"Cmix":{"Prime":"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF","Small_prime":"","Generator":"2"},"AddressSpace":[{"Size":32,"Timestamp":"2022-09-02T10:27:19.6392244-07:00"}],"ClientVersion":"","WhitelistedIds":null,"WhitelistedIpAddresses":null,"RateLimits":{"Capacity":0,"LeakedTokens":0,"LeakDuration":0}}
\ No newline at end of file
diff --git a/examples/sendE2E/index.html b/examples/sendE2E/index.html
deleted file mode 100644
index 27067671ba8e358138343310445fb490bd21dcab..0000000000000000000000000000000000000000
--- a/examples/sendE2E/index.html
+++ /dev/null
@@ -1,70 +0,0 @@
-<!--
-  ~ Copyright © 2020 xx network SEZC                                           ///
-  ~                                                                            ///
-  ~ Use of this source code is governed by a license that can be found in the  ///
-  ~ LICENSE file                                                               ///
-  -->
-
-<!DOCTYPE html>
-<html lang="en">
-<head>
-	<meta charset="UTF-8">
-	<title>Receiver</title>
-
-	<link rel="stylesheet" type="text/css" href="../styles.css">
-	<link rel="shortcut icon" type="image/x-icon" href="receiver-favicon.ico" />
-
-	<script type="text/javascript" src="xxdk.js"></script>
-	<script type="text/javascript" src="../wasm_exec.js"></script>
-	<script>
-        // Use the encoder to convert a string to Uint8Array using enc.encode()
-
-        const go = new Go();
-        WebAssembly.instantiateStreaming(fetch("../xxdk.wasm"), go.importObject).then((result) => {
-            go.run(result.instance);
-
-            LogLevel(0);
-
-            const recipientContactFile = '';
-            const myContactFileName = 'myE2eContact.xxc';
-            const statePath = 'statePathRecipient';
-            const statePass = 'password';
-
-            document.getElementById('ndf-input')
-                .addEventListener('change', function (e) {
-                    let reader = new FileReader();
-                    reader.onload = function (e) {
-                        SendE2e(e.target.result, recipientContactFile, myContactFileName, statePath, statePass);
-                    };
-                    reader.readAsText(e.target.files[0]);
-                }, false);
-        });
-	</script>
-</head>
-<body>
-<h1>Receiver</h1>
-<form id="start-cmix">
-	<div>
-		<label for="ndf-input">Select NDF file to use <code>ndf</code> variable in JS (required):</label><br/>
-		<input type="file" id="ndf-input" required/>
-	</div>
-	<!--	<div>
-			<label for="recipient-contact-input">Recipient contact path (optional):</label><br/>
-			<input type="file" id="recipient-contact-input"/>
-		</div>
-		<div>
-			<label for="session-path">Session path (required):</label><br/>
-			<input type="text" id="session-path" required/>
-		</div>
-		<div>
-			<label for="session-password">Storage password (required):</label><br/>
-			<input type="text" id="session-password" required/>
-		</div>
-		<input type="submit" value="Start">-->
-</form>
-<div id="output" style="padding:10px;border:1px solid red"></div>
-</body>
-
-<script>
-</script>
-</html>
\ No newline at end of file
diff --git a/examples/sendE2E/index2.html b/examples/sendE2E/index2.html
deleted file mode 100644
index 6665b33c9a44dab1e5974de6d742e075d92c2270..0000000000000000000000000000000000000000
--- a/examples/sendE2E/index2.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<!--
-  ~ Copyright © 2020 xx network SEZC                                           ///
-  ~                                                                            ///
-  ~ Use of this source code is governed by a license that can be found in the  ///
-  ~ LICENSE file                                                               ///
-  -->
-
-<!DOCTYPE html>
-<html lang="en">
-<head>
-	<meta charset="UTF-8">
-	<title>Sender</title>
-
-	<link rel="stylesheet" type="text/css" href="../styles.css">
-	<link rel="shortcut icon" type="image/x-icon" href="sender-favicon.ico" />
-	<script type="text/javascript" src="xxdk.js"></script>
-	<script type="text/javascript" src="../wasm_exec.js"></script>
-	<script>
-        // Use the encoder to convert a string to Uint8Array using enc.encode()
-
-        const go = new Go();
-        WebAssembly.instantiateStreaming(fetch("../xxdk.wasm"), go.importObject).then((result) => {
-            go.run(result.instance);
-
-            LogLevel(0);
-
-            const recipientContactFile = '<xxc(2)Q2uq69ry88AeNFKcnpOkCjuCtkYIdv/fgC3F4z6eNUMDrgZ7Ugdw/BAr6bG0KyMpEOL6fulH331JBFOD69e7iZLxkBr3BvZyfyTXL9QtQwpzaDnmPXjyRR5cjAQ6gfk5XQzsrw45Q8qsrKewwZsqGhac0NKJYPiXGa9DTQR6f1VMOY7cco4uquNN5BRo4ucz1gu9w2Ff3N+1Hdz9V1r7xulkkd/78IyoJqHQDgp1eO3q6NZJT55DNsSuS2ZuYQc3yQUyTHH/G7gIF7EwRTU6dhRTGMYpfOQhRT67EVidHOT4e7GSpkWoGzi5XUBA+9N1llRCAwzRJ65lQqJZIC/vNgxJhHR5Cd4AOfHc+KuGlXMpCQkeYjd7a+ggh4RUuPsMW59QlKaBbZ1mbGHPzphFUrJIS63TMcJRrWbGwXHJrvZnD1NYNOM/x/qSR4pTkv8soIHFzgvFbRME5VTfxWfWKSCG/qu3TaWfetnYXmsqhdmnURekF4iSMtyDpVch+86zK5YmuO9Ap18MvdwmnfX5r9xhyPRga/QqmkdEI6ZebuXVYJQGr6Lzsz4z+4AGI0v87j//APsGxvi8fwAAAgA7w6N/pgZWFlB+p/Muirk+VQ==xxc>';
-            const myContactFileName = 'theirE2eContact.xxc';
-            const statePath = 'statePathSender';
-            const statePass = 'password';
-
-            document.getElementById('ndf-input')
-                .addEventListener('change', function (e) {
-                    let reader = new FileReader();
-                    reader.onload = function (e) {
-                        SendE2e(e.target.result, recipientContactFile, myContactFileName, statePath, statePass);
-                    };
-                    reader.readAsText(e.target.files[0]);
-                }, false);
-        });
-	</script>
-</head>
-<body>
-<h1>Sender</h1>
-<form id="start-cmix">
-	<div>
-		<label for="ndf-input">Select NDF file to use <code>ndf</code> variable in JS:</label><br/>
-		<input type="file" id="ndf-input" required/>
-	</div>
-<!--	<div>
-		<label for="ndf-input">Recipient contact path:</label><br/>
-		<input type="file" id="recipient-contact-input" required/>
-	</div>
-	<input type="submit" value="Start">-->
-</form>
-<div id="output" style="padding:10px;border:1px solid red"></div>
-</body>
-
-<script>
-</script>
-</html>
\ No newline at end of file
diff --git a/examples/sendE2E/receiver-favicon.ico b/examples/sendE2E/receiver-favicon.ico
deleted file mode 100644
index 3a7b5826dfdad175084ea63406202334dfe29bf4..0000000000000000000000000000000000000000
Binary files a/examples/sendE2E/receiver-favicon.ico and /dev/null differ
diff --git a/examples/sendE2E/sender-favicon.ico b/examples/sendE2E/sender-favicon.ico
deleted file mode 100644
index 149d6cb2fdb5e20b4a4f80a5797878f73e14d25e..0000000000000000000000000000000000000000
Binary files a/examples/sendE2E/sender-favicon.ico and /dev/null differ
diff --git a/examples/sendE2E/xxdk.js b/examples/sendE2E/xxdk.js
deleted file mode 100644
index 533eead2b67038e3fbc4d16c13cdca758a3da86f..0000000000000000000000000000000000000000
--- a/examples/sendE2E/xxdk.js
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Copyright © 2020 xx network SEZC                                           ///
- *                                                                            ///
- * Use of this source code is governed by a license that can be found in the  ///
- * LICENSE file                                                               ///
- */
-
-async function SendE2e(ndf, recipientContactFile, myContactFileName, statePath, statePassString) {
-    let enc = new TextEncoder();
-    let dec = new TextDecoder();
-
-    const output = document.getElementById("output")
-    const statePass = enc.encode(statePassString);
-
-    // Check if state exists
-    if (localStorage.getItem(statePath) === null) {
-        console.log('getting key ' + statePath + ' returned null; making new cmix');
-
-        output.innerHTML += "Loading new storage\n"
-
-        // Initialize the state
-        NewCmix(ndf, statePath, statePass, '');
-    }
-
-
-    ////////////////////////////////////////////////////////////////////////////
-    // Login to your client session                                           //
-    ////////////////////////////////////////////////////////////////////////////
-
-    // Login with the same statePath and statePass used to call NewCmix
-    let netID = LoadCmix(statePath, statePass, GetDefaultCMixParams());
-    console.log("LoadCmix() " + netID)
-
-    console.log("sleep start")
-    await sleep(3000)
-    console.log("sleep end")
-
-    let net = GetLoadCmix(netID)
-    console.log("loaded cmix: " + net)
-
-    // Get reception identity (automatically created if one does not exist)
-    const identityStorageKey = "identityStorageKey";
-    let identity;
-    try {
-        identity = LoadReceptionIdentity(identityStorageKey, net.GetID());
-    } catch {
-        // If no extant xxdk.ReceptionIdentity, generate and store a new one
-        identity = net.MakeReceptionIdentity();
-
-        StoreReceptionIdentity(identityStorageKey, identity, net.GetID());
-    }
-
-    // Print contact to console. This should probably save a file.
-    const myContactFile = dec.decode(GetContactFromReceptionIdentity(identity))
-    console.log("my contact file content: " + myContactFile);
-
-    // Start file download.
-    download(myContactFileName, myContactFile);
-
-    let confirm = false;
-    let confirmContact;
-    let e2eClient;
-    let authCallbacks = {
-        Confirm: function (contact, receptionId, ephemeralId, roundId) {
-            confirm = true;
-            confirmContact = contact
-            console.log("Confirm:");
-            console.log("contact: " + dec.decode(contact));
-            console.log("receptionId: " + ephemeralId.toString());
-            console.log("ephemeralId: " + roundId.toString());
-
-            output.innerHTML += "Received confirmation from " + ephemeralId.toString() + "<br />"
-        },
-        Request: function (contact, receptionId, ephemeralId, roundId) {
-            console.log("Request:");
-            console.log("contact: " + dec.decode(contact));
-            console.log("receptionId: " + ephemeralId.toString());
-            console.log("ephemeralId: " + roundId.toString());
-
-            e2eClient.Confirm(contact)
-            output.innerHTML += "Received Request from " + ephemeralId.toString() + "<br />"
-        }
-    }
-
-    // Create an E2E client
-    // Pass in auth object which controls auth callbacks for this client
-    const params = GetDefaultE2EParams();
-    console.log("Using E2E parameters: " + dec.decode(params));
-    e2eClient = Login(net.GetID(), authCallbacks, identity, params);
-
-    e2eClient.DeleteAllRequests()
-
-
-    ////////////////////////////////////////////////////////////////////////////
-    // Start network threads                                                  //
-    ////////////////////////////////////////////////////////////////////////////
-
-    // Set networkFollowerTimeout to a value of your choice (seconds)
-    net.StartNetworkFollower(5000);
-
-    output.innerHTML += "Starting network follower<br />"
-
-    // Provide a callback that will be signalled when network health status changes
-    let health = false
-    net.AddHealthCallback({
-        Callback: function (healthy) {
-            health = healthy;
-        }
-    });
-    await sleep(3000)
-
-
-    const n = 100
-    for (let i = 0; (health === false) && (i < n); i++) {
-        await sleep(100)
-    }
-
-    if (health === false) {
-        console.error("Continuing with unhealthy network")
-        output.innerHTML += "Network NOT healthy<br />"
-    } else {
-        output.innerHTML += "Network healthy<br />"
-    }
-
-
-    ////////////////////////////////////////////////////////////////////////////
-    // Register a listener for messages                                       //
-    ////////////////////////////////////////////////////////////////////////////
-
-    let listener = {
-        Hear: function (item) {
-            console.log("Listener heard: " + dec.decode(item));
-            output.innerHTML += "Listener heard: " + dec.decode(item) + "<br />"
-        },
-        Name: function () {
-            return "Listener";
-        }
-    }
-
-    // Listen for all types of messages using catalog.NoType
-    // Listen for messages from all users using id.ZeroUser
-    let zerUser = Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3]);
-    e2eClient.RegisterListener(zerUser, 0, listener);
-
-    output.innerHTML += "Registered listener<br />"
-
-    ////////////////////////////////////////////////////////////////////////////
-    // Connect with the recipient                                             //
-    ////////////////////////////////////////////////////////////////////////////
-
-    // Check that the partner exists
-    if (recipientContactFile !== '') {
-        let exists = false;
-        output.innerHTML += "getting ID from contact<br />"
-        const recipientContactID = GetIDFromContact(recipientContactFile);
-        console.log(typeof recipientContactID)
-        console.log("recipientContactID: " + recipientContactID)
-
-        output.innerHTML += "Checking for " + recipientContactID + "<br />"
-
-
-        const partnerIDS = dec.decode(e2eClient.GetAllPartnerIDs())
-        console.log("partnerIDS: " + partnerIDS)
-        let partners = JSON.parse(partnerIDS);
-
-        for (let i = 0; i < partners.length; i++) {
-            const partnerBytes = base64ToArrayBuffer(partners[i])
-            console.log("partner " + recipientContactID + " == " + i + " " + partnerBytes)
-
-            if (partnerBytes.toString() === recipientContactID.toString()) {
-                console.log("MATCH! partner " + recipientContactID + " matches partner " + i + " " + partnerBytes)
-                exists = true;
-                break
-            }
-        }
-
-        // If the partner does not exist, send a request
-        if (exists === false) {
-            output.innerHTML += "Partner does not exist, Request sent to " + recipientContactID + "<br />"
-            const factList = enc.encode('[]')
-            e2eClient.Request(enc.encode(recipientContactFile), factList)
-
-            for (let i = 0; (i < 600) && (confirm === false); i++) {
-                await sleep(50)
-            }
-            if (confirm === false) {
-                output.innerHTML += "Checking for " + recipientContactIDBase64 + "<br />"
-                console.error(new Error("timed out waiting for confirmation"))
-            }
-
-            const confirmContactID = GetIDFromContact(confirmContact)
-            if (recipientContactID.toString() !== confirmContactID.toString()) {
-                throw new Error("contact ID from confirmation " +
-                    btoa(dec.decode(confirmContactID)) +
-                    " does not match recipient ID " +
-                    recipientContactIDBase64)
-            }
-        } else {
-            output.innerHTML += "Partner exists<br />"
-        }
-
-        ////////////////////////////////////////////////////////////////////////////
-        // Send a message to the recipient                                        //
-        ////////////////////////////////////////////////////////////////////////////
-
-        // Test message
-        const msgBody = "If this message is sent successfully, we'll have established contact with the recipient."
-
-        output.innerHTML += "Sending E2E message<br />"
-        // const paramsObj = JSON.parse(dec.decode(params))
-        // const e2eParams = JSON.stringify(paramsObj.Base)
-        // console.log("e2eParams: " + e2eParams)
-        const e2eSendReport = e2eClient.SendE2E(2, recipientContactID, enc.encode(msgBody), params)
-
-        console.log("e2e send report: " + dec.decode(e2eSendReport))
-        output.innerHTML += "Send e2e: " + dec.decode(e2eSendReport) + "<br />"
-    } else {
-        output.innerHTML += "Partner does not exist<br />"
-    }
-}
-
-function sleep(ms) {
-    return new Promise(resolve => setTimeout(resolve, ms));
-}
-
-function download(filename, text) {
-    let element = document.createElement('a');
-    element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
-    element.setAttribute('download', filename);
-
-    element.style.display = 'none';
-    document.body.appendChild(element);
-
-    element.click();
-
-    document.body.removeChild(element);
-}
-
-function base64ToArrayBuffer(base64) {
-    const binary_string = window.atob(base64);
-    const len = binary_string.length;
-    const bytes = new Uint8Array(len);
-    for (let i = 0; i < len; i++) {
-        bytes[i] = binary_string.charCodeAt(i);
-    }
-    return bytes;
-}
\ No newline at end of file
diff --git a/examples/styles.css b/examples/styles.css
deleted file mode 100644
index 802a6a38cc0fcaab7f49dc0312a05fe63f8c11c5..0000000000000000000000000000000000000000
--- a/examples/styles.css
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright © 2020 xx network SEZC                                           ///
- *                                                                            ///
- * Use of this source code is governed by a license that can be found in the  ///
- * LICENSE file                                                               ///
- */
-
-/******************************************************************************
- * Copyright © 2022 xx foundation                                             *
- *                                                                            *
- * Use of this source code is governed by a license that can be found in the  *
- * LICENSE file.                                                              *
- ******************************************************************************/
-
-/******************************************************************************
- * CSS Reset (meyerweb reset, v2.0 | 20110126)                                *
- ******************************************************************************/
-
-html, body, div, span, applet, object, iframe,
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-b, u, i, center,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td,
-article, aside, canvas, details, embed,
-figure, figcaption, footer, header, hgroup,
-menu, nav, output, ruby, section, summary,
-time, mark, audio, video {
-    margin: 0;
-    padding: 0;
-    border: 0;
-    font-size: 100%;
-    font: inherit;
-    vertical-align: baseline;
-}
-
-/* HTML5 display-role reset for older browsers */
-article, aside, details, figcaption, figure,
-footer, header, hgroup, menu, nav, section {
-    display: block;
-}
-
-body {
-    line-height: 1;
-}
-
-ol, ul {
-    list-style: none;
-}
-
-blockquote, q {
-    quotes: none;
-}
-
-blockquote:before, blockquote:after,
-q:before, q:after {
-    content: '';
-    content: none;
-}
-
-table {
-    border-collapse: collapse;
-    border-spacing: 0;
-}
-
-/******************************************************************************
- * Global Styles                                                              *
- ******************************************************************************/
-body {
-    font-family: "Roboto", "Franklin Gothic Medium", Tahoma, sans-serif;
-}
-
-form {
-    margin:1em;
-}
-
-form div {
-    margin:1em 0;
-}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 388459f1e8332175bfa5641f56586c53d3c8f3ad..b9605aa9bf8b506f1b1906a9a9be0f5e6083446f 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module gitlab.com/elixxir/xxdk-wasm
 go 1.17
 
 require (
+	github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e
 	github.com/hack-pad/go-indexeddb v0.2.0
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/jwalterweatherman v1.1.0
diff --git a/go.sum b/go.sum
index dddf6b946728bf99842ac76a218ef90a850f9983..149c6f8950f60d2b0f214c89460173b5bf01ff1a 100644
--- a/go.sum
+++ b/go.sum
@@ -75,6 +75,7 @@ github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgp
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
diff --git a/main.go b/main.go
index a3c8678bd5054ffec7fcb10c725b710b5f8121ce..45c1bc9439a40937500b611a320efa49d292197a 100644
--- a/main.go
+++ b/main.go
@@ -11,6 +11,7 @@ package main
 
 import (
 	"fmt"
+	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/bindings"
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/elixxir/xxdk-wasm/wasm"
@@ -18,6 +19,14 @@ import (
 	"syscall/js"
 )
 
+func init() {
+	// Overwrites setting the log level to INFO done in bindings so that the
+	// Javascript console can be used
+	ll := wasm.NewJsConsoleLogListener(jww.LevelInfo)
+	jww.SetLogListeners(ll.Listen)
+	jww.SetStdoutThreshold(jww.LevelFatal + 1)
+}
+
 func main() {
 	fmt.Println("Starting xxDK WebAssembly bindings.")
 	fmt.Printf("Client version %s\n", bindings.GetVersion())
@@ -35,6 +44,9 @@ func main() {
 	// wasm/broadcast.go
 	js.Global().Set("NewBroadcastChannel", js.FuncOf(wasm.NewBroadcastChannel))
 
+	// wasm/channels.go
+	js.Global().Set("NewChannelsManager", js.FuncOf(wasm.NewChannelsManager))
+
 	// wasm/cmix.go
 	js.Global().Set("NewCmix", js.FuncOf(wasm.NewCmix))
 	js.Global().Set("LoadCmix", js.FuncOf(wasm.LoadCmix))
@@ -77,6 +89,7 @@ func main() {
 
 	// wasm/logging.go
 	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
+	js.Global().Set("LogToFile", js.FuncOf(wasm.LogToFile))
 	js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter))
 	js.Global().Set("EnableGrpcLogs", js.FuncOf(wasm.EnableGrpcLogs))
 
diff --git a/test/assets/index.html b/test/assets/index.html
deleted file mode 100644
index 9a38b9c590bd683d29c7b538b78138b35530572e..0000000000000000000000000000000000000000
--- a/test/assets/index.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-	<meta charset="UTF-8">
-	<title>xxDk WebAssembly Test</title>
-
-	<script src="wasm_exec.js"></script>
-	<script>
-        const go = new Go();
-        WebAssembly.instantiateStreaming(fetch("xxdk.wasm"), go.importObject).then((result) => {
-            go.run(result.instance);
-        });
-	</script>
-</head>
-<body></body>
-</html>
\ No newline at end of file
diff --git a/test/assets/wasm_exec.js b/test/assets/wasm_exec.js
deleted file mode 100644
index c613dfc656f2799b6b3c922f59003cd3347454a7..0000000000000000000000000000000000000000
--- a/test/assets/wasm_exec.js
+++ /dev/null
@@ -1,643 +0,0 @@
-// 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.
-
-(() => {
-	// Map multiple JavaScript environments to a single common API,
-	// preferring web standards over Node.js API.
-	//
-	// Environments considered:
-	// - Browsers
-	// - Node.js
-	// - Electron
-	// - Parcel
-	// - Webpack
-
-	if (typeof global !== "undefined") {
-		// global already exists
-	} else if (typeof window !== "undefined") {
-		window.global = window;
-	} else if (typeof self !== "undefined") {
-		self.global = self;
-	} else {
-		throw new Error("cannot export Go (neither global, window nor self is defined)");
-	}
-
-	if (!global.require && typeof require !== "undefined") {
-		global.require = require;
-	}
-
-	if (!global.fs && global.require) {
-		const fs = require("fs");
-		if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) {
-			global.fs = fs;
-		}
-	}
-
-	const enosys = () => {
-		const err = new Error("not implemented");
-		err.code = "ENOSYS";
-		return err;
-	};
-
-	if (!global.fs) {
-		let outputBuf = "";
-		global.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 (!global.process) {
-		global.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 (!global.crypto && global.require) {
-		const nodeCrypto = require("crypto");
-		global.crypto = {
-			getRandomValues(b) {
-				nodeCrypto.randomFillSync(b);
-			},
-		};
-	}
-	if (!global.crypto) {
-		throw new Error("global.crypto is not available, polyfill required (getRandomValues only)");
-	}
-
-	if (!global.performance) {
-		global.performance = {
-			now() {
-				const [sec, nsec] = process.hrtime();
-				return sec * 1000 + nsec / 1000000;
-			},
-		};
-	}
-
-	if (!global.TextEncoder && global.require) {
-		global.TextEncoder = require("util").TextEncoder;
-	}
-	if (!global.TextEncoder) {
-		throw new Error("global.TextEncoder is not available, polyfill required");
-	}
-
-	if (!global.TextDecoder && global.require) {
-		global.TextDecoder = require("util").TextDecoder;
-	}
-	if (!global.TextDecoder) {
-		throw new Error("global.TextDecoder is not available, polyfill required");
-	}
-
-	// End of polyfills for common API.
-
-	const encoder = new TextEncoder("utf-8");
-	const decoder = new TextDecoder("utf-8");
-
-	global.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/xxdk-wasm/utils.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,
-				global,
-				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],
-				[global, 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;
-			};
-		}
-	}
-
-	if (
-		typeof module !== "undefined" &&
-		global.require &&
-		global.require.main === module &&
-		global.process &&
-		global.process.versions &&
-		!global.process.versions.electron
-	) {
-		if (process.argv.length < 3) {
-			console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
-			process.exit(1);
-		}
-
-		const go = new Go();
-		go.argv = process.argv.slice(2);
-		go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
-		go.exit = process.exit;
-		WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
-			process.on("exit", (code) => { // Node.js exits if no event handler is pending
-				if (code === 0 && !go.exited) {
-					// deadlock, make Go print error and stack traces
-					go._pendingEvent = { id: 0 };
-					go._resume();
-				}
-			});
-			return go.run(result.instance);
-		}).catch((err) => {
-			console.error(err);
-			process.exit(1);
-		});
-	}
-})();
diff --git a/test/index.html b/test/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..0769a535b3dcb212d86f9c9e5a57fb8fc3eb5c21
--- /dev/null
+++ b/test/index.html
@@ -0,0 +1,23 @@
+<!--////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+/////////////////////////////////////////////////////////////////////////////-->
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="UTF-8">
+	<title>xxDk WebAssembly Test</title>
+
+	<script src="../wasm_exec.js"></script>
+	<script>
+        const go = new Go();
+        WebAssembly.instantiateStreaming(fetch("xxdk.wasm"), go.importObject).then((result) => {
+            go.run(result.instance);
+        });
+	</script>
+</head>
+<body></body>
+</html>
\ No newline at end of file
diff --git a/test/server/main.go b/test/main.go
similarity index 86%
rename from test/server/main.go
rename to test/main.go
index 9172a51e151c12002f4349ea9896f6b21d19726f..c637bd458a3f2ea833dc7050e8b0779ffd1bd1ca 100644
--- a/test/server/main.go
+++ b/test/main.go
@@ -14,12 +14,12 @@ import (
 
 func main() {
 	port := "9090"
-	root := "../assets"
+	root := ""
 	fmt.Printf("Starting server on port %s from %s\n", port, root)
+	fmt.Printf("\thttp://localhost:%s\n", port)
 
 	err := http.ListenAndServe(":"+port, http.FileServer(http.Dir(root)))
 	if err != nil {
-		fmt.Println("Failed to start server", err)
-		return
+		panic(fmt.Sprintf("Failed to start server: %+v", err))
 	}
 }
diff --git a/utils/array.go b/utils/array.go
index ec8b5d202e0b53f47f42e4e640ff82845874fe21..9a1266960e689210f5a32de95800aa9f85e070fd 100644
--- a/utils/array.go
+++ b/utils/array.go
@@ -33,12 +33,23 @@ func Uint8ArrayToBase64(_ js.Value, args []js.Value) interface{} {
 //  - Decoded uint8 array (Uint8Array).
 //  - Throws TypeError if decoding the string fails.
 func Base64ToUint8Array(_ js.Value, args []js.Value) interface{} {
-	b, err := base64.StdEncoding.DecodeString(args[0].String())
+	b, err := base64ToUint8Array(args[0])
 	if err != nil {
 		Throw(TypeError, err)
 	}
 
-	return CopyBytesToJS(b)
+	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
diff --git a/utils/array_test.go b/utils/array_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a420dc844bd3acb13031ecfd6cb0da29814dafa2
--- /dev/null
+++ b/utils/array_test.go
@@ -0,0 +1,112 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+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 recevie 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
index aad9d8a153845f18f8b6590559e3870cfbbf14ff..589979abb11553523cbde7df89b0d9691f03721c 100644
--- a/utils/convert.go
+++ b/utils/convert.go
@@ -28,18 +28,18 @@ func CopyBytesToJS(src []byte) js.Value {
 	return dst
 }
 
-// JsonToJS is a helper that converts JSON bytes input
-// to a [js.Value] of the object subtype.
+// JsToJson converts the Javascript value to JSON.
+func JsToJson(value js.Value) string {
+	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) {
-	jsObj := make(map[string]interface{})
+	var jsObj map[string]interface{}
 	err := json.Unmarshal(inputJson, &jsObj)
 	if err != nil {
-		return js.Value{}, err
+		return js.ValueOf(nil), err
 	}
-	return js.ValueOf(jsObj), nil
-}
 
-// JsToJson converts the Javascript value to JSON.
-func JsToJson(value js.Value) string {
-	return JSON.Call("stringify", value).String()
+	return js.ValueOf(jsObj), nil
 }
diff --git a/utils/convert_test.go b/utils/convert_test.go
index 78d50a7090ce3aec02edf703463f1057bc729574..7a9e1fb6e3ab2f3b8dec72ce4b6e964d936a03a0 100644
--- a/utils/convert_test.go
+++ b/utils/convert_test.go
@@ -8,93 +8,218 @@
 package utils
 
 import (
-	"reflect"
+	"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) {
-	type args struct {
-		src js.Value
-	}
-	tests := []struct {
-		name string
-		args args
-		want []byte
-	}{
-		// TODO: Add test cases.
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := CopyBytesToGo(tt.args.src); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("CopyBytesToGo() = %v, want %v", got, tt.want)
-			}
-		})
+	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 recevie 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) {
-	type args struct {
-		src []byte
-	}
-	tests := []struct {
-		name string
-		args args
-		want js.Value
-	}{
-		// TODO: Add test cases.
+	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 recevie expected string representation of "+
+				"the Uint8Array (%d).\nexpected: %s\nreceived: %s",
+				i, expected, jsString)
+		}
 	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := CopyBytesToJS(tt.args.src); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("CopyBytesToJS() = %v, want %v", got, tt.want)
-			}
-		})
+}
+
+// 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 recevie 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) {
-	type args struct {
-		value js.Value
+	testObj := map[string]interface{}{
+		"nil":    nil,
+		"bool":   true,
+		"int":    1,
+		"float":  1.5,
+		"string": "I am string",
+		"array":  []interface{}{1, 2, 3},
+		"object": map[string]interface{}{"int": 5},
 	}
-	tests := []struct {
-		name string
-		args args
-		want string
-	}{
-		// TODO: Add test cases.
+
+	expected, err := json.Marshal(testObj)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
 	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := JsToJson(tt.args.value); got != tt.want {
-				t.Errorf("JsToJson() = %v, want %v", got, tt.want)
-			}
-		})
+
+	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("Recieved incorrect JSON from 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) {
-	type args struct {
-		src []byte
+	testObj := map[string]interface{}{
+		"nil":    nil,
+		"bool":   true,
+		"int":    1,
+		"float":  1.5,
+		"string": "I am string",
+		"bytes":  []byte{1, 2, 3},
+		"array":  []interface{}{1, 2, 3},
+		"object": map[string]interface{}{"int": 5},
+	}
+	jsonData, err := json.Marshal(testObj)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
 	}
-	tests := []struct {
-		name    string
-		args    args
-		want    js.Value
-		wantErr bool
-	}{
-		// TODO: Add test cases.
+
+	jsObj, err := JsonToJS(jsonData)
+	if err != nil {
+		t.Errorf("Failed to convert JSON to Javascript object: %+v", err)
 	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := JsonToJS(tt.args.src)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("JsonToJS() error = %v, wantErr %v", err, tt.wantErr)
-				return
+
+	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.([]interface{}) {
+				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())
+				}
 			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("JsonToJS() got = %v, want %v", got, tt.want)
+		case "object":
+			if jsVal.Get("int").Int() != val.(map[string]interface{})["int"] {
+				t.Errorf("Incorrect value for key %s."+
+					"\nexpected: %d\nreceived: %d", key,
+					val.(map[string]interface{})["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]interface{}{
+		"nil":    nil,
+		"bool":   true,
+		"int":    1,
+		"float":  1.5,
+		"string": "I am string",
+		"bytes":  []byte{1, 2, 3},
+		"array":  []interface{}{1, 2, 3},
+		"object": map[string]interface{}{"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_js.s b/utils/utils_js.s
index de7b1f139cf6f58bed1bd5d16a16d413a34ffb9d..45c1668a272247a134e6c85508bf5c09f0d7b3f0 100644
--- a/utils/utils_js.s
+++ b/utils/utils_js.s
@@ -1,6 +1,6 @@
-#include "textflag.h"
-
-// Throw enables throwing of Javascript exceptions.
-TEXT ·throw(SB), NOSPLIT, $0
-  CallImport
-  RET
+#include "textflag.h"
+
+// Throw enables throwing of Javascript exceptions.
+TEXT ·throw(SB), NOSPLIT, $0
+  CallImport
+  RET
diff --git a/wasm/authenticatedConnection.go b/wasm/authenticatedConnection.go
index d462797ac75f329c6efdcc4b0964215aa9139d7e..ea2fffd45760c474ce053f015c6b596360422f8b 100644
--- a/wasm/authenticatedConnection.go
+++ b/wasm/authenticatedConnection.go
@@ -27,16 +27,103 @@ func newAuthenticatedConnectionJS(
 	api *bindings.AuthenticatedConnection) map[string]interface{} {
 	ac := AuthenticatedConnection{api}
 	acMap := map[string]interface{}{
-		"IsAuthenticated": js.FuncOf(ac.IsAuthenticated),
+		"IsAuthenticated":  js.FuncOf(ac.IsAuthenticated),
+		"GetId":            js.FuncOf(ac.GetId),
+		"SendE2E":          js.FuncOf(ac.SendE2E),
+		"Close":            js.FuncOf(ac.Close),
+		"GetPartner":       js.FuncOf(ac.GetPartner),
+		"RegisterListener": js.FuncOf(ac.RegisterListener),
 	}
 
 	return acMap
 }
 
+// IsAuthenticated returns true.
+//
+// Returns:
+//  - true (boolean).
 func (ac *AuthenticatedConnection) IsAuthenticated(js.Value, []js.Value) interface{} {
 	return ac.api.IsAuthenticated()
 }
 
+// GetId returns the ID for this [bindings.AuthenticatedConnection] in the
+// authenticatedConnectionTracker.
+//
+// Returns:
+//  - int of the ID
+func (ac *AuthenticatedConnection) GetId(js.Value, []js.Value) interface{} {
+	return ac.api.GetId()
+}
+
+// SendE2E is a wrapper for sending specifically to the
+// AuthenticatedConnection's [partner.Manager].
+//
+// Returns:
+//  - []byte - the JSON marshalled bytes of the E2ESendReport object, which can
+//    be passed into WaitForRoundResult to see if the send succeeded.
+//
+// Parameters:
+//  - args[0] - message type from [catalog.MessageType] (int)
+//  - args[1] - message payload (Uint8Array)
+//
+// Returns a promise:
+//  - Resolves to the JSON of [bindings.E2ESendReport], which can be passed into
+//    cmix.WaitForRoundResult to see if the send succeeded (Uint8Array).
+//  - Rejected with an error if sending fails.
+func (ac *AuthenticatedConnection) SendE2E(_ js.Value, args []js.Value) interface{} {
+	payload := utils.CopyBytesToGo(args[2])
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := ac.api.SendE2E(args[0].Int(), payload)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// Close deletes this AuthenticatedConnection's partner.Manager and releases
+// resources.
+//
+// Returns:
+//  - throws a TypeError if closing fails
+func (ac *AuthenticatedConnection) Close(js.Value, []js.Value) interface{} {
+	return ac.api.Close()
+}
+
+// GetPartner returns the partner.Manager for this AuthenticatedConnection.
+//
+// Returns:
+//  - bytes of the partner's [id.ID] (Uint8Array)
+func (ac *AuthenticatedConnection) GetPartner(js.Value, []js.Value) interface{} {
+	return utils.CopyBytesToJS(ac.api.GetPartner())
+}
+
+// RegisterListener is used for E2E reception and allows for reading data sent
+// from the partner.Manager.
+//
+// Parameters:
+//  - args[0] - message type from [catalog.MessageType] (int)
+//  - args[1] - Javascript object that has functions that implement the
+//    [bindings.Listener] interface
+//
+// Returns:
+//  - throws a TypeError is registering the listener fails
+func (ac *AuthenticatedConnection) RegisterListener(
+	_ js.Value, args []js.Value) interface{} {
+	err := ac.api.RegisterListener(args[0].Int(),
+		&listener{utils.WrapCB(args[1], "Hear"), utils.WrapCB(args[1], "Name")})
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nil
+}
+
 // ConnectWithAuthentication is called by the client (i.e., the one establishing
 // connection with the server). Once a connect.Connection has been established
 // with the server, it then authenticates their identity to the server.
@@ -46,19 +133,22 @@ func (ac *AuthenticatedConnection) IsAuthenticated(js.Value, []js.Value) interfa
 //  - args[1] - marshalled recipient [contact.Contact] (Uint8Array).
 //  - args[3] - JSON of [xxdk.E2EParams] (Uint8Array).
 //
-// Returns:
-//  - Javascript representation of the Connection object
-//  - throws a TypeError if creating loading the parameters or connecting fails
+// Returns a promise:
+//  - Resolves to a Javascript representation of the Connection object.
+//  - Rejected with an error if loading the parameters or connecting fails.
 func (c *Cmix) ConnectWithAuthentication(_ js.Value, args []js.Value) interface{} {
 	recipientContact := utils.CopyBytesToGo(args[1])
 	e2eParamsJSON := utils.CopyBytesToGo(args[2])
 
-	ac, err := c.api.ConnectWithAuthentication(
-		args[0].Int(), recipientContact, e2eParamsJSON)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		ac, err := c.api.ConnectWithAuthentication(
+			args[0].Int(), recipientContact, e2eParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(newAuthenticatedConnectionJS(ac))
+		}
 	}
 
-	return newAuthenticatedConnectionJS(ac)
+	return utils.CreatePromise(promiseFn)
 }
diff --git a/wasm/authenticatedConnection_test.go b/wasm/authenticatedConnection_test.go
index b618514b79779d1d59c87ce46727c9ebf692792e..a50eaf12146438699f6da3a85b0f76b6c051026c 100644
--- a/wasm/authenticatedConnection_test.go
+++ b/wasm/authenticatedConnection_test.go
@@ -35,3 +35,24 @@ func Test_newAuthenticatedConnectionJS(t *testing.T) {
 		}
 	}
 }
+
+// Tests that AuthenticatedConnection has all the methods that
+// [bindings.AuthenticatedConnection] has.
+func Test_AuthenticatedConnectionMethods(t *testing.T) {
+	authType := reflect.TypeOf(&AuthenticatedConnection{})
+	binAuthType := reflect.TypeOf(&bindings.AuthenticatedConnection{})
+
+	if binAuthType.NumMethod() != authType.NumMethod() {
+		t.Errorf("WASM AuthenticatedConnection object does not have all "+
+			"methods from bindings.\nexpected: %d\nreceived: %d",
+			binAuthType.NumMethod(), authType.NumMethod())
+	}
+
+	for i := 0; i < binAuthType.NumMethod(); i++ {
+		method := binAuthType.Method(i)
+
+		if _, exists := authType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/backup_test.go b/wasm/backup_test.go
index 4782f0e1f92c4e4e3bb121a9af95162c129c80bb..8946fc6aefaf2fe15a7fc452e1559194ee41502c 100644
--- a/wasm/backup_test.go
+++ b/wasm/backup_test.go
@@ -32,3 +32,23 @@ func Test_newBackupJS(t *testing.T) {
 		}
 	}
 }
+
+// Tests that Backup has all the methods that [bindings.Backup] has.
+func Test_BackupMethods(t *testing.T) {
+	backupType := reflect.TypeOf(&Backup{})
+	binBackupType := reflect.TypeOf(&bindings.Backup{})
+
+	if binBackupType.NumMethod() != backupType.NumMethod() {
+		t.Errorf("WASM Backup object does not have all methods from bindings."+
+			"\nexpected: %d\nreceived: %d",
+			binBackupType.NumMethod(), backupType.NumMethod())
+	}
+
+	for i := 0; i < binBackupType.NumMethod(); i++ {
+		method := binBackupType.Method(i)
+
+		if _, exists := backupType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/broadcast.go b/wasm/broadcast.go
index 456027b4c63828ac80578ebecc5eea62dea9b7cf..9c14cec1414549eae1fee68872f68f34fa920e80 100644
--- a/wasm/broadcast.go
+++ b/wasm/broadcast.go
@@ -67,7 +67,7 @@ type broadcastListener struct {
 }
 
 func (bl *broadcastListener) Callback(payload []byte, err error) {
-	bl.callback(utils.CopyBytesToJS(payload), err.Error())
+	bl.callback(utils.CopyBytesToJS(payload), utils.JsTrace(err))
 }
 
 // Listen registers a BroadcastListener for a given method. This allows users to
@@ -98,18 +98,24 @@ func (c *Channel) Listen(_ js.Value, args []js.Value) interface{} {
 // Parameters:
 //  - args[0] - payload (Uint8Array).
 //
-// Returns:
-//  - JSON of [bindings.BroadcastReport], which can be passed into
-//    Cmix.WaitForRoundResult to see if the broadcast succeeded (Uint8Array).
-//  - Throws a TypeError if broadcasting fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.BroadcastReport], which can be
+//    passed into Cmix.WaitForRoundResult to see if the send succeeded
+//    (Uint8Array).
+//  - Rejected with an error if broadcasting fails.
 func (c *Channel) Broadcast(_ js.Value, args []js.Value) interface{} {
-	report, err := c.api.Broadcast(utils.CopyBytesToGo(args[0]))
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	payload := utils.CopyBytesToGo(args[0])
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := c.api.Broadcast(payload)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
 	}
 
-	return utils.CopyBytesToJS(report)
+	return utils.CreatePromise(promiseFn)
 }
 
 // BroadcastAsymmetric sends a given payload over the broadcast channel using
@@ -119,19 +125,25 @@ func (c *Channel) Broadcast(_ js.Value, args []js.Value) interface{} {
 //  - args[0] - payload (Uint8Array).
 //  - args[1] - private key (Uint8Array).
 //
-// Returns:
-//  - JSON of [bindings.BroadcastReport], which can be passed into
-//    Cmix.WaitForRoundResult to see if the broadcast succeeded (Uint8Array).
-//  - Throws a TypeError if broadcasting fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.BroadcastReport], which can be
+//    passed into Cmix.WaitForRoundResult to see if the send succeeded
+//    (Uint8Array).
+//  - Rejected with an error if broadcasting fails.
 func (c *Channel) BroadcastAsymmetric(_ js.Value, args []js.Value) interface{} {
-	report, err := c.api.BroadcastAsymmetric(
-		utils.CopyBytesToGo(args[0]), utils.CopyBytesToGo(args[1]))
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	payload := utils.CopyBytesToGo(args[0])
+	privateKey := utils.CopyBytesToGo(args[1])
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := c.api.BroadcastAsymmetric(payload, privateKey)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
 	}
 
-	return utils.CopyBytesToJS(report)
+	return utils.CreatePromise(promiseFn)
 }
 
 // MaxPayloadSize returns the maximum possible payload size which can be
diff --git a/wasm/broadcast_test.go b/wasm/broadcast_test.go
index 4dc4688355f65e2044c4c3d758375c73863b6214..3541c5c09084b29e37c7ef37328969dc27b03d1b 100644
--- a/wasm/broadcast_test.go
+++ b/wasm/broadcast_test.go
@@ -34,3 +34,23 @@ func Test_newChannelJS(t *testing.T) {
 		}
 	}
 }
+
+// Tests that Channel has all the methods that [bindings.Channel] has.
+func Test_ChannelMethods(t *testing.T) {
+	chanType := reflect.TypeOf(&Channel{})
+	binChanType := reflect.TypeOf(&bindings.Channel{})
+
+	if binChanType.NumMethod() != chanType.NumMethod() {
+		t.Errorf("WASM Channel object does not have all methods from bindings."+
+			"\nexpected: %d\nreceived: %d",
+			binChanType.NumMethod(), chanType.NumMethod())
+	}
+
+	for i := 0; i < binChanType.NumMethod(); i++ {
+		method := binChanType.Method(i)
+
+		if _, exists := chanType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/channels.go b/wasm/channels.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca2682d13c23bcb966b70524a0c7be7dcd24f721
--- /dev/null
+++ b/wasm/channels.go
@@ -0,0 +1,475 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package wasm
+
+import (
+	"gitlab.com/elixxir/client/bindings"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"syscall/js"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// Basic Channel API                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// ChannelsManager wraps the [bindings.ChannelsManager] object so its methods
+// can be wrapped to be Javascript compatible.
+type ChannelsManager struct {
+	api *bindings.ChannelsManager
+}
+
+// newChannelsManagerJS creates a new Javascript compatible object
+// (map[string]interface{}) that matches the ChannelsManager structure.
+func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]interface{} {
+	cm := ChannelsManager{api}
+	channelsManagerMap := map[string]interface{}{
+		// Basic Channel API
+		"GetID":         js.FuncOf(cm.GetID),
+		"JoinChannel":   js.FuncOf(cm.JoinChannel),
+		"GetChannels":   js.FuncOf(cm.GetChannels),
+		"GetChannelId":  js.FuncOf(cm.GetChannelId),
+		"GetChannel":    js.FuncOf(cm.GetChannel),
+		"LeaveChannel":  js.FuncOf(cm.LeaveChannel),
+		"ReplayChannel": js.FuncOf(cm.ReplayChannel),
+
+		// Channel Sending Methods and Reports
+		"SendGeneric":      js.FuncOf(cm.SendGeneric),
+		"SendAdminGeneric": js.FuncOf(cm.SendAdminGeneric),
+		"SendMessage":      js.FuncOf(cm.SendMessage),
+		"SendReply":        js.FuncOf(cm.SendReply),
+		"SendReaction":     js.FuncOf(cm.SendReaction),
+
+		// Channel Receiving Logic and Callback Registration
+		"RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler),
+	}
+
+	return channelsManagerMap
+}
+
+// GetID returns the ID for this ChannelsManager in the ChannelsManager tracker.
+//
+// Returns:
+//  - int
+func (ch *ChannelsManager) GetID(js.Value, []js.Value) interface{} {
+	return ch.api.GetID()
+}
+
+// NewChannelsManager constructs a ChannelsManager.
+// FIXME: This is a work in progress and should not be used an event model is
+//  implemented in the style of the bindings layer's AuthCallbacks. Remove this
+//  note when that has been done.
+//
+// Parameters:
+//  - args[0] - ID of ChannelsManager object in tracker (int). This can be
+//    retrieved using [E2e.GetID].
+//  - args[1] - ID of UserDiscovery object in tracker (int). This can be
+//    retrieved using [UserDiscovery.GetID].
+//
+// Returns:
+//  - Javascript representation of the ChannelsManager object.
+//  - Throws a TypeError if logging in fails.
+func NewChannelsManager(_ js.Value, args []js.Value) interface{} {
+	cm, err := bindings.NewChannelsManager(args[0].Int(), args[1].Int())
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return newChannelsManagerJS(cm)
+}
+
+// JoinChannel joins the given channel. It will fail if the channel has already
+// been joined.
+//
+// Parameters:
+//  - args[0] - JSON of [bindings.ChannelDef] (Uint8Array).
+//
+// Returns:
+//  - Throws a TypeError if joining the channel fails.
+func (ch *ChannelsManager) JoinChannel(_ js.Value, args []js.Value) interface{} {
+	channelJson := utils.CopyBytesToGo(args[0])
+
+	err := ch.api.JoinChannel(channelJson)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nil
+}
+
+// GetChannels returns the IDs of all channels that have been joined.
+//
+// Returns:
+//  - JSON of an array of marshalled [id.ID] (Uint8Array).
+//  - Throws a TypeError if getting the channels fails.
+func (ch *ChannelsManager) GetChannels(js.Value, []js.Value) interface{} {
+	channelList, err := ch.api.GetChannels()
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(channelList)
+}
+
+// GetChannelId returns the ID of the channel given the channel's cryptographic
+// information.
+//
+// Parameters:
+//  - args[0] - JSON of [bindings.ChannelDef] (Uint8Array). This can be
+//    retrieved using [Channel.Get].
+//
+// Returns:
+//  - JSON of the channel [id.ID] (Uint8Array).
+//  - Throws a TypeError if getting the channel's ID fails.
+func (ch *ChannelsManager) GetChannelId(_ js.Value, args []js.Value) interface{} {
+	channelJson := utils.CopyBytesToGo(args[0])
+
+	chanID, err := ch.api.GetChannelId(channelJson)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(chanID)
+}
+
+// GetChannel returns the underlying cryptographic structure for a given
+// channel.
+//
+// Parameters:
+//  - args[0] - JSON of the channel [id.ID] (Uint8Array). This can be retrieved
+//    using [ChannelsManager.GetChannelId].
+//
+// Returns:
+//  - JSON of [bindings.ChannelDef] (Uint8Array).
+//  - Throws a TypeError if getting the channel fails.
+func (ch *ChannelsManager) GetChannel(_ js.Value, args []js.Value) interface{} {
+	marshalledChanId := utils.CopyBytesToGo(args[0])
+
+	def, err := ch.api.GetChannel(marshalledChanId)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(def)
+}
+
+// LeaveChannel leaves the given channel. It will return an error if the channel
+// was not previously joined.
+//
+// Parameters:
+//  - args[0] - JSON of the channel [id.ID] (Uint8Array). This can be retrieved
+//    using [ChannelsManager.GetChannelId].
+//
+// Returns:
+//  - Throws a TypeError if the channel does not exist.
+func (ch *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) interface{} {
+	marshalledChanId := utils.CopyBytesToGo(args[0])
+
+	err := ch.api.LeaveChannel(marshalledChanId)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nil
+}
+
+// ReplayChannel replays all messages from the channel within the network's
+// memory (~3 weeks) over the event model.
+//
+// Parameters:
+//  - args[0] - JSON of the channel [id.ID] (Uint8Array). This can be retrieved
+//    using [ChannelsManager.GetChannelId].
+//
+// Returns:
+//  - Throws a TypeError if the replay fails.
+func (ch *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) interface{} {
+	marshalledChanId := utils.CopyBytesToGo(args[0])
+
+	err := ch.api.ReplayChannel(marshalledChanId)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Channel Sending Methods and Reports                                        //
+////////////////////////////////////////////////////////////////////////////////
+
+// SendGeneric is used to send a raw message over a channel. In general, it
+// should be wrapped in a function which defines the wire protocol. If the final
+// message, before being sent over the wire, is too long, this will return an
+// error. Due to the underlying encoding using compression, it isn't possible to
+// define the largest payload that can be sent, but it will always be possible
+// to send a payload of 802 bytes at minimum. The meaning of validUntil depends
+// on the use case.
+//
+// Parameters:
+//  - args[0] - JSON of the channel [id.ID] (Uint8Array). This can be retrieved
+//    using [ChannelsManager.GetChannelId].
+//  - args[1] - The message type of the message. This will be a valid
+//    [channels.MessageType] (int).
+//  - args[2] - The contents of the message (Uint8Array).
+//  - args[3] - The lease of the message. This will be how long the message is
+//    valid until, in milliseconds. As per the [channels.Manager] documentation,
+//    this has different meanings depending on the use case. These use cases may
+//    be generic enough that they will not be enumerated here (int).
+//  - args[4] - JSON of [xxdk.CMIXParams]. If left empty
+//    [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//
+// Returns a promise:
+//  - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//  - Rejected with an error if sending fails.
+func (ch *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) interface{} {
+	marshalledChanId := utils.CopyBytesToGo(args[0])
+	messageType := args[1].Int()
+	message := utils.CopyBytesToGo(args[2])
+	leaseTimeMS := int64(args[3].Int())
+	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := ch.api.SendGeneric(
+			marshalledChanId, messageType, message, leaseTimeMS, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendAdminGeneric is used to send a raw message over a channel encrypted with
+// admin keys, identifying it as sent by the admin. In general, it should be
+// wrapped in a function that defines the wire protocol. If the final message,
+// before being sent over the wire, is too long, this will return an error. The
+// message must be at most 510 bytes long.
+//
+// Parameters:
+//  - args[0] - The PEM-encode admin RSA private key (Uint8Array).
+//  - args[1] - JSON of the channel [id.ID] (Uint8Array). This can be retrieved
+//    using [ChannelsManager.GetChannelId].
+//  - args[2] - The message type of the message. This will be a valid
+//    [channels.MessageType] (int).
+//  - args[3] - The contents of the message (Uint8Array).
+//  - args[4] - The lease of the message. This will be how long the message is
+//    valid until, in milliseconds. As per the [channels.Manager] documentation,
+//    this has different meanings depending on the use case. These use cases may
+//    be generic enough that they will not be enumerated here (int).
+//  - args[5] - JSON of [xxdk.CMIXParams]. If left empty
+//    [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//
+// Returns a promise:
+//  - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//  - Rejected with an error if sending fails.
+func (ch *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) interface{} {
+	adminPrivateKey := utils.CopyBytesToGo(args[0])
+	marshalledChanId := utils.CopyBytesToGo(args[1])
+	messageType := args[2].Int()
+	message := utils.CopyBytesToGo(args[3])
+	leaseTimeMS := int64(args[4].Int())
+	cmixParamsJSON := utils.CopyBytesToGo(args[5])
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := ch.api.SendAdminGeneric(adminPrivateKey,
+			marshalledChanId, messageType, message, leaseTimeMS, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendMessage is used to send a formatted message over a channel.
+// Due to the underlying encoding using compression, it isn't possible to define
+// the largest payload that can be sent, but it will always be possible to send
+// a payload of 798 bytes at minimum.
+//
+// The message will auto delete validUntil after the round it is sent in,
+// lasting forever if [channels.ValidForever] is used.
+//
+// Parameters:
+//  - args[0] - JSON of the channel [id.ID] (Uint8Array). This can be retrieved
+//    using [ChannelsManager.GetChannelId].
+//  - args[1] - The contents of the message (string).
+//  - args[2] - The lease of the message. This will be how long the message is
+//    valid until, in milliseconds. As per the [channels.Manager] documentation,
+//    this has different meanings depending on the use case. These use cases may
+//    be generic enough that they will not be enumerated here (int).
+//  - args[3] - JSON of [xxdk.CMIXParams]. If left empty
+//    [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//
+// Returns a promise:
+//  - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//  - Rejected with an error if sending fails.
+func (ch *ChannelsManager) SendMessage(_ js.Value, args []js.Value) interface{} {
+	marshalledChanId := utils.CopyBytesToGo(args[0])
+	message := args[1].String()
+	leaseTimeMS := int64(args[2].Int())
+	cmixParamsJSON := utils.CopyBytesToGo(args[3])
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := ch.api.SendMessage(
+			marshalledChanId, message, leaseTimeMS, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendReply is used to send a formatted message over a channel.
+// Due to the underlying encoding using compression, it isn't possible to define
+// the largest payload that can be sent, but it will always be possible to send
+// a payload of 766 bytes at minimum.
+//
+// If the message ID the reply is sent to does not exist, then the other side
+// will post the message as a normal message and not a reply.
+// The message will auto delete validUntil after the round it is sent in,
+// lasting forever if ValidForever is used.
+//
+// Parameters:
+//  - args[0] - JSON of the channel [id.ID] (Uint8Array). This can be retrieved
+//    using [ChannelsManager.GetChannelId].
+//  - args[1] - The contents of the message. The message should be at most 510
+//    bytes. This is expected to be Unicode, and thus a string data type is
+//    expected (string).
+//  - args[2] - JSON of [channel.MessageID] of the message you wish to reply to.
+//    This may be found in the [bindings.ChannelSendReport] if replying to your
+//    own. Alternatively, if reacting to another user's message, you may
+//    retrieve it via the [bindings.ChannelMessageReceptionCallback] registered
+//    using  RegisterReceiveHandler (Uint8Array).
+//  - args[3] - The lease of the message. This will be how long the message is
+//    valid until, in milliseconds. As per the [channels.Manager] documentation,
+//    this has different meanings depending on the use case. These use cases may
+//    be generic enough that they will not be enumerated here (int).
+//  - args[4] - JSON of [xxdk.CMIXParams]. If left empty
+//    [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//
+// Returns a promise:
+//  - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//  - Rejected with an error if sending fails.
+func (ch *ChannelsManager) SendReply(_ js.Value, args []js.Value) interface{} {
+	marshalledChanId := utils.CopyBytesToGo(args[0])
+	message := args[1].String()
+	messageToReactTo := utils.CopyBytesToGo(args[2])
+	leaseTimeMS := int64(args[3].Int())
+	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := ch.api.SendReply(marshalledChanId, message,
+			messageToReactTo, leaseTimeMS, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendReaction is used to send a reaction to a message over a channel.
+// The reaction must be a single emoji with no other characters, and will
+// be rejected otherwise.
+// Users will drop the reaction if they do not recognize the reactTo message.
+//
+// Parameters:
+//  - args[0] - JSON of the channel [id.ID] (Uint8Array). This can be retrieved
+//    using [ChannelsManager.GetChannelId].
+//  - args[1] - The user's reaction. This should be a single emoji with no
+//    other characters. As such, a Unicode string is expected (string).
+//  - args[2] - JSON of [channel.MessageID] of the message you wish to reply to.
+//    This may be found in the [bindings.ChannelSendReport] if replying to your
+//    own. Alternatively, if reacting to another user's message, you may
+//    retrieve it via the ChannelMessageReceptionCallback registered using
+//    RegisterReceiveHandler (Uint8Array).
+//  - args[3] - JSON of [xxdk.CMIXParams]. If left empty
+//    [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//
+// Returns a promise:
+//  - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//  - Rejected with an error if sending fails.
+func (ch *ChannelsManager) SendReaction(_ js.Value, args []js.Value) interface{} {
+	marshalledChanId := utils.CopyBytesToGo(args[0])
+	reaction := args[1].String()
+	messageToReactTo := utils.CopyBytesToGo(args[2])
+	cmixParamsJSON := utils.CopyBytesToGo(args[3])
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := ch.api.SendReaction(
+			marshalledChanId, reaction, messageToReactTo, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Channel Receiving Logic and Callback Registration                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// channelMessageReceptionCallback wraps Javascript callbacks to adhere to the
+// [bindings.ChannelMessageReceptionCallback] interface.
+type channelMessageReceptionCallback struct {
+	callback func(args ...interface{}) js.Value
+}
+
+func (cmrCB *channelMessageReceptionCallback) Callback(
+	receivedChannelMessageReport []byte, err error) {
+	cmrCB.callback(utils.CopyBytesToJS(receivedChannelMessageReport),
+		utils.JsTrace(err))
+}
+
+// RegisterReceiveHandler is used to register handlers for non-default message
+// types. They can be processed by modules. It is important that such modules
+// sync up with the event model implementation.
+//
+// There can only be one handler per [channels.MessageType], and this will
+// return an error on any re-registration.
+//
+// Parameters:
+//  - args[0] - The message type of the message. This will be a valid
+//    [channels.MessageType] (int).
+//  - args[1] - Javascript object that has functions that implement the
+//    [bindings.ChannelMessageReceptionCallback] interface. This callback will
+//    be executed when a channel message of the messageType is received.
+//
+// Returns:
+//  - Throws a TypeError if registering the handler fails.
+func (ch *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) interface{} {
+	messageType := args[0].Int()
+	listenerCb := &channelMessageReceptionCallback{
+		utils.WrapCB(args[1], "Callback")}
+
+	err := ch.api.RegisterReceiveHandler(messageType, listenerCb)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nil
+}
diff --git a/wasm/channels_test.go b/wasm/channels_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7f952295a8a911a4cbc5fc0f7c94255f8f1d5c22
--- /dev/null
+++ b/wasm/channels_test.go
@@ -0,0 +1,55 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package wasm
+
+import (
+	"gitlab.com/elixxir/client/bindings"
+	"reflect"
+	"testing"
+)
+
+// Tests that the map representing ChannelsManager returned by
+// newChannelsManagerJS contains all of the methods on ChannelsManager.
+func Test_newChannelsManagerJS(t *testing.T) {
+	cmType := reflect.TypeOf(&ChannelsManager{})
+
+	e2e := newChannelsManagerJS(&bindings.ChannelsManager{})
+	if len(e2e) != cmType.NumMethod() {
+		t.Errorf("ChannelsManager JS object does not have all methods."+
+			"\nexpected: %d\nreceived: %d", cmType.NumMethod(), len(e2e))
+	}
+
+	for i := 0; i < cmType.NumMethod(); i++ {
+		method := cmType.Method(i)
+
+		if _, exists := e2e[method.Name]; !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
+
+// Tests that ChannelsManager has all the methods that
+// [bindings.ChannelsManager] has.
+func Test_ChannelsManagerMethods(t *testing.T) {
+	cmType := reflect.TypeOf(&ChannelsManager{})
+	binCmType := reflect.TypeOf(&bindings.ChannelsManager{})
+
+	if binCmType.NumMethod() != cmType.NumMethod() {
+		t.Errorf("WASM ChannelsManager object does not have all methods from "+
+			"bindings.\nexpected: %d\nreceived: %d",
+			binCmType.NumMethod(), cmType.NumMethod())
+	}
+
+	for i := 0; i < binCmType.NumMethod(); i++ {
+		method := binCmType.Method(i)
+
+		if _, exists := cmType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/cmix.go b/wasm/cmix.go
index 70c2bed182e13601e14a4fc4e9a224bfe7cfcc2f..3b5a050e8ffadf88fe8d23bea8ce3e985e48b013 100644
--- a/wasm/cmix.go
+++ b/wasm/cmix.go
@@ -45,6 +45,7 @@ func newCmixJS(api *bindings.Cmix) map[string]interface{} {
 		"AddHealthCallback":           js.FuncOf(c.AddHealthCallback),
 		"RemoveHealthCallback":        js.FuncOf(c.RemoveHealthCallback),
 		"RegisterClientErrorCallback": js.FuncOf(c.RegisterClientErrorCallback),
+		"TrackServices":               js.FuncOf(c.TrackServices),
 
 		// connect.go
 		"Connect": js.FuncOf(c.Connect),
@@ -102,9 +103,9 @@ func NewCmix(_ js.Value, args []js.Value) interface{} {
 //  - args[1] - password used for storage (Uint8Array)
 //  - args[2] - JSON of [xxdk.CMIXParams] (Uint8Array)
 //
-// Returns:
-//  - A promise that returns a Javascript representation of the Cmix object.
-//  - Throws a error if loading Cmix fails.
+// Returns a promise:
+//  - Resolves to a Javascript representation of the Cmix object.
+//  - Rejected with an error if loading Cmix fails.
 func LoadCmix(_ js.Value, args []js.Value) interface{} {
 	storageDir := args[0].String()
 	password := utils.CopyBytesToGo(args[1])
diff --git a/wasm/cmix_test.go b/wasm/cmix_test.go
index 316b1508c184ad99832119ddcfd53c25e10395c0..703440b4bfa2a0183583407c9fa052a6d47bc09c 100644
--- a/wasm/cmix_test.go
+++ b/wasm/cmix_test.go
@@ -35,3 +35,23 @@ func Test_newCmixJS(t *testing.T) {
 		}
 	}
 }
+
+// Tests that Cmix has all the methods that [bindings.Cmix] has.
+func Test_CmixMethods(t *testing.T) {
+	cmixType := reflect.TypeOf(&Cmix{})
+	binCmixType := reflect.TypeOf(&bindings.Cmix{})
+
+	if binCmixType.NumMethod() != cmixType.NumMethod() {
+		t.Errorf("WASM Cmix object does not have all methods from bindings."+
+			"\nexpected: %d\nreceived: %d",
+			binCmixType.NumMethod(), cmixType.NumMethod())
+	}
+
+	for i := 0; i < binCmixType.NumMethod(); i++ {
+		method := binCmixType.Method(i)
+
+		if _, exists := cmixType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/connect.go b/wasm/connect.go
index 779ff4c9ae37f5f5124af5fe0db4ea12f7aa756b..92e4bed661e8928884f3bdc31aef59a50a92dfe0 100644
--- a/wasm/connect.go
+++ b/wasm/connect.go
@@ -27,7 +27,7 @@ func newConnectJS(api *bindings.Connection) map[string]interface{} {
 	c := Connection{api}
 	connectionMap := map[string]interface{}{
 		// connect.go
-		"GetID":            js.FuncOf(c.GetID),
+		"GetId":            js.FuncOf(c.GetId),
 		"SendE2E":          js.FuncOf(c.SendE2E),
 		"Close":            js.FuncOf(c.Close),
 		"GetPartner":       js.FuncOf(c.GetPartner),
@@ -37,11 +37,11 @@ func newConnectJS(api *bindings.Connection) map[string]interface{} {
 	return connectionMap
 }
 
-// GetID returns the ID for this [bindings.Connection] in the connectionTracker.
+// GetId returns the ID for this [bindings.Connection] in the connectionTracker.
 //
 // Returns:
 //  - int of the ID
-func (c *Connection) GetID(js.Value, []js.Value) interface{} {
+func (c *Connection) GetId(js.Value, []js.Value) interface{} {
 	return c.api.GetId()
 }
 
@@ -56,19 +56,23 @@ func (c *Connection) GetID(js.Value, []js.Value) interface{} {
 //  - args[1] - marshalled recipient [contact.Contact] (Uint8Array).
 //  - args[3] - JSON of [xxdk.E2EParams] (Uint8Array).
 //
-// Returns:
-//  - Javascript representation of the Connection object
-//  - throws a TypeError if creating loading the parameters or connecting fails
+// Returns a promise:
+//  - Resolves to a Javascript representation of the Connection object.
+//  - Rejected with an error if loading the parameters or connecting fails.
 func (c *Cmix) Connect(_ js.Value, args []js.Value) interface{} {
 	recipientContact := utils.CopyBytesToGo(args[1])
 	e2eParamsJSON := utils.CopyBytesToGo(args[2])
-	api, err := c.api.Connect(args[0].Int(), recipientContact, e2eParamsJSON)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		api, err := c.api.Connect(args[0].Int(), recipientContact, e2eParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(newConnectJS(api))
+		}
 	}
 
-	return newConnectJS(api)
+	return utils.CreatePromise(promiseFn)
 }
 
 // SendE2E is a wrapper for sending specifically to the Connection's
@@ -82,17 +86,23 @@ func (c *Cmix) Connect(_ js.Value, args []js.Value) interface{} {
 //  - args[0] - message type from [catalog.MessageType] (int)
 //  - args[1] - message payload (Uint8Array)
 //
-// Returns:
-//  - JSON of [bindings.E2ESendReport], which can be passed into
-//    cmix.WaitForRoundResult to see if the send succeeded (Uint8Array)
-//  - throws a TypeError if sending fails
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.E2ESendReport], which can be passed
+//    into Cmix.WaitForRoundResult to see if the send succeeded (Uint8Array).
+//  - Rejected with an error if sending fails.
 func (c *Connection) SendE2E(_ js.Value, args []js.Value) interface{} {
-	sendReport, err := c.api.SendE2E(args[0].Int(), utils.CopyBytesToGo(args[1]))
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	payload := utils.CopyBytesToGo(args[1])
+
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := c.api.SendE2E(args[0].Int(), payload)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
 	}
-	return utils.CopyBytesToJS(sendReport)
+
+	return utils.CreatePromise(promiseFn)
 }
 
 // Close deletes this Connection's partner.Manager and releases resources.
diff --git a/wasm/connect_test.go b/wasm/connect_test.go
index f9cba409167499bcc33365eb3a730a3c0eaea138..c2acb98aa8b4c11ee216759ff2227a6b79db5816 100644
--- a/wasm/connect_test.go
+++ b/wasm/connect_test.go
@@ -32,3 +32,23 @@ func Test_newConnectJS(t *testing.T) {
 		}
 	}
 }
+
+// Tests that Connection has all the methods that [bindings.Connection] has.
+func Test_ConnectionMethods(t *testing.T) {
+	connType := reflect.TypeOf(&Connection{})
+	binConnType := reflect.TypeOf(&bindings.Connection{})
+
+	if binConnType.NumMethod() != connType.NumMethod() {
+		t.Errorf("WASM Connection object does not have all methods from "+
+			"bindings.\nexpected: %d\nreceived: %d",
+			binConnType.NumMethod(), connType.NumMethod())
+	}
+
+	for i := 0; i < binConnType.NumMethod(); i++ {
+		method := binConnType.Method(i)
+
+		if _, exists := connType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/delivery.go b/wasm/delivery.go
index b0f340978e6a92ccb510bc36c60a8875b309c77f..f6748f22681b9f665f5b02ec04aba8468dec8ef3 100644
--- a/wasm/delivery.go
+++ b/wasm/delivery.go
@@ -34,20 +34,20 @@ func (mdc *messageDeliveryCallback) EventCallback(
 // not occur as a result of both sides of the bindings holding a reference to
 // the same pointer.
 //
-// roundList is a JSON marshalled RoundsList or any JSON marshalled send report
-// that inherits a RoundsList object.
+// roundList is a JSON marshalled [bindings.RoundsList] or any JSON marshalled
+// send report that inherits a [bindings.RoundsList] object.
 //
 // Parameters:
 //  - args[0] - JSON of [bindings.RoundsList] or JSON of any send report that
-//    inherits a [bindings.RoundsList] object (Uint8Array)
+//    inherits a [bindings.RoundsList] object (Uint8Array).
 //  - args[1] - Javascript object that has functions that implement the
-//    [bindings.MessageDeliveryCallback] interface
+//    [bindings.MessageDeliveryCallback] interface.
 //  - args[2] - timeout when the callback will return if no state update occurs,
-//    in milliseconds (int)
+//    in milliseconds (int).
 //
 // Returns:
-//  - throws a TypeError if the parameters are invalid or getting round results
-//    fails
+//  - Throws a TypeError if the parameters are invalid or getting round results
+//    fails.
 func (c *Cmix) WaitForRoundResult(_ js.Value, args []js.Value) interface{} {
 	roundList := utils.CopyBytesToGo(args[0])
 	mdc := &messageDeliveryCallback{utils.WrapCB(args[1], "EventCallback")}
diff --git a/wasm/e2e.go b/wasm/e2e.go
index 70a66436a5b0bad3a5e5544b163e3b0d14c57f38..ef88dd65b1dd0ea6d2b2694f6bd9d4295ddf392d 100644
--- a/wasm/e2e.go
+++ b/wasm/e2e.go
@@ -35,6 +35,7 @@ func newE2eJS(api *bindings.E2e) map[string]interface{} {
 
 		// e2eHandler.go
 		"GetReceptionID":          js.FuncOf(e.GetReceptionID),
+		"DeleteContact":           js.FuncOf(e.DeleteContact),
 		"GetAllPartnerIDs":        js.FuncOf(e.GetAllPartnerIDs),
 		"PayloadSize":             js.FuncOf(e.PayloadSize),
 		"SecondPartitionSize":     js.FuncOf(e.SecondPartitionSize),
diff --git a/wasm/e2eAuth.go b/wasm/e2eAuth.go
index 3d6efa4343e7441a51c328e99defa1fb75873625..45b3b9450e2317e9da79c1b356d1abd50790a6fe 100644
--- a/wasm/e2eAuth.go
+++ b/wasm/e2eAuth.go
@@ -36,9 +36,9 @@ import (
 //  - args[0] - marshalled bytes of the partner [contact.Contact] (Uint8Array).
 //  - args[1] - JSON of [fact.FactList] (Uint8Array).
 //
-// Returns:
-//  - A promise that returns the ID of the round (int).
-//  - Throws an error if the request fails.
+// Returns a promise:
+//  - Resolves to the ID of the round (int).
+//  - Rejected with an error if sending the request fails.
 func (e *E2e) Request(_ js.Value, args []js.Value) interface{} {
 	partnerContact := utils.CopyBytesToGo(args[0])
 	factsListJson := utils.CopyBytesToGo(args[1])
@@ -74,9 +74,9 @@ func (e *E2e) Request(_ js.Value, args []js.Value) interface{} {
 // Parameters:
 //  - args[0] - marshalled bytes of the partner [contact.Contact] (Uint8Array).
 //
-// Returns:
-//  - A promise that returns the ID of the round (int).
-//  - Throws an error if the confirmation fails.
+// Returns a promise:
+//  - Resolves to the ID of the round (int).
+//  - Rejected with an error if sending the confirmation fails.
 func (e *E2e) Confirm(_ js.Value, args []js.Value) interface{} {
 	partnerContact := utils.CopyBytesToGo(args[0])
 
@@ -109,9 +109,9 @@ func (e *E2e) Confirm(_ js.Value, args []js.Value) interface{} {
 // Parameters:
 //  - args[0] - marshalled bytes of the partner [contact.Contact] (Uint8Array).
 //
-// Returns:
-//  - A promise that returns the ID of the round (int).
-//  - Throws an error if the reset fails.
+// Returns a promise:
+//  - Resolves to the ID of the round (int).
+//  - Rejected with an error if sending the reset fails.
 func (e *E2e) Reset(_ js.Value, args []js.Value) interface{} {
 	partnerContact := utils.CopyBytesToGo(args[0])
 
@@ -138,9 +138,9 @@ func (e *E2e) Reset(_ js.Value, args []js.Value) interface{} {
 // Parameters:
 //  - args[0] - marshalled bytes of the partner [contact.Contact] (Uint8Array).
 //
-// Returns:
-//  - A promise that returns the ID of the round (int).
-//  - Throws an error if the confirmation fails.
+// Returns a promise:
+//  - Resolves to the ID of the round (int).
+//  - Rejected with an error if resending the confirmation fails.
 func (e *E2e) ReplayConfirm(_ js.Value, args []js.Value) interface{} {
 	partnerContact := utils.CopyBytesToGo(args[0])
 
diff --git a/wasm/e2eHandler.go b/wasm/e2eHandler.go
index 8d6efc63c5e015936230de093d71cf78c0b9b336..a186be2bac661ec9ec0dbfad164998c6bcf176c6 100644
--- a/wasm/e2eHandler.go
+++ b/wasm/e2eHandler.go
@@ -22,6 +22,22 @@ func (e *E2e) GetReceptionID(js.Value, []js.Value) interface{} {
 	return utils.CopyBytesToJS(e.api.GetReceptionID())
 }
 
+// DeleteContact removes a partner from E2e's storage.
+//
+// Parameters:
+//  - args[0] - marshalled bytes of the partner [id.ID] (Uint8Array).
+//
+// Returns:
+//  - Throws utils.TypeError if deleting the partner fails.
+func (e *E2e) DeleteContact(_ js.Value, args []js.Value) interface{} {
+	err := e.api.DeleteContact(utils.CopyBytesToGo(args[0]))
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+	return nil
+}
+
 // GetAllPartnerIDs returns a list of all partner IDs that the user has an E2E
 // relationship with.
 //
@@ -144,16 +160,15 @@ func (e *E2e) RemoveService(_ js.Value, args []js.Value) interface{} {
 // message type, per the given parameters--encrypted with end-to-end encryption.
 //
 // Parameters:
-//  - args[0] - message type from [catalog.MessageType] (int)
-//  - args[1] - JSON of [id.ID] (Uint8Array)
-//  - args[2] - message payload (Uint8Array)
-//  - args[3] - JSON [e2e.Params] (Uint8Array)
+//  - args[0] - message type from [catalog.MessageType] (int).
+//  - args[1] - JSON of [id.ID] (Uint8Array).
+//  - args[2] - message payload (Uint8Array).
+//  - args[3] - JSON [e2e.Params] (Uint8Array).
 //
-// Returns:
-//  - A promise that returns the JSON of the [bindings.E2ESendReport], which can
-//    be passed into Cmix.WaitForRoundResult to see if the send succeeded
-//    (Uint8Array).
-//  - Throws error if sending fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.E2ESendReport], which can be passed
+//    into Cmix.WaitForRoundResult to see if the send succeeded (Uint8Array).
+//  - Rejected with an error if sending fails.
 func (e *E2e) SendE2E(_ js.Value, args []js.Value) interface{} {
 	recipientId := utils.CopyBytesToGo(args[1])
 	payload := utils.CopyBytesToGo(args[2])
@@ -181,8 +196,8 @@ type processor struct {
 
 func (p *processor) Process(
 	message, receptionId []byte, ephemeralId, roundId int64) {
-	p.process(utils.CopyBytesToJS(message), utils.CopyBytesToJS(receptionId), ephemeralId,
-		roundId)
+	p.process(utils.CopyBytesToJS(message), utils.CopyBytesToJS(receptionId),
+		ephemeralId, roundId)
 }
 
 func (p *processor) String() string {
diff --git a/wasm/e2e_test.go b/wasm/e2e_test.go
index 107086be968767b5b5ce74541e4a2264e43a0e3f..d4b55d21320a42ff5f85e77c289cb91a705f6896 100644
--- a/wasm/e2e_test.go
+++ b/wasm/e2e_test.go
@@ -34,3 +34,23 @@ func Test_newE2eJS(t *testing.T) {
 		}
 	}
 }
+
+// Tests that E2e has all the methods that [bindings.E2e] has.
+func Test_E2eMethods(t *testing.T) {
+	e2eType := reflect.TypeOf(&E2e{})
+	binE2eType := reflect.TypeOf(&bindings.E2e{})
+
+	if binE2eType.NumMethod() != e2eType.NumMethod() {
+		t.Errorf("WASM E2e object does not have all methods from bindings."+
+			"\nexpected: %d\nreceived: %d",
+			binE2eType.NumMethod(), e2eType.NumMethod())
+	}
+
+	for i := 0; i < binE2eType.NumMethod(); i++ {
+		method := binE2eType.Method(i)
+
+		if _, exists := e2eType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/fileTransfer.go b/wasm/fileTransfer.go
index 1a7d7953f29f42d54f569f75caf6547bce3f4a0c..24a1767b8b96151ba13c6f0aea3f0cca740ef9df 100644
--- a/wasm/fileTransfer.go
+++ b/wasm/fileTransfer.go
@@ -56,7 +56,7 @@ type receiveFileCallback struct {
 }
 
 func (rfc *receiveFileCallback) Callback(payload []byte, err error) {
-	rfc.callback(utils.CopyBytesToJS(payload), err.Error())
+	rfc.callback(utils.CopyBytesToJS(payload), utils.JsTrace(err))
 }
 
 // fileTransferSentProgressCallback wraps Javascript callbacks to adhere to the
@@ -67,7 +67,8 @@ type fileTransferSentProgressCallback struct {
 
 func (spc *fileTransferSentProgressCallback) Callback(
 	payload []byte, t *bindings.FilePartTracker, err error) {
-	spc.callback(utils.CopyBytesToJS(payload), newFilePartTrackerJS(t), err.Error())
+	spc.callback(utils.CopyBytesToJS(payload), newFilePartTrackerJS(t),
+		utils.JsTrace(err))
 }
 
 // fileTransferReceiveProgressCallback wraps Javascript callbacks to adhere to
@@ -78,7 +79,8 @@ type fileTransferReceiveProgressCallback struct {
 
 func (rpc *fileTransferReceiveProgressCallback) Callback(
 	payload []byte, t *bindings.FilePartTracker, err error) {
-	rpc.callback(utils.CopyBytesToJS(payload), newFilePartTrackerJS(t), err.Error())
+	rpc.callback(utils.CopyBytesToJS(payload), newFilePartTrackerJS(t),
+		utils.JsTrace(err))
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -123,22 +125,25 @@ func InitFileTransfer(_ js.Value, args []js.Value) interface{} {
 //  - args[4] - duration to wait between progress callbacks triggering (string).
 //    Reference [time.ParseDuration] for info on valid duration strings.
 //
-// Returns:
-//  - A unique ID for this file transfer (Uint8Array).
-//  - Throws a TypeError if sending fails.
+// Returns a promise:
+//  - Resolves to a unique ID for this file transfer (Uint8Array).
+//  - Rejected with an error if sending fails.
 func (f *FileTransfer) Send(_ js.Value, args []js.Value) interface{} {
 	payload := utils.CopyBytesToGo(args[0])
 	recipientID := utils.CopyBytesToGo(args[1])
 	retry := float32(args[2].Float())
 	spc := &fileTransferSentProgressCallback{utils.WrapCB(args[3], "Callback")}
 
-	ftID, err := f.api.Send(payload, recipientID, retry, spc, args[4].String())
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		ftID, err := f.api.Send(payload, recipientID, retry, spc, args[4].String())
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(ftID))
+		}
 	}
 
-	return utils.CopyBytesToJS(ftID)
+	return utils.CreatePromise(promiseFn)
 }
 
 // Receive returns the full file on the completion of the transfer. It deletes
diff --git a/wasm/fileTransfer_test.go b/wasm/fileTransfer_test.go
index bbdf8d98c05e4ecd2dcafd6df61b10cbd1317bed..b339903f19465f9bc8a5f27f68d589884bb3f812 100644
--- a/wasm/fileTransfer_test.go
+++ b/wasm/fileTransfer_test.go
@@ -33,6 +33,26 @@ func Test_newFileTransferJS(t *testing.T) {
 	}
 }
 
+// Tests that FileTransfer has all the methods that [bindings.FileTransfer] has.
+func Test_FileTransferMethods(t *testing.T) {
+	ftType := reflect.TypeOf(&FileTransfer{})
+	binFtType := reflect.TypeOf(&bindings.FileTransfer{})
+
+	if binFtType.NumMethod() != ftType.NumMethod() {
+		t.Errorf("WASM FileTransfer object does not have all methods from "+
+			"bindings.\nexpected: %d\nreceived: %d",
+			binFtType.NumMethod(), ftType.NumMethod())
+	}
+
+	for i := 0; i < binFtType.NumMethod(); i++ {
+		method := binFtType.Method(i)
+
+		if _, exists := ftType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
+
 // Tests that the map representing FilePartTracker returned by
 // newFilePartTrackerJS contains all of the methods on FilePartTracker.
 func Test_newFilePartTrackerJS(t *testing.T) {
@@ -52,3 +72,24 @@ func Test_newFilePartTrackerJS(t *testing.T) {
 		}
 	}
 }
+
+// Tests that FilePartTracker has all the methods that
+// [bindings.FilePartTracker] has.
+func Test_FilePartTrackerMethods(t *testing.T) {
+	fptType := reflect.TypeOf(&FilePartTracker{})
+	binFptType := reflect.TypeOf(&bindings.FilePartTracker{})
+
+	if binFptType.NumMethod() != fptType.NumMethod() {
+		t.Errorf("WASM FilePartTracker object does not have all methods from "+
+			"bindings.\nexpected: %d\nreceived: %d",
+			binFptType.NumMethod(), fptType.NumMethod())
+	}
+
+	for i := 0; i < binFptType.NumMethod(); i++ {
+		method := binFptType.Method(i)
+
+		if _, exists := fptType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/follow.go b/wasm/follow.go
index 8da6a4d72b192590b0d8e84b861d1804b785940a..c4d6cbf46bc6533d6ed99dd293f88d6e8d51cc5f 100644
--- a/wasm/follow.go
+++ b/wasm/follow.go
@@ -87,10 +87,19 @@ func (c *Cmix) StopNetworkFollower(js.Value, []js.Value) interface{} {
 // Parameters:
 //  - args[0] - timeout when stopping threads in milliseconds (int)
 //
-// Returns:
-//  - returns true if the network is healthy (boolean)
+// Returns a promise:
+//  - A promise that resolves if the network is healthy and rejects if the
+//    network is not healthy.
 func (c *Cmix) WaitForNetwork(_ js.Value, args []js.Value) interface{} {
-	return c.api.WaitForNetwork(args[0].Int())
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		if c.api.WaitForNetwork(args[0].Int()) {
+			resolve()
+		} else {
+			reject()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
 }
 
 // NetworkFollowerStatus gets the state of the network follower. It returns a
@@ -191,8 +200,32 @@ func (ce *clientError) Report(source, message, trace string) {
 //
 // Parameters:
 //  - args[0] - Javascript object that has functions that implement the
-//    [bindings.ClientError] interface
+//    [bindings.ClientError] interface.
 func (c *Cmix) RegisterClientErrorCallback(_ js.Value, args []js.Value) interface{} {
 	c.api.RegisterClientErrorCallback(&clientError{utils.WrapCB(args[0], "Report")})
 	return nil
 }
+
+// trackServicesCallback adheres to the [bindings.TrackServicesCallback]
+// interface.
+type trackServicesCallback struct {
+	callback func(args ...interface{}) js.Value
+}
+
+func (tsc *trackServicesCallback) Callback(marshalData []byte, err error) {
+	tsc.callback(utils.CopyBytesToJS(marshalData), utils.JsTrace(err))
+}
+
+// TrackServices will return, via a callback, the list of services that the
+// backend keeps track of, which is formally referred to as a
+// [message.ServiceList]. This may be passed into other bindings call that may
+// need context on the available services for this client.
+//
+// Parameters:
+//  - args[0] - Javascript object that has functions that implement the
+//    [bindings.TrackServicesCallback] interface.
+func (c *Cmix) TrackServices(_ js.Value, args []js.Value) interface{} {
+	c.api.TrackServices(
+		&trackServicesCallback{utils.WrapCB(args[0], "Callback")})
+	return nil
+}
diff --git a/wasm/group.go b/wasm/group.go
index d19da538da36abca4fde43c428529976f6b97eba..12f8f6d688fa8bb537f364a15cdd3ba5a1cd0043 100644
--- a/wasm/group.go
+++ b/wasm/group.go
@@ -81,24 +81,26 @@ func NewGroupChat(_ js.Value, args []js.Value) interface{} {
 //    optional  parameter and may be nil. If nil the group will be assigned the
 //    default name (Uint8Array).
 //
-// Returns:
-//  - JSON of [bindings.GroupReport], which can be passed into
-//    Cmix.WaitForRoundResult to see if the group request message send
-//    succeeded.
-//  - Throws a TypeError if making the group fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.GroupReport], which can be passed
+//    into Cmix.WaitForRoundResult to see if the send succeeded (Uint8Array).
+//  - Rejected with an error if making the group fails.
 func (g *GroupChat) MakeGroup(_ js.Value, args []js.Value) interface{} {
 	// (membershipBytes, message, name []byte) ([]byte, error)
 	membershipBytes := utils.CopyBytesToGo(args[0])
 	message := utils.CopyBytesToGo(args[1])
 	name := utils.CopyBytesToGo(args[2])
 
-	report, err := g.api.MakeGroup(membershipBytes, message, name)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := g.api.MakeGroup(membershipBytes, message, name)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
 	}
 
-	return utils.CopyBytesToJS(report)
+	return utils.CreatePromise(promiseFn)
 }
 
 // ResendRequest resends a group request to all members in the group.
@@ -107,19 +109,21 @@ func (g *GroupChat) MakeGroup(_ js.Value, args []js.Value) interface{} {
 //  - args[0] - group's ID (Uint8Array). This can be found in the report
 //  returned by GroupChat.MakeGroup.
 //
-// Returns:
-//  - JSON of [bindings.GroupReport] (Uint8Array), which can be passed into
-//    Cmix.WaitForRoundResult to see if the group request message send
-//    succeeded.
-//  - Throws a TypeError if resending the request fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.GroupReport], which can be passed
+//    into Cmix.WaitForRoundResult to see if the send succeeded (Uint8Array).
+//  - Rejected with an error if resending the request fails.
 func (g *GroupChat) ResendRequest(_ js.Value, args []js.Value) interface{} {
-	report, err := g.api.ResendRequest(utils.CopyBytesToGo(args[0]))
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := g.api.ResendRequest(utils.CopyBytesToGo(args[0]))
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
 	}
 
-	return utils.CopyBytesToJS(report)
+	return utils.CreatePromise(promiseFn)
 }
 
 // JoinGroup allows a user to join a group when a request is received.
@@ -170,20 +174,25 @@ func (g *GroupChat) LeaveGroup(_ js.Value, args []js.Value) interface{} {
 //  - args[2] - the tag associated with the message (string). This tag may be
 //    empty.
 //
-// Returns:
-//  - JSON of [bindings.GroupSendReport] (Uint8Array), which can be passed into
-//    Cmix.WaitForRoundResult to see if the group message send succeeded.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.GroupSendReport], which can be
+//    passed into Cmix.WaitForRoundResult to see if the send succeeded
+//    (Uint8Array).
+//  - Rejected with an error if sending the message to the group fails.
 func (g *GroupChat) Send(_ js.Value, args []js.Value) interface{} {
 	groupId := utils.CopyBytesToGo(args[0])
 	message := utils.CopyBytesToGo(args[1])
 
-	report, err := g.api.Send(groupId, message, args[2].String())
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := g.api.Send(groupId, message, args[2].String())
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
 	}
 
-	return utils.CopyBytesToJS(report)
+	return utils.CreatePromise(promiseFn)
 }
 
 // GetGroups returns a list of group IDs that the user is a member of.
@@ -357,8 +366,9 @@ type groupChatProcessor struct {
 
 func (gcp *groupChatProcessor) Process(decryptedMessage, msg,
 	receptionId []byte, ephemeralId, roundId int64, err error) {
-	gcp.callback(utils.CopyBytesToJS(decryptedMessage), utils.CopyBytesToJS(msg),
-		utils.CopyBytesToJS(receptionId), ephemeralId, roundId, err.Error())
+	gcp.callback(utils.CopyBytesToJS(decryptedMessage),
+		utils.CopyBytesToJS(msg), utils.CopyBytesToJS(receptionId), ephemeralId,
+		roundId, utils.JsTrace(err))
 }
 
 func (gcp *groupChatProcessor) String() string {
diff --git a/wasm/group_test.go b/wasm/group_test.go
index cc64e5f8396c8bf7490e79f172a9d204eb705ba7..1350fc4a34f6f0d4fb826e06ef61e4fc2e1246aa 100644
--- a/wasm/group_test.go
+++ b/wasm/group_test.go
@@ -35,22 +35,62 @@ func Test_newGroupChatJS(t *testing.T) {
 	}
 }
 
+// Tests that GroupChat has all the methods that [bindings.GroupChat] has.
+func Test_GroupChatMethods(t *testing.T) {
+	gcType := reflect.TypeOf(&GroupChat{})
+	binGcType := reflect.TypeOf(&bindings.GroupChat{})
+
+	if binGcType.NumMethod() != gcType.NumMethod() {
+		t.Errorf("WASM GroupChat object does not have all methods from "+
+			"bindings.\nexpected: %d\nreceived: %d",
+			binGcType.NumMethod(), gcType.NumMethod())
+	}
+
+	for i := 0; i < binGcType.NumMethod(); i++ {
+		method := binGcType.Method(i)
+
+		if _, exists := gcType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
+
 // Tests that the map representing Group returned by newGroupJS contains all of
 // the methods on Group.
 func Test_newGroupJS(t *testing.T) {
-	gType := reflect.TypeOf(&Group{})
+	grpType := reflect.TypeOf(&Group{})
 
 	g := newGroupJS(&bindings.Group{})
-	if len(g) != gType.NumMethod() {
+	if len(g) != grpType.NumMethod() {
 		t.Errorf("Group JS object does not have all methods."+
-			"\nexpected: %d\nreceived: %d", gType.NumMethod(), len(g))
+			"\nexpected: %d\nreceived: %d", grpType.NumMethod(), len(g))
 	}
 
-	for i := 0; i < gType.NumMethod(); i++ {
-		method := gType.Method(i)
+	for i := 0; i < grpType.NumMethod(); i++ {
+		method := grpType.Method(i)
 
 		if _, exists := g[method.Name]; !exists {
 			t.Errorf("Method %s does not exist.", method.Name)
 		}
 	}
 }
+
+// Tests that Group has all the methods that [bindings.Group] has.
+func Test_GroupMethods(t *testing.T) {
+	grpType := reflect.TypeOf(&Group{})
+	binGrpType := reflect.TypeOf(&bindings.Group{})
+
+	if binGrpType.NumMethod() != grpType.NumMethod() {
+		t.Errorf("WASM Group object does not have all methods from bindings."+
+			"\nexpected: %d\nreceived: %d",
+			binGrpType.NumMethod(), grpType.NumMethod())
+	}
+
+	for i := 0; i < binGrpType.NumMethod(); i++ {
+		method := binGrpType.Method(i)
+
+		if _, exists := grpType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/identity.go b/wasm/identity.go
index c94de98d55f6352a9846871f74afb9e3b870944e..2b0f2d392217bdc789da0be953c10b443e0fe818 100644
--- a/wasm/identity.go
+++ b/wasm/identity.go
@@ -68,33 +68,39 @@ func LoadReceptionIdentity(_ js.Value, args []js.Value) interface{} {
 // MakeReceptionIdentity generates a new cryptographic identity for receiving
 // messages.
 //
-// Returns:
-//  - JSON of the [xxdk.ReceptionIdentity] object (Uint8Array)
-//  - throws a TypeError if creating a new identity fails
+// Returns a promise:
+//  - Resolves to the JSON of the [xxdk.ReceptionIdentity] object (Uint8Array).
+//  - Rejected with an error if creating a new identity fails.
 func (c *Cmix) MakeReceptionIdentity(js.Value, []js.Value) interface{} {
-	ri, err := c.api.MakeReceptionIdentity()
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		ri, err := c.api.MakeReceptionIdentity()
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(ri))
+		}
 	}
 
-	return utils.CopyBytesToJS(ri)
+	return utils.CreatePromise(promiseFn)
 }
 
 // MakeLegacyReceptionIdentity generates the legacy identity for receiving
 // messages.
 //
-// Returns:
-//  - JSON of the [xxdk.ReceptionIdentity] object (Uint8Array)
-//  - throws a TypeError if creating a new legacy identity fails
+// Returns a promise:
+//  - Resolves to the JSON of the [xxdk.ReceptionIdentity] object (Uint8Array).
+//  - Rejected with an error if creating a new legacy identity fails.
 func (c *Cmix) MakeLegacyReceptionIdentity(js.Value, []js.Value) interface{} {
-	ri, err := c.api.MakeLegacyReceptionIdentity()
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		ri, err := c.api.MakeLegacyReceptionIdentity()
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(ri))
+		}
 	}
 
-	return utils.CopyBytesToJS(ri)
+	return utils.CreatePromise(promiseFn)
 }
 
 // GetReceptionRegistrationValidationSignature returns the signature provided by
@@ -104,7 +110,8 @@ func (c *Cmix) MakeLegacyReceptionIdentity(js.Value, []js.Value) interface{} {
 //  - signature (Uint8Array)
 func (c *Cmix) GetReceptionRegistrationValidationSignature(
 	js.Value, []js.Value) interface{} {
-	return utils.CopyBytesToJS(c.api.GetReceptionRegistrationValidationSignature())
+	return utils.CopyBytesToJS(
+		c.api.GetReceptionRegistrationValidationSignature())
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -135,13 +142,13 @@ func GetContactFromReceptionIdentity(_ js.Value, args []js.Value) interface{} {
 // GetIDFromContact returns the ID in the [contact.Contact] object.
 //
 // Parameters:
-//  - args[0] - marshalled bytes of [contact.Contact] (string)
+//  - args[0] - marshalled bytes of [contact.Contact] (Uint8Array)
 //
 // Returns:
 //  - marshalled [id.ID] object (Uint8Array)
 //  - throws a TypeError if loading the ID from the contact file fails
 func GetIDFromContact(_ js.Value, args []js.Value) interface{} {
-	cID, err := bindings.GetIDFromContact([]byte(args[0].String()))
+	cID, err := bindings.GetIDFromContact(utils.CopyBytesToGo(args[0]))
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 		return nil
diff --git a/wasm/logging.go b/wasm/logging.go
index e309addce9185edb6ec255abb0ffaf1d34ad12f6..2f636d1b7ecccd62334bd621ec450ca08832cc3a 100644
--- a/wasm/logging.go
+++ b/wasm/logging.go
@@ -10,6 +10,8 @@
 package wasm
 
 import (
+	"fmt"
+	"github.com/armon/circbuf"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/bindings"
@@ -19,6 +21,10 @@ import (
 	"syscall/js"
 )
 
+// logListeners is a list of all registered log listeners. This is used to add
+// additional log listener without overwriting previously registered listeners.
+var logListeners []jww.LogListener
+
 // LogLevel sets level of logging. All logs at the set level and below will be
 // displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
 // messages will be printed).
@@ -40,82 +46,90 @@ import (
 // Returns:
 //  - Throws TypeError if the log level is invalid.
 func LogLevel(_ js.Value, args []js.Value) interface{} {
-	level := args[0].Int()
-	if level < 0 || level > 6 {
-		err := errors.Errorf("log level is not valid: log level: %d", level)
+	threshold := jww.Threshold(args[0].Int())
+	if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
+		err := errors.Errorf("log level is not valid: log level: %d", threshold)
 		utils.Throw(utils.TypeError, err)
 		return nil
 	}
 
-	threshold := jww.Threshold(level)
 	jww.SetLogThreshold(threshold)
 	jww.SetFlags(log.LstdFlags | log.Lmicroseconds)
 
-	ll := &LogListener{threshold, js.Global().Get("console")}
-	jww.SetLogListeners(ll.Listen)
+	ll := NewJsConsoleLogListener(threshold)
+	logListeners = append(logListeners, ll.Listen)
+	jww.SetLogListeners(logListeners...)
 	jww.SetStdoutThreshold(jww.LevelFatal + 1)
 
+	msg := fmt.Sprintf("Log level set to: %s", threshold)
 	switch threshold {
 	case jww.LevelTrace:
 		fallthrough
 	case jww.LevelDebug:
 		fallthrough
 	case jww.LevelInfo:
-		jww.INFO.Printf("Log level set to: %s", threshold)
+		jww.INFO.Print(msg)
 	case jww.LevelWarn:
-		jww.WARN.Printf("Log level set to: %s", threshold)
+		jww.WARN.Print(msg)
 	case jww.LevelError:
-		jww.ERROR.Printf("Log level set to: %s", threshold)
+		jww.ERROR.Print(msg)
 	case jww.LevelCritical:
-		jww.CRITICAL.Printf("Log level set to: %s", threshold)
+		jww.CRITICAL.Print(msg)
 	case jww.LevelFatal:
-		jww.FATAL.Printf("Log level set to: %s", threshold)
+		jww.FATAL.Print(msg)
 	}
 
 	return nil
 }
 
-// console contains the Javascript console object, which provides access to the
-// browser's debugging console. This structure detects logging types and prints
-// it using the correct logging method.
-type console struct {
-	call string
-	js.Value
-}
-
-func (c *console) Write(p []byte) (n int, err error) {
-	c.Call(c.call, string(p))
-	return len(p), nil
-}
-
-type LogListener struct {
-	jww.Threshold
-	js.Value
-}
+// LogToFile enables logging to a file that can be downloaded.
+//
+// Parameters:
+//  - args[0] - log level (int).
+//  - args[1] - log file name (string).
+//  - args[2] - max log file size, in bytes (int).
+//
+// Returns:
+//  - A Javascript representation of the LogFile object, which allows accessing
+//    the contents of the log file and other metadata.
+func LogToFile(_ js.Value, args []js.Value) interface{} {
+	threshold := jww.Threshold(args[0].Int())
+	if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
+		err := errors.Errorf("log level is not valid: log level: %d", threshold)
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
 
-func (ll *LogListener) Listen(t jww.Threshold) io.Writer {
-	if t < ll.Threshold {
+	// Create new log file output
+	ll, err := NewLogFile(args[1].String(), threshold, args[2].Int())
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
 		return nil
 	}
 
-	switch t {
+	logListeners = append(logListeners, ll.Listen)
+	jww.SetLogListeners(logListeners...)
+
+	msg := fmt.Sprintf("Outputting log to file %s of max size %d with level %s",
+		ll.name, ll.b.Size(), threshold)
+	switch threshold {
 	case jww.LevelTrace:
-		return &console{"debug", ll.Value}
+		fallthrough
 	case jww.LevelDebug:
-		return &console{"log", ll.Value}
+		fallthrough
 	case jww.LevelInfo:
-		return &console{"info", ll.Value}
+		jww.INFO.Print(msg)
 	case jww.LevelWarn:
-		return &console{"warn", ll.Value}
+		jww.WARN.Print(msg)
 	case jww.LevelError:
-		return &console{"error", ll.Value}
+		jww.ERROR.Print(msg)
 	case jww.LevelCritical:
-		return &console{"error", ll.Value}
+		jww.CRITICAL.Print(msg)
 	case jww.LevelFatal:
-		return &console{"error", ll.Value}
-	default:
-		return &console{"log", ll.Value}
+		jww.FATAL.Print(msg)
 	}
+
+	return newLogFileJS(ll)
 }
 
 // logWriter wraps Javascript callbacks to adhere to the [bindings.LogWriter]
@@ -145,3 +159,172 @@ func EnableGrpcLogs(_ js.Value, args []js.Value) interface{} {
 	bindings.EnableGrpcLogs(&logWriter{args[0].Invoke})
 	return nil
 }
+
+////////////////////////////////////////////////////////////////////////////////
+// Javascript Console Log Listener                                            //
+////////////////////////////////////////////////////////////////////////////////
+
+// console contains the Javascript console object, which provides access to the
+// browser's debugging console. This structure detects logging types and prints
+// it using the correct logging method.
+type console struct {
+	call string
+	js.Value
+}
+
+// Write writes the data to the Javascript console at the level specified by the
+// call.
+func (c *console) Write(p []byte) (n int, err error) {
+	c.Call(c.call, string(p))
+	return len(p), nil
+}
+
+// JsConsoleLogListener redirects log output to the Javascript console.
+type JsConsoleLogListener struct {
+	jww.Threshold
+	js.Value
+
+	trace    *console
+	debug    *console
+	info     *console
+	error    *console
+	warn     *console
+	critical *console
+	fatal    *console
+	def      *console
+}
+
+// NewJsConsoleLogListener initialises a new log listener that listener for the
+// specific threshold and prints the logs to the Javascript console.
+func NewJsConsoleLogListener(threshold jww.Threshold) *JsConsoleLogListener {
+	consoleObj := js.Global().Get("console")
+	return &JsConsoleLogListener{
+		Threshold: threshold,
+		Value:     consoleObj,
+		trace:     &console{"debug", consoleObj},
+		debug:     &console{"log", consoleObj},
+		info:      &console{"info", consoleObj},
+		warn:      &console{"warn", consoleObj},
+		error:     &console{"error", consoleObj},
+		critical:  &console{"error", consoleObj},
+		fatal:     &console{"error", consoleObj},
+		def:       &console{"log", consoleObj},
+	}
+}
+
+// Listen is called for every logging event. This function adheres to the
+// [jwalterweatherman.LogListener] type.
+func (ll *JsConsoleLogListener) Listen(t jww.Threshold) io.Writer {
+	if t < ll.Threshold {
+		return nil
+	}
+
+	switch t {
+	case jww.LevelTrace:
+		return ll.trace
+	case jww.LevelDebug:
+		return ll.debug
+	case jww.LevelInfo:
+		return ll.info
+	case jww.LevelWarn:
+		return ll.warn
+	case jww.LevelError:
+		return ll.error
+	case jww.LevelCritical:
+		return ll.critical
+	case jww.LevelFatal:
+		return ll.fatal
+	default:
+		return ll.def
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Log File Log Listener                                                      //
+////////////////////////////////////////////////////////////////////////////////
+
+// LogFile represents a virtual log file in memory. It contains a circular
+// buffer that limits the log file, overwriting the oldest logs.
+type LogFile struct {
+	name      string
+	threshold jww.Threshold
+	b         *circbuf.Buffer
+}
+
+func NewLogFile(name string, threshold jww.Threshold, maxSize int) (*LogFile, error) {
+	// Create new buffer of the specified size
+	b, err := circbuf.NewBuffer(int64(maxSize))
+	if err != nil {
+		return nil, err
+	}
+
+	return &LogFile{
+		name:      name,
+		threshold: threshold,
+		b:         b,
+	}, nil
+}
+
+// newLogFileJS creates a new Javascript compatible object
+// (map[string]interface{}) that matches the LogFile structure.
+func newLogFileJS(lf *LogFile) map[string]interface{} {
+	logFile := map[string]interface{}{
+		"Name":      js.FuncOf(lf.Name),
+		"Threshold": js.FuncOf(lf.Threshold),
+		"GetFile":   js.FuncOf(lf.GetFile),
+		"MaxSize":   js.FuncOf(lf.MaxSize),
+		"Size":      js.FuncOf(lf.Size),
+	}
+
+	return logFile
+}
+
+// Listen is called for every logging event. This function adheres to the
+// [jwalterweatherman.LogListener] type.
+func (lf *LogFile) Listen(t jww.Threshold) io.Writer {
+	if t < lf.threshold {
+		return nil
+	}
+
+	return lf.b
+}
+
+// Name returns the name of the log file.
+//
+// Returns:
+//  - File name (string).
+func (lf *LogFile) Name(js.Value, []js.Value) interface{} {
+	return lf.name
+}
+
+// Threshold returns the log level threshold used in the file
+//
+// Returns:
+//  - Log level (string).
+func (lf *LogFile) Threshold(js.Value, []js.Value) interface{} {
+	return lf.threshold.String()
+}
+
+// GetFile returns the entire log file.
+//
+// Returns:
+//  - Log file contents (string).
+func (lf *LogFile) GetFile(js.Value, []js.Value) interface{} {
+	return string(lf.b.Bytes())
+}
+
+// MaxSize returns the max size, in bytes, that the log file is allowed to be.
+//
+// Returns:
+//  - Max file size (int).
+func (lf *LogFile) MaxSize(js.Value, []js.Value) interface{} {
+	return lf.b.Size()
+}
+
+// Size returns the current size, in bytes, written to the log file.
+//
+// Returns:
+//  - Current file size (int).
+func (lf *LogFile) Size(js.Value, []js.Value) interface{} {
+	return lf.b.TotalWritten()
+}
diff --git a/wasm/ndf.go b/wasm/ndf.go
index 7e00d3d988973c444afef910876cbecc67606fe5..0d99bc9e58f4680a232d86d29956c51d5be5a85c 100644
--- a/wasm/ndf.go
+++ b/wasm/ndf.go
@@ -23,15 +23,20 @@ import (
 // Parameters:
 //  - args[0] - The URL to download from (string).
 //  - args[1] - The NDF certificate (string).
-// Returns:
-//  - JSON of the NDF (Uint8Array).
+//
+// Returns a promise:
+//  - Resolves to the JSON of the NDF ([ndf.NetworkDefinition]) (Uint8Array).
+//  - Rejected with an error if downloading fails.
 func DownloadAndVerifySignedNdfWithUrl(_ js.Value, args []js.Value) interface{} {
-	ndf, err := bindings.DownloadAndVerifySignedNdfWithUrl(
-		args[0].String(), args[1].String())
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		ndf, err := bindings.DownloadAndVerifySignedNdfWithUrl(
+			args[0].String(), args[1].String())
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(ndf))
+		}
 	}
 
-	return utils.CopyBytesToJS(ndf)
+	return utils.CreatePromise(promiseFn)
 }
diff --git a/wasm/params.go b/wasm/params.go
index accafbc3166e80f6d489510e7cc5f9fc0beabae2..71c7331642a6411dfb56cacaa1bccfabb3259ddf 100644
--- a/wasm/params.go
+++ b/wasm/params.go
@@ -20,7 +20,7 @@ import (
 // to change cMix settings.
 //
 // Returns:
-//  - JSON of [xxdk.CMIXParams] (Uint8Array)
+//  - JSON of [xxdk.CMIXParams] (Uint8Array).
 func GetDefaultCMixParams(js.Value, []js.Value) interface{} {
 	return utils.CopyBytesToJS(bindings.GetDefaultCMixParams())
 }
@@ -30,7 +30,7 @@ func GetDefaultCMixParams(js.Value, []js.Value) interface{} {
 // to change E2E settings.
 //
 // Returns:
-//  - JSON of [xxdk.E2EParams] (Uint8Array)
+//  - JSON of [xxdk.E2EParams] (Uint8Array).
 func GetDefaultE2EParams(js.Value, []js.Value) interface{} {
 	return utils.CopyBytesToJS(bindings.GetDefaultE2EParams())
 }
@@ -40,7 +40,7 @@ func GetDefaultE2EParams(js.Value, []js.Value) interface{} {
 // modify the JSON to change file transfer settings.
 //
 // Returns:
-//  - JSON of [fileTransfer.Params] (Uint8Array)
+//  - JSON of [fileTransfer.Params] (Uint8Array).
 func GetDefaultFileTransferParams(js.Value, []js.Value) interface{} {
 	return utils.CopyBytesToJS(bindings.GetDefaultFileTransferParams())
 }
@@ -50,7 +50,7 @@ func GetDefaultFileTransferParams(js.Value, []js.Value) interface{} {
 // the JSON to change single use settings.
 //
 // Returns:
-//  - JSON of [single.RequestParams] (Uint8Array)
+//  - JSON of [single.RequestParams] (Uint8Array).
 func GetDefaultSingleUseParams(js.Value, []js.Value) interface{} {
 	return utils.CopyBytesToJS(bindings.GetDefaultSingleUseParams())
 }
@@ -60,7 +60,7 @@ func GetDefaultSingleUseParams(js.Value, []js.Value) interface{} {
 // modify the JSON to change single use settings.
 //
 // Returns:
-//  - JSON of [fileTransfer.e2e.Params] (Uint8Array)
+//  - JSON of [fileTransfer.e2e.Params] (Uint8Array).
 func GetDefaultE2eFileTransferParams(js.Value, []js.Value) interface{} {
 	return utils.CopyBytesToJS(bindings.GetDefaultSingleUseParams())
 }
diff --git a/wasm/restlike.go b/wasm/restlike.go
index a12d8ee19b5e6e5501174a88f94ae40ce345f955..94c789b1a8a6852363d0e0afaf277264d7dff52b 100644
--- a/wasm/restlike.go
+++ b/wasm/restlike.go
@@ -23,23 +23,29 @@ import (
 //  - args[2] - JSON of [bindings.RestlikeMessage] (Uint8Array).
 //  - args[3] - JSON of [xxdk.E2EParams] (Uint8Array).
 //
-// Returns:
-//  - JSON of [bindings.RestlikeMessage] (Uint8Array).
-//  - Throws a TypeError if parsing the parameters or making the request fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.RestlikeMessage], which can be
+//    passed into Cmix.WaitForRoundResult to see if the send succeeded
+//    (Uint8Array).
+//  - Rejected with an error if parsing the parameters or making the request
+//    fails.
 func RestlikeRequest(_ js.Value, args []js.Value) interface{} {
 	cmixId := args[0].Int()
 	connectionID := args[1].Int()
 	request := utils.CopyBytesToGo(args[2])
 	e2eParamsJSON := utils.CopyBytesToGo(args[3])
 
-	msg, err := bindings.RestlikeRequest(
-		cmixId, connectionID, request, e2eParamsJSON)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		msg, err := bindings.RestlikeRequest(
+			cmixId, connectionID, request, e2eParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(msg))
+		}
 	}
 
-	return utils.CopyBytesToJS(msg)
+	return utils.CreatePromise(promiseFn)
 }
 
 // RestlikeRequestAuth performs an authenticated restlike request.
@@ -50,21 +56,27 @@ func RestlikeRequest(_ js.Value, args []js.Value) interface{} {
 //  - args[2] - JSON of [bindings.RestlikeMessage] (Uint8Array).
 //  - args[3] - JSON of [xxdk.E2EParams] (Uint8Array).
 //
-// Returns:
-//  - JSON of [bindings.RestlikeMessage] (Uint8Array).
-//  - Throws a TypeError if parsing the parameters or making the request fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.RestlikeMessage], which can be
+//    passed into Cmix.WaitForRoundResult to see if the send succeeded
+//    (Uint8Array).
+//  - Rejected with an error if parsing the parameters or making the request
+//    fails.
 func RestlikeRequestAuth(_ js.Value, args []js.Value) interface{} {
 	cmixId := args[0].Int()
 	authConnectionID := args[1].Int()
 	request := utils.CopyBytesToGo(args[2])
 	e2eParamsJSON := utils.CopyBytesToGo(args[3])
 
-	msg, err := bindings.RestlikeRequestAuth(
-		cmixId, authConnectionID, request, e2eParamsJSON)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		msg, err := bindings.RestlikeRequestAuth(
+			cmixId, authConnectionID, request, e2eParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(msg))
+		}
 	}
 
-	return utils.CopyBytesToJS(msg)
+	return utils.CreatePromise(promiseFn)
 }
diff --git a/wasm/restlikeSingle.go b/wasm/restlikeSingle.go
index bcc5e93a74d67028b0489a2e5148d1c0323b307f..c446da67dcbf127076ba5c50f085026a1e29f17d 100644
--- a/wasm/restlikeSingle.go
+++ b/wasm/restlikeSingle.go
@@ -22,7 +22,7 @@ type restlikeCallback struct {
 }
 
 func (rlc *restlikeCallback) Callback(payload []byte, err error) {
-	rlc.callback(utils.CopyBytesToJS(payload), err.Error())
+	rlc.callback(utils.CopyBytesToJS(payload), utils.JsTrace(err))
 }
 
 // RequestRestLike sends a restlike request to a given contact.
@@ -33,22 +33,28 @@ func (rlc *restlikeCallback) Callback(payload []byte, err error) {
 //  - args[2] - JSON of [bindings.RestlikeMessage] (Uint8Array).
 //  - args[3] - JSON of [single.RequestParams] (Uint8Array).
 //
-// Returns:
-//  - JSON of [restlike.Message] (Uint8Array).
-//  - Throws a TypeError if parsing the parameters or making the request fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.Message], which can be passed into
+//    Cmix.WaitForRoundResult to see if the send succeeded (Uint8Array).
+//  - Rejected with an error if parsing the parameters or making the request
+//    fails.
 func RequestRestLike(_ js.Value, args []js.Value) interface{} {
 	e2eID := args[0].Int()
 	recipient := utils.CopyBytesToGo(args[1])
 	request := utils.CopyBytesToGo(args[2])
 	paramsJSON := utils.CopyBytesToGo(args[3])
 
-	msg, err := bindings.RequestRestLike(e2eID, recipient, request, paramsJSON)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		msg, err := bindings.RequestRestLike(
+			e2eID, recipient, request, paramsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(msg))
+		}
 	}
 
-	return utils.CopyBytesToJS(msg)
+	return utils.CreatePromise(promiseFn)
 }
 
 // AsyncRequestRestLike sends an asynchronous restlike request to a given
@@ -74,12 +80,13 @@ func AsyncRequestRestLike(_ js.Value, args []js.Value) interface{} {
 	paramsJSON := utils.CopyBytesToGo(args[3])
 	cb := &restlikeCallback{utils.WrapCB(args[4], "Callback")}
 
-	err := bindings.AsyncRequestRestLike(
-		e2eID, recipient, request, paramsJSON, cb)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
+	go func() {
+		err := bindings.AsyncRequestRestLike(
+			e2eID, recipient, request, paramsJSON, cb)
+		if err != nil {
+			utils.Throw(utils.TypeError, err)
+		}
+	}()
 
 	return nil
 }
diff --git a/wasm/secrets.go b/wasm/secrets.go
index 5a7b7127af4cfa5d5292f941553580fc672c086c..cf7275efaa2dc01b26257868c60f27d05779317e 100644
--- a/wasm/secrets.go
+++ b/wasm/secrets.go
@@ -20,7 +20,7 @@ import (
 //
 // Parameters:
 //  - args[0] - The size of secret. It should be set to 32, but can be set
-//   higher in certain cases, but not lower (int).
+//    higher in certain cases, but not lower (int).
 //
 // Returns:
 //  - secret password (Uint8Array).
diff --git a/wasm/single.go b/wasm/single.go
index 7a55ffa7a876b5ab60b42d683ce7e852357f5cc3..f14509967745f601499901bff6c7ed3052467d0e 100644
--- a/wasm/single.go
+++ b/wasm/single.go
@@ -31,10 +31,11 @@ import (
 //    is a Javascript object that has functions that implement the
 //    [bindings.SingleUseResponse] interface.
 //
-// Returns:
-//  - JSON [bindings.SingleUseSendReport], which can be passed into
-//    Cmix.WaitForRoundResult to see if the send succeeded (Uint8Array).
-//  - Throws a TypeError if transmission fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.SingleUseSendReport], which can be
+//    passed into Cmix.WaitForRoundResult to see if the send succeeded
+//    (Uint8Array).
+//  - Rejected with an error if transmission fails.
 func TransmitSingleUse(_ js.Value, args []js.Value) interface{} {
 	e2eID := args[0].Int()
 	recipient := utils.CopyBytesToGo(args[1])
@@ -43,14 +44,17 @@ func TransmitSingleUse(_ js.Value, args []js.Value) interface{} {
 	paramsJSON := utils.CopyBytesToGo(args[4])
 	responseCB := &singleUseResponse{utils.WrapCB(args[5], "Callback")}
 
-	report, err := bindings.TransmitSingleUse(
-		e2eID, recipient, tag, payload, paramsJSON, responseCB)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := bindings.TransmitSingleUse(
+			e2eID, recipient, tag, payload, paramsJSON, responseCB)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
 	}
 
-	return utils.CopyBytesToJS(report)
+	return utils.CreatePromise(promiseFn)
 }
 
 // Listen starts a single-use listener on a given tag using the passed in E2e
@@ -115,7 +119,7 @@ type singleUseCallback struct {
 }
 
 func (suc *singleUseCallback) Callback(callbackReport []byte, err error) {
-	suc.callback(utils.CopyBytesToJS(callbackReport), err.Error())
+	suc.callback(utils.CopyBytesToJS(callbackReport), utils.JsTrace(err))
 }
 
 // singleUseResponse wraps Javascript callbacks to adhere to the
@@ -125,5 +129,5 @@ type singleUseResponse struct {
 }
 
 func (sur *singleUseResponse) Callback(responseReport []byte, err error) {
-	sur.callback(utils.CopyBytesToJS(responseReport), err.Error())
+	sur.callback(utils.CopyBytesToJS(responseReport), utils.JsTrace(err))
 }
diff --git a/wasm/single_test.go b/wasm/single_test.go
index 48017c5d3a5d5f6a0459d33cbdbd864be5e615b2..d1276075615ae01015e22ddef0b1063c11bb6ddc 100644
--- a/wasm/single_test.go
+++ b/wasm/single_test.go
@@ -10,6 +10,7 @@
 package wasm
 
 import (
+	"gitlab.com/elixxir/client/bindings"
 	"reflect"
 	"testing"
 )
@@ -34,6 +35,26 @@ func Test_newStopperJS(t *testing.T) {
 	}
 }
 
+// Tests that Stopper has all the methods that [bindings.Stopper] has.
+func Test_StopperMethods(t *testing.T) {
+	stopperType := reflect.TypeOf(&Stopper{})
+	binStopperType := reflect.TypeOf(bindings.Stopper(&stopper{}))
+
+	if binStopperType.NumMethod() != stopperType.NumMethod() {
+		t.Errorf("WASM Stopper object does not have all methods from bindings."+
+			"\nexpected: %d\nreceived: %d",
+			binStopperType.NumMethod(), stopperType.NumMethod())
+	}
+
+	for i := 0; i < binStopperType.NumMethod(); i++ {
+		method := binStopperType.Method(i)
+
+		if _, exists := stopperType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
+
 type stopper struct{}
 
 func (s *stopper) Stop() {}
diff --git a/wasm/ud.go b/wasm/ud.go
index 1c0198a753bfe5c0c2f11ccdd0ef3f06f6cd9618..570419544ff518b22096e87ab8a5e4176c426306 100644
--- a/wasm/ud.go
+++ b/wasm/ud.go
@@ -286,7 +286,7 @@ type udLookupCallback struct {
 }
 
 func (ulc *udLookupCallback) Callback(contactBytes []byte, err error) {
-	ulc.callback(utils.CopyBytesToJS(contactBytes), err.Error())
+	ulc.callback(utils.CopyBytesToJS(contactBytes), utils.JsTrace(err))
 }
 
 // LookupUD returns the public key of the passed ID as known by the user
@@ -300,10 +300,11 @@ func (ulc *udLookupCallback) Callback(contactBytes []byte, err error) {
 //  - args[3] - JSON of [id.ID] for the user to look up (Uint8Array).
 //  - args[4] - JSON of [single.RequestParams] (Uint8Array).
 //
-// Returns:
-//  - JSON of [bindings.SingleUseSendReport], which can be passed into
-//    Cmix.WaitForRoundResult to see if the send succeeded.
-//  - Throws a TypeError if the lookup fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.SingleUseSendReport], which can be
+//    passed into Cmix.WaitForRoundResult to see if the send succeeded
+//    (Uint8Array).
+//  - Rejected with an error if the lookup fails.
 func LookupUD(_ js.Value, args []js.Value) interface{} {
 	e2eID := args[0].Int()
 	udContact := utils.CopyBytesToGo(args[1])
@@ -311,14 +312,17 @@ func LookupUD(_ js.Value, args []js.Value) interface{} {
 	lookupId := utils.CopyBytesToGo(args[3])
 	singleRequestParamsJSON := utils.CopyBytesToGo(args[4])
 
-	report, err := bindings.LookupUD(
-		e2eID, udContact, cb, lookupId, singleRequestParamsJSON)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := bindings.LookupUD(
+			e2eID, udContact, cb, lookupId, singleRequestParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
 	}
 
-	return utils.CopyBytesToJS(report)
+	return utils.CreatePromise(promiseFn)
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -332,7 +336,7 @@ type udSearchCallback struct {
 }
 
 func (usc *udSearchCallback) Callback(contactListJSON []byte, err error) {
-	usc.callback(utils.CopyBytesToJS(contactListJSON), err.Error())
+	usc.callback(utils.CopyBytesToJS(contactListJSON), utils.JsTrace(err))
 }
 
 // SearchUD searches user discovery for the passed Facts. The searchCallback
@@ -347,10 +351,11 @@ func (usc *udSearchCallback) Callback(contactListJSON []byte, err error) {
 //  - args[2] - JSON of [fact.FactList] (Uint8Array).
 //  - args[4] - JSON of [single.RequestParams] (Uint8Array).
 //
-// Returns:
-//  - JSON of [bindings.SingleUseSendReport], which can be passed into
-//    Cmix.WaitForRoundResult to see if the send succeeded.
-//  - Throws a TypeError if the search fails.
+// Returns a promise:
+//  - Resolves to the JSON of the [bindings.SingleUseSendReport], which can be
+//    passed into Cmix.WaitForRoundResult to see if the send succeeded
+//    (Uint8Array).
+//  - Rejected with an error if the search fails.
 func SearchUD(_ js.Value, args []js.Value) interface{} {
 	e2eID := args[0].Int()
 	udContact := utils.CopyBytesToGo(args[1])
@@ -358,12 +363,15 @@ func SearchUD(_ js.Value, args []js.Value) interface{} {
 	factListJSON := utils.CopyBytesToGo(args[3])
 	singleRequestParamsJSON := utils.CopyBytesToGo(args[4])
 
-	report, err := bindings.SearchUD(
-		e2eID, udContact, cb, factListJSON, singleRequestParamsJSON)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	promiseFn := func(resolve, reject func(args ...interface{}) js.Value) {
+		sendReport, err := bindings.SearchUD(
+			e2eID, udContact, cb, factListJSON, singleRequestParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
 	}
 
-	return utils.CopyBytesToJS(report)
+	return utils.CreatePromise(promiseFn)
 }
diff --git a/wasm/ud_test.go b/wasm/ud_test.go
index 37761fa9ef38e233bc675f57a4004be2c68d1ea5..40ae06bc11a71baf48072834eb8917c426d96593 100644
--- a/wasm/ud_test.go
+++ b/wasm/ud_test.go
@@ -34,3 +34,24 @@ func Test_newUserDiscoveryJS(t *testing.T) {
 		}
 	}
 }
+
+// Tests that UserDiscovery has all the methods that [bindings.UserDiscovery]
+// has.
+func Test_UserDiscoveryMethods(t *testing.T) {
+	udType := reflect.TypeOf(&UserDiscovery{})
+	binUdType := reflect.TypeOf(&bindings.UserDiscovery{})
+
+	if binUdType.NumMethod() != udType.NumMethod() {
+		t.Errorf("WASM UserDiscovery object does not have all methods from "+
+			"bindings.\nexpected: %d\nreceived: %d",
+			binUdType.NumMethod(), udType.NumMethod())
+	}
+
+	for i := 0; i < binUdType.NumMethod(); i++ {
+		method := binUdType.Method(i)
+
+		if _, exists := udType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/version.go b/wasm/version.go
index 141be70df02118c3cfe924879ebd5199ed2133d5..affd9d9419d4b861595cd70dfb98bde243b4c46c 100644
--- a/wasm/version.go
+++ b/wasm/version.go
@@ -14,7 +14,7 @@ import (
 	"syscall/js"
 )
 
-// GetVersion returns the xxdk.SEMVER.
+// GetVersion returns the [xxdk.SEMVER].
 //
 // Returns:
 //  - string
@@ -22,7 +22,7 @@ func GetVersion(js.Value, []js.Value) interface{} {
 	return bindings.GetVersion()
 }
 
-// GetGitVersion returns the xxdk.GITVERSION.
+// GetGitVersion returns the [xxdk.GITVERSION].
 //
 // Returns:
 //  - string
@@ -30,7 +30,7 @@ func GetGitVersion(js.Value, []js.Value) interface{} {
 	return bindings.GetGitVersion()
 }
 
-// GetDependencies returns the xxdk.DEPENDENCIES.
+// GetDependencies returns the [xxdk.DEPENDENCIES].
 //
 // Returns:
 //  - string
diff --git a/examples/wasm_exec.js b/wasm_exec.js
similarity index 100%
rename from examples/wasm_exec.js
rename to wasm_exec.js