Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ container bundle (`make xcframework` + `make uniffi`); see
[`ios/truapi-host/README.md`](ios/truapi-host/README.md).
Native bindings expose the canonical Rust domain and protocol value types;
native-only adapter types are limited to lifecycle and callback behavior.
Both platforms expose `TrUAPIHostRuntime`, the process-owned native runtime; wallet
hosts that manage their own statement-store SSO session can additionally call
`handleSsoRequest` (routes one decrypted remote message through the core,
returning a typed outcome: response bytes to post back, a disconnect marker, or
ignored) and `prepareDisconnectRequest` (builds the SCALE-encoded wire message
for a wallet-initiated disconnect). Response posting and session-record cleanup
remain on the wallet side.

### JS Host SDKs

Expand Down
19 changes: 19 additions & 0 deletions ios/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,25 @@ The core's `Permissions` platform trait has two methods, and so does `HostCallba

Both return a `Bool` granted flag; the host renders the typed request in its own prompt UI. The same typed values drive the `TrUAPIHostCore` permission admin API (`permissionAuthorizationStatus`, `setPermissionAuthorizationStatus`), which reads and updates the persisted decisions without prompting.

## SSO session handling

`TrUAPIHostRuntime` exposes two methods for wallet-owned SSO sessions. Meaningful request answering requires `activateLocalSession` to have been called first; `prepareDisconnectRequest` needs no session.

```swift
func handleSsoRequest(message: Data) async throws -> SsoRequestOutcome
func prepareDisconnectRequest() -> Data
```

`handleSsoRequest(message:)` takes one SCALE-encoded `RemoteMessage` exactly as decrypted from the statement-store session and routes it through the Rust core. The returned `SsoRequestOutcome` is the generated UniFFI enum (no Swift mirror):

- `.response(message:)` — SCALE-encoded reply; post it back over the same session.
- `.disconnected` — the peer ended the session; tear down the transport and records on the wallet side.
- `.ignored` — the message was not a request; nothing to post.

Confirmation-gated requests suspend on `confirmUserAction`, so `handleSsoRequest` can take arbitrarily long. Always call it from a `Task`, never the main thread.

`prepareDisconnectRequest()` returns the SCALE-encoded `Disconnected` message to post when the wallet is ending the session. Posting and record cleanup (host entry, device record, device-removed broadcast) stay with the wallet.

## Statement-store allowance renewal

Statement-store allowances are granted per period, so a host has to re-register the accounts it wants to keep writing. They are not revoked the moment the period ends: `Resources.StmtStoreGraceWindow` keeps an ended period's allowances active until cleanup catches up, 48 hours on `paseo-next-v2`. The runtime owns the ledger and the registration; the app owns only the schedule.
Expand Down
18 changes: 18 additions & 0 deletions ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,24 @@ public final class TrUAPIHostRuntime: @unchecked Sendable {
try inner.activateLocalSession(secret: secret, liteUsername: liteUsername)
}

/// Answer one decrypted SSO remote message from the wallet-managed
/// statement-store session. `message` is one SCALE-encoded
/// `RemoteMessage` exactly as decrypted. `.response` carries the
/// SCALE-encoded reply to post back over the same session;
/// `.disconnected` means the peer ended the session (perform native
/// teardown); `.ignored` means the message was not a request.
/// Confirmation-gated requests await `confirmUserAction`, so this can
/// take arbitrarily long — call from a `Task`, never the main thread.
public func handleSsoRequest(message: Data) async throws -> SsoRequestOutcome {
try await inner.handleSsoRequest(message: message)
}

/// Build the SCALE-encoded `Disconnected` message to post over a
/// session the wallet is ending; record cleanup stays with the wallet.
public func prepareDisconnectRequest() -> Data {
inner.prepareDisconnectRequest()
}

public func notifyChainResponse(connectionId: UInt32, json: String) {
inner.notifyChainResponse(connectionId: connectionId, json: json)
}
Expand Down
170 changes: 170 additions & 0 deletions ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3360,6 +3360,19 @@ public protocol NativeTrUApiHostRuntimeProtocol: AnyObject, Sendable {
*/
func disconnect()

/**
* Answer one decrypted SSO remote message from a wallet-managed
* statement-store session.
*
* `message` is one SCALE-encoded `RemoteMessage` exactly as decrypted from
* the session statement. The bytes are deliberately opaque at this
* boundary: the wallet forwards wire encodings verbatim and never
* constructs them. Session control and transport stay with the wallet —
* `Disconnected` is reported, never handled here. Confirmation-gated
* requests await `confirm_user_action`, so this can take arbitrarily long.
*/
func handleSsoRequest(message: Data) async throws -> SsoRequestOutcome

/**
* The in-process loop's own cadence: at most an hour, tightening to land
* just after the next period boundary.
Expand Down Expand Up @@ -3390,6 +3403,15 @@ public protocol NativeTrUApiHostRuntimeProtocol: AnyObject, Sendable {
*/
func openProductExecution(callbacks: HostCallbacks, chatCallbacks: NativeChatCallbacks?, executionConfig: NativeProductExecutionConfig) throws -> NativeProductExecution

/**
* Build the SCALE-encoded `Disconnected` message a wallet posts over a
* session it is ending. Each call carries a fresh opaque message id,
* like every outgoing SSO message; receivers detect disconnect by
* message variant, not id. Posting and record cleanup stay with the
* wallet.
*/
func prepareDisconnectRequest() -> Data

/**
* Run one renewal pass now and report what each tracked target got.
*
Expand Down Expand Up @@ -3517,6 +3539,33 @@ open func disconnect() {try! rustCall() {
}
}

/**
* Answer one decrypted SSO remote message from a wallet-managed
* statement-store session.
*
* `message` is one SCALE-encoded `RemoteMessage` exactly as decrypted from
* the session statement. The bytes are deliberately opaque at this
* boundary: the wallet forwards wire encodings verbatim and never
* constructs them. Session control and transport stay with the wallet —
* `Disconnected` is reported, never handled here. Confirmation-gated
* requests await `confirm_user_action`, so this can take arbitrarily long.
*/
open func handleSsoRequest(message: Data)async throws -> SsoRequestOutcome {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_truapi_server_fn_method_nativetruapihostruntime_handle_sso_request(
self.uniffiCloneHandle(),FfiConverterData.lower(message)
)
},
pollFunc: ffi_truapi_server_rust_future_poll_rust_buffer,
completeFunc: ffi_truapi_server_rust_future_complete_rust_buffer,
freeFunc: ffi_truapi_server_rust_future_free_rust_buffer,
liftFunc: FfiConverterTypeSsoRequestOutcome_lift,
errorHandler: FfiConverterTypeHostRejection_lift
)
}

/**
* The in-process loop's own cadence: at most an hour, tightening to land
* just after the next period boundary.
Expand Down Expand Up @@ -3577,6 +3626,22 @@ open func openProductExecution(callbacks: HostCallbacks, chatCallbacks: NativeCh
FfiConverterTypeNativeProductExecutionConfig_lower(executionConfig),uniffiCallStatus
)
})
}

/**
* Build the SCALE-encoded `Disconnected` message a wallet posts over a
* session it is ending. Each call carries a fresh opaque message id,
* like every outgoing SSO message; receivers detect disconnect by
* message variant, not id. Posting and record cleanup stay with the
* wallet.
*/
open func prepareDisconnectRequest() -> Data {
return try! FfiConverterData.lift(try! rustCall() {
uniffiCallStatus in
uniffi_truapi_server_fn_method_nativetruapihostruntime_prepare_disconnect_request(
self.uniffiCloneHandle(),uniffiCallStatus
)
})
}

/**
Expand Down Expand Up @@ -5335,6 +5400,105 @@ public func FfiConverterTypeProductRuntimeError_lower(_ value: ProductRuntimeErr
}


/**
* FFI projection of the canonical
* [`SsoRequestOutcome`](crate::host_logic::sso::messages::SsoRequestOutcome),
* concrete because UniFFI cannot export generics.
*
* Variants carry SCALE-encoded wire bytes rather than decoded Rust types because
* the wallet forwards encodings verbatim and never constructs them — the opaque
* bytes are the correct boundary representation here.
*/

public enum SsoRequestOutcome: Equatable, Hashable {

/**
* SCALE-encoded response to post back over the session.
*/
case response(
/**
* SCALE-encoded `RemoteMessage` response ready to submit over the
* session statement store.
*/message: Data
)
/**
* The peer ended the session; the wallet tears down its transport and
* records (host entry, device record, device-removed broadcast).
*/
case disconnected
/**
* Not a request; nothing to post.
*/
case ignored





}

#if compiler(>=6)
extension SsoRequestOutcome: Sendable {}
#endif

#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeSsoRequestOutcome: FfiConverterRustBuffer {
typealias SwiftType = SsoRequestOutcome

public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SsoRequestOutcome {
let variant: Int32 = try readInt(&buf)
switch variant {

case 1: return .response(message: try FfiConverterData.read(from: &buf)
)

case 2: return .disconnected

case 3: return .ignored

default: throw UniffiInternalError.unexpectedEnumCase
}
}

public static func write(_ value: SsoRequestOutcome, into buf: inout [UInt8]) {
switch value {


case let .response(message):
writeInt(&buf, Int32(1))
FfiConverterData.write(message, into: &buf)


case .disconnected:
writeInt(&buf, Int32(2))


case .ignored:
writeInt(&buf, Int32(3))

}
}
}


#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeSsoRequestOutcome_lift(_ buf: RustBuffer) throws -> SsoRequestOutcome {
return try FfiConverterTypeSsoRequestOutcome.lift(buf)
}

#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeSsoRequestOutcome_lower(_ value: SsoRequestOutcome) -> RustBuffer {
return FfiConverterTypeSsoRequestOutcome.lower(value)
}



/**
* Outcome of renewing one target.
*/
Expand Down Expand Up @@ -6316,6 +6480,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_truapi_server_checksum_method_nativetruapihostruntime_disconnect() != 38487) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_truapi_server_checksum_method_nativetruapihostruntime_handle_sso_request() != 21060) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_truapi_server_checksum_method_nativetruapihostruntime_next_statement_renewal_delay() != 33452) {
return InitializationResult.apiChecksumMismatch
}
Expand All @@ -6328,6 +6495,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_truapi_server_checksum_method_nativetruapihostruntime_open_product_execution() != 49537) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_truapi_server_checksum_method_nativetruapihostruntime_prepare_disconnect_request() != 17252) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_truapi_server_checksum_method_nativetruapihostruntime_renew_statement_allowances() != 11225) {
return InitializationResult.apiChecksumMismatch
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,11 @@ void uniffi_truapi_server_fn_method_nativetruapihostruntime_activate_local_sessi
void uniffi_truapi_server_fn_method_nativetruapihostruntime_disconnect(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVETRUAPIHOSTRUNTIME_HANDLE_SSO_REQUEST
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVETRUAPIHOSTRUNTIME_HANDLE_SSO_REQUEST
uint64_t uniffi_truapi_server_fn_method_nativetruapihostruntime_handle_sso_request(uint64_t ptr, RustBuffer message
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVETRUAPIHOSTRUNTIME_NEXT_STATEMENT_RENEWAL_DELAY
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVETRUAPIHOSTRUNTIME_NEXT_STATEMENT_RENEWAL_DELAY
RustBuffer uniffi_truapi_server_fn_method_nativetruapihostruntime_next_statement_renewal_delay(uint64_t ptr, RustCallStatus *_Nonnull out_status
Expand All @@ -856,6 +861,11 @@ void uniffi_truapi_server_fn_method_nativetruapihostruntime_notify_chain_respons
uint64_t uniffi_truapi_server_fn_method_nativetruapihostruntime_open_product_execution(uint64_t ptr, uint64_t callbacks, RustBuffer chat_callbacks, RustBuffer execution_config, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVETRUAPIHOSTRUNTIME_PREPARE_DISCONNECT_REQUEST
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVETRUAPIHOSTRUNTIME_PREPARE_DISCONNECT_REQUEST
RustBuffer uniffi_truapi_server_fn_method_nativetruapihostruntime_prepare_disconnect_request(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVETRUAPIHOSTRUNTIME_RENEW_STATEMENT_ALLOWANCES
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_NATIVETRUAPIHOSTRUNTIME_RENEW_STATEMENT_ALLOWANCES
RustBuffer uniffi_truapi_server_fn_method_nativetruapihostruntime_renew_statement_allowances(uint64_t ptr, RustCallStatus *_Nonnull out_status
Expand Down Expand Up @@ -1519,6 +1529,12 @@ uint16_t uniffi_truapi_server_checksum_method_nativetruapihostruntime_activate_l
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVETRUAPIHOSTRUNTIME_DISCONNECT
uint16_t uniffi_truapi_server_checksum_method_nativetruapihostruntime_disconnect(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVETRUAPIHOSTRUNTIME_HANDLE_SSO_REQUEST
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVETRUAPIHOSTRUNTIME_HANDLE_SSO_REQUEST
uint16_t uniffi_truapi_server_checksum_method_nativetruapihostruntime_handle_sso_request(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVETRUAPIHOSTRUNTIME_NEXT_STATEMENT_RENEWAL_DELAY
Expand All @@ -1543,6 +1559,12 @@ uint16_t uniffi_truapi_server_checksum_method_nativetruapihostruntime_notify_cha
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVETRUAPIHOSTRUNTIME_OPEN_PRODUCT_EXECUTION
uint16_t uniffi_truapi_server_checksum_method_nativetruapihostruntime_open_product_execution(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVETRUAPIHOSTRUNTIME_PREPARE_DISCONNECT_REQUEST
#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVETRUAPIHOSTRUNTIME_PREPARE_DISCONNECT_REQUEST
uint16_t uniffi_truapi_server_checksum_method_nativetruapihostruntime_prepare_disconnect_request(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_NATIVETRUAPIHOSTRUNTIME_RENEW_STATEMENT_ALLOWANCES
Expand Down
8 changes: 8 additions & 0 deletions ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,12 @@ final class StubHostBridge: HostBridge {
func devicePermission(request _: HostDevicePermissionRequest) async throws -> Bool { false }
func remotePermission(request _: RemotePermission) async throws -> Bool { false }
func featureSupported(request _: HostFeatureSupportedRequest) async throws -> Bool { true }
func supportedChains() throws -> HostChainSet { HostChainSet(network: "", chains: []) }
func localStorageRead(key: String) throws -> Data? { try storage.read(key: key) }

func localStorageWrite(key: String, value: Data) throws {
try storage.write(key: key, value: value)
}

func localStorageClear(key: String) throws { try storage.clear(key: key) }
}
Loading