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