From 7a79b9dff3dfdae006d64ea996f4814845f1f391 Mon Sep 17 00:00:00 2001 From: 8e8b2c <138928994+8e8b2c@users.noreply.github.com> Date: Mon, 29 Apr 2024 14:56:52 +0100 Subject: [PATCH] 8e8b2c/rocket server (#8) * feat: rocket server * chore: rationalise ports * feat: metadata & wallets rocket endpoint + e2e * chore: up version client package * chore: add rocket to CI * fix: rm tty from ci * chore: debugging * chore: more debugging * chore: try chown in CI * fix: rocket docket tag --- .github/workflows/e2e.yml | 44 +- .gitignore | 3 +- Cargo.lock | 631 +++++++++++++++++- Cargo.toml | 15 + .../Cargo.toml | 18 + .../src/health.rs | 20 + .../src/holochain.rs | 37 + .../src/lib.rs | 3 + .../src/username_registry.rs | 142 ++++ crates/game_identity_rocket_server/Cargo.toml | 12 + .../game_identity_rocket_server/src/main.rs | 50 ++ .../src/utils/cors.rs | 22 + .../src/utils/mod.rs | 1 + crates/game_identity_rocket_state/Cargo.toml | 16 + .../src/holochain.rs | 195 ++++++ crates/game_identity_rocket_state/src/lib.rs | 1 + crates/game_identity_rocket_types/Cargo.toml | 19 + .../src/endpoint.rs | 49 ++ .../game_identity_rocket_types/src/error.rs | 132 ++++ crates/game_identity_rocket_types/src/lib.rs | 6 + .../game_identity_rocket_types/src/result.rs | 5 + .../username_registry/username_attestation.rs | 15 + .../src/username_attestation.rs | 15 + docker/{ => misc_hc}/Dockerfile | 6 + docker/{ => misc_hc}/prepare_sandbox.sh | 10 +- docker/{ => misc_hc}/readme.md | 0 docker/{ => misc_hc}/run_holo_dev_server.sh | 0 docker/{ => misc_hc}/run_local_services.sh | 0 docker/{ => misc_hc}/run_sandbox.sh | 17 +- docker/rocket/Dockerfile | 5 + package-lock.json | 23 +- packages/client/package.json | 2 +- packages/e2e/package.json | 6 +- packages/e2e/src/main.ts | 7 +- packages/e2e/tests/evm-wallet-binding.test.js | 93 +++ packages/e2e/tests/metadata.test.js | 26 + packages/e2e/tests/username.test.js | 14 +- packages/e2e/tests/utils/rocket.js | 5 + packages/e2e/tests/utils/testcontainers.js | 37 + scripts/Dockerfile | 4 + scripts/build_docker_images.sh | 10 +- scripts/build_rocket_bookworm.sh | 39 ++ scripts/build_rocket_builder.sh | 7 + 43 files changed, 1718 insertions(+), 44 deletions(-) create mode 100644 crates/game_identity_rocket_controllers/Cargo.toml create mode 100644 crates/game_identity_rocket_controllers/src/health.rs create mode 100644 crates/game_identity_rocket_controllers/src/holochain.rs create mode 100644 crates/game_identity_rocket_controllers/src/lib.rs create mode 100644 crates/game_identity_rocket_controllers/src/username_registry.rs create mode 100644 crates/game_identity_rocket_server/Cargo.toml create mode 100644 crates/game_identity_rocket_server/src/main.rs create mode 100644 crates/game_identity_rocket_server/src/utils/cors.rs create mode 100644 crates/game_identity_rocket_server/src/utils/mod.rs create mode 100644 crates/game_identity_rocket_state/Cargo.toml create mode 100644 crates/game_identity_rocket_state/src/holochain.rs create mode 100644 crates/game_identity_rocket_state/src/lib.rs create mode 100644 crates/game_identity_rocket_types/Cargo.toml create mode 100644 crates/game_identity_rocket_types/src/endpoint.rs create mode 100644 crates/game_identity_rocket_types/src/error.rs create mode 100644 crates/game_identity_rocket_types/src/lib.rs create mode 100644 crates/game_identity_rocket_types/src/result.rs rename docker/{ => misc_hc}/Dockerfile (91%) rename docker/{ => misc_hc}/prepare_sandbox.sh (90%) rename docker/{ => misc_hc}/readme.md (100%) rename docker/{ => misc_hc}/run_holo_dev_server.sh (100%) rename docker/{ => misc_hc}/run_local_services.sh (100%) rename docker/{ => misc_hc}/run_sandbox.sh (71%) create mode 100644 docker/rocket/Dockerfile create mode 100644 packages/e2e/tests/evm-wallet-binding.test.js create mode 100644 packages/e2e/tests/utils/rocket.js create mode 100644 scripts/Dockerfile create mode 100755 scripts/build_rocket_bookworm.sh create mode 100755 scripts/build_rocket_builder.sh diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 604aa8d..0432bf4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -41,6 +41,31 @@ jobs: path: workdir overwrite: true + build-rocket: + name: Build Rocket + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - run: scripts/build_rocket_builder.sh + - name: Cargo build in docker + run: | + docker run -i \ + -v "$(pwd):/proj" \ + -w /proj rocket-builder bash -c \ + "cargo build --release --package game_identity_rocket_server" + - run: ls -l target + # - run: ls -l target/release + - run: whoami + - run: sudo chown -R $(whoami) target + - name: Ready artifact dir + run: mkdir artifact && mv target/release/game_identity_rocket_server artifact/ + - uses: actions/upload-artifact@v4 + if: always() + with: + name: rocket + path: artifact + overwrite: true + dna-tests: name: DNA Tests runs-on: ubuntu-20.04 @@ -82,7 +107,9 @@ jobs: e2e: name: Build and run end-to-end tests - needs: build-wasm + needs: + - build-wasm + - build-rocket runs-on: ubuntu-20.04 steps: - name: Checkout the repo @@ -91,12 +118,19 @@ jobs: uses: actions/download-artifact@v4 with: name: happ_workdir - path: docker/happ_workdir + path: docker/misc_hc/happ_workdir + - name: Download rocket binary + uses: actions/download-artifact@v4 + with: + name: rocket + path: rocket-artifact + - run: mv rocket-artifact/game_identity_rocket_server docker/rocket/ - name: Build images run: | - docker build --target local-services -t game-identity/local-services docker - docker build --target authority-agent-sandbox -t game-identity/authority-agent-sandbox docker - docker build --target holo-dev-server -t game-identity/holo-dev-server docker + docker build --target local-services -t game-identity/local-services docker/misc_hc + docker build --target authority-agent-sandbox -t game-identity/authority-agent-sandbox docker/misc_hc + docker build --target holo-dev-server -t game-identity/holo-dev-server docker/misc_hc + docker build -t game-identity/rocket docker/rocket - name: Setup Node uses: actions/setup-node@v3 with: diff --git a/.gitignore b/.gitignore index c68384c..4efe223 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules dist -docker/happ_workdir +docker/misc_hc/happ_workdir +docker/rocket/game_identity_rocket_server diff --git a/Cargo.lock b/Cargo.lock index a3cbd64..0d09640 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,17 @@ dependencies = [ "opaque-debug 0.3.1", ] +[[package]] +name = "again" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05802a5ad4d172eaf796f7047b42d0af9db513585d16d4169660a21613d34b93" +dependencies = [ + "log", + "rand 0.7.3", + "wasm-timer", +] + [[package]] name = "ahash" version = "0.3.8" @@ -568,8 +579,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5" dependencies = [ - "async-stream-impl", + "async-stream-impl 0.2.1", + "futures-core", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl 0.3.5", "futures-core", + "pin-project-lite", ] [[package]] @@ -583,6 +605,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2 1.0.79", + "quote 1.0.35", + "syn 2.0.52", +] + [[package]] name = "async-task" version = "4.7.0" @@ -600,6 +633,21 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -746,6 +794,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +[[package]] +name = "binascii" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" + [[package]] name = "bincode" version = "1.3.3" @@ -1341,6 +1395,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding 2.3.1", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1900,6 +1965,39 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "devise" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8" +dependencies = [ + "devise_codegen", + "devise_core", +] + +[[package]] +name = "devise_codegen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6" +dependencies = [ + "devise_core", + "quote 1.0.35", +] + +[[package]] +name = "devise_core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" +dependencies = [ + "bitflags 2.4.2", + "proc-macro2 1.0.79", + "proc-macro2-diagnostics", + "quote 1.0.35", + "syn 2.0.52", +] + [[package]] name = "diff" version = "0.1.13" @@ -2012,6 +2110,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "ecdsa" version = "0.12.4" @@ -2057,6 +2161,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", "serde", "sha2 0.10.8", "subtle", @@ -2473,6 +2578,20 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" +[[package]] +name = "figment" +version = "0.10.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "752eb150770d6f51eb24d60e3ff84a2c24ccc5e5b3b0f550917ce5ec77c13fe4" +dependencies = [ + "atomic 0.6.0", + "pear", + "serde", + "toml 0.8.2", + "uncased", + "version_check", +] + [[package]] name = "filetime" version = "0.2.23" @@ -2522,7 +2641,7 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "serde", - "strum", + "strum 0.18.0", "strum_macros 0.18.0", ] @@ -2740,6 +2859,59 @@ dependencies = [ "byteorder", ] +[[package]] +name = "game_identity_rocket_controllers" +version = "0.0.1" +dependencies = [ + "bs58 0.5.0", + "game_identity_rocket_state", + "game_identity_rocket_types", + "game_identity_types", + "holo_hash", + "holochain_zome_types", + "rocket", + "rocket_okapi", +] + +[[package]] +name = "game_identity_rocket_server" +version = "0.1.0" +dependencies = [ + "game_identity_rocket_controllers", + "game_identity_rocket_state", + "game_identity_rocket_types", + "rocket", + "rocket_cors", + "rocket_okapi", +] + +[[package]] +name = "game_identity_rocket_state" +version = "0.0.1" +dependencies = [ + "game_identity_rocket_types", + "holochain_client", + "holochain_conductor_api", + "holochain_zome_types", + "rocket", + "serde", +] + +[[package]] +name = "game_identity_rocket_types" +version = "0.0.1" +dependencies = [ + "holochain_client", + "rocket", + "rocket_okapi", + "schemars", + "serde", + "serde_variant", + "snafu", + "strum 0.26.2", + "strum_macros 0.26.2", +] + [[package]] name = "game_identity_tests" version = "0.0.1" @@ -2784,6 +2956,19 @@ dependencies = [ "trilean", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.12.4" @@ -2893,6 +3078,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "gloo-timers" version = "0.2.6" @@ -3263,7 +3454,7 @@ dependencies = [ "shrinkwraprs", "sodoken", "structopt", - "strum", + "strum 0.18.0", "subtle-encoding", "task-motel", "tempfile", @@ -3320,6 +3511,29 @@ dependencies = [ "tracing-futures", ] +[[package]] +name = "holochain_client" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841a137fe0bc68791fb93d9d54aff305094c6402bb6550bdca75ff6d33c35df4" +dependencies = [ + "again", + "anyhow", + "async-trait", + "ed25519-dalek", + "holo_hash", + "holochain_conductor_api", + "holochain_serialized_bytes", + "holochain_state", + "holochain_types", + "holochain_websocket", + "holochain_zome_types", + "parking_lot 0.12.1", + "rand 0.8.5", + "serde", + "url 2.5.0", +] + [[package]] name = "holochain_conductor_api" version = "0.2.6" @@ -3632,7 +3846,7 @@ dependencies = [ "serde_with", "serde_yaml", "shrinkwraprs", - "strum", + "strum 0.18.0", "strum_macros 0.18.0", "tempfile", "thiserror", @@ -3668,7 +3882,7 @@ checksum = "9d2d55f258ce7477b5f0c483598d867d819e63b3500d3996bf9a52ecf6d1ede8" dependencies = [ "holochain_types", "holochain_util", - "strum", + "strum 0.18.0", "strum_macros 0.18.0", "toml 0.5.11", "walkdir", @@ -3774,7 +3988,7 @@ dependencies = [ "serde_bytes", "serde_yaml", "shrinkwraprs", - "strum", + "strum 0.18.0", "subtle", "subtle-encoding", "thiserror", @@ -4025,6 +4239,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg 1.1.0", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -4035,6 +4250,7 @@ checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] [[package]] @@ -4069,6 +4285,12 @@ dependencies = [ "opentelemetry_api", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "input_buffer" version = "0.4.0" @@ -4389,7 +4611,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f95ebed32687df0bc6b7824eb3cce8288749afe8cef3f18bc4ff7041103234d" dependencies = [ - "async-stream", + "async-stream 0.2.1", "base64 0.13.1", "err-derive", "futures-core", @@ -4703,6 +4925,21 @@ dependencies = [ "value-bag", ] +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if 1.0.0", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru" version = "0.8.1" @@ -4780,7 +5017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c769962ac75a6ea437f0922b27834bcccd4c013d591383a16ae5731e3ef0f3f3" dependencies = [ "async-std", - "async-stream", + "async-stream 0.2.1", "dns-parser", "err-derive", "futures-core", @@ -4949,6 +5186,8 @@ dependencies = [ "memchr", "mime", "spin 0.9.8", + "tokio", + "tokio-util", "version_check", ] @@ -5245,6 +5484,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "okapi" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64853d7ab065474e87696f7601cee817d200e86c42e04004e005cb3e20c3c5" +dependencies = [ + "log", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -5595,6 +5846,29 @@ dependencies = [ "sha2 0.9.9", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi 1.0.1", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2 1.0.79", + "proc-macro2-diagnostics", + "quote 1.0.35", + "syn 2.0.52", +] + [[package]] name = "pem" version = "1.1.1" @@ -5808,7 +6082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ "diff", - "yansi", + "yansi 0.5.1", ] [[package]] @@ -5897,6 +6171,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2 1.0.79", + "quote 1.0.35", + "syn 2.0.52", + "version_check", + "yansi 1.0.1", +] + [[package]] name = "proptest" version = "1.4.0" @@ -6396,6 +6683,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ref-cast" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" +dependencies = [ + "proc-macro2 1.0.79", + "quote 1.0.35", + "syn 2.0.52", +] + [[package]] name = "regalloc2" version = "0.5.1" @@ -6646,6 +6953,133 @@ dependencies = [ "serde_bytes", ] +[[package]] +name = "rocket" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7bb57ccb26670d73b6a47396c83139447b9e7878cab627fdfe9ea8da489150" +dependencies = [ + "async-stream 0.3.5", + "async-trait", + "atomic 0.5.3", + "binascii", + "bytes", + "either", + "figment", + "futures", + "indexmap 2.2.5", + "log", + "memchr", + "multer", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "rand 0.8.5", + "ref-cast", + "rocket_codegen", + "rocket_http", + "serde", + "serde_json", + "state", + "tempfile", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "ubyte", + "version_check", + "yansi 1.0.1", +] + +[[package]] +name = "rocket_codegen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2238066abf75f21be6cd7dc1a09d5414a671f4246e384e49fe3f8a4936bd04c" +dependencies = [ + "devise", + "glob", + "indexmap 2.2.5", + "proc-macro2 1.0.79", + "quote 1.0.35", + "rocket_http", + "syn 2.0.52", + "unicode-xid 0.2.4", + "version_check", +] + +[[package]] +name = "rocket_cors" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfac3a1df83f8d4fc96aa41dba3b86c786417b7fc0f52ec76295df2ba781aa69" +dependencies = [ + "http", + "log", + "regex", + "rocket", + "serde", + "serde_derive", + "unicase", + "unicase_serde", + "url 2.5.0", +] + +[[package]] +name = "rocket_http" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a1663694d059fe5f943ea5481363e48050acedd241d46deb2e27f71110389e" +dependencies = [ + "cookie", + "either", + "futures", + "http", + "hyper", + "indexmap 2.2.5", + "log", + "memchr", + "pear", + "percent-encoding 2.3.1", + "pin-project-lite", + "ref-cast", + "serde", + "smallvec 1.13.1", + "stable-pattern", + "state", + "time", + "tokio", + "uncased", +] + +[[package]] +name = "rocket_okapi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e059407ecef9ee2f071fc971e10444fcf942149deb028879d6d8ca61a7ce9edc" +dependencies = [ + "log", + "okapi", + "rocket", + "rocket_okapi_codegen", + "schemars", + "serde", + "serde_json", +] + +[[package]] +name = "rocket_okapi_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb96114e69e5d7f80bfa0948cbc0120016e9b460954abe9eed37e9a2ad3f999" +dependencies = [ + "darling 0.13.4", + "proc-macro2 1.0.79", + "quote 1.0.35", + "rocket_http", + "syn 1.0.109", +] + [[package]] name = "rpassword" version = "5.0.1" @@ -6890,6 +7324,31 @@ dependencies = [ "parking_lot 0.12.1", ] +[[package]] +name = "schemars" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +dependencies = [ + "proc-macro2 1.0.79", + "quote 1.0.35", + "serde_derive_internals", + "syn 1.0.109", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -7057,6 +7516,17 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2 1.0.79", + "quote 1.0.35", + "syn 1.0.109", +] + [[package]] name = "serde_json" version = "1.0.109" @@ -7090,6 +7560,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_variant" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432" +dependencies = [ + "serde", +] + [[package]] name = "serde_with" version = "1.14.0" @@ -7345,6 +7824,27 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +[[package]] +name = "snafu" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75976f4748ab44f6e5332102be424e7c2dc18daeaf7e725f2040c3ebb133512e" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b19911debfb8c2fb1107bc6cb2d61868aaf53a988449213959bb1b5b1ed95f" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.79", + "quote 1.0.35", + "syn 2.0.52", +] + [[package]] name = "socket2" version = "0.4.10" @@ -7433,12 +7933,30 @@ dependencies = [ "unicode_categories", ] +[[package]] +name = "stable-pattern" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" +dependencies = [ + "memchr", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -7529,6 +8047,12 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" + [[package]] name = "strum_macros" version = "0.18.0" @@ -7554,6 +8078,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.79", + "quote 1.0.35", + "rustversion", + "syn 2.0.52", +] + [[package]] name = "subprocess" version = "0.2.9" @@ -8021,6 +8558,18 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -8050,6 +8599,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap 2.2.5", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -8341,6 +8892,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ubyte" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" +dependencies = [ + "serde", +] + [[package]] name = "ucd-trie" version = "0.1.6" @@ -8365,6 +8925,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "unicase" version = "2.7.0" @@ -8374,6 +8944,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicase_serde" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef53697679d874d69f3160af80bc28de12730a985d57bdf2b47456ccb8b11f1" +dependencies = [ + "serde", + "unicase", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -8776,6 +9356,21 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmer" version = "4.2.4" @@ -9023,6 +9618,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -9314,6 +9918,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +dependencies = [ + "is-terminal", +] + [[package]] name = "yasna" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index ab41e04..a9ee7f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ opt-level = "z" opt-level = "z" [workspace] +resolver = "2" members = ["crates/*"] [workspace.dependencies] @@ -21,6 +22,11 @@ holochain = { version = "0.2.6", default-features = false, features = [ "test_utils", ] } holochain_keystore = "0.2.6" +holochain_client = "0.4.8" +holochain_conductor_api = "0.2.6" +holochain_zome_types = "0.2.6" +rocket = "0.5.0" +rocket_okapi = { version = "0.8.0", features = ["swagger"] } [workspace.dependencies.game_identity_types] path = "crates/game_identity_types" @@ -36,3 +42,12 @@ path = "crates/username_registry_coordinator" [workspace.dependencies.signer_coordinator] path = "crates/signer_coordinator" + +[workspace.dependencies.game_identity_rocket_types] +path = "crates/game_identity_rocket_types" + +[workspace.dependencies.game_identity_rocket_state] +path = "crates/game_identity_rocket_state" + +[workspace.dependencies.game_identity_rocket_controllers] +path = "crates/game_identity_rocket_controllers" diff --git a/crates/game_identity_rocket_controllers/Cargo.toml b/crates/game_identity_rocket_controllers/Cargo.toml new file mode 100644 index 0000000..ad27e0d --- /dev/null +++ b/crates/game_identity_rocket_controllers/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "game_identity_rocket_controllers" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "game_identity_rocket_controllers" + +[dependencies] +game_identity_types = { workspace = true } +game_identity_rocket_types = { workspace = true } +game_identity_rocket_state = { workspace = true } +holo_hash = { workspace = true } +holochain_zome_types = { workspace = true } +bs58 = { workspace = true } +rocket = { workspace = true } +rocket_okapi = { workspace = true } diff --git a/crates/game_identity_rocket_controllers/src/health.rs b/crates/game_identity_rocket_controllers/src/health.rs new file mode 100644 index 0000000..f725beb --- /dev/null +++ b/crates/game_identity_rocket_controllers/src/health.rs @@ -0,0 +1,20 @@ +use game_identity_rocket_types::{endpoint::BlankResponse, result::JsonResult}; +use rocket::{get, serde::json::Json}; +use rocket_okapi::openapi; + +/// # +/// ## Confirm the API is alive +/// +/// *This is intended to be called by tests or monitoring services, not the game.* +/// +/// Always returns a blank success response. +#[openapi(tag = "health")] +#[get("/health")] +pub fn bare() -> JsonResult { + Ok(Json(BlankResponse { success: true })) +} + +#[get("/")] +pub fn home() -> String { + String::from("OK") +} diff --git a/crates/game_identity_rocket_controllers/src/holochain.rs b/crates/game_identity_rocket_controllers/src/holochain.rs new file mode 100644 index 0000000..1a689e3 --- /dev/null +++ b/crates/game_identity_rocket_controllers/src/holochain.rs @@ -0,0 +1,37 @@ +use game_identity_rocket_state::holochain::HolochainClientState; +use game_identity_rocket_types::{endpoint::*, result::*}; +use rocket::{get, serde::json::Json, State}; +use rocket_okapi::openapi; + +/// # +/// ## Get the App Info from the connected holochain conductor +/// +/// *This is intended for internal testing only.* +/// +#[openapi(tag = "holochain")] +#[get("/holochain/app_info")] +pub async fn app_info( + holochain_state: &State, +) -> JsonResult { + let app_info = holochain_state.client.app_info.clone(); + + Ok(Json(AppInfoResponse { + installed_app_id: app_info.installed_app_id, + })) +} + +/// # +/// ## Call 'ping' on conductor via signed zome call +/// +/// *This is intended for internal testing only.* +/// +#[openapi(tag = "holochain")] +#[get("/holochain/ping")] +pub async fn ping(holochain_state: &State) -> JsonResult { + holochain_state + .client + .call_zome::<(), ()>("game_identity", "ping", "ping", ()) + .await?; + + Ok(Json(BlankResponse { success: true })) +} diff --git a/crates/game_identity_rocket_controllers/src/lib.rs b/crates/game_identity_rocket_controllers/src/lib.rs new file mode 100644 index 0000000..9756af1 --- /dev/null +++ b/crates/game_identity_rocket_controllers/src/lib.rs @@ -0,0 +1,3 @@ +pub mod health; +pub mod holochain; +pub mod username_registry; diff --git a/crates/game_identity_rocket_controllers/src/username_registry.rs b/crates/game_identity_rocket_controllers/src/username_registry.rs new file mode 100644 index 0000000..9c60c9b --- /dev/null +++ b/crates/game_identity_rocket_controllers/src/username_registry.rs @@ -0,0 +1,142 @@ +use std::collections::HashMap; + +use game_identity_rocket_state::holochain::HolochainClientState; +use game_identity_rocket_types::{endpoint::*, result::*, ApiError}; +use game_identity_types::{ChainWalletSignature, UsernameAttestation, WalletAttestation}; +use holo_hash::AgentPubKey; +use holochain_zome_types::Record; +use rocket::{get, serde::json::Json, State}; +use rocket_okapi::openapi; + +/// # +/// ## Get the list of agents who have registered usernames, alongside any wallet they have bound +/// +/// * This method can only be called if the connected holochain client is the authority agent * +/// +/// This method currently has no pagination and should be used sparingly. +#[openapi(tag = "username_registry")] +#[get("/username_registry")] +pub async fn bare( + holochain_state: &State, +) -> JsonResult { + let records: Vec = holochain_state + .client + .call_zome( + "game_identity", + "username_registry", + "get_all_username_attestations", + (), + ) + .await?; + let items = records + .into_iter() + .map(|record| { + let username_attestation: UsernameAttestation = record + .entry() + .to_app_option() + .map_err(|_| ApiError::Holochain { + message: "Failed to decode entry from response record".into(), + })? + .ok_or(ApiError::Holochain { + message: "Record entry data not present".into(), + })?; + let item = UsernameRegistryItem { + agent_pubkey_b64: username_attestation.agent.to_string(), + username: username_attestation.username, + }; + Ok(item) + }) + .collect::>>()?; + + Ok(Json(UsernameRegistryResponse { + success: true, + items, + })) +} + +/// # +/// ## Get the lists of EVM and Solana wallet public keys that have been bound to this address +/// +#[openapi(tag = "username_registry")] +#[get("/username_registry//wallets")] +pub async fn wallets( + agent_pubkey_b64: String, + holochain_state: &State, +) -> JsonResult { + let agent_pubkey = AgentPubKey::try_from(agent_pubkey_b64.clone()) + .map_err(|_| ApiError::AgentPubKeyB64Invalid { agent_pubkey_b64 })?; + let records: Vec = holochain_state + .client + .call_zome( + "game_identity", + "username_registry", + "get_wallet_attestations_for_agent", + agent_pubkey, + ) + .await?; + let wallet_attestations = records + .into_iter() + .map(|record| { + let wallet_attestation: WalletAttestation = record + .entry() + .to_app_option() + .map_err(|_| ApiError::Holochain { + message: "Failed to decode entry from response record".into(), + })? + .ok_or(ApiError::Holochain { + message: "Record entry data not present".into(), + })?; + Ok(wallet_attestation) + }) + .collect::>>()?; + let evm_addresses = wallet_attestations + .iter() + .filter_map(|wa| match wa.chain_wallet_signature { + ChainWalletSignature::Evm { evm_address, .. } => Some(evm_address.to_string()), + _ => None, + }) + .collect(); + + let solana_addresses = wallet_attestations + .iter() + .filter_map(|wa| match wa.chain_wallet_signature { + ChainWalletSignature::Solana { solana_address, .. } => { + Some(bs58::encode(solana_address.as_bytes()).into_string()) + } + _ => None, + }) + .collect(); + + Ok(Json(UsernameRegistryWalletsResponse { + success: true, + evm_addresses, + solana_addresses, + })) +} + +/// # +/// ## Get the metadata key-values set by a particular agent +/// +#[openapi(tag = "username_registry")] +#[get("/username_registry//metadata")] +pub async fn metadata( + agent_pubkey_b64: String, + holochain_state: &State, +) -> JsonResult { + let agent_pubkey = AgentPubKey::try_from(agent_pubkey_b64.clone()) + .map_err(|_| ApiError::AgentPubKeyB64Invalid { agent_pubkey_b64 })?; + let metadata: HashMap = holochain_state + .client + .call_zome( + "game_identity", + "username_registry", + "get_metadata", + agent_pubkey, + ) + .await?; + + Ok(Json(UsernameRegistryMetadataResponse { + success: true, + metadata, + })) +} diff --git a/crates/game_identity_rocket_server/Cargo.toml b/crates/game_identity_rocket_server/Cargo.toml new file mode 100644 index 0000000..9636f9e --- /dev/null +++ b/crates/game_identity_rocket_server/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "game_identity_rocket_server" +version = "0.1.0" +edition = "2021" + +[dependencies] +rocket = { workspace = true } +rocket_cors = "0.6.0" +rocket_okapi = { workspace = true } +game_identity_rocket_types = { workspace = true } +game_identity_rocket_state = { workspace = true } +game_identity_rocket_controllers = { workspace = true } diff --git a/crates/game_identity_rocket_server/src/main.rs b/crates/game_identity_rocket_server/src/main.rs new file mode 100644 index 0000000..50c394f --- /dev/null +++ b/crates/game_identity_rocket_server/src/main.rs @@ -0,0 +1,50 @@ +use game_identity_rocket_controllers as controllers; +use game_identity_rocket_state as state; +use rocket_cors::catch_all_options_routes; +use rocket_okapi::openapi_get_routes; +use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig}; +use std::env; +use std::error::Error; + +mod utils; + +fn get_docs() -> SwaggerUIConfig { + SwaggerUIConfig { + url: "/openapi.json".to_string(), + deep_linking: true, + ..Default::default() + } +} + +#[rocket::main] +async fn main() -> Result<(), Box> { + // Setup Holochain app agent + let holochain_state = state::holochain::get_holochain_client_state().await?; + + // Setup CORs fairing + let cors = utils::cors::create_cors_fairing()?; + + // Setup & Create rocket app + rocket::build() + .attach(cors.clone()) + .manage(cors) + .manage(holochain_state) + .mount( + "/", + openapi_get_routes![ + controllers::username_registry::bare, + controllers::username_registry::wallets, + controllers::username_registry::metadata, + controllers::health::bare, + controllers::holochain::app_info, + controllers::holochain::ping, + ], + ) + .mount("/swagger", make_swagger_ui(&get_docs())) + .mount("/", catch_all_options_routes()) + .mount("/", rocket::routes![controllers::health::home]) + .launch() + .await?; + + Ok(()) +} diff --git a/crates/game_identity_rocket_server/src/utils/cors.rs b/crates/game_identity_rocket_server/src/utils/cors.rs new file mode 100644 index 0000000..0964303 --- /dev/null +++ b/crates/game_identity_rocket_server/src/utils/cors.rs @@ -0,0 +1,22 @@ +use rocket::http::Method; +use rocket_cors::{AllowedOrigins, Cors, Error}; + +pub fn create_cors_fairing() -> Result { + rocket_cors::CorsOptions { + allowed_origins: AllowedOrigins::all(), + allowed_methods: vec![ + Method::Get, + Method::Post, + Method::Patch, + Method::Options, + Method::Put, + ] + .into_iter() + .map(From::from) + .collect(), + send_wildcard: true, + allow_credentials: false, + ..Default::default() + } + .to_cors() +} diff --git a/crates/game_identity_rocket_server/src/utils/mod.rs b/crates/game_identity_rocket_server/src/utils/mod.rs new file mode 100644 index 0000000..3bd498a --- /dev/null +++ b/crates/game_identity_rocket_server/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod cors; diff --git a/crates/game_identity_rocket_state/Cargo.toml b/crates/game_identity_rocket_state/Cargo.toml new file mode 100644 index 0000000..df68a2f --- /dev/null +++ b/crates/game_identity_rocket_state/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "game_identity_rocket_state" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "game_identity_rocket_state" + +[dependencies] +serde = { workspace = true } +rocket = { workspace = true } +game_identity_rocket_types = { workspace = true } +holochain_client = { workspace = true } +holochain_conductor_api = { workspace = true } +holochain_zome_types = { workspace = true } diff --git a/crates/game_identity_rocket_state/src/holochain.rs b/crates/game_identity_rocket_state/src/holochain.rs new file mode 100644 index 0000000..6d39dc3 --- /dev/null +++ b/crates/game_identity_rocket_state/src/holochain.rs @@ -0,0 +1,195 @@ +use game_identity_rocket_types::{error::*, result::*}; + +use holochain_client::{ + AdminWebsocket, AppAgentWebsocket, AppInfo, AppWebsocket, AuthorizeSigningCredentialsPayload, + ClientAgentSigner, +}; +use holochain_conductor_api::{CellInfo, ProvisionedCell}; +use holochain_zome_types::{CellId, ExternIO}; +use std::env; +use std::fmt::Debug; +use std::sync::Arc; + +pub struct HolochainClient { + pub app_agent_ws: AppAgentWebsocket, + pub app_info: AppInfo, +} + +#[derive(Clone)] +pub struct HolochainClientState { + pub client: Arc, +} + +pub async fn get_holochain_client_state() -> Result { + let client = HolochainClient::new_from_env().await?; + let state = HolochainClientState { + client: Arc::new(client), + }; + Ok(state) +} + +impl HolochainClient { + pub async fn new_from_env() -> Result { + let hostname = + env::var("HOLOCHAIN_HOST_NAME").map_err(|_| ApiError::ServerConfigError { + message: "env variable HOLOCHAIN_HOST_NAME is not defined".into(), + })?; + let admin_ws_port = + env::var("HOLOCHAIN_ADMIN_WS_PORT").map_err(|_| ApiError::ServerConfigError { + message: "env variable HOLOCHAIN_ADMIN_WS_PORT is not defined".into(), + })?; + let admin_ws_url = format!("ws://{}:{}", hostname, admin_ws_port); + let app_ws_port = + env::var("HOLOCHAIN_APP_WS_PORT").map_err(|_| ApiError::ServerConfigError { + message: "env variable HOLOCHAIN_APP_WS_PORT is not defined".into(), + })?; + let app_ws_url = format!("ws://{}:{}", hostname, app_ws_port); + let app_id = env::var("HOLOCHAIN_APP_ID").map_err(|_| ApiError::ServerConfigError { + message: "env variable HOLOCHAIN_APP_ID is not defined".into(), + })?; + let role_names: Vec = env::var("HOLOCHAIN_CELL_ROLES") + .map_err(|_| ApiError::ServerConfigError { + message: "env variable HOLOCHAIN_CELL_ROLES is not defined".into(), + })? + .split(',') + .map(String::from) + .collect(); + if role_names.is_empty() { + return Err(ApiError::ServerConfigError { + message: "HOLOCHAIN_CELL_ROLES is empty".into(), + }); + } + + Self::new(admin_ws_url, app_ws_url, app_id, role_names).await + } + + pub async fn new( + admin_ws_url: String, + app_ws_url: String, + app_id: String, + role_names: Vec, + ) -> Result { + // Setup AdminWebsocket + rocket::debug!("Connecting to AdminWebsocket: {}", &admin_ws_url); + let mut admin_ws = + AdminWebsocket::connect(admin_ws_url) + .await + .map_err(|err| ApiError::Holochain { + message: format!( + "Failed to connect to holochain admin websocket with error: {:?}", + err + ), + })?; + + // Setup AppWebsocket + rocket::debug!("Connecting to AppWebsocket: {}", &app_ws_url); + let mut app_ws = + AppWebsocket::connect(app_ws_url) + .await + .map_err(|err| ApiError::Holochain { + message: format!( + "Failed to connect to holochain app websocket with error: {:?}", + err + ), + })?; + + // Get app info & agent pub key + let app_info = app_ws + .app_info(app_id.clone()) + .await + .map_err(|e| ApiError::Holochain { + message: format!("Failed to get app info for app-id '{}': {:?}", &app_id, e), + })? + .ok_or(ApiError::Holochain { + message: "App info is None".into(), + })?; + + // Setup signing credentials + let mut signer = ClientAgentSigner::default(); + for role_name in role_names { + let cell_id = Self::role_name_to_cell_id(&app_info, role_name)?; + + let credentials = admin_ws + .authorize_signing_credentials(AuthorizeSigningCredentialsPayload { + cell_id: cell_id.clone(), + functions: None, + }) + .await + .unwrap(); + signer.add_credentials(cell_id, credentials); + } + + let app_agent_ws = AppAgentWebsocket::from_existing(app_ws, app_id, signer.into()) + .await + .unwrap(); + Ok(HolochainClient { + app_agent_ws, + app_info, + }) + } + + pub async fn call_zome<'a, I, O>( + &self, + role_name: &str, + zome_name: &str, + fn_name: &str, + payload: I, + ) -> Result + where + I: serde::Serialize + Debug, + O: serde::de::DeserializeOwned + Debug, + { + rocket::debug!("CALL ZOME:\nrole: {role_name}\nzome: {zome_name}\nfn: {fn_name}\n payload: {payload:#?}"); + + // Prepare Unsigned Zome Call + let cell_id = Self::role_name_to_cell_id(&self.app_info, role_name.into())?; + + let payload = ExternIO::encode(payload).map_err(|_| ApiError::Holochain { + message: "Failed to encode zome call payload".into(), + })?; + + // Send Zome Call + + let result = self + .app_agent_ws + .clone() + .call_zome(cell_id.into(), zome_name.into(), fn_name.into(), payload) + .await + .map_err(|e| ApiError::HolochainConductor { + message: format!("{:?}", e), + })?; + + let output = ExternIO::decode(&result).map_err(|_| ApiError::Holochain { + message: "Failed to decode zome call result".into(), + })?; + + Ok(output) + } + + fn role_name_to_cell_id(app_info: &AppInfo, role_name: String) -> Result { + let cell_info = &app_info + .cell_info + .get(&role_name) + .ok_or(ApiError::Holochain { + message: format!("Cell with role_name {} not found", role_name), + })? + .clone(); + + let provisioned_cell_info: Vec = cell_info + .iter() + .filter_map(|ci| -> Option { + match ci { + CellInfo::Provisioned(provisioned_cell) => Some(provisioned_cell.clone()), + _ => None, + } + }) + .collect(); + + let first_provisioned_cell_info = + provisioned_cell_info.first().ok_or(ApiError::Holochain { + message: format!("Provisioned Cell with role_name {} not found", role_name), + })?; + + Ok(first_provisioned_cell_info.clone().cell_id) + } +} diff --git a/crates/game_identity_rocket_state/src/lib.rs b/crates/game_identity_rocket_state/src/lib.rs new file mode 100644 index 0000000..aa39f56 --- /dev/null +++ b/crates/game_identity_rocket_state/src/lib.rs @@ -0,0 +1 @@ +pub mod holochain; diff --git a/crates/game_identity_rocket_types/Cargo.toml b/crates/game_identity_rocket_types/Cargo.toml new file mode 100644 index 0000000..0928dfa --- /dev/null +++ b/crates/game_identity_rocket_types/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "game_identity_rocket_types" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "game_identity_rocket_types" + +[dependencies] +serde = { workspace = true } +serde_variant = "0.1.3" +snafu = "0.8.2" +rocket = { workspace = true } +rocket_okapi = { workspace = true } +schemars = "0.8.16" +holochain_client = { workspace = true } +strum = "0.26.2" +strum_macros = "0.26.2" diff --git a/crates/game_identity_rocket_types/src/endpoint.rs b/crates/game_identity_rocket_types/src/endpoint.rs new file mode 100644 index 0000000..5afe8b2 --- /dev/null +++ b/crates/game_identity_rocket_types/src/endpoint.rs @@ -0,0 +1,49 @@ +use std::collections::HashMap; + +use rocket_okapi::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug)] +pub struct BlankRequest { + pub auth_token: String, +} + +#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug)] +pub struct BlankResponse { + pub success: bool, +} +unsafe impl Send for BlankResponse {} + +#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug)] +pub struct AppInfoResponse { + pub installed_app_id: String, +} +unsafe impl Send for AppInfoResponse {} + +#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug)] +pub struct UsernameRegistryItem { + pub agent_pubkey_b64: String, + pub username: String, +} + +#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug)] +pub struct UsernameRegistryResponse { + pub success: bool, + pub items: Vec, +} +unsafe impl Send for UsernameRegistryResponse {} + +#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug)] +pub struct UsernameRegistryWalletsResponse { + pub success: bool, + pub evm_addresses: Vec, + pub solana_addresses: Vec, +} +unsafe impl Send for UsernameRegistryWalletsResponse {} + +#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Debug)] +pub struct UsernameRegistryMetadataResponse { + pub success: bool, + pub metadata: HashMap, +} +unsafe impl Send for UsernameRegistryMetadataResponse {} diff --git a/crates/game_identity_rocket_types/src/error.rs b/crates/game_identity_rocket_types/src/error.rs new file mode 100644 index 0000000..cdfd9dd --- /dev/null +++ b/crates/game_identity_rocket_types/src/error.rs @@ -0,0 +1,132 @@ +use holochain_client::ConductorApiError; +use rocket::debug; +use rocket::{ + http::{ContentType, Status}, + response::{self, Responder, Response}, + serde::{json::serde_json, Serialize}, + Request, +}; +use rocket_okapi::okapi::openapi3::Responses; +use rocket_okapi::okapi::schemars::{self, Map}; +use rocket_okapi::{gen::OpenApiGenerator, response::OpenApiResponderInner, OpenApiError}; +use schemars::JsonSchema; +use serde_variant::to_variant_name; +use snafu::prelude::*; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +#[derive(Debug, Snafu, JsonSchema, EnumIter, Serialize)] +pub enum ApiError { + #[snafu(display("AgentPubKey is not valid {}", agent_pubkey_b64))] + AgentPubKeyB64Invalid { agent_pubkey_b64: String }, + + #[snafu(display("Holochain Error: {}", message))] + Holochain { message: String }, + + #[snafu(display("Holochain Conductor Api Error: {}", message))] + HolochainConductor { message: String }, + + #[snafu(display("Api IO Error"))] + IoError, + + #[snafu(display("Api Parse Error"))] + ParseError, + + #[snafu(display("Server Config Error: {}", message))] + ServerConfigError { message: String }, +} + +#[allow(clippy::all)] +impl OpenApiResponderInner for ApiError { + fn responses(_generator: &mut OpenApiGenerator) -> Result { + use rocket_okapi::okapi::openapi3::{RefOr, Response as OpenApiReponse}; + + let mut responses = Map::new(); + + let error_descriptions: Vec = ApiError::iter() + .map(|e| format!("| {} | {} |", to_variant_name(&e).unwrap().to_string(), e)) + .collect(); + + responses.insert( + "400".to_string(), + RefOr::Object(OpenApiReponse { + description: format!( + " +### Application Error Responses (HTTP Error Code 400) + +JSON Response Format: +```json +{{ + success: false, + error_type: \"InvalidAuthToken\", + message: \"Invalid auth token\" +}} +``` + +
+
+### All Error Types +| error_type | message | +| ----- | ------- | +{} + +", + error_descriptions.join("\n") + ), + ..Default::default() + }), + ); + + Ok(Responses { + responses, + ..Default::default() + }) + } +} + +#[derive(Serialize, Debug, Clone)] +struct ApiErrorJson { + success: bool, + error_type: String, + message: String, +} + +impl<'r> Responder<'r, 'static> for ApiError { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { + // Prepare error + let error = ApiErrorJson { + success: false, + error_type: to_variant_name(&self).unwrap().to_string(), + message: format!("{}", self), + }; + + // Convert object to json + let body = serde_json::to_string(&error).unwrap(); + + debug!("RESPONSE API ERROR {:?}", error); + + Response::build() + .sized_body(body.len(), std::io::Cursor::new(body)) + .header(ContentType::JSON) + .status(Status::new(400)) + .ok() + } +} + +impl From> for ApiError { + fn from(err: rocket::serde::json::Error) -> Self { + use rocket::serde::json::Error::*; + match err { + Io(_io_error) => ApiError::IoError, + Parse(_raw_data, _parse_error) => ApiError::ParseError, + } + } +} + +impl From for ApiError { + fn from(err: ConductorApiError) -> Self { + ApiError::Holochain { + message: format!("{:?}", err), + } + } +} diff --git a/crates/game_identity_rocket_types/src/lib.rs b/crates/game_identity_rocket_types/src/lib.rs new file mode 100644 index 0000000..0c54dd0 --- /dev/null +++ b/crates/game_identity_rocket_types/src/lib.rs @@ -0,0 +1,6 @@ +pub mod endpoint; +pub use endpoint::*; +pub mod error; +pub use error::*; +pub mod result; +pub use result::*; diff --git a/crates/game_identity_rocket_types/src/result.rs b/crates/game_identity_rocket_types/src/result.rs new file mode 100644 index 0000000..fe6e457 --- /dev/null +++ b/crates/game_identity_rocket_types/src/result.rs @@ -0,0 +1,5 @@ +use super::error::ApiError; + +pub type JsonResult = std::result::Result, ApiError>; + +pub type Result = std::result::Result; diff --git a/crates/game_identity_tests/src/tests/username_registry/username_attestation.rs b/crates/game_identity_tests/src/tests/username_registry/username_attestation.rs index 146cac2..89be6c8 100644 --- a/crates/game_identity_tests/src/tests/username_registry/username_attestation.rs +++ b/crates/game_identity_tests/src/tests/username_registry/username_attestation.rs @@ -268,6 +268,13 @@ async fn all_can_get_username_attestation_for_agent() { let setup = TestSetup::authority_and_alice().await; setup.conductors.exchange_peer_info().await; + // Authority's complete list of attestations initially empty + let all_records1: Vec = setup + .authority_call("username_registry", "get_all_username_attestations", ()) + .await + .unwrap(); + assert_eq!(all_records1, vec![]); + // Authority creates an UsernameAttestation let _: Record = setup .authority_call( @@ -291,6 +298,7 @@ async fn all_can_get_username_attestation_for_agent() { .await .unwrap(); let entry = maybe_record + .clone() .unwrap() .entry() .to_app_option::() @@ -320,6 +328,13 @@ async fn all_can_get_username_attestation_for_agent() { assert_eq!(entry2.username, "username1"); assert_eq!(entry2.agent, fake_agent_pubkey_1()); + + // Authority can see the attestation in their complete list + let all_records2: Vec = setup + .authority_call("username_registry", "get_all_username_attestations", ()) + .await + .unwrap(); + assert_eq!(all_records2, vec![maybe_record.unwrap()]); } #[tokio::test(flavor = "multi_thread")] diff --git a/crates/username_registry_coordinator/src/username_attestation.rs b/crates/username_registry_coordinator/src/username_attestation.rs index 1a3dad1..ad3421b 100644 --- a/crates/username_registry_coordinator/src/username_attestation.rs +++ b/crates/username_registry_coordinator/src/username_attestation.rs @@ -57,6 +57,21 @@ pub fn does_agent_have_username(agent: AgentPubKey) -> ExternResult { Ok(count > 0) } +#[hdk_extern] +pub fn get_all_username_attestations(_: ()) -> ExternResult> { + let my_pubkey = agent_info()?.agent_initial_pubkey; + if my_pubkey != get_authority_agent()? { + return Err(wasm_error!(WasmErrorInner::Host( + "Only callable by authority agent".into() + ))); + } + let username_attestation_type: EntryType = UnitEntryTypes::UsernameAttestation.try_into()?; + let filter = ChainQueryFilter::new() + .include_entries(true) + .entry_type(username_attestation_type); + query(filter) +} + /// Called by the user who wishes to register a username. Returns a UsernameAttestation Record. #[hdk_extern] pub fn sign_username_to_attest(username: String) -> ExternResult { diff --git a/docker/Dockerfile b/docker/misc_hc/Dockerfile similarity index 91% rename from docker/Dockerfile rename to docker/misc_hc/Dockerfile index 8a1d8b6..9bf321b 100644 --- a/docker/Dockerfile +++ b/docker/misc_hc/Dockerfile @@ -37,6 +37,7 @@ COPY prepare_sandbox.sh /prepare_sandbox.sh ARG NETWORK_SEED="" ARG HOLOCHAIN_LAIR_PASSWORD="" +ARG HOLOCHAIN_APP_WS_PORT="3335" RUN bash /prepare_sandbox.sh COPY run_sandbox.sh /run.sh @@ -44,6 +45,7 @@ RUN chmod +x /run.sh ENV BOOTSTRAP_SERVER_OVERRIDE= ENV SIGNAL_SERVER_OVERRIDE= +ENV HOLOCHAIN_ADMIN_WS_PORT= CMD ["/run.sh"] @@ -59,4 +61,8 @@ RUN chmod +x /run.sh ENV BOOTSTRAP_SERVER= ENV SIGNAL_SERVER= +ENV ADMIN_WS_PORT_EXPOSED= +ENV ADMIN_WS_PORT_INTERNAL= +ENV APP_WS_PORT_EXPOSED= +ENV APP_WS_PORT_INTERNAL= CMD ["/run.sh"] diff --git a/docker/prepare_sandbox.sh b/docker/misc_hc/prepare_sandbox.sh similarity index 90% rename from docker/prepare_sandbox.sh rename to docker/misc_hc/prepare_sandbox.sh index 321b1f4..8326b69 100644 --- a/docker/prepare_sandbox.sh +++ b/docker/misc_hc/prepare_sandbox.sh @@ -9,11 +9,12 @@ SANDBOX_PATH=/prebuilt_sandbox SANDBOX_ENV_PATH=$SANDBOX_PATH/.env UNPACKED_HAPP_PATH=$SANDBOX_PATH/unpacked APP_WS_PORT=3333 -APP_ID=game-identity +APP_ID=game_identity # External build args: # NETWORK_SEED # HOLOCHAIN_LAIR_PASSWORD +# HOLOCHAIN_APP_WS_PORT echo "Creating sandbox" mkdir $SANDBOX_PATH @@ -21,6 +22,13 @@ echo $HOLOCHAIN_LAIR_PASSWORD | hc sandbox --piped \ create -n 1 --root $SANDBOX_PATH -d conductor --in-process-lair \ network -b https://bootstrap.holo.host webrtc wss://signal.holo.host +if [ ! -z "$HOLOCHAIN_APP_WS_PORT" ]; then + echo "Adding app port $HOLOCHAIN_APP_WS_PORT" + echo $HOLOCHAIN_LAIR_PASSWORD | hc sandbox --piped \ + call -e $CONDUCTOR_PATH \ + add-app-ws $HOLOCHAIN_APP_WS_PORT +fi + echo "Adding agent and initialising lair keystore" NEWAGENT_STDOUT=`echo $HOLOCHAIN_LAIR_PASSWORD | hc \ sandbox --piped call -e $CONDUCTOR_PATH new-agent` diff --git a/docker/readme.md b/docker/misc_hc/readme.md similarity index 100% rename from docker/readme.md rename to docker/misc_hc/readme.md diff --git a/docker/run_holo_dev_server.sh b/docker/misc_hc/run_holo_dev_server.sh similarity index 100% rename from docker/run_holo_dev_server.sh rename to docker/misc_hc/run_holo_dev_server.sh diff --git a/docker/run_local_services.sh b/docker/misc_hc/run_local_services.sh similarity index 100% rename from docker/run_local_services.sh rename to docker/misc_hc/run_local_services.sh diff --git a/docker/run_sandbox.sh b/docker/misc_hc/run_sandbox.sh similarity index 71% rename from docker/run_sandbox.sh rename to docker/misc_hc/run_sandbox.sh index 3332896..1be8905 100644 --- a/docker/run_sandbox.sh +++ b/docker/misc_hc/run_sandbox.sh @@ -7,14 +7,16 @@ HC_DATA_PATH=/opt/hc_data ACTIVE_PROPS_TAG_PATH=$ACTIVE_SANDBOX_PATH/props_tag ACTIVE_CONDUCTOR_PATH=$ACTIVE_SANDBOX_PATH/conductor ACTIVE_CONDUCTOR_CONFIG_PATH=$ACTIVE_CONDUCTOR_PATH/conductor-config.yaml -APP_WS_PORT_INTERNAL=3333 -APP_WS_PORT_EXTERNAL=3334 MIN_EPHEMERAL_UDP_PORT=40000 MAX_EPHEMERAL_UDP_PORT=40255 # External env args: # BOOTSTRAP_SERVER_OVERRIDE # SIGNAL_SERVER_OVERRIDE +# ADMIN_WS_PORT_INTERNAL +# ADMIN_WS_PORT_EXPOSED +# APP_WS_PORT_INTERNAL +# APP_WS_PORT_EXPOSED if ! [[ -f $ACTIVE_PROPS_TAG_PATH ]]; then echo "First run - copying prebuilt sandbox" @@ -39,10 +41,21 @@ if [ ! -z "$SIGNAL_SERVER_OVERRIDE" ]; then yq -i ".network.transport_pool[0].signal_url = \"$SIGNAL_SERVER_OVERRIDE\"" $ACTIVE_CONDUCTOR_CONFIG_PATH fi +if [ ! -z "$ADMIN_WS_PORT_INTERNAL" ]; then + echo "Overriding admin websocket port" + yq -i ".admin_interfaces[0].driver.port = $ADMIN_WS_PORT_INTERNAL" $ACTIVE_CONDUCTOR_CONFIG_PATH + yq -i ".admin_interfaces[0].driver.allowed_origins = \"*\"" $ACTIVE_CONDUCTOR_CONFIG_PATH +fi + echo "Pinning ephemeral UDP port range $MIN_EPHEMERAL_UDP_PORT-$MAX_EPHEMERAL_UDP_PORT" yq -i ".network.tuning_params.tx5_min_ephemeral_udp_port = \"$MIN_EPHEMERAL_UDP_PORT\"" $ACTIVE_CONDUCTOR_CONFIG_PATH yq -i ".network.tuning_params.tx5_max_ephemeral_udp_port = \"$MAX_EPHEMERAL_UDP_PORT\"" $ACTIVE_CONDUCTOR_CONFIG_PATH +echo "Forwarding exposed port $ADMIN_WS_PORT_EXPOSED to local admin port $ADMIN_WS_PORT_INTERNAL" +socat TCP-LISTEN:$ADMIN_WS_PORT_EXPOSED,fork TCP:localhost:$ADMIN_WS_PORT_INTERNAL & +echo "Forwarding exposed port $APP_WS_PORT_EXPOSED to local admin port $APP_WS_PORT_INTERNAL" +socat TCP-LISTEN:$APP_WS_PORT_EXPOSED,fork TCP:localhost:$APP_WS_PORT_INTERNAL & + echo "Starting holochain" echo $HOLOCHAIN_LAIR_PASSWORD | RUST_LOG=debug holochain \ -c $ACTIVE_CONDUCTOR_CONFIG_PATH --piped \ No newline at end of file diff --git a/docker/rocket/Dockerfile b/docker/rocket/Dockerfile new file mode 100644 index 0000000..18dc0de --- /dev/null +++ b/docker/rocket/Dockerfile @@ -0,0 +1,5 @@ +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y libssl3 libssl-dev ca-certificates libpq-dev +COPY game_identity_rocket_server /usr/local/bin/game_identity_rocket_server +RUN chmod +x /usr/local/bin/game_identity_rocket_server +ENTRYPOINT ["/usr/local/bin/game_identity_rocket_server"] diff --git a/package-lock.json b/package-lock.json index 2fa3bd1..4e94174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -606,7 +606,6 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/@bitgo/blake2b/-/blake2b-3.2.4.tgz", "integrity": "sha512-46PEgEVPxecNJ/xczggIllSxIkFIvvbVM0OfIDdNJ5qpFHUeBCkNIiGdzC3fYZlsv7bVTdUZOj79GcFBLMYBqA==", - "dev": true, "dependencies": { "@bitgo/blake2b-wasm": "^3.2.3", "nanoassert": "^2.0.0" @@ -616,7 +615,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/@bitgo/blake2b-wasm/-/blake2b-wasm-3.2.3.tgz", "integrity": "sha512-NaurBrMaEpjfg7EdUJgW/c6byt27O6q1ZaxB5Ita10MjjYjUu0SyYF4q7JPNxpHF/lMxb0YZakOxigbDBu9Jjw==", - "dev": true, "dependencies": { "nanoassert": "^1.0.0" } @@ -624,8 +622,7 @@ "node_modules/@bitgo/blake2b-wasm/node_modules/nanoassert": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", - "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==", - "dev": true + "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==" }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", @@ -1087,10 +1084,9 @@ "link": true }, "node_modules/@holochain/client": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@holochain/client/-/client-0.16.10.tgz", - "integrity": "sha512-Urgx6vXNux2XD/4+eOxou2FKwlUBu/g/BxUSZLX6wEaYWH9wDYXR5f3VToUrAhXGF5ssc08jb8jLiQZ23k62EQ==", - "dev": true, + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@holochain/client/-/client-0.16.11.tgz", + "integrity": "sha512-QXPQeV6poCXedqpPxjAnYJcZE53Z4v/lbkeXfdxTcZRYjRILuxsT40zLDRWQR5A+NH+Cz0iTnLPmi4v9vpZY8g==", "dependencies": { "@bitgo/blake2b": "^3.2.4", "@holochain/serialization": "^0.1.0-beta-rc.3", @@ -5323,14 +5319,12 @@ "node_modules/libsodium": { "version": "0.7.13", "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.13.tgz", - "integrity": "sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw==", - "dev": true + "integrity": "sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw==" }, "node_modules/libsodium-wrappers": { "version": "0.7.13", "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz", "integrity": "sha512-kasvDsEi/r1fMzKouIDv7B8I6vNmknXwGiYodErGuESoFTohGSKZplFtVxZqHaoQ217AynyIFgnOVRitpHs0Qw==", - "dev": true, "dependencies": { "libsodium": "^0.7.13" } @@ -5571,8 +5565,7 @@ "node_modules/nanoassert": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", - "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", - "dev": true + "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -7443,7 +7436,8 @@ "version": "0.0.0", "dependencies": { "@holo-host/web-sdk": "0.6.17-prerelease", - "@holochain-game-identity/client": "file:../client" + "@holochain-game-identity/client": "file:../client", + "@holochain/client": "^0.16.7" }, "devDependencies": { "concurrently": "^8.2.2", @@ -7453,6 +7447,7 @@ "jest-puppeteer": "^10.0.1", "testcontainers": "^10.7.2", "typescript": "^5.2.2", + "viem": "^2.8.13", "vite": "^5.1.6" } }, diff --git a/packages/client/package.json b/packages/client/package.json index ae1f77b..4dcb72e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@holochain-game-identity/client", - "version": "1.0.5", + "version": "1.0.6", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 63d40e8..f8c6d5b 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -7,12 +7,13 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "test": "DEBUG=e2e npx jest --maxWorkers=1 --detectOpenHandles", + "test": "DEBUG='e2e' npx jest --maxWorkers=1 --detectOpenHandles", "e2e": "concurrently -r --kill-others \"npm run dev\" \"npm run test\"" }, "dependencies": { "@holo-host/web-sdk": "0.6.17-prerelease", - "@holochain-game-identity/client": "file:../client" + "@holochain-game-identity/client": "file:../client", + "@holochain/client": "^0.16.7" }, "devDependencies": { "concurrently": "^8.2.2", @@ -22,6 +23,7 @@ "jest-puppeteer": "^10.0.1", "testcontainers": "^10.7.2", "typescript": "^5.2.2", + "viem": "^2.8.13", "vite": "^5.1.6" } } diff --git a/packages/e2e/src/main.ts b/packages/e2e/src/main.ts index d296ee5..4db9c40 100644 --- a/packages/e2e/src/main.ts +++ b/packages/e2e/src/main.ts @@ -1,5 +1,5 @@ import "./style.css"; -import { AppAgentWebsocket } from "@holochain/client"; +import { AppAgentWebsocket, encodeHashToBase64 } from "@holochain/client"; import { HolochainGameIdentityClient } from "@holochain-game-identity/client"; import WebSdkApi, { ChaperoneState } from "@holo-host/web-sdk"; @@ -18,6 +18,8 @@ function untilSignedIn(holoClient: WebSdkApi) { }); } +const global = window as any; + async function createClient() { const holoClient = await WebSdkApi.connect({ chaperoneUrl: "http://localhost:24274", @@ -26,7 +28,7 @@ async function createClient() { // Hand off the puppeteer to fill out iframe await untilSignedIn(holoClient); - + global.agentPubKeyB64 = encodeHashToBase64(holoClient.myPubKey); const gameIdentityClient = new HolochainGameIdentityClient( holoClient as unknown as AppAgentWebsocket ); @@ -34,7 +36,6 @@ async function createClient() { return gameIdentityClient; } -const global = window as any; global.gameIdentityClientProm = createClient().then((client) => { global.gameIdentityClient = client; }); diff --git a/packages/e2e/tests/evm-wallet-binding.test.js b/packages/e2e/tests/evm-wallet-binding.test.js new file mode 100644 index 0000000..04336b5 --- /dev/null +++ b/packages/e2e/tests/evm-wallet-binding.test.js @@ -0,0 +1,93 @@ +const { privateKeyToAccount } = require("viem/accounts"); +const { startTestContainers } = require("./utils/testcontainers"); +const { loadPageAndRegister } = require("./utils/holo"); +const { rocketFetch } = require("./utils/rocket"); + +describe("EVM Wallet Binding", () => { + let testContainers; + beforeEach(async () => { + testContainers = await startTestContainers(); + }, 60_000); + afterEach(async () => { + await Promise.all([testContainers.stop(), jestPuppeteer.resetPage()]); + }); + + it("should register only one username", async () => { + debug("Started test"); + await loadPageAndRegister("test@test.com", "test1234"); + debug("Loaded chaperone and registered agent"); + + // Starts with no username + await expect( + page.evaluate(() => window.gameIdentityClient.getBoundWallets()) + ).resolves.toEqual([]); + debug("Checked bound wallets initially empty"); + + const agentPubKeyB64 = await page.evaluate(() => window.agentPubKeyB64); + + await expect( + rocketFetch(`username_registry/${agentPubKeyB64}/wallets`) + ).resolves.toEqual({ + success: true, + evm_addresses: [], + solana_addresses: [], + }); + debug("Checked rocket serves empty wallet list"); + + // Setup EVM signer in memory + // First account of seed phrase: test test test test test test test test test test test junk + let evmPrivateKey = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + const account = privateKeyToAccount(evmPrivateKey); + + // Get and sign the binding message + const message = await page.evaluate( + (address) => + window.gameIdentityClient.getEvmWalletBindingMessage(address), + account.address + ); + const evmSignature = await account.signMessage({ message }); + + // Submit the signature + await expect( + page.evaluate( + (addr, sig) => + window.gameIdentityClient.submitEvmWalletBinding(addr, sig), + account.address, + evmSignature + ) + ).resolves.toBeUndefined(); + debug("Submitted wallet binding signature"); + + // Poll bound wallets until defined (gossiping) + while (true) { + const boundWallets = await page.evaluate(() => + window.gameIdentityClient.getBoundWallets() + ); + if (boundWallets.length) { + expect(boundWallets).toEqual([ + { type: "evm", checksummedAddress: account.address }, + ]); + break; + } + } + debug("Polled bound wallets until correctly gossiped"); + + // Poll rocket until bound wallet gossiped + while (true) { + const data = await rocketFetch( + `username_registry/${agentPubKeyB64}/wallets` + ); + if (data.evm_addresses.length > 0) { + expect(data).toEqual({ + success: true, + evm_addresses: [account.address], + solana_addresses: [], + }); + break; + } + await new Promise((r) => setTimeout(r, 500)); + } + debug("Polled bound wallets on rocket until correctly gossiped"); + }, 120_000); +}); diff --git a/packages/e2e/tests/metadata.test.js b/packages/e2e/tests/metadata.test.js index e934439..e2a658e 100644 --- a/packages/e2e/tests/metadata.test.js +++ b/packages/e2e/tests/metadata.test.js @@ -1,5 +1,6 @@ const { startTestContainers } = require("./utils/testcontainers"); const { loadPageAndRegister } = require("./utils/holo"); +const { rocketFetch } = require("./utils/rocket"); describe("metadata", () => { let testContainers; @@ -22,6 +23,16 @@ describe("metadata", () => { ).resolves.toBeNull(); debug("Checked profile-picture metadata initially null"); + const agentPubKeyB64 = await page.evaluate(() => window.agentPubKeyB64); + + await expect( + rocketFetch(`username_registry/${agentPubKeyB64}/metadata`) + ).resolves.toEqual({ + success: true, + metadata: {}, + }); + debug("Checked rocket serves empty metadata"); + await expect( page.evaluate(() => window.gameIdentityClient.setMetadata("profile-picture", "image1.jpg") @@ -66,5 +77,20 @@ describe("metadata", () => { page.evaluate(() => window.gameIdentityClient.getMetadata("location")) ).resolves.toBe("moon"); debug("Checked location metadata unchanged"); + + // Poll metadata until defined (gossiping) + while (true) { + const data = await rocketFetch( + `username_registry/${agentPubKeyB64}/metadata` + ); + if ( + data.metadata.location === "moon" && + data.metadata["profile-picture"] === "image2.jpg" + ) { + break; + } + await new Promise((r) => setTimeout(r, 500)); + } + debug("Polled rocket metadata until metadata matched expected"); }, 120_000); }); diff --git a/packages/e2e/tests/username.test.js b/packages/e2e/tests/username.test.js index 86e616b..ca53b1c 100644 --- a/packages/e2e/tests/username.test.js +++ b/packages/e2e/tests/username.test.js @@ -1,5 +1,6 @@ const { startTestContainers } = require("./utils/testcontainers"); const { loadPageAndRegister } = require("./utils/holo"); +const { rocketFetch } = require("./utils/rocket"); describe("username", () => { let testContainers; @@ -21,6 +22,12 @@ describe("username", () => { ).resolves.toBeNull(); debug("Checked username initially null"); + await expect(rocketFetch("username_registry")).resolves.toEqual({ + success: true, + items: [], + }); + debug("Checked rocket serves empty user list"); + // First register succeeds await expect( page.evaluate(() => @@ -29,7 +36,7 @@ describe("username", () => { ).resolves.toBeUndefined(); debug("Registered username"); - // Poll username until define (gossiping) + // Poll username until defined (gossiping) while (true) { const result = await page.evaluate(() => window.gameIdentityClient.getUsername() @@ -48,5 +55,10 @@ describe("username", () => { ) ).rejects.toSatisfy((error) => error.message.includes("InvalidCommit")); debug("Checked second registration fails"); + + await expect(rocketFetch("username_registry")).resolves.toSatisfy( + (data) => data.items.length === 1 && data.items[0].username === "test1234" + ); + debug("Checked rocket serves single user"); }, 120_000); }); diff --git a/packages/e2e/tests/utils/rocket.js b/packages/e2e/tests/utils/rocket.js new file mode 100644 index 0000000..0329b37 --- /dev/null +++ b/packages/e2e/tests/utils/rocket.js @@ -0,0 +1,5 @@ +module.exports.rocketFetch = async (path) => { + const res = await fetch(`http://localhost:8000/${path}`); + const data = await res.json(); + return data; +}; diff --git a/packages/e2e/tests/utils/testcontainers.js b/packages/e2e/tests/utils/testcontainers.js index 5ca9339..2b6df67 100644 --- a/packages/e2e/tests/utils/testcontainers.js +++ b/packages/e2e/tests/utils/testcontainers.js @@ -1,4 +1,5 @@ const { GenericContainer, Network } = require("testcontainers"); +const createDebug = require("debug"); const BOOTSTRAP_PORT = 51804; const SIGNAL_PORT = 51805; @@ -20,6 +21,16 @@ function startAuthorityContainer(network, localServicesIp) { .withEnvironment({ BOOTSTRAP_SERVER_OVERRIDE: `http://${localServicesIp}:${BOOTSTRAP_PORT}`, SIGNAL_SERVER_OVERRIDE: `ws://${localServicesIp}:${SIGNAL_PORT}`, + ADMIN_WS_PORT_INTERNAL: 3333, + ADMIN_WS_PORT_EXPOSED: 3334, + APP_WS_PORT_INTERNAL: 3335, + APP_WS_PORT_EXPOSED: 3336, + }) + .withLogConsumer((stream) => { + const logInfo = createDebug("e2e:authority:info"); + const logErr = createDebug("e2e:authority:error"); + stream.on("data", logInfo); + stream.on("err", logErr); }) .withCommand("/run.sh") .start(); @@ -40,6 +51,29 @@ function startHoloContainer(network, localServicesIp) { .start(); } +function startRocketContainer(network, authorityIp) { + return new GenericContainer("game-identity/rocket") + .withExposedPorts({ host: 8000, container: 8000 }) + .withNetwork(network) + .withEnvironment({ + ROCKET_ADDRESS: "0.0.0.0", + ROCKET_LOG_LEVEL: "debug", + HOLOCHAIN_HOST_NAME: authorityIp, + HOLOCHAIN_ADMIN_WS_PORT: 3334, + HOLOCHAIN_APP_WS_PORT: 3336, + HOLOCHAIN_APP_ID: "game_identity", + HOLOCHAIN_CELL_ROLES: "game_identity", + }) + .withLogConsumer((stream) => { + const logInfo = createDebug("e2e:rocket:info"); + const logErr = createDebug("e2e:rocket:error"); + stream.on("data", logInfo); + stream.on("err", logErr); + }) + .withCommand("/usr/local/bin/game_identity_rocket_server") + .start(); +} + module.exports.startTestContainers = async () => { debug("Begin test container setup"); network = await new Network().start(); @@ -55,6 +89,8 @@ module.exports.startTestContainers = async () => { ); const holoContainerProm = startHoloContainer(network, localServiceIp); authorityContainer = await authorityContainerProm; + const authorityIp = authorityContainer.getIpAddress(network.getName()); + const rocketContainer = await startRocketContainer(network, authorityIp); holoContainer = await holoContainerProm; debug("Started authority-agent-sandbox and holo-dev-server"); debug("Finished test container setup"); @@ -65,6 +101,7 @@ module.exports.startTestContainers = async () => { localServicesContainer.stop(), authorityContainer.stop(), holoContainer.stop(), + rocketContainer.stop(), ]); await network.stop(); debug("Finished test container teardown"); diff --git a/scripts/Dockerfile b/scripts/Dockerfile new file mode 100644 index 0000000..096e5a5 --- /dev/null +++ b/scripts/Dockerfile @@ -0,0 +1,4 @@ +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y libssl3 libssl-dev ca-certificates libpq-dev +COPY game_identity_rocket_server /usr/local/bin/game_identity_rocket_server +ENTRYPOINT ["/usr/local/bin/game_identity_rocket_server"] diff --git a/scripts/build_docker_images.sh b/scripts/build_docker_images.sh index 43f8fd5..30a53b5 100755 --- a/scripts/build_docker_images.sh +++ b/scripts/build_docker_images.sh @@ -2,9 +2,9 @@ cd -- "$( dirname -- "$0" )" cd .. -rm -rf docker/happ_workdir -cp -r workdir docker/happ_workdir +rm -rf docker/misc_hc/happ_workdir +cp -r workdir docker/misc_hc/happ_workdir -docker build --target local-services -t game-identity/local-services docker -docker build --target authority-agent-sandbox -t game-identity/authority-agent-sandbox docker -docker build --target holo-dev-server -t game-identity/holo-dev-server docker +docker build --target local-services -t game-identity/local-services docker/misc_hc +docker build --target authority-agent-sandbox -t game-identity/authority-agent-sandbox docker/misc_hc +docker build --target holo-dev-server -t game-identity/holo-dev-server docker/misc_hc diff --git a/scripts/build_rocket_bookworm.sh b/scripts/build_rocket_bookworm.sh new file mode 100755 index 0000000..191f15b --- /dev/null +++ b/scripts/build_rocket_bookworm.sh @@ -0,0 +1,39 @@ +#! /bin/bash +cd -- "$( dirname -- "$0" )" + +cd .. +PROJ_DIR=$(pwd) +CACHE_DIR=$HOME/.cache +DOCKER_HOST_TARGET=$CACHE_DIR/game_identity_target_bookworm +DOCKER_HOST_CARGO_HOME=$CACHE_DIR/bookworm_cargo_home +BUILDER_ID_DIR=$CACHE_DIR/rocket-builder-id +CONTAINER_CARGO_HOME=/usr/local/cargo/ +CONTAINER_TARGET=/target + + +if ! [ -d $DOCKER_HOST_CARGO_HOME ]; then + echo "Copying cargo home..." + id=$(docker create rocket-builder) + mkdir -p $HOME/.cache/ + docker cp $id:$CONTAINER_CARGO_HOME $DOCKER_HOST_CARGO_HOME + docker rm $id +fi + +if ! [ -f $BUILDER_ID_DIR ]; then + echo "Creating builder container..." + docker create -i -t \ + -v "$PROJ_DIR:/proj" \ + -v "$DOCKER_HOST_CARGO_HOME:$CONTAINER_CARGO_HOME" \ + -v "$DOCKER_HOST_TARGET:$CONTAINER_TARGET" \ + -w /proj \ + rocket-builder > $BUILDER_ID_DIR +fi + +id=$(head -n 1 $BUILDER_ID_DIR) +docker start $id +echo "Starting build..." +docker exec $id bash -c \ + "CARGO_TARGET_DIR=/target cargo build --release --package game_identity_rocket_server" + +echo "Copying built binary to docker context..." +cp $DOCKER_HOST_TARGET/release/game_identity_rocket_server $PROJ_DIR/docker/rocket/ diff --git a/scripts/build_rocket_builder.sh b/scripts/build_rocket_builder.sh new file mode 100755 index 0000000..7c018ee --- /dev/null +++ b/scripts/build_rocket_builder.sh @@ -0,0 +1,7 @@ +docker build --tag rocket-builder - <<'EOT' +FROM rust:1.75.0-bookworm +RUN apt-get update && apt-get install -y libssl3 libssl-dev build-essential git libpq-dev +RUN wget https://go.dev/dl/go1.21.1.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.21.1.linux-amd64.tar.gz +ENV PATH="${PATH}:/usr/local/go/bin" +RUN export PATH="${PATH}:/usr/local/go/bin" +EOT