bsv-rs 0.3.5

BSV blockchain SDK for Rust - primitives, script, transactions, and more
Documentation
# WalletWire Protocol Module
> Binary protocol for efficient wallet communication

## Overview

The WalletWire protocol provides efficient binary serialization for wallet operations. It enables remote wallet communication over any transport (HTTP, WebSocket, IPC) with minimal overhead. The protocol is binary-compatible with the TypeScript and Go BSV SDK implementations.

## Files

| File | Purpose | Lines |
|------|---------|-------|
| `mod.rs` | Module root, WalletWire trait, status/counterparty codes | ~173 |
| `calls.rs` | WalletCall enum (28 call codes), TryFrom/Display/method_name impls | ~197 |
| `encoding.rs` | WireReader/WireWriter with Go-compatible nil sentinel and complex type support | ~2554 |
| `processor.rs` | Generic server-side processor over WalletInterface | ~949 |
| `transceiver.rs` | Client-side message serialization and full wallet interface (all 28 methods) | ~1628 |

## Architecture

```
┌───────────────────────────┐         ┌───────────────────────────┐
│  WalletWireTransceiver    │         │  WalletWireProcessor      │
│        (Client)           │         │        (Server)           │
└───────────┬───────────────┘         └───────────┬───────────────┘
            │                                     │
            │       Binary Message                │
            │ ───────────────────────────────────>│
            │                                     │
            │       Binary Response               │
            │ <───────────────────────────────────│
            │                                     │
            └─────────────────────────────────────┘
                        WalletWire
                       (Transport)
```

## Wire Protocol Format

### Request Frame

```
┌─────────┬──────────────┬────────────┬────────────────┐
│ Call    │ Originator   │ Originator │ Serialized     │
│ Code    │ Length       │ String     │ Parameters     │
│ (1 byte)│ (1 byte)     │ (N bytes)  │ (variable)     │
└─────────┴──────────────┴────────────┴────────────────┘
```

### Response Frame (Success)

```
┌─────────┬────────────────────────────────────────────┐
│ Error=0 │ Serialized Result                          │
│ (1 byte)│ (variable length)                          │
└─────────┴────────────────────────────────────────────┘
```

### Response Frame (Error)

```
┌─────────┬────────────────┬────────────────┬──────────┐
│ Error≠0 │ Message Length │ Error Message  │ Stack    │
│ (1 byte)│ (varint)       │ (UTF-8)        │ (varint) │
└─────────┴────────────────┴────────────────┴──────────┘
```

## Call Codes

| Code | Method | Code | Method |
|------|--------|------|--------|
| 1 | createAction | 15 | createSignature |
| 2 | signAction | 16 | verifySignature |
| 3 | abortAction | 17 | acquireCertificate |
| 4 | listActions | 18 | listCertificates |
| 5 | internalizeAction | 19 | proveCertificate |
| 6 | listOutputs | 20 | relinquishCertificate |
| 7 | relinquishOutput | 21 | discoverByIdentityKey |
| 8 | getPublicKey | 22 | discoverByAttributes |
| 9 | revealCounterpartyKeyLinkage | 23 | isAuthenticated |
| 10 | revealSpecificKeyLinkage | 24 | waitForAuthentication |
| 11 | encrypt | 25 | getHeight |
| 12 | decrypt | 26 | getHeaderForHeight |
| 13 | createHmac | 27 | getNetwork |
| 14 | verifyHmac | 28 | getVersion |

`WalletCall` provides: `as_u8()`, `method_name()` (returns camelCase name), `TryFrom<u8>`, and `Display`.

## Serialization Rules

### Go-Compatible Nil Sentinel

Optional strings, bytes, string arrays, and string maps use `VarInt(u64::MAX)` (9 bytes: `FF FF FF FF FF FF FF FF FF`) as a nil/absent sentinel, matching the Go SDK's `VarInt(math.MaxUint64)` encoding. This is used in `read/write_optional_string`, `read/write_optional_bytes`, `read/write_string_array`, `read/write_string_map`, and `read/write_optional_string_map`.

Empty optional strings and empty optional bytes are treated the same as `None` — both write the nil sentinel. This matches Go SDK behavior.

### Signed VarInt (ZigZag Encoding)

Used for optional numeric values and signed counts (e.g., optional input/output arrays). ZigZag encoding maps signed integers to unsigned:
- Non-negative `n` is encoded as `2n`
- Negative `n` is encoded as `2|n| - 1`

This allows -1 (representing `null`/`None`) to be encoded as a single byte (0x01).

### Type Encoding

| Type | Encoding |
|------|----------|
| `u8`, `i8` | 1 byte |
| `u16`, `u32`, `u64` | Little-endian |
| `varint` | Variable (1-9 bytes) |
| `signed_varint` | ZigZag encoded |
| UTF-8 string | `varint(len) + bytes` |
| Optional string | `varint(len or MaxUint64) + bytes` (Go-compat nil sentinel) |
| Optional bytes | `varint(len or MaxUint64) + bytes` (Go-compat nil sentinel) |
| Optional bool | `i8(-1=None, 0=false, 1=true)` |
| String array | `varint(count or MaxUint64) + strings...` (Go-compat nil sentinel) |
| String map | `varint(count or MaxUint64) + sorted (key, value)...` (Go-compat nil sentinel) |
| Optional string map | `varint(count or MaxUint64) + sorted (key, value)...` (Go-compat nil sentinel) |
| Outpoint | `32 bytes (txid) + varint(vout)` |
| Public key | 33 bytes (compressed) |
| Counterparty | `0=None, 11=Self, 12=Anyone, else=33-byte pubkey` |
| Protocol ID | `u8(security_level) + string(name)` |
| Optional Protocol ID | `u8(255) + empty string` for None |
| Action status | `i8(1-8 or -1=unknown)` |
| Query mode | `0=any, 1=all` |
| Output include | `0=locking scripts, 1=entire transactions` |
| SendWithResultStatus | `0=unproven, 1=sending, 2=failed` |

### Counterparty Encoding

```rust
pub mod counterparty_codes {
    pub const UNDEFINED: u8 = 0;   // None
    pub const SELF: u8 = 11;       // Counterparty::Self_
    pub const ANYONE: u8 = 12;     // Counterparty::Anyone
    // Other values: first byte of 33-byte compressed public key
}
```

### Action Status Codes

```rust
pub mod status_codes {
    pub const COMPLETED: i8 = 1;
    pub const UNPROCESSED: i8 = 2;
    pub const SENDING: i8 = 3;
    pub const UNPROVEN: i8 = 4;
    pub const UNSIGNED: i8 = 5;
    pub const NOSEND: i8 = 6;
    pub const NONFINAL: i8 = 7;
    pub const FAILED: i8 = 8;
    pub const UNKNOWN: i8 = -1;
}
```

## WireReader / WireWriter

`WireReader<'a>` and `WireWriter` wrap the primitives `Reader`/`Writer` with Go-compatible nil sentinel support and wallet-specific type methods.

**Basic types:** `read/write_u8`, `i8`, `u16_le`, `u32_le`, `u64_le`, `var_int`, `signed_var_int`, `optional_var_int`, `bytes`, plus `read_remaining`/`remaining`/`is_empty`/`position` on reader and `len`/`is_empty`/`as_bytes`/`into_bytes`/`with_capacity` on writer.

**Wire protocol types (both reader and writer):**
- `string`, `optional_string`, `optional_bytes`, `string_array`
- `optional_bool`, `outpoint`, `outpoint_string`, `counterparty`
- `protocol_id`, `optional_protocol_id`, `action_status`
- `txid_hex`, `pubkey_hex` (reader only), `query_mode`, `optional_query_mode`
- `output_include`, `optional_output_include`
- `string_map`, `optional_string_map`
- `send_with_result_status`, `send_with_result`, `send_with_result_array`
- `sign_action_spend`, `sign_action_spends`
- `wallet_certificate`, `optional_wallet_certificate`
- `identity_certifier`, `optional_identity_certifier`, `identity_certificate`
- `wallet_payment`, `optional_wallet_payment`
- `basket_insertion`, `optional_basket_insertion`
- `internalize_output`
- `wallet_action_input`, `wallet_action_output`, `wallet_action`
- `wallet_output`

**Writer-only:** `optional_string_array` (writes `None` as nil sentinel, `Some` as count + elements).

`WireWriter` implements `Default`.

**WalletCertificate wire format (Go-compatible):** `type(32 raw bytes, base64) -> serial(32 raw bytes, base64) -> subject(33 pubkey) -> certifier(33 pubkey) -> revocation outpoint -> fields(sorted map) -> signature(remaining bytes, no length prefix)`.

**IdentityCertificate wire format (Go-compatible):** `VarInt(cert_len) + cert_bytes (WalletCertificate format) -> optional certifier_info -> optional publicly_revealed_keyring -> optional decrypted_fields`.

**WalletAction wire format (Go-compatible):** `txid(32) -> satoshis(unsigned VarInt) -> status(i8) -> isOutgoing(optional bool) -> description(string) -> labels(string array) -> version(VarInt) -> lockTime(VarInt) -> inputs(VarInt count or MaxUint64) -> outputs(VarInt count or MaxUint64)`.

**WalletActionOutput field order:** `OutputIndex -> Satoshis -> LockingScript -> Spendable -> OutputDescription -> Basket -> Tags -> CustomInstructions`.

**WalletOutput field order:** `Outpoint -> Satoshis -> LockingScript -> CustomInstructions -> Tags -> Labels` (no `spendable` field on wire, defaults to `true`).

**WalletActionInput:** `sequence_number` is VarInt (not u32_le), matching Go.

**String maps:** Keys sorted lexicographically before writing, matching Go SDK.

**Tests:** Comprehensive roundtrip tests for every type including edge cases (empty collections, unicode strings, large data, all security levels, all action statuses, negative satoshis).

## Processor (Server-Side)

`WalletWireProcessor<W: WalletInterface>` handles incoming binary messages, dispatches to wallet methods, and returns serialized responses.

**Configuration:**
- `new(wallet)` - default (mainnet, version "0.1.0")
- `with_config(wallet, network, version)` - custom network/version
- `wallet()`, `network()`, `version()` - accessors

**Fully implemented handlers (delegated to wallet):**
- getPublicKey, encrypt, decrypt, createHmac, verifyHmac
- createSignature, verifySignature
- revealCounterpartyKeyLinkage, revealSpecificKeyLinkage
- isAuthenticated, waitForAuthentication, getHeight, getHeaderForHeight

**Processor-local handlers:**
- getNetwork (uses processor's configured network, single byte: 0x00=mainnet, 0x01=testnet)
- getVersion (uses processor's configured version, raw UTF-8 bytes, no length prefix)

**Stub handlers (return error, require full wallet implementation):**
- createAction, signAction, abortAction, listActions, internalizeAction
- listOutputs, relinquishOutput
- acquireCertificate, listCertificates, proveCertificate, relinquishCertificate
- discoverByIdentityKey, discoverByAttributes

All 28 call codes are dispatched in the match statement. Stub handlers return descriptive errors indicating a full wallet implementation is required.

**Error codes:** 0=success, 1=generic, 6=invalid parameter, 7=insufficient funds.

## Transceiver (Client-Side)

`WalletWireTransceiver<T: WalletWire>` serializes all 28 wallet methods into binary messages and deserializes responses.

- `new(wire)` - creates transceiver with wire transport
- `wire()` - access underlying transport

**All 28 methods fully implemented:**
- **Key ops:** get_public_key, encrypt, decrypt, create_hmac, verify_hmac, create_signature, verify_signature
- **Key linkage:** reveal_counterparty_key_linkage, reveal_specific_key_linkage
- **Action ops:** create_action (with full options: sign_and_process, trust_self, known_txids, no_send, no_send_change, send_with, randomize_outputs), sign_action, abort_action, list_actions, internalize_action
- **Output ops:** list_outputs, relinquish_output
- **Certificate ops:** acquire_certificate (both Direct and Issuance protocols), list_certificates, prove_certificate, relinquish_certificate
- **Discovery ops:** discover_by_identity_key, discover_by_attributes
- **Chain ops:** get_height, get_network, get_version, get_header, wait_for_authentication, is_authenticated

**Helper methods:** `parse_certificate_from_binary`, `parse_discovery_result` (internal).

**Certificate binary format in transceiver response parsing:** `type(32 bytes) -> subject(33 bytes) -> serial(32 bytes) -> certifier(33 bytes) -> outpoint -> varint(sig_len) + sig -> fields map`. Note: this order differs from the `WireWriter::write_wallet_certificate` Go-compatible format (which puts serial before subject).

## Public Exports

```rust
pub use calls::WalletCall;
pub use encoding::{WireReader, WireWriter};
pub use processor::WalletWireProcessor;
pub use transceiver::WalletWireTransceiver;

#[async_trait]
pub trait WalletWire: Send + Sync {
    async fn transmit_to_wallet(&self, message: &[u8]) -> Result<Vec<u8>, Error>;
}

pub mod counterparty_codes { /* UNDEFINED=0, SELF=11, ANYONE=12 */ }
pub mod status_codes { /* COMPLETED=1 .. FAILED=8, UNKNOWN=-1 */ }
```

## Testing

Tests use a loopback wire pattern connecting transceiver directly to processor:

```rust
struct LoopbackWire {
    processor: Arc<WalletWireProcessor<ProtoWallet>>,
}

impl WalletWire for LoopbackWire {
    async fn transmit_to_wallet(&self, message: &[u8]) -> Result<Vec<u8>, Error> {
        self.processor.process_message(message).await
    }
}
```

**Test coverage:**
- encoding.rs: Roundtrip tests for all primitive and complex types (signed varint, strings, optional values, outpoints, counterparty, protocol IDs, action status, query mode, output include, string maps, optional string arrays, send-with-results, sign-action-spends, wallet certificates, identity certifiers/certificates, wallet payments, basket insertions, internalize outputs, wallet action inputs/outputs/actions, wallet outputs). Edge cases include unicode strings, large data, empty collections, negative satoshis, and all enum variants.
- processor.rs: Tests for getPublicKey, getNetwork, getVersion, isAuthenticated, invalid call codes, and createAction error.
- transceiver.rs: End-to-end loopback tests for get_public_key, encrypt/decrypt roundtrip, create/verify HMAC, create/verify signature, is_authenticated, get_network, get_version.

Run tests:

```bash
cargo test wallet::wire --features wallet
```

## Cross-SDK Compatibility

The wire format is binary-compatible with:
- [TypeScript SDK](https://github.com/bitcoin-sv/ts-sdk) - `WalletWireProcessor`, `WalletWireTransceiver`
- [Go SDK](https://github.com/bitcoin-sv/go-sdk) - `wallet/serializer`

Messages serialized by one SDK can be deserialized by another. Key compatibility details:
- **Nil sentinel**: `VarInt(u64::MAX)` (9 bytes) matches Go's `VarInt(math.MaxUint64)`
- **getNetwork**: Single byte (0x00=mainnet, 0x01=testnet), not a length-prefixed string
- **getVersion**: Raw UTF-8 bytes with no length prefix
- **WalletAction satoshis**: Unsigned VarInt (cast to i64 on read)
- **WalletAction version/lockTime**: VarInt (not u32 little-endian)
- **WalletActionInput sequence_number**: VarInt (not u32 little-endian)
- **WalletOutput**: No `spendable` field on wire (defaults to `true`)
- **String maps**: Keys sorted lexicographically before writing
- **Empty optional strings/bytes**: Treated same as None (both write nil sentinel)

54 Go wire test vectors in `tests/vectors/wallet_wire/` validate cross-SDK compatibility. See `tests/wallet_wire_cross_sdk_tests.rs` for the 42 cross-SDK roundtrip tests.

## Dependencies

- `async-trait` - For async trait methods in `WalletWire`
- Internal: `primitives::encoding::{Reader, Writer}` for base serialization
- Internal: `primitives::{from_hex, to_hex, PublicKey, from_base64, to_base64}` for type conversions
- Internal: `wallet::types` for all wallet type definitions
- Internal: `wallet::interface::WalletInterface` for processor generics

## Related Documentation

- `../CLAUDE.md` - Wallet module documentation
- `../interface.rs` - WalletInterface trait definition
- `../types.rs` - Core wallet type definitions
- `../proto_wallet.rs` - ProtoWallet implementation
- `../../primitives/encoding/` - Base Reader/Writer