Purpose: Track covenant lifecycle, cache blockchain data, manage local persistence
Complexity: Medium - Database design + blockchain state reconciliation
State Management bridges the gap between blockchain (source of truth) and app (needs fast UI):
Key principle: Blockchain is always right. Local state is cache + UI optimizations.
1. My Covenants (Created or Funded)
Table: covenants
- covenant_id (TEXT, PRIMARY KEY)
- role (TEXT: "sender" or "seller")
- recipient_cash_account (TEXT)
- amount (REAL)
- buffer (REAL)
- total_bch (REAL)
- status (TEXT: "created", "funded", "claimed", "expired", "aborted")
- created_at (INTEGER, Unix timestamp)
- expires_at (INTEGER)
- funded_tx_id (TEXT, nullable)
- claimed_tx_id (TEXT, nullable)
Why store? Fast lookup (“Show me all my active covenants”), offline access
2. My Listings
Table: listings
- listing_id (TEXT, PRIMARY KEY)
- type (TEXT: "buy" or "sell")
- currency (TEXT)
- amount_min (REAL)
- amount_max (REAL)
- payment_methods (TEXT, JSON array)
- created_at (INTEGER)
- expires_at (INTEGER)
- active (BOOLEAN)
Why store? Know which listings I have active, edit/delist quickly
3. Cached Bulletin Board Listings
Table: cached_listings
- listing_id (TEXT, PRIMARY KEY)
- seller_cash_account (TEXT)
- currency (TEXT)
- amount_min (REAL)
- amount_max (REAL)
- payment_methods (TEXT, JSON)
- rating (REAL, nullable)
- cached_at (INTEGER, timestamp)
- ttl (INTEGER, seconds)
Why store? Avoid querying Electrum every time user opens app (5-minute cache)
4. Cash Account Cache
Table: cash_account_cache
- cash_account (TEXT, PRIMARY KEY, e.g., "Elena#142")
- address (TEXT, BCH address)
- resolved_at (INTEGER, timestamp)
- ttl (INTEGER, 86400 seconds = 24 hours)
Why store? Cash Account resolution is slow (Electrum query), cache aggressively
5. Wallet Metadata
Table: wallet_metadata
- key (TEXT, PRIMARY KEY)
- value (TEXT)
Examples:
- "last_used_index" → "5" (HD wallet derivation)
- "cash_account" → "Isabel#142"
- "nostr_pubkey" → "npub1..."
Why store? Small configuration values that don’t fit elsewhere
6. Notification History (Auto-Funding Log)
Table: notification_history
- id (INTEGER, PRIMARY KEY AUTOINCREMENT)
- notification_text (TEXT)
- parsed_amount (REAL, nullable)
- parsed_reference (TEXT, nullable)
- covenant_id (TEXT, nullable)
- action_taken (TEXT: "funded", "no_match", "error")
- timestamp (INTEGER)
Why store? Debug auto-funding issues, show user history (“You funded 3 covenants today”)
❌ Don’t store:
Why? Security (keys), privacy (other users), performance (too much data)
Why Room?
Example entity:
@Entity(tableName = "covenants")
data class Covenant(
@PrimaryKey val covenant_id: String,
val role: String, // "sender" or "seller"
val recipient_cash_account: String,
val amount: Double,
val buffer: Double,
val total_bch: Double,
val status: String, // "created", "funded", "claimed", "expired", "aborted"
val created_at: Long,
val expires_at: Long,
val funded_tx_id: String?,
val claimed_tx_id: String?
)
@Dao
interface CovenantDao {
@Query("SELECT * FROM covenants WHERE status = 'funded' ORDER BY created_at DESC")
fun getActiveCovenants(): LiveData<List<Covenant>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCovenant(covenant: Covenant)
@Query("UPDATE covenants SET status = :status WHERE covenant_id = :id")
fun updateCovenantStatus(id: String, status: String)
}
Why Core Data?
Alternative: SQLite.swift (lighter weight, less magic)
Why IndexedDB?
Alternative: localStorage (simpler, but synchronous and small)
Created → Funded → Claimed (success path)
↘ Expired (timeout)
↘ Aborted (price drop)
Pseudocode:
function trackCovenantState(covenant_id):
covenant = getCovenantFromDB(covenant_id)
// Query blockchain for current state
blockchain_state = electrumQuery("blockchain.covenant.get_status", {
covenant_id: covenant_id
})
// Reconcile local state with blockchain
if blockchain_state.funded and covenant.status == "created":
// State changed: created → funded
updateCovenantStatus(covenant_id, "funded")
showNotification("Covenant funded: " + covenant.recipient_cash_account)
else if blockchain_state.claimed and covenant.status == "funded":
// State changed: funded → claimed
updateCovenantStatus(covenant_id, "claimed")
showNotification("Covenant claimed by " + covenant.recipient_cash_account)
else if blockchain_state.expired and covenant.status == "funded":
// State changed: funded → expired
updateCovenantStatus(covenant_id, "expired")
showNotification("Covenant expired, funds returned")
else if blockchain_state.aborted:
// Price dropped >7%, covenant aborted
updateCovenantStatus(covenant_id, "aborted")
showNotification("Covenant aborted (price drop)")
Problem: Polling blockchain every second is wasteful (battery, bandwidth)
Solution: Electrum subscriptions (push notifications)
Pseudocode:
function subscribeToCovenantUpdates(covenant_id):
electrum.subscribe("covenant.status_changed", {
covenant_id: covenant_id,
callback: (new_status) => {
// Electrum pushes update when covenant state changes
updateCovenantStatus(covenant_id, new_status)
refreshUI()
}
})
Electrum WebSocket message:
{
"method": "covenant.status_changed",
"params": {
"covenant_id": "abc123...",
"new_status": "funded",
"tx_id": "def456..."
}
}
Benefit: Near-instant updates (no polling), low battery usage
TTL: 5 minutes (balances freshness vs performance)
Pseudocode:
function getCachedListings(currency):
cached = db.query("SELECT * FROM cached_listings WHERE currency = ? AND cached_at + ttl > ?",
currency, now())
if cached.length > 0:
return cached // Cache hit
// Cache miss - query Electrum
fresh_listings = queryElectrumListings(currency)
// Store in cache
for listing in fresh_listings:
db.insert("cached_listings", {
...listing,
cached_at: now(),
ttl: 300 // 5 minutes
})
return fresh_listings
Manual refresh:
function refreshBulletinBoard():
db.execute("DELETE FROM cached_listings") // Clear cache
queryElectrumListings(currency) // Re-fetch
TTL: 24 hours (Cash Accounts rarely change)
Pseudocode:
function resolveCashAccount(cash_account):
cached = db.query("SELECT address FROM cash_account_cache WHERE cash_account = ? AND resolved_at + ttl > ?",
cash_account, now())
if cached:
return cached.address // Cache hit
// Cache miss - query Electrum
address = electrumQuery("blockchain.cash_account.resolve", {name: cash_account})
// Store in cache
db.insert("cash_account_cache", {
cash_account: cash_account,
address: address,
resolved_at: now(),
ttl: 86400 // 24 hours
})
return address
Invalidation trigger: If Cash Account resolution fails, clear cache and retry
Local state can drift from blockchain:
Pseudocode:
function reconcileCovenants():
local_covenants = db.query("SELECT * FROM covenants WHERE status != 'claimed' AND status != 'expired'")
for covenant in local_covenants:
blockchain_state = electrumQuery("blockchain.covenant.get_status", {
covenant_id: covenant.covenant_id
})
if blockchain_state.status != covenant.status:
log("Reconciling covenant " + covenant.covenant_id + ": " + covenant.status + " → " + blockchain_state.status)
updateCovenantStatus(covenant.covenant_id, blockchain_state.status)
When to run:
Performance: Only query covenants that user created/funded (small set)
Problem: User creates covenant while offline
Solution: Queue for broadcast when connectivity returns
Pseudocode:
Table: queued_actions
- id (INTEGER, PRIMARY KEY AUTOINCREMENT)
- action_type (TEXT: "create_covenant", "fund_covenant", "send_nostr_dm")
- payload (TEXT, JSON)
- created_at (INTEGER)
- retry_count (INTEGER)
function queueAction(action_type, payload):
db.insert("queued_actions", {
action_type: action_type,
payload: json_encode(payload),
created_at: now(),
retry_count: 0
})
showNotification("Action queued (offline). Will process when online.")
function processQueuedActions():
if not isOnline():
return // Still offline
queued = db.query("SELECT * FROM queued_actions ORDER BY created_at ASC")
for action in queued:
try:
if action.action_type == "create_covenant":
createCovenant(json_decode(action.payload))
else if action.action_type == "fund_covenant":
fundCovenant(json_decode(action.payload))
else if action.action_type == "send_nostr_dm":
sendNostrDM(json_decode(action.payload))
// Success - remove from queue
db.delete("queued_actions", action.id)
catch Error:
// Retry later
db.update("queued_actions", {retry_count: action.retry_count + 1}, action.id)
if action.retry_count > 3:
// Too many failures - notify user
showNotification("Failed to process queued action. Manual intervention needed.")
db.delete("queued_actions", action.id)
Trigger: Run processQueuedActions() when connectivity returns
Bulletin board cache: Available offline (stale but usable)
function getBulletinBoardListings(currency):
if isOnline():
return getCachedListings(currency) // Might refresh cache
else:
// Offline - use stale cache
cached = db.query("SELECT * FROM cached_listings WHERE currency = ?", currency)
showWarning("Using cached listings (offline)")
return cached
Covenant state: Last known state (might be outdated)
if not isOnline():
showWarning("Offline. Covenant states may be outdated. Reconnect to sync.")
Problem: App updates might need new database columns/tables
Solution: Migration scripts
Example (Room):
@Database(entities = [Covenant::class, Listing::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
companion object {
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// Add new column: funded_tx_id
database.execSQL("ALTER TABLE covenants ADD COLUMN funded_tx_id TEXT")
}
}
}
}
val db = Room.databaseBuilder(context, AppDatabase::class.java, "asgaya.db")
.addMigrations(MIGRATION_1_2)
.build()
Testing: Always test migration on old database (don’t lose user data)
Seed phrase: User’s responsibility (write on paper)
Database: Optional (all state can be reconstructed from blockchain)
Scenario: User loses phone, restores on new device with seed phrase
Steps:
covenants = electrumQuery("blockchain.covenant.list_by_sender", {
sender_address: my_address
})
What’s lost: Notification history, cached bulletin board (not critical)
What’s preserved: All covenants, listings, wallet balance (blockchain-backed)
try:
db.query("SELECT * FROM covenants")
catch DatabaseCorruption:
log_error("Database corrupted")
show_error("Database corrupted. Restore from seed phrase?")
if user_confirms:
db.delete_database()
restore_from_seed_phrase()
Problem: Local state says “created”, blockchain says “funded”
Resolution: Blockchain wins (always)
if local_state != blockchain_state:
log_warning("Sync conflict: " + covenant_id + " local=" + local_state + " blockchain=" + blockchain_state)
updateCovenantStatus(covenant_id, blockchain_state) // Override local
Problem: User created covenant on Device A, opens app on Device B
Solution: Query blockchain on startup (find all covenants for my address)
function syncMissingCovenants():
my_covenants_blockchain = electrumQuery("blockchain.covenant.list_by_sender", {
sender_address: my_address
})
my_covenants_local = db.query("SELECT covenant_id FROM covenants")
for bc_covenant in my_covenants_blockchain:
if bc_covenant.covenant_id not in my_covenants_local:
// Found covenant not in local DB
db.insert("covenants", bc_covenant)
log("Synced missing covenant: " + bc_covenant.covenant_id)
Uses:
Used by:
Status: Phase 0 - Core infrastructure
Updated: 2026-06-25
Complexity: Medium (database design + sync logic)
Research: See RS070 (implementation documentation strategy)
—
| 🏠 Home | ↑ Android App | 📖 Glossary |