From c83b0bd7e45a0096f4cf6a16ed2dc99115e6bc3d Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Tue, 4 Aug 2026 10:05:23 -0300 Subject: [PATCH 1/2] feat: publish io.parity:truapi-host as an AAR --- .github/workflows/ci-android.yml | 35 +++ .github/workflows/release-android.yml | 76 +++++++ Makefile | 9 +- README.md | 4 +- android/truapi-host/README.md | 85 +++---- android/truapi-host/build.gradle.kts | 30 +-- android/truapi-host/consumer-rules.pro | 2 +- .../truapi-codegen/tests/golden_rust_emit.rs | 9 +- .../src/bin/review-fixtures.rs | 12 + rust/crates/truapi-platform/src/lib.rs | 2 + .../truapi-platform/src/review_fixtures.rs | 211 ++++++++++++++++++ .../truapi-platform/tests/review_fixtures.rs | 104 +++++++++ scripts/codegen.sh | 2 +- 13 files changed, 503 insertions(+), 78 deletions(-) create mode 100644 .github/workflows/ci-android.yml create mode 100644 .github/workflows/release-android.yml create mode 100644 rust/crates/truapi-platform/src/bin/review-fixtures.rs create mode 100644 rust/crates/truapi-platform/src/review_fixtures.rs create mode 100644 rust/crates/truapi-platform/tests/review_fixtures.rs diff --git a/.github/workflows/ci-android.yml b/.github/workflows/ci-android.yml new file mode 100644 index 00000000..878e9309 --- /dev/null +++ b/.github/workflows/ci-android.yml @@ -0,0 +1,35 @@ +# Compile gate for the Android host module: regenerates the UniFFI Kotlin +# bindings and compiles the shell against them whenever the module or the +# native surface changes. No cross-compile, no publishing. +name: ci-android + +on: + pull_request: + paths: + - "android/**" + - "rust/crates/truapi-server/**" + - "rust/crates/truapi-platform/**" + - "rust/crates/uniffi-bindgen-cli/**" + - ".github/workflows/ci-android.yml" + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: "8.9" + - name: Generate UniFFI Kotlin bindings + run: make uniffi-kotlin + - name: Compile the shell against the bindings + run: gradle :truapi-host:assembleRelease diff --git a/.github/workflows/release-android.yml b/.github/workflows/release-android.yml new file mode 100644 index 00000000..50a03f26 --- /dev/null +++ b/.github/workflows/release-android.yml @@ -0,0 +1,76 @@ +# Builds and publishes the Android host AAR (io.parity:truapi-host) to GitHub +# Packages. The AAR bundles the per-ABI Rust core (ws-bridge feature), the +# UniFFI Kotlin bindings generated from the same source, and the Kotlin shell. +name: release-android + +on: + workflow_dispatch: + inputs: + version: + description: "Version to publish (e.g. 0.1.0 or 0.1.0-rc.1)" + required: true + push: + tags: + - "truapi-host-android@*" + +permissions: + contents: read + packages: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Resolve version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" + else + echo "version=${GITHUB_REF_NAME#truapi-host-android@}" >> "$GITHUB_OUTPUT" + fi + + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android + + - uses: Swatinem/rust-cache@v2 + + - name: Install cargo-ndk + run: cargo install cargo-ndk --locked + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: "8.9" + + - name: Generate UniFFI Kotlin bindings + run: make uniffi-kotlin + + - name: Cross-compile libtruapi_server.so for all ABIs + env: + ANDROID_NDK_HOME: ${{ env.ANDROID_NDK_LATEST_HOME }} + run: make android-jni + + - name: Verify AAR inputs + run: | + set -e + for abi in arm64-v8a armeabi-v7a x86 x86_64; do + test -f "android/truapi-host/src/main/jniLibs/$abi/libtruapi_server.so" \ + || { echo "missing .so for $abi"; exit 1; } + done + test -f android/truapi-host/src/main/kotlin/generated/uniffi/truapi_server/truapi_server.kt + + - name: Publish to GitHub Packages + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gradle :truapi-host:publishReleasePublicationToGitHubPackagesRepository \ + -PtruapiHostVersion="${{ steps.version.outputs.version }}" diff --git a/Makefile b/Makefile index 88cd1a2e..6f86f856 100644 --- a/Makefile +++ b/Makefile @@ -115,8 +115,8 @@ uniffi-kotlin: ## Regenerate Kotlin UniFFI bindings from the truapi-server cdyli --out-dir $(UNIFFI_KOTLIN_OUT) # Android ABIs to cross-compile the cdylib for. arm64 + armv7 cover physical -# devices; x86_64 covers the emulator on Intel/Apple-silicon hosts. -ANDROID_ABIS ?= arm64-v8a armeabi-v7a x86_64 +# devices; x86 + x86_64 cover emulators. +ANDROID_ABIS ?= arm64-v8a armeabi-v7a x86 x86_64 ANDROID_JNILIBS := android/truapi-host/src/main/jniLibs android-jni: ## Cross-compile libtruapi_server.so for Android ABIs into jniLibs (needs cargo-ndk + NDK). @@ -124,8 +124,11 @@ android-jni: ## Cross-compile libtruapi_server.so for Android ABIs into jniLibs $(CARGO) ndk $(foreach abi,$(ANDROID_ABIS),-t $(abi)) \ -o $(ANDROID_JNILIBS) \ build --release -p truapi-server --features ws-bridge + # cargo-ndk also copies dependency cdylib intermediates (hash-suffixed, + # statically linked into libtruapi_server.so already); keep only ours. + find $(ANDROID_JNILIBS) -name '*.so' ! -name 'libtruapi_server.so' -delete -android-publish-local: uniffi-kotlin ## Generate Kotlin bindings, then publish the AAR to ~/.m2 (needs Gradle + JDK 17). The AAR does not bundle the cdylib; consumers build it per ABI (see android-jni). +android-publish-local: uniffi-kotlin ## Generate Kotlin bindings, then publish the AAR to ~/.m2 as io.parity:truapi-host:0.0.0-local (needs Gradle + JDK 17). Run `make android-jni` first to bundle the per-ABI cdylibs into the AAR. gradle :truapi-host:publishReleasePublicationToMavenLocal test: ## Run Rust + TypeScript client tests. diff --git a/README.md b/README.md index b215ee1d..e102f615 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,9 @@ js/packages/ (shared host types), `/web` (iframe + Web Worker), `/worker-runtime` ios/truapi-host/ Swift host adapter package over the truapi-server UniFFI core -android/truapi-host/ Kotlin host adapter package over the truapi-server UniFFI core +android/truapi-host/ Kotlin host adapter package over the truapi-server UniFFI core; + published to GitHub Packages as io.parity:truapi-host (AAR with + per-ABI cdylibs; see android/truapi-host/README.md) playground/ Interactive Next.js playground (truapi-playground.dot) hosts/dotli/ dotli host, vendored as a submodule hosts/ios/ polkadot-app-ios-v2, vendored as a submodule (build/test against the core) diff --git a/android/truapi-host/README.md b/android/truapi-host/README.md index 78fbcb0e..56c533e8 100644 --- a/android/truapi-host/README.md +++ b/android/truapi-host/README.md @@ -2,13 +2,11 @@ *Kotlin wrapper around the TrUAPI Rust core (UniFFI). Wire decoding, request routing, and subscription lifecycle stay in the Rust core; products connect through the localhost WebSocket bridge.* -> **Status:** the JitPack distribution described below is the intended packaging but is **not yet wired up** — there is no `jitpack.yml` at the repo root, so the "add the JitPack repo and depend on the tag" flow does not work today. Until it is added, integrate locally with `make android-publish-local` + `mavenLocal()`, or build the module directly. The rest of this doc describes the target design. - -Intended distribution: a Maven artifact built on demand from git tags by [JitPack](https://jitpack.io/), no Maven Central account required on either side. +Distribution: a Maven AAR published to GitHub Packages by the `release-android` workflow. Each release bundles, built from the same source tree: `libtruapi_server.so` for arm64-v8a, armeabi-v7a, x86 and x86_64 (built with the `ws-bridge` feature), the UniFFI Kotlin bindings (`uniffi.truapi_server.*`), and the Kotlin host adapter (`io.parity.truapi.*`). Consumers need no Rust toolchain or NDK. ## Consume -Add the JitPack Maven repository and the artifact to your app's Gradle build: +Add the GitHub Packages repository and the artifact to your app's Gradle build (GitHub Packages requires authentication even for public repos — any GitHub account token with `read:packages` works): ```kotlin // settings.gradle.kts @@ -16,7 +14,13 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven { url = uri("https://jitpack.io") } + maven { + url = uri("https://maven.pkg.github.com/paritytech/truapi") + credentials { + username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") + password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") + } + } } } ``` @@ -24,14 +28,10 @@ dependencyResolutionManagement { ```kotlin // app/build.gradle.kts dependencies { - implementation("com.github.paritytech.truapi:truapi-host:0.1.0") + implementation("io.parity:truapi-host:0.1.0") } ``` -JitPack fetches the tag `0.1.0` from `paritytech/truapi`, runs `make android-publish-local` against it (driven by `jitpack.yml` at the repo root, including UniFFI binding generation), and serves the resulting AAR + POM + sources jar. First fetch takes ~1 minute while JitPack builds; subsequent consumers hit the cache. - -The artifact bundles the Kotlin host adapter (`io.parity.truapi.*`) and the generated UniFFI bindings (`uniffi.truapi_server.*`). It does **not** bundle the native `libtruapi_server.so` cdylib, integrators build that per Android ABI and drop it into their app's `src/main/jniLibs//` (see "Linking the cdylib" below). - The consuming app must declare `android.permission.INTERNET` — the localhost WebSocket bridge binds a `127.0.0.1` TCP socket, which requires it even for loopback. ### Compatibility @@ -238,72 +238,41 @@ main.post { core.disconnect() ``` -## Linking the cdylib +## The cdylib -The native runtime ships separately. JNA looks for `libtruapi_server.so` in the standard `jniLibs` paths; bundle the per-ABI builds under: +The released AAR bundles `libtruapi_server.so` for all four ABIs under its `jni/` directory; JNA loads it from there without any consumer setup. -``` -src/main/jniLibs/arm64-v8a/libtruapi_server.so -src/main/jniLibs/armeabi-v7a/libtruapi_server.so -src/main/jniLibs/x86_64/libtruapi_server.so +When iterating on the core from a source checkout instead of the published artifact, cross-compile into this module's `jniLibs` with: + +```bash +make android-jni # needs cargo-ndk, the NDK, and the four Android rust targets ``` -Cross-build the cdylib for each Android ABI from the truapi monorepo. Two options, pick whichever fits the host app's existing toolchain: +or point the `mozilla-rust-android-gradle` plugin at `rust/crates/truapi-server` from the host app's own build (polkadot-app-android-v2 does this while it still builds from a checkout). -**Option A: `mozilla-rust-android-gradle` plugin.** Recommended if the host app already uses it (polkadot-app-android-v2 does, for `bandersnatch-crypto`). Vendor `paritytech/truapi` as a git submodule, add a small Gradle module that points the plugin at `rust/crates/truapi-server`: +## Maintainers: cutting a release -```kotlin -// app/build.gradle.kts (or a dedicated :truapi-cdylib module) -plugins { - alias(libs.plugins.mozilla.rust.android) -} +Releases are built and published by `.github/workflows/release-android.yml`: -cargo { - module = "/truapi/rust/crates/truapi-server" - libname = "truapi_server" - targets = listOf("arm64", "arm", "x86_64") - profile = "release" - features { defaultAnd(arrayOf("ws-bridge")) } -} +1. Tag the commit to release: `git tag truapi-host-android@0.1.0 && git push origin truapi-host-android@0.1.0` (or run the `release-android` workflow manually with a version input). +2. The workflow cross-compiles the cdylib for all four ABIs, regenerates the Kotlin bindings via the `codegen` cargo profile, and publishes `io.parity:truapi-host:` to GitHub Packages. -tasks.matching { it.name.matches("merge.*JniLibFolders".toRegex()) }.configureEach { - inputs.dir(layout.buildDirectory.dir("rustJniLibs/android")) - dependsOn("cargoBuild") -} -``` - -**Option B: `cargo-ndk` from the command line.** Standalone, no Gradle plugin required: +Host apps that decode `UserConfirmationReview` payloads should regenerate their golden decoder fixtures against the release: ```bash -cargo install cargo-ndk -cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 \ - -o app/src/main/jniLibs \ - build --release -p truapi-server --features ws-bridge +cargo run -p truapi-platform --bin review-fixtures ``` -Both options require the Android NDK installed and the matching Rust targets (`rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android`). - -Pre-built per-ABI `.so` files bundled inside the AAR are tracked as a follow-up so consumers eventually don't need a Rust toolchain at all. - -## Maintainers: cutting a release - -JitPack builds on demand from any git tag in `paritytech/truapi`, so a release is just: - -1. Bump `publicationVersion` in `android/truapi-host/build.gradle.kts`. -2. Commit. Open a PR. Merge. -3. Tag the merge commit with the version: `git tag truapi-host-android@0.1.0 && git push origin truapi-host-android@0.1.0`. - -That's the entire release flow, the iOS Swift Package follows the same pattern. The first consumer to pull the tag will trigger JitPack to build the artifact; subsequent fetches hit the cache. +prints one `NAME=0x` line per review variant. The same hex is pinned by `rust/crates/truapi-platform/tests/review_fixtures.rs`, so a variant reorder or field change fails in this repo's CI before it can break a host's decoder. -For local development, publish into the dev `~/.m2`: +For local development, publish into `~/.m2`: ```bash -gradle :truapi-host:publishReleasePublicationToMavenLocal -# or +make android-jni # optional: bundle the cdylibs into the local AAR make android-publish-local ``` -The artifact lands under `~/.m2/repository/io/parity/truapi-host-android//`. Consumers pointing at `mavenLocal()` can resolve it via `io.parity:truapi-host-android:`. These local coordinates differ from the JitPack consumer coordinate (`com.github.paritytech.truapi:truapi-host:`): JitPack derives the group and artifactId from the repo and Gradle subproject, overriding the `io.parity:truapi-host-android` coordinates set in `build.gradle.kts`. +The artifact lands under `~/.m2/repository/io/parity/truapi-host/0.0.0-local/`; consumers pointing at `mavenLocal()` resolve it as `io.parity:truapi-host:0.0.0-local`. ## Regenerating the UniFFI bindings diff --git a/android/truapi-host/build.gradle.kts b/android/truapi-host/build.gradle.kts index 14de3c76..99c27aab 100644 --- a/android/truapi-host/build.gradle.kts +++ b/android/truapi-host/build.gradle.kts @@ -1,6 +1,6 @@ // TrUAPI Android host adapter. // -// Publishes `io.parity:truapi-host-android` to Maven. Products running in a +// Publishes `io.parity:truapi-host` to Maven. Products running in a // `WebView` connect to the Rust core via its localhost WebSocket bridge // (`TrUAPIHostCore.startWsBridge`); the Rust core (compiled to // `libtruapi_server.so`) handles wire decoding, routing, subscription @@ -58,14 +58,12 @@ dependencies { api("net.java.dev.jna:jna:5.14.0@aar") } -// Coordinates for the local Maven publication (`publishToMavenLocal`). -// Distribution is via JitPack: a git tag drives `jitpack.yml`, and JitPack -// derives the consumer coordinates from the repo + subproject as -// `com.github.paritytech.truapi:truapi-host:`, overriding the group and -// artifactId below. These fields only matter for local testing. +// Coordinates for the Maven publication. Releases are published to GitHub +// Packages by .github/workflows/release-android.yml, which passes the real +// version via -PtruapiHostVersion; local publishes default to 0.0.0-local. val publicationGroup = "io.parity" -val publicationArtifact = "truapi-host-android" -val publicationVersion = "0.1.0" +val publicationArtifact = "truapi-host" +val publicationVersion = (findProperty("truapiHostVersion") as String?) ?: "0.0.0-local" group = publicationGroup version = publicationVersion @@ -114,11 +112,17 @@ publishing { } repositories { - // Maven Local for `gradle publishToMavenLocal` during development - // and for JitPack's build environment (see `jitpack.yml`). - // Consumers fetch the published artifact via JitPack at - // `com.github.paritytech.truapi:truapi-host:` after the - // repo is tagged. + // Maven Local for `gradle publishToMavenLocal` during development. mavenLocal() + // Release target: the release-android workflow publishes here with + // the workflow's GITHUB_TOKEN. + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/paritytech/truapi") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } } } diff --git a/android/truapi-host/consumer-rules.pro b/android/truapi-host/consumer-rules.pro index 759ad9b2..6036d995 100644 --- a/android/truapi-host/consumer-rules.pro +++ b/android/truapi-host/consumer-rules.pro @@ -1,4 +1,4 @@ -# ProGuard / R8 rules applied to consumers of `io.parity:truapi-host-android`. +# ProGuard / R8 rules applied to consumers of `io.parity:truapi-host`. # # JNA reflects into our generated UniFFI types at runtime, so the bindings # package and the public Kotlin surface must survive shrinking. diff --git a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs index 50465b2b..c2c07492 100644 --- a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs +++ b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs @@ -59,7 +59,14 @@ fn produce_rustdoc_json_for_package( package: &str, ) -> PathBuf { let output = Command::new("cargo") - .args(["+nightly", "rustdoc", "-p", package, "--target-dir"]) + .args([ + "+nightly", + "rustdoc", + "-p", + package, + "--lib", + "--target-dir", + ]) .arg(target_dir) .args(["--", "-Z", "unstable-options", "--output-format", "json"]) .current_dir(workspace_root) diff --git a/rust/crates/truapi-platform/src/bin/review-fixtures.rs b/rust/crates/truapi-platform/src/bin/review-fixtures.rs new file mode 100644 index 00000000..69b8f263 --- /dev/null +++ b/rust/crates/truapi-platform/src/bin/review-fixtures.rs @@ -0,0 +1,12 @@ +//! Prints canonical SCALE hex for each [`truapi_platform::UserConfirmationReview`] +//! sample, one `NAME=0x` line per sample, for host apps to pin as decoder +//! fixtures. + +use truapi_platform::review_fixtures; + +fn main() { + for (name, review) in review_fixtures::all() { + let hex = review_fixtures::encode_hex(&review); + println!("{name}=0x{hex}"); + } +} diff --git a/rust/crates/truapi-platform/src/lib.rs b/rust/crates/truapi-platform/src/lib.rs index 25e85f94..255bb601 100644 --- a/rust/crates/truapi-platform/src/lib.rs +++ b/rust/crates/truapi-platform/src/lib.rs @@ -27,6 +27,8 @@ use truapi::latest::{ use truapi::v01::HostAccountSignVrfRequest; use url::Url; +pub mod review_fixtures; + /// Role-neutral runtime configuration supplied by the embedding host. #[non_exhaustive] #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/rust/crates/truapi-platform/src/review_fixtures.rs b/rust/crates/truapi-platform/src/review_fixtures.rs new file mode 100644 index 00000000..a2041a30 --- /dev/null +++ b/rust/crates/truapi-platform/src/review_fixtures.rs @@ -0,0 +1,211 @@ +//! Canonical sample [`UserConfirmationReview`] values for host decoder fixtures. +//! +//! Host apps decode confirmation reviews from opaque SCALE bytes against +//! hand-written models, pinned by golden hex fixtures. These samples are the +//! canonical source of that hex: `cargo run -p truapi-platform --bin +//! review-fixtures` prints one `NAME=0x` line per sample. The golden test +//! in `tests/review_fixtures.rs` pins the same hex so any encoding or +//! variant-order change fails in this repo instead of silently breaking a +//! host's decoder. + +use parity_scale_codec::Encode; +use truapi::latest::{ + AllocatableResource, DerivationIndex, HostSignPayloadData, ProductAccountId, + ProductProofContext, RawPayload, RingLocation, +}; +use truapi::v01::{ + HostAccountSignVrfRequest, HostSignPayloadRequest, HostSignPayloadWithLegacyAccountRequest, + HostSignRawRequest, HostSignRawWithLegacyAccountRequest, LegacyAccountTxPayload, + ProductAccountTxPayload, RingLocationJunction, VrfTranscriptItem, +}; + +use crate::{ + AccountAccessReview, AccountAliasReview, CreateProofReview, CreateTransactionReview, + IdentityDisclosureReview, PreimageSubmitReview, ResourceAllocationReview, SignPayloadReview, + SignRawReview, SignVrfReview, StatementStoreProductSignReview, UserConfirmationReview, +}; + +/// SCALE-encode a review as lowercase hex without a `0x` prefix. +pub fn encode_hex(review: &UserConfirmationReview) -> String { + review.encode().iter().map(|b| format!("{b:02x}")).collect() +} + +/// Sample product account shared by the signing fixtures. +fn product_account() -> ProductAccountId { + ProductAccountId { + dot_ns_identifier: "demo-product.dot".into(), + derivation_index: DerivationIndex::Left(7), + } +} + +/// Sample extrinsic payload shared by the payload-signing fixtures. +fn payload_data() -> HostSignPayloadData { + HostSignPayloadData { + block_hash: vec![0xaa, 0xbb], + block_number: vec![0x2a], + era: vec![0x00], + genesis_hash: vec![0x90, 0xb5], + method: vec![0xde, 0xad, 0xbe, 0xef], + nonce: vec![0x05], + spec_version: vec![0x01], + tip: vec![0x00], + transaction_version: vec![0x02], + signed_extensions: vec!["CheckNonce".into(), "CheckWeight".into()], + version: 4, + asset_id: Some(vec![0xfe]), + metadata_hash: None, + mode: Some(1), + with_signed_transaction: Some(true), + } +} + +/// Sample ring location shared by the alias/proof fixtures. +fn ring_location() -> RingLocation { + RingLocation { + chain_id: [0x33; 32], + junctions: vec![ + RingLocationJunction::PalletInstance(42), + RingLocationJunction::CollectionId(vec![0x07]), + ], + } +} + +/// One named sample per [`UserConfirmationReview`] variant, in variant-index +/// order (multi-shape variants contribute one sample per inner shape). +pub fn all() -> Vec<(&'static str, UserConfirmationReview)> { + vec![ + ( + "SIGN_PAYLOAD_PRODUCT", + UserConfirmationReview::SignPayload(SignPayloadReview::Product( + HostSignPayloadRequest { + account: product_account(), + payload: payload_data(), + }, + )), + ), + ( + "SIGN_PAYLOAD_LEGACY", + UserConfirmationReview::SignPayload(SignPayloadReview::LegacyAccount( + HostSignPayloadWithLegacyAccountRequest { + signer: "5LegacySignerAddr".into(), + payload: payload_data(), + }, + )), + ), + ( + "SIGN_RAW_PRODUCT_BYTES", + UserConfirmationReview::SignRaw(SignRawReview::Product(HostSignRawRequest { + account: product_account(), + payload: RawPayload::Bytes { + bytes: vec![0xca, 0xfe, 0xba, 0xbe], + }, + })), + ), + ( + "SIGN_RAW_LEGACY_PAYLOAD", + UserConfirmationReview::SignRaw(SignRawReview::LegacyAccount( + HostSignRawWithLegacyAccountRequest { + signer: "5LegacySignerAddr".into(), + payload: RawPayload::Payload { + payload: "hello world".into(), + }, + }, + )), + ), + ( + "STATEMENT_STORE_PRODUCT_SIGN", + UserConfirmationReview::StatementStoreProductSign(StatementStoreProductSignReview { + account: product_account(), + payload: vec![0x51, 0x52, 0x53], + }), + ), + ( + "CREATE_TX_PRODUCT", + UserConfirmationReview::CreateTransaction(CreateTransactionReview::Product( + ProductAccountTxPayload { + signer: product_account(), + genesis_hash: [0x11; 32], + call_data: vec![0xde, 0xad, 0xbe, 0xef], + extensions: vec![], + tx_ext_version: 0, + }, + )), + ), + ( + "CREATE_TX_LEGACY", + UserConfirmationReview::CreateTransaction(CreateTransactionReview::LegacyAccount( + LegacyAccountTxPayload { + signer: [0x22; 32], + genesis_hash: [0x11; 32], + call_data: vec![0xde, 0xad, 0xbe, 0xef], + extensions: vec![], + tx_ext_version: 0, + }, + )), + ), + ( + "ACCOUNT_ALIAS", + UserConfirmationReview::AccountAlias(AccountAliasReview { + calling_product_id: "demo-product.dot".into(), + context: ProductProofContext { + product_id: "demo-product.dot".into(), + suffix: DerivationIndex::Left(7), + }, + ring_location: ring_location(), + }), + ), + ( + "CREATE_PROOF", + UserConfirmationReview::CreateProof(CreateProofReview { + calling_product_id: "demo-product.dot".into(), + context: ProductProofContext { + product_id: "demo-product.dot".into(), + suffix: DerivationIndex::Left(7), + }, + ring_location: ring_location(), + message: vec![0x4d, 0x4d], + }), + ), + ( + "IDENTITY_DISCLOSURE", + UserConfirmationReview::IdentityDisclosure(IdentityDisclosureReview { + product_id: "demo-product.dot".into(), + }), + ), + ( + "RESOURCE_ALLOCATION", + UserConfirmationReview::ResourceAllocation(ResourceAllocationReview { + calling_product_id: "demo-product.dot".into(), + resources: vec![ + AllocatableResource::StatementStoreAllowance, + AllocatableResource::AutoSigning, + ], + }), + ), + ( + "PREIMAGE_SUBMIT", + UserConfirmationReview::PreimageSubmit(PreimageSubmitReview { size: 1024 }), + ), + ( + "ACCOUNT_ACCESS", + UserConfirmationReview::AccountAccess(AccountAccessReview { + requesting_product_id: "demo-product.dot".into(), + target_product_id: "other-product.dot".into(), + }), + ), + ( + "SIGN_VRF", + UserConfirmationReview::SignVrf(SignVrfReview { + calling_product_id: "demo-product.dot".into(), + request: HostAccountSignVrfRequest { + account: product_account(), + transcript_label: b"demo-transcript".to_vec(), + items: vec![VrfTranscriptItem { + label: b"item".to_vec(), + value: vec![0x01, 0x02], + }], + }, + }), + ), + ] +} diff --git a/rust/crates/truapi-platform/tests/review_fixtures.rs b/rust/crates/truapi-platform/tests/review_fixtures.rs new file mode 100644 index 00000000..6f93557a --- /dev/null +++ b/rust/crates/truapi-platform/tests/review_fixtures.rs @@ -0,0 +1,104 @@ +//! Golden-fixture pin for `review_fixtures`: any change to the +//! [`UserConfirmationReview`] encoding (variant order, field order, field +//! types) turns this red here, instead of silently breaking a host app's +//! decoder. + +use truapi_platform::UserConfirmationReview; +use truapi_platform::review_fixtures::{all, encode_hex}; + +/// (name, canonical SCALE hex) — regenerate with +/// `cargo run -p truapi-platform --bin review-fixtures` after an intentional +/// protocol change, and notify host apps to regenerate their fixtures. +const EXPECTED: &[(&str, &str)] = &[ + ( + "SIGN_PAYLOAD_PRODUCT", + "00004064656d6f2d70726f647563742e646f74000700000008aabb042a04000890b510deadbeef04050401040004020828436865636b4e6f6e63652c436865636b576569676874040000000104fe0001010000000101", + ), + ( + "SIGN_PAYLOAD_LEGACY", + "000144354c65676163795369676e65724164647208aabb042a04000890b510deadbeef04050401040004020828436865636b4e6f6e63652c436865636b576569676874040000000104fe0001010000000101", + ), + ( + "SIGN_RAW_PRODUCT_BYTES", + "01004064656d6f2d70726f647563742e646f7400070000000010cafebabe", + ), + ( + "SIGN_RAW_LEGACY_PAYLOAD", + "010144354c65676163795369676e657241646472012c68656c6c6f20776f726c64", + ), + ( + "STATEMENT_STORE_PRODUCT_SIGN", + "024064656d6f2d70726f647563742e646f7400070000000c515253", + ), + ( + "CREATE_TX_PRODUCT", + "03004064656d6f2d70726f647563742e646f740007000000111111111111111111111111111111111111111111111111111111111111111110deadbeef0000", + ), + ( + "CREATE_TX_LEGACY", + "03012222222222222222222222222222222222222222222222222222222222222222111111111111111111111111111111111111111111111111111111111111111110deadbeef0000", + ), + ( + "ACCOUNT_ALIAS", + "044064656d6f2d70726f647563742e646f744064656d6f2d70726f647563742e646f740007000000333333333333333333333333333333333333333333333333333333333333333308002a010407", + ), + ( + "CREATE_PROOF", + "054064656d6f2d70726f647563742e646f744064656d6f2d70726f647563742e646f740007000000333333333333333333333333333333333333333333333333333333333333333308002a010407084d4d", + ), + ( + "IDENTITY_DISCLOSURE", + "064064656d6f2d70726f647563742e646f74", + ), + ( + "RESOURCE_ALLOCATION", + "074064656d6f2d70726f647563742e646f74080003", + ), + ("PREIMAGE_SUBMIT", "080004000000000000"), + ( + "ACCOUNT_ACCESS", + "094064656d6f2d70726f647563742e646f74446f746865722d70726f647563742e646f74", + ), + ( + "SIGN_VRF", + "0a4064656d6f2d70726f647563742e646f744064656d6f2d70726f647563742e646f7400070000003c64656d6f2d7472616e73637269707404106974656d080102", + ), +]; + +#[test] +fn encodings_are_stable() { + let samples = all(); + assert_eq!(samples.len(), EXPECTED.len(), "sample count drifted"); + for ((name, review), (expected_name, expected_hex)) in samples.iter().zip(EXPECTED) { + assert_eq!(name, expected_name, "sample order drifted"); + assert_eq!( + &encode_hex(review), + expected_hex, + "{name}: encoding changed — regenerate host fixtures" + ); + } +} + +/// Compile-time tripwire: adding a [`UserConfirmationReview`] variant without +/// a sample fails the `match` below, forcing `review_fixtures::all()` (and +/// host fixtures) to be extended in the same change. +#[test] +fn every_variant_has_a_sample() { + let mut seen = std::collections::BTreeSet::new(); + for (_, review) in all() { + seen.insert(match review { + UserConfirmationReview::SignPayload(_) => "SignPayload", + UserConfirmationReview::SignRaw(_) => "SignRaw", + UserConfirmationReview::StatementStoreProductSign(_) => "StatementStoreProductSign", + UserConfirmationReview::CreateTransaction(_) => "CreateTransaction", + UserConfirmationReview::AccountAlias(_) => "AccountAlias", + UserConfirmationReview::CreateProof(_) => "CreateProof", + UserConfirmationReview::IdentityDisclosure(_) => "IdentityDisclosure", + UserConfirmationReview::ResourceAllocation(_) => "ResourceAllocation", + UserConfirmationReview::PreimageSubmit(_) => "PreimageSubmit", + UserConfirmationReview::AccountAccess(_) => "AccountAccess", + UserConfirmationReview::SignVrf(_) => "SignVrf", + }); + } + assert_eq!(seen.len(), 11, "one sample missing for a review variant"); +} diff --git a/scripts/codegen.sh b/scripts/codegen.sh index 7dedd7eb..a2ee0f9f 100755 --- a/scripts/codegen.sh +++ b/scripts/codegen.sh @@ -29,7 +29,7 @@ cd "$ROOT" unset DYLD_LIBRARY_PATH cargo +nightly rustdoc -p truapi -- -Z unstable-options --output-format json -cargo +nightly rustdoc -p truapi-platform -- -Z unstable-options --output-format json +cargo +nightly rustdoc -p truapi-platform --lib -- -Z unstable-options --output-format json cargo run -p truapi-codegen -- \ --input target/doc/truapi.json \ --output js/packages/truapi/src/generated \ From 84f149ecc810db1fc4049568c73303845a804d40 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Tue, 4 Aug 2026 10:53:00 -0300 Subject: [PATCH 2/2] Temporary release push branch --- .github/workflows/release-android.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-android.yml b/.github/workflows/release-android.yml index 50a03f26..cb0cd587 100644 --- a/.github/workflows/release-android.yml +++ b/.github/workflows/release-android.yml @@ -12,6 +12,11 @@ on: push: tags: - "truapi-host-android@*" + # Temporary, remove once the pipeline is proven: publish an rc build on + # every push to the introducing branch, since workflow_dispatch is only + # available after this file lands on the default branch. + branches: + - "vf/android-host-artifact" permissions: contents: read @@ -28,8 +33,11 @@ jobs: run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" - else + elif [[ "$GITHUB_REF" == refs/tags/* ]]; then echo "version=${GITHUB_REF_NAME#truapi-host-android@}" >> "$GITHUB_OUTPUT" + else + # Branch push (temporary trigger): unique rc per run. + echo "version=0.1.0-rc.${{ github.run_number }}" >> "$GITHUB_OUTPUT" fi - uses: dtolnay/rust-toolchain@stable