# hive-rs Implementation Plan
A step-by-step plan for building a Rust client library that mirrors the full public API of `@hiveio/dhive` (v1.3.2).
---
## Phase 1: Project Scaffolding & Core Types
### Step 1.1 — Initialize Cargo project and dependencies
Create `Cargo.toml` with all dependencies, feature flags, and metadata.
**File:** `Cargo.toml`
```toml
[package]
name = "hive-rs"
version = "0.1.0"
edition = "2021"
rust-version = "1.75"
license = "BSD-3-Clause"
description = "A Rust client library for the Hive blockchain with 1:1 dhive API parity"
repository = "https://github.com/Vheissu/hive-rs"
[features]
default = ["rustls"]
rustls = ["reqwest/rustls-tls"]
native-tls = ["reqwest/native-tls"]
testnet = []
[dependencies]
reqwest = { version = "0.12", default-features = false, features = ["json", "http2"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
secp256k1 = { version = "0.29", features = ["recovery", "rand-std", "hashes"] }
sha2 = "0.10"
ripemd = "0.1"
aes = "0.8"
cbc = "0.1"
bs58 = "0.5"
rand = "0.8"
thiserror = "2"
tracing = "0.1"
chrono = { version = "0.4", features = ["serde"] }
hex = "0.4"
[dev-dependencies]
tokio-test = "0.4"
wiremock = "0.6"
```
**File:** `src/lib.rs` — crate root with module declarations and re-exports (initially just the module structure, re-exports added as modules are built).
**Actions:**
- Create `Cargo.toml`
- Create `src/lib.rs` with all `mod` declarations
- Create empty `mod.rs` files for each submodule directory: `api/`, `crypto/`, `serialization/`, `types/`, `transport/`, `utils/`
- Run `cargo check` to verify the skeleton compiles
---
### Step 1.2 — Error types
Define the crate-wide error enum and `Result` alias. Every subsequent module depends on this.
**File:** `src/error.rs`
**Types to implement:**
- `HiveError` enum with variants: `Rpc`, `Transport`, `Serialization`, `InvalidKey`, `Signing`, `AllNodesFailed`, `Timeout`, `InvalidAsset`, `Other`
- `pub type Result<T> = std::result::Result<T, HiveError>`
- `impl From<reqwest::Error> for HiveError`
- `impl From<serde_json::Error> for HiveError`
**Tests:** Unit test that each error variant displays correctly.
---
### Step 1.3 — Asset type
The `Asset` struct is used pervasively across all types and operations. Build it early.
**File:** `src/types/asset.rs`
**Types to implement:**
- `AssetSymbol` enum: `Hive`, `Hbd`, `Vests`, `Custom(String)`
- `Asset` struct: `amount: i64`, `precision: u8`, `symbol: AssetSymbol`
**Methods:**
- `Asset::hive(f64)`, `Asset::hbd(f64)`, `Asset::vests(f64)` — constructors
- `Asset::from_string(&str) -> Result<Self>` — parse `"1.000 HIVE"`
- `impl Display` — output `"1.000 HIVE"` format
- `impl FromStr`
- Custom `Serialize` / `Deserialize` — JSON string format `"1.000 HIVE"`
- `steem_symbols(&self) -> (i64, u8, &str)` — remap HIVE→STEEM, HBD→SBD for binary serialization (critical for wire compatibility)
**Precision rules:**
- HIVE/HBD/STEEM/SBD/TESTS/TBD → 3
- VESTS → 6
**Tests:**
- Parse and round-trip `"1.000 HIVE"`, `"0.001 HBD"`, `"123456.789000 VESTS"`
- Negative amounts: `"-100.333 SBD"`
- JSON serialize/deserialize round-trip
- `steem_symbols()` mapping is correct
---
### Step 1.4 — Core type definitions (non-operation types)
Define all the struct types that mirror dhive interfaces. These are pure data types with serde derives. Build them all in one pass since they have minimal interdependencies.
**Files and types:**
**`src/types/authority.rs`**
- `Authority { weight_threshold: u32, account_auths: Vec<(String, u16)>, key_auths: Vec<(String, u16)> }`
**`src/types/price.rs`**
- `Price { base: Asset, quote: Asset }`
**`src/types/block.rs`**
- `BlockHeader { previous, timestamp, witness, transaction_merkle_root, extensions }`
- `SignedBlockHeader` (extends BlockHeader with `witness_signature`)
- `SignedBlock` (extends SignedBlockHeader with `transactions`, `block_id`, `signing_key`, `transaction_ids`)
**`src/types/transaction.rs`**
- `Transaction { ref_block_num: u16, ref_block_prefix: u32, expiration: String, operations: Vec<Operation>, extensions: Vec<String> }`
- `SignedTransaction { transaction fields + signatures: Vec<String> }`
- `TransactionConfirmation { id, block_num, trx_num, expired }`
- `TransactionStatus { status }`
**`src/types/account.rs`**
- `ExtendedAccount` — all fields from dhive's Account/ExtendedAccount interfaces
- `AccountReputation { account, reputation }`
- `OwnerHistory`, `RecoveryRequest`
- `AccountHistoryEntry`
**`src/types/chain.rs`**
- `DynamicGlobalProperties` — all 40+ fields exactly as in the tech spec
- `ChainProperties { account_creation_fee: Asset, maximum_block_size: u32, hbd_interest_rate: u16 }`
- `FeedHistory`, `ScheduledHardfork`, `RewardFund`, `Version`
**`src/types/comment.rs`**
- `Comment` — base comment fields
- `Discussion` — extended comment with reply/vote data
- `BeneficiaryRoute { account: String, weight: u16 }`
- `ActiveVote`, `DisqussionQuery`, `DiscussionQueryCategory` enum
**`src/types/rc.rs`**
- `RCAccount`, `RCParams`, `RCPool`, `Manabar`
**`src/types/misc.rs`**
- `VestingDelegation`, `ExpiringVestingDelegation`
- `Witness`, `WitnessProps`
- `OrderBook`, `OpenOrder`, `MarketTrade`, `MarketBucket`
- `SavingsWithdraw`, `ConversionRequest`, `CollateralizedConversionRequest`
- `FollowEntry`, `FollowCount`, `BlogEntry`, `BlogEntryLight`
- `Escrow`, `Proposal`, `RecurrentTransfer`, `AppliedOperation`
- `CommunityDetail`, `CommunityRole`, `Notification`
- `PostsQuery`, `AccountPostsQuery`, `CommunityQuery`, `ListCommunitiesQuery`, `CommunityRolesQuery`, `AccountNotifsQuery`
- `ChainId` struct with `mainnet()` and `testnet()` constructors
**`src/types/mod.rs`** — re-export everything
**Serde conventions:**
- Use `#[serde(rename_all = "snake_case")]` on enums
- Use field-level `#[serde(rename = "...")]` where the JSON field names differ from snake_case Rust
- Use `#[serde(default)]` for optional/newer fields that may not always be present in API responses
- Use `Option<T>` for nullable fields
**Tests:** Deserialize sample JSON responses (captured from a real Hive node) into each type.
---
### Step 1.5 — Operation types
Define the `Operation` enum with all 50 variant types and their corresponding structs. This is large but mechanical.
**File:** `src/types/operation.rs`
**Enum:**
```rust
pub enum Operation {
Vote(VoteOperation), // 0
Comment(CommentOperation), // 1
Transfer(TransferOperation), // 2
// ... all 50 variants through RecurrentTransfer = 49
}
```
**Each variant's struct** with all fields matching dhive's serialization order exactly. Every field name must match the JSON-RPC field name. Operation structs use `#[derive(Debug, Clone, Serialize, Deserialize)]`.
**Critical detail — field order matters:** The binary serializer will serialize fields in declaration order, so struct field order must match dhive's serialization order exactly. Document this constraint with a comment at the top of the file.
**Operation struct definitions** (full field list for each, from dhive's `serializer.ts`):
- `VoteOperation { voter, author, permlink, weight: i16 }`
- `CommentOperation { parent_author, parent_permlink, author, permlink, title, body, json_metadata }`
- `TransferOperation { from, to, amount: Asset, memo }`
- `TransferToVestingOperation { from, to, amount: Asset }`
- `WithdrawVestingOperation { account, vesting_shares: Asset }`
- `LimitOrderCreateOperation { owner, orderid: u32, amount_to_sell: Asset, min_to_receive: Asset, fill_or_kill: bool, expiration: String }`
- `LimitOrderCancelOperation { owner, orderid: u32 }`
- `FeedPublishOperation { publisher, exchange_rate: Price }`
- `ConvertOperation { owner, requestid: u32, amount: Asset }`
- `AccountCreateOperation { fee: Asset, creator, new_account_name, owner: Authority, active: Authority, posting: Authority, memo_key: String, json_metadata }`
- `AccountUpdateOperation { account, owner: Option<Authority>, active: Option<Authority>, posting: Option<Authority>, memo_key: String, json_metadata }`
- `WitnessUpdateOperation { owner, url, block_signing_key: String, props: ChainProperties, fee: Asset }`
- `AccountWitnessVoteOperation { account, witness, approve: bool }`
- `AccountWitnessProxyOperation { account, proxy }`
- `CustomOperation { required_auths: Vec<String>, id: u16, data: Vec<u8> }`
- `ReportOverProductionOperation { reporter, first_block: SignedBlockHeader, second_block: SignedBlockHeader }`
- `DeleteCommentOperation { author, permlink }`
- `CustomJsonOperation { required_auths: Vec<String>, required_posting_auths: Vec<String>, id: String, json: String }`
- `CommentOptionsOperation { author, permlink, max_accepted_payout: Asset, percent_hbd: u16, allow_votes: bool, allow_curation_rewards: bool, extensions: Vec<CommentOptionsExtension> }`
- `SetWithdrawVestingRouteOperation { from_account, to_account, percent: u16, auto_vest: bool }`
- `LimitOrderCreate2Operation { owner, orderid: u32, amount_to_sell: Asset, fill_or_kill: bool, exchange_rate: Price, expiration: String }`
- `ClaimAccountOperation { creator, fee: Asset, extensions: Vec<()> }`
- `CreateClaimedAccountOperation { creator, new_account_name, owner: Authority, active: Authority, posting: Authority, memo_key: String, json_metadata, extensions: Vec<()> }`
- `RequestAccountRecoveryOperation { recovery_account, account_to_recover, new_owner_authority: Authority, extensions: Vec<()> }`
- `RecoverAccountOperation { account_to_recover, new_owner_authority: Authority, recent_owner_authority: Authority, extensions: Vec<()> }`
- `ChangeRecoveryAccountOperation { account_to_recover, new_recovery_account, extensions: Vec<()> }`
- `EscrowTransferOperation { from, to, agent, escrow_id: u32, hbd_amount: Asset, hive_amount: Asset, fee: Asset, ratification_deadline: String, escrow_expiration: String, json_meta: String }`
- `EscrowDisputeOperation { from, to, agent, who, escrow_id: u32 }`
- `EscrowReleaseOperation { from, to, agent, who, receiver, escrow_id: u32, hbd_amount: Asset, hive_amount: Asset }`
- `EscrowApproveOperation { from, to, agent, who, escrow_id: u32, approve: bool }`
- `TransferToSavingsOperation { from, to, amount: Asset, memo }`
- `TransferFromSavingsOperation { from, request_id: u32, to, amount: Asset, memo }`
- `CancelTransferFromSavingsOperation { from, request_id: u32 }`
- `CustomBinaryOperation { required_owner_auths: Vec<String>, required_active_auths: Vec<String>, required_posting_auths: Vec<String>, required_auths: Vec<Authority>, id: String, data: Vec<u8> }`
- `DeclineVotingRightsOperation { account, decline: bool }`
- `ResetAccountOperation { reset_account, account_to_reset, new_owner_authority: Authority }`
- `SetResetAccountOperation { account, current_reset_account, reset_account }`
- `ClaimRewardBalanceOperation { account, reward_hive: Asset, reward_hbd: Asset, reward_vests: Asset }`
- `DelegateVestingSharesOperation { delegator, delegatee, vesting_shares: Asset }`
- `AccountCreateWithDelegationOperation { fee: Asset, delegation: Asset, creator, new_account_name, owner: Authority, active: Authority, posting: Authority, memo_key: String, json_metadata, extensions: Vec<()> }`
- `WitnessSetPropertiesOperation { owner, props: Vec<(String, Vec<u8>)>, extensions: Vec<()> }`
- `AccountUpdate2Operation { account, owner: Option<Authority>, active: Option<Authority>, posting: Option<Authority>, memo_key: Option<String>, json_metadata, posting_json_metadata, extensions: Vec<()> }`
- `CreateProposalOperation { creator, receiver, start_date: String, end_date: String, daily_pay: Asset, subject, permlink, extensions: Vec<()> }`
- `UpdateProposalVotesOperation { voter, proposal_ids: Vec<i64>, approve: bool, extensions: Vec<()> }`
- `RemoveProposalOperation { proposal_owner, proposal_ids: Vec<i64>, extensions: Vec<()> }`
- `UpdateProposalOperation { proposal_id: i64, creator, daily_pay: Asset, subject, permlink, end_date: Option<String>, extensions: Vec<()> }`
- `CollateralizedConvertOperation { owner, requestid: u32, amount: Asset }`
- `RecurrentTransferOperation { from, to, amount: Asset, memo, recurrence: u16, executions: u16, extensions: Vec<()> }`
**Also define:**
- `CommentOptionsExtension` enum (variant 0 = `Beneficiaries(Vec<BeneficiaryRoute>)`)
- `OperationName` — string enum for use with `make_bit_mask_filter`
**Custom JSON serde for Operation:** Operations arrive from the JSON-RPC as `["operation_name", { ...fields }]` (a two-element array). Implement custom `Serialize`/`Deserialize` for `Operation` to handle this tagged tuple format.
**Tests:**
- Deserialize sample operations from JSON
- Verify operation ID mapping (variant discriminant matches expected numeric ID)
---
### Step 1.6 — types/mod.rs re-exports
Wire up `src/types/mod.rs` to re-export all types from all submodules so they're accessible via `hive_rs::types::*`.
---
## Phase 2: Transport & Client
### Step 2.1 — JSON-RPC transport
Build the HTTP transport layer that sends JSON-RPC 2.0 requests.
**File:** `src/transport/http.rs`
**Struct:** `HttpTransport`
- Wraps `reqwest::Client` (configured for HTTP/2)
- Holds the current node URL
- Sends requests in the format: `{ "id": 0, "jsonrpc": "2.0", "method": "call", "params": [api, method, params] }`
- Parses JSON-RPC responses, extracting `result` or converting `error` into `HiveError::Rpc`
- Respects the configured timeout
**JSON-RPC response handling:**
- Check for `"error"` field → return `HiveError::Rpc { code, message, data }`
- Extract `"result"` field → deserialize into target type `T`
**Tests:** Use `wiremock` to mock a JSON-RPC endpoint, verify request format and response parsing.
---
### Step 2.2 — Failover transport
Build the failover wrapper around `HttpTransport`.
**File:** `src/transport/failover.rs`
**Struct:** `FailoverTransport`
- Holds a list of node URLs and current index
- Tracks consecutive failures per node
- When failures >= `failover_threshold`, rotates to next node
- Cycles through all nodes before restarting (tracks rounds)
- Configurable backoff: exponential with jitter (default), or custom `BackoffStrategy`
**`BackoffStrategy` enum:**
- `Exponential { base_ms: u64, max_ms: u64 }`
- `Linear { step_ms: u64, max_ms: u64 }`
- `Fixed { ms: u64 }`
**Backoff formula** (matching dhive): `min((tries * 10)^2, 10000)` ms with optional jitter.
**Failure conditions** (all trigger failover counter increment):
- `reqwest::Error` (connection refused, DNS failure, timeout)
- HTTP 5xx status codes
- JSON-RPC error responses do NOT trigger failover (they indicate the node is working but the request was bad)
**Tests:**
- Multiple nodes configured, first node fails → switches to second
- All nodes fail → returns `HiveError::AllNodesFailed`
- Node recovers after switching away → eventually returns to it
- Use `wiremock` with multiple mock servers
---
### Step 2.3 — Client struct
Build the root `Client` struct that exposes all sub-APIs.
**File:** `src/client.rs`
**Structs:**
- `ClientOptions { timeout, failover_threshold, address_prefix, chain_id, backoff }`
- `impl Default for ClientOptions` — sensible mainnet defaults
- `Client` — holds `Arc<ClientInner>` where `ClientInner` owns the transport and options
**Client fields (sub-APIs):**
- `database: DatabaseApi`
- `broadcast: BroadcastApi`
- `blockchain: Blockchain`
- `hivemind: HivemindApi`
- `rc: RcApi`
- `keys: AccountByKeyApi`
- `transaction: TransactionStatusApi`
**Methods:**
- `Client::new(nodes: Vec<&str>, options: ClientOptions) -> Self`
- `Client::new_default() -> Self` — uses `["https://api.hive.blog", "https://api.openhive.network"]` with default options
- `Client::call<T: DeserializeOwned>(api: &str, method: &str, params: Value) -> Result<T>` — raw JSON-RPC escape hatch
**Internal sharing:** All sub-APIs hold a reference (via `Arc`) to the shared transport. This avoids cloning the HTTP client.
**Tests:** Construct a client, verify sub-APIs are accessible, verify `call()` sends correct JSON-RPC.
---
### Step 2.4 — lib.rs re-exports
Wire up `src/lib.rs` to re-export key types at the crate root for ergonomic access:
- `Client`, `ClientOptions`
- `PrivateKey`, `PublicKey`, `Signature`
- `Asset`, `AssetSymbol`
- `Operation` and all operation structs
- `Result`, `HiveError`
- Utility functions: `get_vesting_share_price`, `get_vests`, `make_bit_mask_filter`, `build_witness_update_op`
---
## Phase 3: Read APIs
### Step 3.1 — DatabaseAPI
Implement all read methods. Each method is a thin wrapper that calls the appropriate JSON-RPC method and deserializes the response.
**File:** `src/api/database.rs`
**Implementation pattern** (every method follows this):
```rust
pub async fn get_accounts(&self, accounts: &[&str]) -> Result<Vec<ExtendedAccount>> {
self.client.call("condenser_api", "get_accounts", json!([accounts])).await
}
```
**Full method list** (47 methods, grouped):
Account methods (6):
- `get_accounts`, `get_account_count`, `get_account_history`, `get_account_reputations`, `get_owner_history`, `get_recovery_request`
Content methods (5):
- `get_content`, `get_content_replies`, `get_discussions`, `get_discussions_by_author_before_date`, `get_active_votes`
Chain state methods (9):
- `get_dynamic_global_properties`, `get_chain_properties`, `get_feed_history`, `get_current_median_history_price`, `get_hardfork_version`, `get_next_scheduled_hardfork`, `get_reward_fund`, `get_config`, `get_version`
Witness methods (2):
- `get_active_witnesses`, `get_witness_by_account`
Vesting/delegation methods (2):
- `get_vesting_delegations`, `get_expiring_vesting_delegations`
Market methods (5):
- `get_order_book`, `get_open_orders`, `get_recent_trades`, `get_market_history`, `get_market_history_buckets`
Savings/conversion methods (4):
- `get_savings_withdraw_from`, `get_savings_withdraw_to`, `get_conversion_requests`, `get_collateralized_conversion_requests`
Follow methods (4):
- `get_followers`, `get_following`, `get_follow_count`, `get_reblogged_by`
Blog methods (2):
- `get_blog`, `get_blog_entries`
Transaction verification methods (3):
- `get_potential_signatures`, `get_required_signatures`, `verify_authority`
Key lookup (1): `get_key_references`
Escrow (1): `get_escrow`
Proposals (2): `find_proposals`, `list_proposals`
Recurrent transfers (1): `find_recurrent_transfers`
Block methods (3): `get_ops_in_block`, `get_block`, `get_block_header`
**`get_discussions` special handling:** The `by` parameter maps to different RPC method names:
- `DiscussionQueryCategory::Trending` → `"condenser_api.get_discussions_by_trending"`
- `DiscussionQueryCategory::Created` → `"condenser_api.get_discussions_by_created"`
- etc.
**Tests:** Integration tests against a live Hive node (gated behind a feature flag or `#[ignore]`). Unit tests with `wiremock` for response parsing.
---
### Step 3.2 — HivemindAPI
**File:** `src/api/hivemind.rs`
All methods call the `bridge` API namespace:
- `get_ranked_posts` → `bridge.get_ranked_posts`
- `get_account_posts` → `bridge.get_account_posts`
- `get_community` → `bridge.get_community`
- `list_communities` → `bridge.list_communities`
- `get_community_roles` → `bridge.get_community_roles`
- `get_account_notifications` → `bridge.get_account_notifications`
- `get_discussion` → `bridge.get_discussion`
- `get_post` → `bridge.get_post`
---
### Step 3.3 — RCAPI
**File:** `src/api/rc.rs`
Methods call the `rc_api` namespace:
- `find_rc_accounts` → `rc_api.find_rc_accounts`
- `get_resource_params` → `rc_api.get_resource_params`
- `get_resource_pool` → `rc_api.get_resource_pool`
- `calculate_cost` — client-side computation using RC params
---
### Step 3.4 — AccountByKeyAPI and TransactionStatusAPI
**File:** `src/api/account_by_key.rs`
- `get_key_references` → `account_by_key_api.get_key_references`
**File:** `src/api/transaction_status.rs`
- `find_transaction` → `transaction_status_api.find_transaction`
---
## Phase 4: Cryptography
### Step 4.1 — Crypto utilities
Low-level hash functions used by all crypto code.
**File:** `src/crypto/utils.rs`
**Functions:**
- `sha256(data: &[u8]) -> [u8; 32]` — single SHA-256
- `double_sha256(data: &[u8]) -> [u8; 32]` — SHA-256 of SHA-256
- `ripemd160(data: &[u8]) -> [u8; 20]` — RIPEMD-160
- `sha512(data: &[u8]) -> [u8; 64]` — SHA-512
**Tests:** Hash known inputs and verify against known outputs.
---
### Step 4.2 — PublicKey
**File:** `src/crypto/keys.rs` (public key portion)
**Struct:** `PublicKey { key: secp256k1::PublicKey, prefix: String }`
**Encoding format:** `{prefix}{base58(compressed_33_bytes || ripemd160(compressed_33_bytes)[0..4])}`
**Methods:**
- `PublicKey::from_string(s: &str) -> Result<Self>` — parse `"STM7abc..."`
- Strip the prefix (first 3 chars)
- Base58 decode the remainder
- Last 4 bytes = checksum, first 33 bytes = compressed point
- Verify checksum: `ripemd160(compressed_point)[0..4] == checksum`
- `PublicKey::to_string(&self) -> String` — encode back to `"STM..."` format
- `PublicKey::is_null(&self) -> bool` — check if this is the null key (33 zero bytes)
- `impl Display`, `impl FromStr`
**Null key:** The string suffix `"1111111111111111111111111111111114T1Anm"` represents a 33-byte all-zeros key.
**Tests:**
- Parse known public keys from dhive test vectors
- Round-trip encode/decode
- Null key detection
---
### Step 4.3 — PrivateKey
**File:** `src/crypto/keys.rs` (private key portion)
**Struct:** `PrivateKey { secret: secp256k1::SecretKey }`
**WIF format:** `base58(0x80 || raw_32_bytes || double_sha256(0x80 || raw_32_bytes)[0..4])`
**Methods:**
- `PrivateKey::from_wif(wif: &str) -> Result<Self>`
- Base58 decode
- Verify checksum: `double_sha256(payload[0..33])[0..4] == payload[33..37]`
- Extract raw key: `payload[1..33]` (skip 0x80 prefix byte)
- `PrivateKey::from_login(username: &str, password: &str, role: KeyRole) -> Self`
- Seed = `username + role_string + password` (simple string concatenation, e.g., `"fooactivebarman"`)
- Raw key = `sha256(seed)`
- `PrivateKey::from_seed(seed: &str) -> Self` — `sha256(seed)` as raw key
- `PrivateKey::from_bytes(bytes: [u8; 32]) -> Result<Self>`
- `PrivateKey::generate() -> Self` — random key via `rand`
- `PrivateKey::to_wif(&self) -> String`
- `PrivateKey::public_key(&self) -> PublicKey`
- `PrivateKey::sign(&self, digest: &[u8; 32]) -> Result<Signature>` — see Step 4.4
- `PrivateKey::get_shared_secret(&self, public_key: &PublicKey) -> [u8; 64]` — ECDH shared secret for memo encryption
**`KeyRole` enum:** `Owner`, `Active`, `Posting`, `Memo` — each maps to lowercase string for seed generation.
**Tests:**
- `from_login("foo", "barman", Active)` → verify resulting public key matches dhive's `STM87F7tN56tAUL2C6J9Gzi9HzgNpZdi6M2cLQo7TjDU5v178QsYA`
- WIF encode/decode round-trip
- `from_wif` with known WIF string → known public key
- Random key generation → valid key
---
### Step 4.4 — Signature and canonical signing
**File:** `src/crypto/signature.rs`
**Struct:** `Signature { data: [u8; 65] }`
**Wire format (65 bytes):**
- Byte 0: `recovery_id + 31` (Graphene convention: +27 for uncompressed, +31 for compressed keys)
- Bytes 1-64: compact signature (`r || s`, 32 bytes each)
**Canonical signature check** (must match dhive exactly):
```rust
fn is_canonical(sig: &[u8; 64]) -> bool {
!(sig[0] & 0x80 != 0) // r high bit not set
&& !(sig[0] == 0 && sig[1] & 0x80 == 0) // r not zero-padded
&& !(sig[32] & 0x80 != 0) // s high bit not set
&& !(sig[32] == 0 && sig[33] & 0x80 == 0) // s not zero-padded
}
```
**Signing with nonce retry** (must match dhive's loop):
```
attempts = 0
loop:
attempts += 1
extra_data = sha256(message || [attempts as u8])
signature = secp256k1_sign_with_extra_data(message, secret_key, extra_data)
if is_canonical(signature):
break
```
The `extra_data` is mixed into RFC 6979 deterministic nonce generation. The `secp256k1` Rust crate does not directly support this; we will need to either:
- Use the crate's custom nonce function support, or
- Implement RFC 6979 with extra data manually, or
- Use the `secp256k1-sys` C bindings directly with `secp256k1_ecdsa_sign` and a custom nonce function
**Investigate:** Check if the `secp256k1` Rust crate's `sign_ecdsa_with_noncedata` (or equivalent) supports passing extra entropy that matches dhive's behavior. If not, implement a custom nonce function.
**Methods:**
- `Signature::from_bytes(data: [u8; 65]) -> Self`
- `Signature::from_hex(hex: &str) -> Result<Self>`
- `Signature::to_hex(&self) -> String`
- `Signature::recover(&self, digest: &[u8; 32]) -> Result<PublicKey>` — recover the signer's public key
- `Signature::is_canonical(&self) -> bool`
**Tests:**
- Sign a known message with a known key → verify signature matches dhive output
- Canonical check: construct canonical and non-canonical signatures, verify the check
- Recover public key from signature → matches signer's public key
- `from_hex` / `to_hex` round-trip
---
## Phase 5: Binary Serialization
### Step 5.1 — Serializer infrastructure
Build the binary serializer that matches Hive's `fc::raw::pack` format.
**File:** `src/serialization/serializer.rs`
**Trait:**
```rust
pub trait HiveSerialize {
fn hive_serialize(&self, buf: &mut Vec<u8>);
}
```
**File:** `src/serialization/types.rs`
**Primitive serialization functions** (all little-endian):
- `write_u8(buf, val)` — 1 byte
- `write_u16(buf, val)` — 2 bytes LE
- `write_u32(buf, val)` — 4 bytes LE
- `write_u64(buf, val)` — 8 bytes LE
- `write_i8(buf, val)` — 1 byte signed
- `write_i16(buf, val)` — 2 bytes LE signed
- `write_i32(buf, val)` — 4 bytes LE signed
- `write_i64(buf, val)` — 8 bytes LE signed
- `write_varint32(buf, val)` — LEB128 variable-length encoding
- `write_bool(buf, val)` — 1 byte (0x00 or 0x01)
- `write_string(buf, val)` — varint32 byte-length + UTF-8 bytes (byte count, not char count!)
- `write_date(buf, date_str)` — parse `"2017-07-15T16:51:19"` as UTC, write as u32 LE (seconds since epoch). Must append `'Z'` to input string before parsing, matching dhive.
- `write_public_key(buf, key_str)` — 33 bytes compressed point. If null key, write 33 zero bytes.
- `write_asset(buf, asset)` — 16 bytes total:
- i64 LE: amount in satoshis
- u8: precision
- 7 bytes: ASCII symbol, null-padded right. **HIVE→STEEM, HBD→SBD remapping happens here.**
- `write_optional<T>(buf, opt, serialize_fn)` — 0x00 if None, 0x01 + T if Some
- `write_array<T>(buf, items, serialize_fn)` — varint32 count + each item
- `write_flat_map<K,V>(buf, pairs, key_fn, val_fn)` — varint32 count + each (key, value)
- `write_authority(buf, auth)` — weight_threshold(u32) + account_auths(FlatMap) + key_auths(FlatMap)
- `write_price(buf, price)` — base(Asset) + quote(Asset)
- `write_chain_properties(buf, props)` — account_creation_fee(Asset) + maximum_block_size(u32) + hbd_interest_rate(u16)
- `write_void_array(buf)` — writes varint32(0). Used for `extensions: Vec<()>` fields.
- `write_variable_binary(buf, data)` — varint32 length + raw bytes
**Tests against dhive test vectors:**
- `"10.000 STEEM"` → `102700000000000003535445454d0000`
- `"123456.789000 VESTS"` → `081a99be1c0000000656455354530000`
- `"-100.333 SBD"` → `1378feffffffffff0353424400000000`
- Date `"2017-07-15T16:51:19"` → `07486a59`
- Date `"2000-01-01T00:00:00"` → `80436d38`
- String `"Hellooo from Swaeden!"` (with special chars) → `1548656c6cc3b6206672c3b66d205377c3a464656e21` (note: the "ö" and "ä" in the original are multi-byte UTF-8)
- Empty string `""` → `00`
- PublicKey `"STM5832HKCJzs6K3rRCsZ1PidTKgjF38ZJb718Y3pCW92HEMsCGPf"` → `021ec205b7c084b96814310c8acb4a0048e82b236f1878acc273fd1cfd03dac7e1`
- Price `{"base":"1.000 STEEM","quote":"0.523 SBD"}` → `e80300000000000003535445454d00000b020000000000000353424400000000`
---
### Step 5.2 — Operation serialization
Implement `HiveSerialize` for each of the 50 operation types and for the `Operation` enum itself.
**File:** `src/serialization/serializer.rs` (or a dedicated `src/serialization/operations.rs`)
**Operation enum serialization:**
1. Write varint32 with the operation's numeric ID (0-49)
2. Delegate to the variant's `hive_serialize` method
**For each operation struct**, implement `hive_serialize` that writes fields in the exact order defined by dhive's `serializer.ts`. The order is critical — even a single field swap will produce an invalid transaction digest.
**`comment_options` extensions special handling:**
- The extensions field is `Array(StaticVariant([BeneficiariesSerializer]))`
- Each extension: varint32(variant_id=0) + varint32(beneficiary_count) + for each: String(account) + u16(weight)
**`witness_set_properties` special handling:**
- `props` is a `FlatMap(String, VariableBinary)` — keys sorted alphabetically, values are individually binary-serialized property values
**Pow (14) and Pow2 (30):** These are legacy operations. Implement them as passthrough (serialize the raw data) or leave as unimplemented with a clear error message. dhive doesn't have serializers for these either.
**Operations 47-49** (`UpdateProposal`, `CollateralizedConvert`, `RecurrentTransfer`): These are not in the current dhive source but are in the tech spec. Implement based on the Hive C++ source (`operation.hpp`). The field orders are:
- 47 `update_proposal`: proposal_id(i64), creator(String), daily_pay(Asset), subject(String), permlink(String), end_date(Optional(Date)), extensions(VoidArray)
- 48 `collateralized_convert`: owner(String), requestid(u32), amount(Asset)
- 49 `recurrent_transfer`: from(String), to(String), amount(Asset), memo(String), recurrence(u16), executions(u16), extensions(VoidArray)
**Tests:**
- Transfer operation test vector from dhive:
- `["transfer", {"amount": 1, "from": "foo", "memo": "wedding present", "to": "bar"}]`
- → `0203666f6f03626172e80300000000000003535445454d00000f77656464696e672070726573656e74`
- Breakdown: `02`(op_id=2) + `03666f6f`("foo") + `03626172`("bar") + `e80300000000000003535445454d0000`(1.000 STEEM asset) + `0f77656464696e672070726573656e74`("wedding present")
- Full transaction serialization test vector from dhive:
- `{"ref_block_num":1234, "ref_block_prefix":1122334455, "expiration":"2017-07-15T16:51:19", "operations":[["vote",{"voter":"foo","author":"bar","permlink":"baz","weight":10000}]], "extensions":["long-pants"]}`
- → `d204f776e54207486a59010003666f6f036261720362617a1027010a6c6f6e672d70616e7473`
---
### Step 5.3 — Transaction serialization and digest
Implement serialization for the `Transaction` struct and the digest computation.
**File:** `src/serialization/serializer.rs` (transaction portion)
**Transaction serialization** (field order):
1. `ref_block_num` — u16 LE
2. `ref_block_prefix` — u32 LE
3. `expiration` — Date (u32 LE, seconds since epoch)
4. `operations` — Array(Operation): varint32 count + each operation
5. `extensions` — Array(String): varint32 count + each string
**Transaction digest computation:**
```rust
pub fn transaction_digest(tx: &Transaction, chain_id: &ChainId) -> [u8; 32] {
let mut buf = Vec::new();
tx.hive_serialize(&mut buf);
let mut to_hash = Vec::new();
to_hash.extend_from_slice(&chain_id.bytes); // 32 bytes
to_hash.extend_from_slice(&buf);
sha256(&to_hash)
}
```
**Transaction ID computation:**
```rust
pub fn generate_trx_id(tx: &Transaction) -> String {
let mut buf = Vec::new();
tx.hive_serialize(&mut buf);
let hash = sha256(&buf);
hex::encode(&hash)[..40].to_string() // first 40 hex chars
}
```
**`ref_block_prefix` derivation** (used when constructing transactions):
```rust
// head_block_id is a hex string like "00abc123..."
// ref_block_prefix = bytes 4-7 read as u32 LE
let block_id_bytes = hex::decode(head_block_id)?;
let ref_block_prefix = u32::from_le_bytes(block_id_bytes[4..8].try_into()?);
```
**Tests:**
- Compute digest for the dhive test transaction and verify it matches
- `generate_trx_id` for a known transaction
---
### Step 5.4 — Deserializer (optional, lower priority)
**File:** `src/serialization/deserializer.rs`
Implement `HiveDeserialize` trait for reading binary-serialized data back into Rust types. This is needed for:
- Parsing binary data in `witness_set_properties`
- Potentially parsing block data from raw binary sources
**Lower priority** — most data comes via JSON-RPC (JSON), not binary. Implement only the types needed for `witness_set_properties` initially.
---
## Phase 6: Transaction Signing & BroadcastAPI
### Step 6.1 — Transaction signing
Bring together crypto and serialization to sign transactions.
**File:** `src/crypto/keys.rs` (signing methods, building on Step 4.3 and 4.4)
**`sign_transaction` function:**
```rust
pub fn sign_transaction(
transaction: &Transaction,
keys: &[&PrivateKey],
chain_id: &ChainId,
) -> Result<SignedTransaction> {
let digest = transaction_digest(transaction, chain_id);
let signatures: Vec<String> = keys.iter()
.map(|k| k.sign(&digest).map(|s| s.to_hex()))
.collect::<Result<Vec<_>>>()?;
Ok(SignedTransaction {
ref_block_num: transaction.ref_block_num,
ref_block_prefix: transaction.ref_block_prefix,
expiration: transaction.expiration.clone(),
operations: transaction.operations.clone(),
extensions: transaction.extensions.clone(),
signatures,
})
}
```
**Tests:**
- Sign a transaction with a known key → verify the signature matches dhive output for the same transaction
- Sign with multiple keys → verify multiple signatures
- Verify signed transaction can be validated via `verify_authority` RPC call (integration test)
---
### Step 6.2 — BroadcastAPI core methods
**File:** `src/api/broadcast.rs`
**Core methods:**
`create_transaction` — builds a transaction with correct `ref_block_num`, `ref_block_prefix`, and `expiration`:
```rust
pub async fn create_transaction(
&self,
operations: Vec<Operation>,
expiration: Option<Duration>,
) -> Result<Transaction> {
let props = self.client.database.get_dynamic_global_properties().await?;
let ref_block_num = props.head_block_number & 0xFFFF;
let block_id_bytes = hex::decode(&props.head_block_id)?;
let ref_block_prefix = u32::from_le_bytes(block_id_bytes[4..8].try_into()?);
let expire_time = expiration.unwrap_or(Duration::from_secs(60));
let expiration_time = parse_hive_time(&props.time)? + expire_time;
let expiration_str = format_hive_time(expiration_time); // "2024-01-01T00:00:00" format
Ok(Transaction {
ref_block_num: ref_block_num as u16,
ref_block_prefix,
expiration: expiration_str,
operations,
extensions: vec![],
})
}
```
`sign_transaction` — sign without broadcasting:
```rust
pub fn sign_transaction(
&self,
transaction: &Transaction,
keys: &[&PrivateKey],
) -> Result<SignedTransaction> {
crate::crypto::sign_transaction(transaction, keys, &self.chain_id)
}
```
`send` — broadcast an already-signed transaction:
```rust
pub async fn send(
&self,
transaction: SignedTransaction,
) -> Result<TransactionConfirmation> {
self.client.call("condenser_api", "broadcast_transaction_synchronous", json!([transaction])).await
}
```
`send_operations` — convenience: create + sign + broadcast:
```rust
pub async fn send_operations(
&self,
operations: Vec<Operation>,
key: &PrivateKey,
) -> Result<TransactionConfirmation> {
let tx = self.create_transaction(operations, None).await?;
let signed = self.sign_transaction(&tx, &[key])?;
self.send(signed).await
}
```
---
### Step 6.3 — BroadcastAPI convenience methods
All the convenience methods (`vote`, `comment`, `transfer`, etc.) follow the same pattern:
```rust
pub async fn vote(
&self,
params: VoteOperation,
key: &PrivateKey,
) -> Result<TransactionConfirmation> {
self.send_operations(vec![Operation::Vote(params)], key).await
}
```
Implement all 30+ convenience methods listed in the tech spec. Each is a one-liner wrapping `send_operations`.
**Special case — `comment_with_options`:** Sends two operations in one transaction: `[Operation::Comment(comment), Operation::CommentOptions(options)]`.
**Special case — `witness_set_properties`:** Uses `build_witness_update_op` utility to convert human-readable props into the binary-encoded format before creating the operation.
**Tests:**
- Unit test: verify each convenience method creates the correct operation variant
- Integration test (testnet): broadcast a vote, transfer, and custom_json
---
## Phase 7: Memo Encryption
### Step 7.1 — Memo encode/decode
**File:** `src/crypto/memo.rs`
**Encryption flow** (must match dhive byte-for-byte):
1. Generate a unique nonce (see `unique_nonce` below)
2. Compute shared secret: `sha512(ECDH(sender_private, receiver_public))`
- ECDH: multiply `receiver_public` by `sender_private` → shared point
- Take the 33-byte compressed representation of the shared point
- SHA-512 hash it → 64 bytes
3. AES key: shared_secret bytes [0..32]
4. IV: shared_secret bytes [32..48] (first 16 bytes of second half)
5. AES-256-CBC encrypt the message (with PKCS7 padding)
6. Checksum: `sha256(shared_secret)[0..4]` (first 4 bytes)
7. Serialize the memo: `from_public(33) + to_public(33) + nonce(8) + checksum(4) + message_length(varint) + encrypted_bytes`
8. Base58-encode the serialized bytes
9. Prepend `"#"` to the result
**Decryption flow:**
1. Strip the `"#"` prefix
2. Base58-decode
3. Parse: from_public(33) + to_public(33) + nonce(8) + checksum(4) + rest = encrypted message
4. Compute shared secret using receiver's private key and sender's public key (from the memo)
5. Verify checksum matches
6. AES-256-CBC decrypt
7. Return plaintext
**`unique_nonce` function:**
- Uses current time in milliseconds + a counter to ensure uniqueness
- Format: `u64` value
**Tests:**
- Encrypt with hive-rs, decrypt with hive-rs (round-trip)
- Encrypt a known message with known keys → verify output matches dhive
- Decrypt a memo encrypted by dhive
- Error on invalid checksum
- Error on non-"#" prefixed input
---
## Phase 8: Blockchain Streaming
### Step 8.1 — Blockchain struct and streaming
**File:** `src/api/blockchain.rs`
**Structs:**
- `BlockchainMode { Irreversible, Latest }`
- `BlockchainStreamOptions { from: Option<u32>, to: Option<u32>, mode: BlockchainMode }`
**Stream implementation strategy:**
Use `async_stream` or manual `Stream` implementation. The stream:
1. Determines the starting block number:
- If `from` is specified, use it
- Otherwise, fetch current block number via `get_dynamic_global_properties`
2. Enters a polling loop:
- Fetch `get_dynamic_global_properties` to get latest block number
- For `Irreversible` mode: use `last_irreversible_block_num`
- For `Latest` mode: use `head_block_number`
- Fetch all blocks from current position to latest (sequentially)
- Yield each block
- If caught up, sleep for 3 seconds (Hive block time) before polling again
- If `to` is specified and reached, end the stream
3. Handles gaps gracefully (if a block is missing, retry)
**Methods:**
- `get_blocks(options) -> impl Stream<Item = Result<SignedBlock>>`
- `get_block_numbers(options) -> impl Stream<Item = Result<u32>>`
- `get_operations(options) -> impl Stream<Item = Result<AppliedOperation>>` — flattens operations from blocks
- `get_current_block_num(mode) -> Result<u32>`
- `get_current_block_header(mode) -> Result<BlockHeader>`
- `get_current_block(mode) -> Result<SignedBlock>`
**Tests:**
- Stream 5 blocks from a known range and verify continuity
- `get_current_block_num` returns a reasonable value
- Stream with `to` parameter terminates correctly
---
## Phase 9: Utilities
### Step 9.1 — Asset helper functions
**File:** `src/utils/asset_helpers.rs`
- `get_vesting_share_price(props: &DynamicGlobalProperties) -> Price` — calculates VESTS-to-HIVE price from `total_vesting_fund_hive` / `total_vesting_shares`
- `get_vests(props: &DynamicGlobalProperties, hive_power: &Asset) -> Asset` — converts HP to VESTS
---
### Step 9.2 — Operation bitmask filter
**File:** `src/utils/mod.rs` (or inline in utils)
- `make_bit_mask_filter(operations: &[OperationName]) -> (u64, u64)` — builds the two u64 values needed for `get_account_history`'s operation filter parameter
---
### Step 9.3 — Witness update builder
**File:** `src/utils/mod.rs`
- `build_witness_update_op(owner: &str, props: WitnessProps) -> WitnessSetPropertiesOperation`
- Serializes each property value to binary individually
- Sorts props alphabetically by key
- Returns the operation with binary-encoded prop values
---
### Step 9.4 — Nonce generation
**File:** `src/utils/nonce.rs`
- `unique_nonce() -> u64` — generates a unique nonce from current timestamp + counter (for memo encryption)
---
## Phase 10: Integration Testing & Polish
### Step 10.1 — Conformance test suite
Create a test suite that verifies byte-identical output with dhive for all critical operations.
**File:** `tests/conformance.rs`
**Generate test vectors from dhive:**
1. Write a Node.js script that uses dhive to:
- Serialize each operation type to binary → capture hex output
- Sign known transactions → capture signature hex
- Compute transaction digests → capture digest hex
- Encrypt/decrypt memos → capture encrypted output
2. Store these as JSON fixtures in `tests/fixtures/`
3. Rust tests read fixtures and verify hive-rs produces identical output
**Test categories:**
- Asset serialization (all symbol types, positive/negative amounts)
- Each operation type serialization (at least one test per operation)
- Transaction digest computation
- Transaction signing (signature must match for same key + same transaction)
- Public key encoding/decoding
- Private key WIF encoding/decoding
- Memo encryption (known key pair + known nonce → deterministic output)
---
### Step 10.2 — Integration tests against Hive testnet
**File:** `tests/integration.rs` (gated behind `#[cfg(feature = "integration")]`)
**Tests:**
- Connect to testnet, fetch `get_dynamic_global_properties`
- Fetch accounts, verify response parses correctly
- Broadcast a vote on testnet
- Broadcast a transfer on testnet
- Broadcast a custom_json on testnet
- Stream blocks for 10 seconds, verify block numbers are sequential
- Failover: configure first node as dead endpoint, verify automatic switch to second node
- Memo: encrypt a memo, send it in a transfer, read the transfer back, decrypt the memo
---
### Step 10.3 — Documentation
- Add doc comments (`///`) to all public types and methods
- Include usage examples in doc comments for key entry points (`Client`, `PrivateKey`, `Asset`)
- Add a top-level crate doc comment in `lib.rs` with a quick-start example matching the tech spec's usage example
- Ensure `cargo doc` builds cleanly with no warnings
---
### Step 10.4 — Final lib.rs re-exports and cleanup
Review and finalize all public re-exports from `src/lib.rs`:
```rust
pub use client::{Client, ClientOptions};
pub use error::{HiveError, Result};
pub use crypto::keys::{PrivateKey, PublicKey, KeyRole};
pub use crypto::signature::Signature;
pub use crypto::memo;
pub use types::*;
pub use utils::{get_vesting_share_price, get_vests, make_bit_mask_filter, build_witness_update_op};
```
Ensure the developer experience matches the tech spec's usage example.
---
## Critical Implementation Details (Reference)
These are bite-level details discovered from analyzing dhive source code that must be replicated exactly:
1. **All integers are LITTLE ENDIAN** on the wire
2. **Strings use varint32 byte-length prefix** (byte count, not char count) + raw UTF-8 bytes
3. **Asset wire format is 16 bytes:** i64 LE satoshis + u8 precision + 7-byte null-padded ASCII symbol. **HIVE→STEEM and HBD→SBD** remapping happens during serialization only
4. **Operation IDs are varint32-encoded**, followed by fields in strict order defined in dhive's `serializer.ts`
5. **Canonical signatures are mandatory:** both `r` and `s` must have their high bit unset and must not be zero-padded. Retry signing with incrementing nonce counter until canonical
6. **Signature recovery ID offset is +31** (not +27), because Graphene uses compressed keys
7. **Transaction digest** = `sha256(chain_id_bytes || serialized_tx_bytes)`. Mainnet chain ID is 32 zero bytes
8. **Public keys use RIPEMD-160 checksum** (4 bytes). Private keys (WIF) use **double SHA-256 checksum** (4 bytes)
9. **Dates** are UTC Unix timestamps as u32 LE. Input strings have `'Z'` appended before parsing (format: `"2017-07-15T16:51:19"`)
10. **`extensions` fields** using `Array(Void)` must always serialize as empty (varint32 = 0, single byte `0x00`)
11. **`ref_block_prefix`** = bytes 4-7 of hex-decoded `head_block_id`, read as u32 LE
12. **Signing nonce retry:** `extra_data = sha256(message || [attempt_counter])` where counter starts at 1
13. **Null public key** (33 zero bytes) has the string representation ending in `"1111111111111111111111111111111114T1Anm"`
14. **`comment_options` extensions** use `StaticVariant` encoding: varint32(0) for beneficiaries variant, then Array of `{account: String, weight: u16}`
15. **`witness_set_properties` props** are sorted alphabetically by key name, values are individually binary-serialized
---
## Dependency Graph Summary
```
Phase 1 (Types) ──────────────────────────────────────────────┐
1.1 Scaffolding │
1.2 Error types │
1.3 Asset type │
1.4 Core type definitions │
1.5 Operation types │
1.6 types/mod.rs re-exports │
│
Phase 2 (Transport & Client) ─────────────────────────────────┤
2.1 HTTP transport (depends on 1.2) │
2.2 Failover transport (depends on 2.1) │
2.3 Client struct (depends on 2.2, all types) │
2.4 lib.rs re-exports │
│
Phase 3 (Read APIs) ──────────────────────────────────────────┤
3.1 DatabaseAPI (depends on 2.3, all types) │
3.2 HivemindAPI (depends on 2.3) │
3.3 RCAPI (depends on 2.3) │
3.4 AccountByKeyAPI + TxStatusAPI (depends on 2.3) │
│
Phase 4 (Crypto) ─────────────────────────────────────────────┤
4.1 Crypto utilities (no deps) │
4.2 PublicKey (depends on 4.1) │
4.3 PrivateKey (depends on 4.1, 4.2) │
4.4 Signature (depends on 4.1, 4.2, 4.3) │
│
Phase 5 (Serialization) ──────────────────────────────────────┤
5.1 Serializer primitives (depends on 1.3, 4.2) │
5.2 Operation serialization (depends on 5.1, 1.5) │
5.3 Transaction serialization (depends on 5.1, 5.2, 4.1) │
5.4 Deserializer (optional, depends on 5.1) │
│
Phase 6 (Signing & Broadcasting) ─────────────────────────────┤
6.1 Transaction signing (depends on 4.4, 5.3) │
6.2 BroadcastAPI core (depends on 6.1, 3.1, 2.3) │
6.3 BroadcastAPI convenience (depends on 6.2) │
│
Phase 7 (Memo) ───────────────────────────────────────────────┤
7.1 Memo encode/decode (depends on 4.3) │
│
Phase 8 (Streaming) ──────────────────────────────────────────┤
8.1 Blockchain streaming (depends on 3.1, 2.3) │
│
Phase 9 (Utilities) ──────────────────────────────────────────┤
9.1 Asset helpers (depends on 1.3, 1.4) │
9.2 Bitmask filter (depends on 1.5) │
9.3 Witness update builder (depends on 5.1, 1.5) │
9.4 Nonce generation (no deps) │
│
Phase 10 (Testing & Polish) ──────────────────────────────────┘
10.1 Conformance tests (depends on everything)
10.2 Integration tests (depends on everything)
10.3 Documentation (depends on everything)
10.4 Final re-exports (depends on everything)
```