Evolution SDK
Architecture

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 address
  • getUtxos() - Query UTxOs at wallet address
  • getBalance() - 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 keys
  • signMessage(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 client type through conditional types. The compiler selects client type based on wallet type:

[1] Client Config: Provides wallet instance. Wallet type determines all downstream types.

[2] ReadOnlyClient: Created from ReadOnlyWallet. Transaction builder's build() returns TransactionResultBase (unsigned transaction). No sign() method exists—calling it is compilation error.

[3] SigningClient / ApiWalletClient: Created from SigningWallet or ApiWallet. Transaction builder's build() returns SignBuilder which has sign() method. Type system guarantees signing capability exists.

[3] SigningClient / ApiWalletClient: Created from SigningWallet or ApiWallet. Transaction builder's build() returns SignBuilder which has sign() method. Type system guarantees signing capability exists.

Integration Points

The wallet layer integrates with other architectural components:

Client Factory: createClient(config) uses conditional types to return appropriate client type based on wallet. Type narrowing happens at construction—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: Conditional type TxBuilderResultType<W> maps wallet types to client types:

W extends SigningWallet | ApiWallet ? SigningTransactionBuilder : ReadOnlyTransactionBuilder

Compiler 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.