Wallet Layer
Why wallets are separated by capability through the type system
Abstract
Wallet capabilities vary fundamentally: read-only wallets observe addresses and UTxOs but cannot sign; signing wallets can sign transactions but differ in key management (seed phrase, private key, CIP-30 browser API). The wallet layer captures these differences through the type system, making signing operations unavailable at compile time when using read-only wallets. This capability-based separation prevents calling .sign() on clients that cannot sign—not through runtime checks, but through type constraints that make invalid operations inexpressible.
Purpose and Scope
This document explains why wallet capabilities determine client types, how the type system enforces capability boundaries, and the architectural benefits of compile-time capability checking. It covers the wallet type hierarchy, client type determination, and capability-driven type safety.
Not covered: Specific wallet implementations (see wallet guides), key derivation details (see BIP-32/BIP-44 specifications), or CIP-30 protocol details (see CIP-30 standard).
Design Philosophy
Without type-level capability separation, applications rely on runtime checks: "Does this wallet support signing? If not, throw error." This defers errors to runtime and allows code to attempt operations that will inevitably fail. The question "can this wallet sign?" must be answered repeatedly throughout the codebase.
The architecture encodes capability in types. A ReadOnlyWallet produces a ReadOnlyClient which has no .sign() method—attempting to call it is a compilation error. A SigningWallet produces a SigningClient where .sign() exists and is type-safe. The compiler enforces capability boundaries before code runs.
This separation provides security by design: applications needing only monitoring cannot accidentally expose signing capability. The type system makes "read-only" genuinely read-only at the language level, not through defensive programming.
This separation provides security by design: applications needing only monitoring cannot accidentally expose signing capability. The type system makes "read-only" genuinely read-only at the language level, not through defensive programming.
Wallet Capability Hierarchy
Wallets separate into two capability levels, with signing wallets further divided by key management approach:
[1] Wallet Base: Common operations all wallets support:
address()- Get wallet addressgetUtxos()- Query UTxOs at wallet addressgetBalance()- Query total balance
[2] ReadOnlyWallet: Base operations only. No signing methods exist. Produced from address or credential without keys.
[3] SigningWallet: Base operations plus signing:
signTx(transaction)- Sign transaction with wallet keyssignMessage(message)- Sign arbitrary message
Three signing implementations:
-
SeedWallet: HD wallet from mnemonic (12/15/24 words)
-
PrivateKeyWallet: Extended private key (xprv)
-
ApiWallet: CIP-30 browser wallet API (Nami, Eternl, Flint, hardware wallets)
-
ApiWallet: CIP-30 browser wallet API (Nami, Eternl, Flint, hardware wallets)
Client Type Determination
Wallet capability determines the signing side of staged client assembly. The final client type depends on whether a provider is also present:
[1] ClientAssembly: Chain-scoped starting point.
[2] ReadClient: Provider-backed client with no wallet capability yet.
[3] AddressClient: Wallet identity only, created with .withAddress().
[4] OfflineSignerClient: Signing capability without provider-backed reads or transaction building, created with .withSeed(), .withPrivateKey(), or .withCip30().
[5] ReadOnlyClient: Read-capable client with wallet identity. Transaction builder's build() returns TransactionResultBase (unsigned transaction). No sign() method exists.
[6] SigningClient: Full provider-backed signing client. Transaction builder's build() returns SignBuilder, so sign() is available.
Integration Points
The wallet layer integrates with other architectural components:
Client Factory: client(chain) uses staged methods like .withAddress(), .withSeed(), .withPrivateKey(), and .withCip30() to return the appropriate client type. Type narrowing happens during assembly—no runtime type guards needed in application code.
Transaction Builder: Builder type (ReadOnlyTransactionBuilder vs SigningTransactionBuilder) determined by wallet capability. Read-only builders cannot produce SignBuilder, only TransactionResultBase.
Type System Enforcement: The staged client surface narrows transaction builder access based on the assembled capability set:
ReadOnlyClient -> ReadOnlyTransactionBuilder
SigningClient -> SigningTransactionBuilderCompiler automatically selects correct return type based on wallet parameter.
Effect-TS Integration: All wallet operations available as Effect values (wallet.Effect.*). Transaction builder uses Effect API for compositional error handling.
Related Topics
- Client Architecture - How clients compose with wallets and providers
- Transaction Flow - How wallet type affects build/sign/submit flow
- Deferred Execution - Change address resolution from wallet