# Host API Proposal ## Changelog ### v0.4 - 2026-01-12 * Renamed `storage_*` methods to `local_storage_*`; * Removed direct permissions request, now methods with mutation logic will return additional `PermissionDenied` error; * Changed chat section to support multiple chat rooms and bots. * Restored methods for statement store query, subscribe and submit ### v0.3 - 2026-01-03 * Define subscription logic. * Moved message version from `Payload` to each individual action; * Fixed `ChatMessage::RichText` enum value; * Added `ChatContactRegistrationStatus` enum; ### v0.2 – 2025-12-27 Removed methods for statement store querying and submitting, all chain interaction should be done with JSON-RPC calls. ### v0.1 – 2025-12-18 First implementation ## Overview `Host API` is a protocol designed to connect Products and Host applications by providing a set of methods for communication. Host API is language-agnostic. All code examples are written in Rust, but authors can easily map these interfaces into other languages. ## Requirements ### Product * As a product developer, I want to be able to get information about user accounts; * As a product developer, I want to be able to initiate transaction signing; * As a product developer, I want to be able to integrate with host chat extension; * As a product developer, I want to be able to save Product data into Hosts local storage; * As a product developer, I want to be able to interact with chains through Host API; ### Technical * Solution MUST provide a transport layer between host and product. * Message format MUST be well-defined and serializable to support different platforms. ### General Interface ```rust // Host fn handshake( version: ProtocolVersion ) -> Result<(), HandshakeErr>; fn feature_supported( feature: Feature ) -> Result<bool, GenericErr>; // Storage fn local_storage_read( key: LocalStorageKey ) -> Result<Option<LocalStorageValue>, LocalStorageErr>; fn local_storage_write( key: LocalStorageKey value: LocalStorageValue ) -> Result<(), LocalStorageErr>; fn local_storage_clear( key: LocalStorageKey ) -> Result<(), LocalStorageErr>; // Account fn account_get( domain: ProductAccountId ) -> Result<Account, RequestCredentialsErr>; fn account_get_alias( domain: ProductAccountId ) -> Result<ContextualAlias, RequestCredentialsErr>; fn account_create_proof( domain: ProductAccountId, ring: RingLocation, message: Vec<u8> ) -> Result<RingVrfProof, CreateProofErr>; fn get_non_product_accounts() -> Result<Account[], RequestCredentialsErr>; // Signing fn create_transaction( accountId: ProductAccountId, payload: VersionedTxPayload ) -> Result<Vec<u8>, CreateTransactionErr>; fn create_transaction_with_non_product_account( accountId: AccountId, payload: VersionedTxPayload ) -> Result<Vec<u8>, CreateTransactionErr>; fn sign_raw( payload: SigningPayloadRaw ) -> Result<SigningResult, SigningErr>; fn sign_payload( payload: SigningPayloadJSON ) -> Result<SigningResult, SigningErr>; // Chat fn chat_create_room( room: ChatRoomRequest ) -> Result<ChatRoomRegistrationResult, ChatRoomRegistrationErr>; fn chat_list_subscribe(callback: fn(Vector<ChatRoom>)) -> Result<Subscriber, GenericErr>; fn chat_post_message( roomId: str, message: ChatMessage ) -> Result<ChatPostMessageResult, ChatMessagePostingErr>; fn chat_action_subscribe( callback: fn(ChatAction) ) -> Result<Subscriber, GenericErr>; // Statement Store fn statement_store_query( topics: Vec<Topic> ) -> Result<Vec<SignedStatement>, GenericErr>; fn statement_store_subscribe( topics: Vec<Topic>, callback: fn(Vec<SignedStatement>) ) -> Result<Subscriber, GenericErr>; fn statement_store_create_proof( account: ProductAccountId, statement: Statement ) -> Result<StatementProof, StatementProofErr>; fn statement_store_submit( statement: SignedStatement ) -> Result<(), GenericErr>; // Chain interation fn jsonrpc_message_send( chain: GenesisHash, message: str ) -> Result<(), GenericError>; fn jsonrpc_message_subscribe( chain: GenesisHash, callback: fn(str) ) -> Result<Subscriber, GenericErr>; ``` ## Transport Communication between Host and Product can be implemented with any IPC protocol. The body of an IPC message is a serialized `Message` (byte array). The IPC implementation may vary depending on the environment. ### Serialization Messages are serializable structs that can be passed between peers. Message serialization is built on [JAM codec](https://github.com/paritytech/jam-codec). All examples in this proposal skip JAM codec derive implementation calls, but they are implied. The field order of structs and enums matters. `Result` is also treated as a serializable enum. :::spoiler Note on JAM codec [JAM codec](https://github.com/paritytech/jam-codec) is based on SCALE codec with native support of the `Compact` type. ::: ### Interface Each message can be defined as: ```rust struct Message { requestId: str, payload: Payload } ``` `Payload` is an enum of possible **actions**. Actions MUST follow the order of Host API methods defined above for correct indices during serialization. Actions with defined payload MUST be versioned using `Versioned` enum: ```rust enum Versioned { V1(Message) // ... } ``` Actions MUST be derived from Host API methods using the following algorithm: - For request functions, actions should be derived as follows: - Request - Name: `method_name + '_request'` - Argument: `Versioned<(arg1, arg2, ...)>` - Response - Name: `method_name + '_response'` - Argument: `Versioned<Result<ReturnValue, ReturnError>>` - For subscriptions, there should be four different messages: - Subscribe - Name: `method_name + '_start'` - Argument: tuple of all arguments except callback `Versioned<(arg1, arg2, ...)>` - Unsubscribe - Name: `method_name + '_stop'` - Argument: none - Interrupt - Name: `method_name + '_interrupt'` - Argument: none - Receive - Name: `method_name + '_receive'` - Argument: versioned argument of callback function `Versioned<Message>` Actions MUST be defined in the given order. Example: ```rust enum Payload { handshake_request(Versioned::V1(HandshakeVersion)), handshake_response(Versioned::V1(Result<(), GenericErr>)), // ... jsonrpc_message_send_request(Versioned::V1((ChainId, str))), jsonrpc_message_send_response(Versioned::V1(Result<(), GenericErr>)), jsonrpc_message_subscribe_start(Versioned::V1(ChainId)), jsonrpc_message_subscribe_stop, jsonrpc_message_subscribe_interrupt, jsonrpc_message_subscribe_receive(Versioned::V1(str)) // ... } ``` ### Rules #### Requests Each Host or Product MUST send a response message for every request. Request and response MUST share the same `requestId` for matching on each side. #### Subscription `start`, `receive`, `interrupt` and `stop` calls MUST share the same `requestId` for matching inside subscription handlers. When a subscription starts, the consumer MUST notify the provider with a `start` message. When the consumer wants to unsubscribe, it MUST send a `stop` message. The provider MUST send data updates with a `receive` message. If the provider has trouble providing data, it CAN send an `interrupt` message to the consumer. The consumer MAY react to an `interrupt` message by notifying the application layer. Returned `Subscriber` interface depends on implementation, but generic interface can look like this: ```rust struct Susbcriber { unsubscribe: fn(), onInterrupt fn(fn()) } ``` ## API Sections ### Common Interfaces ```rust type GenesisHash = Vec<u8>; struct GenericErr { reason: str } ``` ### Host Calls #### Handshake Handshake calls should be bidirectional. Both Host and Product can send handshake requests, and both MUST respond to them. Handshake implementations CAN include a timeout of 10 seconds, after which the connection is marked as failed and the method should return a Timeout error. The handshake result can be cached. The handshake request contains `ProtocolVersion`, which is the version of the encoder in `u8`. The host or product should switch its encoding/decoding mode when `ProtocolVersion` is received. For JAM codec, `ProtocolVersion = 1`. A successful handshake request MUST be the first request processed by Host API. If any other request was sent before a successful handshake response, it should fail. ```rust enum HandshakeErr { Timeout, UnsupportedProtocolVersion, Unknown(GenericErr) } type ProtocolVersion = u8; fn handshake( version: ProtocolVersion ) -> Result<(), HandshakeErr> ``` #### Feature Support The feature support request aims to configure the Product according to the Host context. ```rust enum Feature { Chain(GenesisHash) } fn feature_supported(feature: Feature) -> Result<bool, GenericErr>; ``` ### Local storage Local storage is a basic key-value storage implemented on the Host side. Each Product can read, store, and clear only its own values. A basic Host implementation can rely on a local DB, but it can also use some kind of on-chain data storage. ```rust enum LocalStorageErr { Full, Unknown(GenericErr) } type LocalStorageKey = str type LocalStorageValue = Vec<u8> fn local_storage_read( key: LocalStorageKey ) -> Result<Option<LocalStorageValue>, LocalStorageErr> fn local_storage_write( key: LocalStorageKey, value: LocalStorageValue ) -> Result<(), LocalStorageErr> fn local_storage_clear( key: LocalStorageKey ) -> Result<(), LocalStorageErr> ``` ### Accounts More on this part can be found [here](https://hackmd.io/@valentunn/BkXioNVbZe). * **Product account** - account that belongs to the derivation hierechy described in [Appendix](#account-derivation). Those account are inherent to the Mobile App and are derived from the root user account * **Non-product account (NPA)** - other accounts that has been imported to PAPP in addition to the root account. Importing such an account allows user to utilize their existing account in the new system (e.g. in products) ```rust enum RequestCredentialsErr { NotConnected, Rejected, DomainNotValid, Unknown(GenericErr) } enum CreateProofErr { RingNotFound, Rejected, Unknown(GenericErr) } type AccountId = [u8; 32]; type PublicKey = Vec<u8>; type DotNsIdentifier = str; type DerivationIndex = u32; type ProductAccountId = (DotNsIdentifier, DerivationIndex); struct Account = { public_key: PublicKey, name: Option<str> } struct ContextualAlias = { context: [u8; 32], alias: RingVrgAlias } struct RingLocationHint { pallet_instance: Option<u32> } struct RingLocation { genesis_hash: GenesisHash, // blake2b32(ringRoot). ringRoot itself is quite large so might not fit into sss ring_root_hash: Vec<u8>, // We expect PAPP to be able to identify the ring solely based on genesisHash+ringRoot // However, there might be some hints that allow for more efficient lookup hints: Option<RingLocationHint> } type RingVrfProof = Vec<u8>; fn account_get( domain: ProductAccountId ) -> Result<Account, RequestCredentialsErr>; fn account_get_alias( domain: ProductAccountId ) -> Result<ContextualAlias, RequestCredentialsErr>; fn account_create_proof( domain: ProductAccountId, ring: RingLocation, message: Vec<u8> ) -> Result<RingVrfProof, CreateProofErr>; fn get_non_product_accounts() -> Result<Account[], RequestCredentialsErr>; ``` ### Signing #### Create Transaction Based on https://github.com/polkadot-js/api/issues/6213, but omitting the `version` field. This format is capable of supporting both V4 and V5 extrinsics. There are two different methods for creating a transaction: `create_transaction` and `create_transaction_with_non_product_account`. `create_transaction` is bound to the Host API account model; `create_transaction_with_non_product_account`, on the other hand, can request signing with any non product account, and the host should decide how to find or derive accounts for signing using `signer` field as a reference. ```rust enum CreateTransactionErr { FailedToDecode, Rejected, // Failed to infer missing extensions, some extension is unsupported, etc. NotSupported(str), PermissionDenied, Unknown(GenericErr), } struct TxPayloadExtensionV1 { id: str, extra: Vec<u8>, additional_signed: Vec<u8> } struct TxPayloadContext { metadata: Vec<u8>, token_symbol: str, token_decimals: u32, best_block_height: u32 } struct TxPayloadV1 = { signer: Option<str>, call_data: Vec<u8>, extensions: Vec<TxPayloadExtensionV1>, tx_ext_version: u8, context: TxPayloadContext } enum VersionedTxPayload { V1(TxPayloadV1) } fn create_transaction( account_id: ProductAccountId, payload: VersionedTxPayload ) -> Result<Vec<u8>, CreateTransactionErr>; fn create_transaction_with_non_product_account( payload: VersionedTxPayload ) -> Result<Vec<u8>, CreateTransactionErr>; ``` #### Signing Raw Signing of raw bytes. The interface implementation is similar to `signRaw` from `injectedWeb3`, added for backward compatibility. ```rust enum SigningErr { FailedToDecode, Rejected, PermissionDenied, Unknown(GenericErr) } enum RawPayload { Bytes(Vec<u8>), Payload(str) } struct SigningPayloadRaw { address: str, data: RawPayload } struct SigningResult { signature: Vec<u8>, signed_transaction: Option<Vec<u8>> } fn sign_raw( payload: SigningPayloadRaw ) -> Result<SigningResult, SigningErr>; ``` #### Signing JSON Payload Signing of JSON payload. The interface implementation is similar to `signPayload` from `injectedWeb3`, added for backward compatibility. ```rust enum SigningErr { FailedToDecode, Rejected, PermissionDenied, Unknown(GenericErr) } struct SigningPayload { address: str, block_hash: Vec<u8>, block_number: Vec<u8>, era: Vec<u8>, genesis_hash: GenesisHash, method: Vec<u8>, nonce: Vec<u8>, spec_version: Vec<u8>, tip: Vec<u8>, transaction_version: Vec<u8>, signed_extensions: Vec<str>, version: u32, asset_id: Option<Vec<u8>>, metadata_hash: Option<Vec<u8>>, mode: Option<u32>, with_signed_transaction: Option<bool> } struct SigningResult { signature: Vec<u8>, signed_transaction: Option<Vec<u8>> } fn sign_payload( payload: SigningPayload ) -> Result<SigningResult, SigningErr>; ``` ### Chat This API section corresponds to Product ↔ Chat integration. There are two types of chat interactions - Room Extension and Bot Extension. #### Room Extension Product can create multiple rooms, that corresponds to direct product ↔ user interactions. ##### Room Registration In case of Room Extension, Product MUST register itself as a room before sending any message. Host MUST add the Product to contact list on the first call; If Product requests creation of room with same `roomId`, Host MUST deduplicate requests and send `Exists` status. `roomId` MUST be uniq and stable across product presentations. ```rust enum ChatRoomRegistrationErr { PermissionDenied, Unknown(GenericErr) } enum ChatRoomRegistrationStatus { New, Exists } struct ChatRoomRequest { room_id: str, name: str, icon: str // URL or base64-encoded image for contact } struct ChatRoomRegistrationResult { status: ChatRoomRegistrationStatus } fn chat_create_room( room: ChatRoomRequest ) -> Result<ChatRoomRegistrationResult, ChatRoomRegistrationErr>; ``` #### Receiving chat list Product can receive chat rooms via subscription. ```rust struct ChatRoomParticipation { RoomHost, Bot } struct ChatRoom { room_id: str, participating_as: ChatRoomParticipation } fn chat_list_subscribe(callback: fn(Vector<ChatRoom>)) -> Result<Subscriber, GenericErr> ``` #### Sending Message ```rust enum ChatMessagePostingErr { MessageTooLargem, Unknown(GenericErr) } struct ChatAction { action_id: str, title: str } enum ChatActionLayout { Column, Grid } struct ChatActions { text: Option<str>, actions: Vec<ChatAction>, layout: ChatActionLayout } struct ChatMedia { url: str } struct ChatRichText { text: Option<str>, media: Vec<ChatMedia> } struct ChatFile { url: str, file_name: str, mime_type: str, size_bytes: u64, text: Option<str> } struct ChatReaction { message_id: str, emoji: str } enum ChatMessageContent { Text(str), RichText(ChatRichText), Actions(ChatActions), File(ChatFile), Reaction(ChatReaction), ReactionRemoved(ChatReaction) } struct ChatPostMessageResult { message_id: str } fn chat_post_message( room_id: str, payload: ChatMessageContent ) -> Result<ChatPostMessageResult, ChatMessagePostingErr> ``` #### Subscribing to Events A Product can subscribe to user actions and react to them. ```rust struct ActionTrigger { message_id: str, action_id: str } enum ChatActionPayload { // ChatMessageContent is defined above MessagePosted(ChatMessageContent) ActionTriggered(ActionTrigger) } struct ReceivedChatAction { room_id: str, peer: str, payload: ChatActionPayload } fn chat_action_subscribe( callback: fn(ReceivedChatAction) ) -> Result<Subscriber, GenericErr> ``` ### Statement Store A Product MAY want to integrate with the statement store directly. #### Common structs ```rust type Topic = [u8; 32]; type Channel = [u8; 32]; type DecryptionKey = [u8; 32]; struct Sr25519StatementProof { signature: [u8; 64] signer: [u8; 32] } struct Ed25519StatementProof { signature: [u8; 64] signer: [u8; 32] } struct EcdsaStatementProof { signature: [u8; 65] signer: [u8; 33] } struct OnChainStatementProof { who: [u8; 32] blockHash: [u8; 32] event: u64 } enum StatementProof { Sr25519(Sr25519StatementProof) Ed25519(Ed25519StatementProof) Ecdsa(EcdsaStatementProof) OnChain(OnChainStatementProof) } struct Statement { proof: Option<StatementProof>, decryption_key: Option<DecryptionKey>, priority: Option<u32>, channel: Option<Channel>, topics: Vec<Topic>, data: Option<Vec<u8>> } struct SignedStatement { proof: StatementProof, decryption_key: Option<DecryptionKey>, priority: Option<u32>, channel: Option<Channel>, topics: Vec<Topic>, data: Option<Vec<u8>> } ``` #### Receiving Statements ```rust fn statement_store_query( topics: Vec<Topic> ) -> Result<Vec<SignedStatement>, GenericErr>; fn statement_store_subscribe( topics: Vec<Topic>, callback: fn(Vec<SignedStatement>) ) -> Result<Subscriber, GenericErr>; ``` #### Creating Proof Before submitting a statement, the Product MUST create a statement proof and write it to the `proof` field. ```rust enum StatementProofErr { UnableToSign, UnknownAccount, Unknown(GenericErr) } fn statement_store_create_proof( // See Accounts section for details account: ProductAccountId, statement: Statement ) -> Result<StatementProof, StatementProofErr> ``` #### Submitting Statement After Generating proof product can submit statement to the store ```rust fn statement_store_submit( statement: SignedStatement ) -> Result<(), GenericErr>; ``` ### JSON RPC A Product's main way to interact with the world is through chain JSON RPC calls. The Product MUST redirect all chain requests through Host API methods. At the SDK level, this can be defined as a custom PJS/PAPI provider. All messages are already serialized as strings for compatibility. ```rust // Sending message fn jsonrpc_message_send( chain: GenesisHash, message: str ) -> Result<(), GenericError> // Receiving messages fn jsonrpc_message_subscribe( chain: GenesisHash, callback: fn(str) ) -> Result<Subscriber, GenericErr> ``` ### Bulletin Chain Access TBD (See [implementation](https://github.com/paritytech/polkadot-bulletin-chain))