Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ddf0786
feat(core): implement ring-vrf signing
pgherveou Jul 17, 2026
d59fdb1
Merge branch 'main' into feat/signing-host-ring-vrf
pgherveou Jul 20, 2026
5911457
Merge branch 'main' into feat/signing-host-ring-vrf
pgherveou Jul 21, 2026
01513f9
update
pgherveou Jul 23, 2026
bc2a5fe
analyse and fix diff with ~/github/polkadot-app-android-v2/
pgherveou Jul 23, 2026
1aed8f4
fixup
pgherveou Jul 23, 2026
5489b13
fixes
pgherveou Jul 23, 2026
132f667
fix(core): align host compatibility
pgherveou Jul 23, 2026
7c96e27
Merge main into feat/signing-host-ring-vrf
pgherveou Jul 23, 2026
5dd8ef1
ci: refresh generated fixture and licenses
pgherveou Jul 23, 2026
6868f0b
fix(sso-responder): bound the replay-dedup set to cap memory
TarikGul Jul 24, 2026
f7999e3
chore(codegen): refresh stale host-callbacks golden
TarikGul Jul 24, 2026
eaaa22b
fix(signing-host): name the beneficiary product in the allocation review
TarikGul Jul 24, 2026
d8168ad
chore(codegen): refresh host-callbacks golden for ResourceAllocationR…
TarikGul Jul 24, 2026
0bed52d
fix(signing-host): use a dedicated review for statement-store proof s…
TarikGul Jul 24, 2026
16009a1
Clippy / fmt
TarikGul Jul 24, 2026
a4e8e40
Merge branch 'feat/signing-host-ring-vrf' of github.com:paritytech/tr…
TarikGul Jul 24, 2026
103431c
fix(server): align runtime SCALE codecs
pgherveou Jul 23, 2026
0e7e96e
update
pgherveou Jul 24, 2026
fa9f1fb
refactor(api): adopt Send async traits
pgherveou Jul 24, 2026
bd69991
feat(ios): integrate the shared Rust runtime core
Jul 24, 2026
8aebcf3
fix(playground): run account proof diagnosis
Jul 24, 2026
2da2e8b
feat(ios): dispatch native frames across Tokio workers
Jul 24, 2026
f29b3c2
refactor(native): share the Tokio bridge runtime
Jul 24, 2026
f57a7da
refactor(native): clarify shared executor naming
Jul 24, 2026
f78a446
Merge remote-tracking branch 'origin/main' into worktree-issue-96-mob…
Aug 4, 2026
e16db68
chore(ios): sync host submodule with PR 1290
Aug 4, 2026
73b941d
fix(sso): restore iOS crypto compatibility
Aug 4, 2026
c0b978c
feat(sso): expose native responder lifecycle
Aug 4, 2026
f8f3fc8
fix(sso): align Rust responder with iOS wallet
Aug 4, 2026
cb1b79a
fix(sso): enforce RFC-0022 across all hosts
Aug 4, 2026
121ad2b
chore(ios): sync Rust-only SSO cleanup
Aug 5, 2026
6c36073
chore(ios): sync SSO cleanup with latest main
Aug 5, 2026
568396b
fix(sso): scope responder lifecycle to native hosts
Aug 5, 2026
b7dde46
fix(ios): link the Rust core from its Swift package
Aug 5, 2026
d2a869d
chore(ios): sync Rust core CI integration
Aug 5, 2026
99ebcb8
chore(ios): sync RFC-0022 URL tests
Aug 5, 2026
278417b
chore(ios): sync Swift 6 warning fixes
Aug 5, 2026
cc6f795
chore(ios): sync secure host callbacks
Aug 5, 2026
a39d50c
Merge remote-tracking branch 'origin/main' into worktree-issue-96-mob…
Aug 5, 2026
d47c359
Merge branch 'main' into worktree-issue-96-mobile-followup
pgherveou Aug 7, 2026
3d2507e
fix(sso): restore X25519 wallet crypto and align pairing flow with mo…
pgherveou Aug 8, 2026
3543cdc
Merge remote-tracking branch 'origin/main' into worktree-issue-96-mob…
pgherveou Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions docs/ios-sso-simulator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# iOS SSO simulator runbook

Use this runbook to validate the shared Rust SSO responder inside
`polkadot-app-ios-v2` against `truapi-host pairing-host`. These details were
confirmed with an iOS 18.3.1 simulator and the Paseo Next v2 test network.

## Build the Rust library first

The Xcode project links the already-built simulator archive. Rebuild it after
every Rust core change or Xcode can succeed while embedding stale Rust code.

```bash
# repository root
cargo build -p truapi-server \
--release \
--features ws-bridge \
--target aarch64-apple-ios-sim
```

## Build the correct iOS flavor

Use an arm64 iOS 18.3 simulator and the Nightly/Paseo flags. A plain Debug
build selects PreviewNet and cannot pair with a CLI using `paseo-next-v2`.
The iOS 26 simulator exposed unrelated keychain/onboarding failures during
this flow, so it is not the reference test device yet.

```bash
IOS_SSO_SIMULATOR_ID=<simulator-udid>

cd hosts/ios
xcodebuild \
-project polkadot-app.xcodeproj \
-scheme polkadot-app \
-configuration Debug \
-destination "platform=iOS Simulator,id=${IOS_SSO_SIMULATOR_ID}" \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES \
TRUAPI_SWIFT_FLAGS='-DF_DEV -DNIGHTLY -DTESTNET_FEATURE -DIOS_PASEO_E2E' \
RUN_IN_CI=true \
build
```

Do not set `CODE_SIGNING_ALLOWED=NO`: that produces an app the simulator will
not launch. Without `RUN_IN_CI=true`, the Xcode pre-actions run SwiftFormat
across the checkout; inspect `git status` afterward if the safeguard was
omitted. Do not pipe `xcodebuild` through `tail` or a similar filter while
automating this run: the wrapper can return while the underlying build still
owns `XCBuildData/build.db`, making the next invocation fail with “database is
locked”. Use the unpiped command (optionally with `-quiet`) and wait for its
exit status.

Keep `RUN_IN_CI=true` even for the local simulator build. It skips the
format/lint build phases, which otherwise recurse into a local
`source_packages` checkout: formatting roughly 9,500 dependency and project
files took almost ten minutes, rewrote unrelated tracked files, and SwiftLint
then failed on dependency-owned violations. Run formatting and linting as
separate, intentionally scoped checks instead.

If that formatter has already touched an ignored `source_packages` directory,
do not reuse it: generated bridge sources in dependencies can become invalid
Swift (for example, a `get(index:)` call can be reformatted as an accessor).
Move the tainted cache aside for recovery and give Xcode a clean package-cache
path outside the checkout together with fresh DerivedData:

```bash
mv source_packages "/tmp/truapi-ios-source-packages-formatted-$(date +%s)"

xcodebuild \
-project polkadot-app.xcodeproj \
-scheme polkadot-app \
-configuration Debug \
-destination "platform=iOS Simulator,id=${IOS_SSO_SIMULATOR_ID}" \
-derivedDataPath /tmp/truapi-ios-sso-clean-dd \
-clonedSourcePackagesDirPath /tmp/truapi-ios-source-packages-clean \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES \
TRUAPI_SWIFT_FLAGS='-DF_DEV -DNIGHTLY -DTESTNET_FEATURE -DIOS_PASEO_E2E' \
RUN_IN_CI=true \
build
```

After merging iOS `main` or updating binary dependencies, Xcode can fail with
“header has been modified since the module file was built” for a framework
such as WebRTC (`RTCPeerConnection.h`). The copied framework header is newer
than the explicit precompiled module cached in DerivedData. Run the same
project, scheme, and simulator destination with `xcodebuild clean`, then
rebuild; deleting the simulator or changing Rust code does not address this
cache mismatch.

For integration tests that use `@testable import Products`, do not reuse
DerivedData from a normal app build. The package module in that cache was
compiled without testability and Xcode reports it as incompatible. Use a
dedicated test DerivedData directory, pass `ENABLE_TESTABILITY=YES` for the
`DevCI` configuration, and give the integration-test target the Rust archive
search path explicitly:

```bash
xcodebuild test \
-project polkadot-app.xcodeproj \
-scheme polkadot-appIntegrationTests \
-configuration DevCI \
-destination "platform=iOS Simulator,id=${IOS_SSO_SIMULATOR_ID}" \
-derivedDataPath /tmp/truapi-ios-integration-tests \
ENABLE_TESTABILITY=YES \
LIBRARY_SEARCH_PATHS="$(pwd)/../../target/aarch64-apple-ios-sim/release"
```

Without the explicit library path the app target can compile while the test
bundle still fails to link with `library 'truapi_server' not found`.

Install the resulting signed Debug app:

```bash
IOS_SSO_APP_PATH=<derived-data-path>/Build/Products/Debug-iphonesimulator/polkadot-app.app
xcrun simctl install "$IOS_SSO_SIMULATOR_ID" "$IOS_SSO_APP_PATH"
xcrun simctl launch "$IOS_SSO_SIMULATOR_ID" io.pcf.polkadotapp.develop
```

## Prepare a real iOS identity

Recover or create a disposable RFC-0022 test wallet and make sure its `uid.dot`
identity plus `peopl.dot` LitePeople membership are registered on Paseo Next
v2 before importing the same mnemonic into the app. A wallet claimed through
the older native `//wallet` flow is not an RFC-0022 test identity and will fail
alias, proof, allowance, and legacy-identity signing checks even when the
shared core is working correctly.

All hosts use the same RFC-0022 derivations. `platformType` is metadata only;
it must never select account, ring-VRF, allowance, or ECDH key material.

On a fresh simulator, the app can remain on “Waiting for network connection”
until Safari has made the simulator's first network request. Open any HTTPS
page once, then relaunch the app.

## Pair the CLI

The Debug app registers `polkadotappdev://`, while the CLI prints the
production `polkadotapp://` deeplink. Replace only that scheme before opening
the deeplink in the Debug simulator. The app accepts both schemes when parsing
the handshake.

Use `truapi-playground.dot` for the generated battery. Using
`headless-playground.dot` makes the signing examples request the wrong product
accounts and produces misleading permission failures.

```bash
./target/debug/truapi-host pairing-host \
--base-path /tmp/truapi-ios-pairing-host-e2e \
--network paseo-next-v2 \
--product-id truapi-playground.dot \
--auto-accept \
--log-level info \
--script rust/crates/truapi-host-cli/js/scripts/battery.ts
```

Approve the sensitive operations in the simulator. The supported baseline is
46 passing examples. The remaining 19 examples are the currently unwired Chat
(6), Coin Payment (9), and Payment (4) service families.

## Recover a simulator without erasing it

If temporary onboarding defaults were injected, remove them through the
simulator's `cfprefsd` domain before relaunching. Editing or inspecting the
preferences plist directly is not authoritative while `cfprefsd` is running.

```bash
xcrun simctl terminate "$IOS_SSO_SIMULATOR_ID" io.pcf.polkadotapp.develop
xcrun simctl spawn "$IOS_SSO_SIMULATOR_ID" \
defaults delete io.pcf.polkadotapp.develop username
xcrun simctl spawn "$IOS_SSO_SIMULATOR_ID" \
defaults delete io.pcf.polkadotapp.develop usernameClaimed
xcrun simctl spawn "$IOS_SSO_SIMULATOR_ID" \
defaults delete io.pcf.polkadotapp.develop isPerson
xcrun simctl launch "$IOS_SSO_SIMULATOR_ID" io.pcf.polkadotapp.develop
```

Use `xcrun simctl spawn "$IOS_SSO_SIMULATOR_ID" defaults read
io.pcf.polkadotapp.develop` when diagnosing those values. If the app reports
“Environment has been reset”, clear injected values and use the app's Start
Over/recovery flow rather than adding more defaults.

## Failure signatures

- `BlockHeaderNotFound` during alias/proof means the native JSON-RPC engine
advertised ChainHead but did not return the finalized header. The Rust ring
resolver falls back to legacy `chain_*`/`state_*` RPC for this snapshot.
- `channelPriorityTooLow` on the second rapid SSO request means two statements
reused an expiry priority. Rust statement priorities are process-locally
monotonic so calls created in the same second remain strictly ordered.
- A legacy signer “not available in this CLI wallet” means the requested
account is not the RFC-0022 `uid.dot` identity derived from the active root
entropy. Check that the simulator imported the RFC-provisioned mnemonic.
- A ten-second timeout after approving VRF is a CLI diagnosis timeout, not a
cryptographic failure. Interactive SSO methods use the remote-response
timeout in the battery runner.
5 changes: 5 additions & 0 deletions docs/local-e2e-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ job.
The order matters: each layer assumes the layer below it builds clean.
Skip a step only if you are certain the change cannot affect that layer.

For the Rust-core SSO flow in the native iOS app, use the dedicated
[iOS SSO simulator runbook](ios-sso-simulator.md). It records the build
configuration and simulator recovery details that are easy to miss when the
app is rebuilt.

```
Rust crates → codegen → @parity/truapi → playground → dotli iframe
```
Expand Down
11 changes: 8 additions & 3 deletions ios/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Run `rebuild.sh` after changing anything host-visible — the `NativeTrUApiCore`

For local iteration without publishing, flip `useLocalBinary = true` in the root `Package.swift` to build against `Binaries/` directly; flip it back before committing.

The embedding app implements the UniFFI-generated `HostCallbacks` protocol directly (defined in `truapi_server.swift`): navigation, push, permissions, auth state, scoped + core storage, chain JSON-RPC, confirmations, preimage, theme, and feature support. UI-decision callbacks are `async` and awaited by the Rust core.
The embedding app implements the UniFFI-generated `HostCallbacks` protocol directly (defined in `truapi_server.swift`): navigation, push, permissions, auth state, paired-peer disconnects, scoped + core storage, chain JSON-RPC, confirmations, preimage, theme, and feature support. UI-decision callbacks are `async` and awaited by the Rust core.

## Integrating in an iOS app

Expand Down Expand Up @@ -127,13 +127,18 @@ final class MyCallbacks: HostCallbacks, @unchecked Sendable {

// Core-owned auth state stream: render `.connected`/`.disconnected` as the
// account badge and `.loginFailed` as a retryable error. This core is a
// signing host it owns the signer and never pairs — so `.pairing` and
// signing host, so it does not enter the remote-login flow: `.pairing` and
// `.authenticating` are not emitted and `core.cancelLogin()` is inert.
// Activate the session with `core.activateLocalSession(secret:...)`.
// Activate the signer with `core.activateLocalSession(secret:...)`; serving
// paired product hosts uses the separate responder-pairing methods.
func authStateChanged(state: AuthState) {
DispatchQueue.main.async { /* render the state */ }
}

func pairingPeerDisconnected(peer: NativePairingPeer) {
DispatchQueue.main.async { /* remove the persisted peer and update UI */ }
}

func coreStorageRead(key: Data) throws -> Data? { coreStorage[key] }
func coreStorageWrite(key: Data, value: Data) throws { coreStorage[key] = value }
func coreStorageClear(key: Data) throws { coreStorage.removeValue(forKey: key) }
Expand Down
58 changes: 58 additions & 0 deletions ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,31 @@ public enum PairingDeeplinkScheme: Sendable {
}
}

/// Stable identity for a paired product host. Persist these two public keys so
/// the Rust responder can restore its encrypted Statement Store subscription
/// after the native app restarts.
public struct PairingPeer: Sendable, Hashable {
public let statementAccountId: Data
public let encryptionPublicKey: Data

public init(statementAccountId: Data, encryptionPublicKey: Data) {
self.statementAccountId = statementAccountId
self.encryptionPublicKey = encryptionPublicKey
}

fileprivate init(native: NativePairingPeer) {
statementAccountId = native.statementAccountId
encryptionPublicKey = native.encryptionPublicKey
}

fileprivate var native: NativePairingPeer {
NativePairingPeer(
statementAccountId: statementAccountId,
encryptionPublicKey: encryptionPublicKey
)
}
}

/// Static product and pairing config supplied before the Rust core handles
/// product calls. One core instance represents one product identity.
///
Expand Down Expand Up @@ -215,6 +240,11 @@ public protocol TrUAPIHostCoreProtocol: AnyObject {
func disconnect()
func cancelLogin()
func activateLocalSession(secret: Data, liteUsername: String?) throws
func respondToPairing(deeplink: String) throws -> PairingPeer
func resumePairing(peer: PairingPeer) throws
func disconnectPairing(peer: PairingPeer) throws
func suspendPairing(peer: PairingPeer) throws
func suspendAllPairings()
func permissionAuthorizationStatus(
request: PermissionAuthorizationRequest
) throws -> PermissionAuthorizationStatus
Expand Down Expand Up @@ -287,6 +317,34 @@ public final class TrUAPIHostCore: TrUAPIHostCoreProtocol {
try inner.activateLocalSession(secret: secret, liteUsername: liteUsername)
}

/// Answer a pairing deeplink and start serving the session in Rust. This
/// returns once the handshake statement has been accepted; session traffic
/// continues on the core's background pool.
public func respondToPairing(deeplink: String) throws -> PairingPeer {
PairingPeer(native: try inner.respondToPairing(deeplink: deeplink))
}

/// Restore the Rust responder for a persisted pairing host.
public func resumePairing(peer: PairingPeer) throws {
try inner.resumePairing(peer: peer.native)
}

/// Notify one pairing host of a local disconnect and stop serving it.
public func disconnectPairing(peer: PairingPeer) throws {
try inner.disconnectPairing(peer: peer.native)
}

/// Stop one responder without sending a disconnect. The peer can be
/// resumed later from its persisted public keys.
public func suspendPairing(peer: PairingPeer) throws {
try inner.suspendPairing(peer: peer.native)
}

/// Stop every responder without changing persisted pairings.
public func suspendAllPairings() {
inner.suspendAllPairings()
}

/// Read a stored permission authorization status without prompting.
public func permissionAuthorizationStatus(
request: PermissionAuthorizationRequest
Expand Down
Loading
Loading