use crate::db::data_generated::fb::*; use crate::unified::orchard_as_unified; use anyhow::Result; use orchard::keys::{FullViewingKey, Scope}; use rusqlite::{params, Connection, OptionalExtension}; use std::collections::HashMap; use zcash_client_backend::address::RecipientAddress; use zcash_client_backend::encoding::AddressCodec; use zcash_primitives::consensus::Network; pub fn get_account_list(connection: &Connection) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let mut stmt = connection.prepare("WITH notes AS (SELECT a.id_account, a.name, CASE WHEN r.spent IS NULL THEN r.value ELSE 0 END AS nv FROM accounts a LEFT JOIN received_notes r ON a.id_account = r.account), \ accounts2 AS (SELECT id_account, name, COALESCE(sum(nv), 0) AS balance FROM notes GROUP by id_account) \ SELECT a.id_account, a.name, a.balance FROM accounts2 a")?; let rows = stmt.query_map([], |row| { let id: u32 = row.get("id_account")?; let name: String = row.get("name")?; let balance: i64 = row.get("balance")?; let name = builder.create_string(&name); let account = Account::create( &mut builder, &AccountArgs { id, name: Some(name), balance: balance as u64, }, ); Ok(account) })?; let mut accounts = vec![]; for r in rows { accounts.push(r?); } let accounts = builder.create_vector(&accounts); let accounts = AccountVec::create( &mut builder, &AccountVecArgs { accounts: Some(accounts), }, ); builder.finish(accounts, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn get_active_account(connection: &Connection) -> Result { let id = connection .query_row( "SELECT value FROM properties WHERE name = 'account'", [], |row| { let value: String = row.get(0)?; let value: u32 = value.parse().unwrap(); Ok(value) }, ) .optional()? .unwrap_or(0); let id = get_available_account_id(connection, id)?; set_active_account(connection, id)?; Ok(id) } pub fn set_active_account(connection: &Connection, id: u32) -> Result<()> { connection.execute( "INSERT INTO properties(name, value) VALUES ('account',?1) \ ON CONFLICT (name) DO UPDATE SET value = excluded.value", [id], )?; Ok(()) } pub fn get_available_account_id(connection: &Connection, id: u32) -> Result { let r = connection .query_row("SELECT 1 FROM accounts WHERE id_account = ?1", [id], |_| { Ok(()) }) .optional()?; if r.is_some() { return Ok(id); } let r = connection .query_row("SELECT MAX(id_account) FROM accounts", [], |row| { let id: Option = row.get(0)?; Ok(id) })? .unwrap_or(0); Ok(r) } pub fn get_t_addr(connection: &Connection, id: u32) -> Result { let address = connection .query_row( "SELECT address FROM taddrs WHERE account = ?1", [id], |row| { let address: String = row.get(0)?; Ok(address) }, ) .optional()?; Ok(address.unwrap_or(String::new())) } pub fn get_sk(connection: &Connection, id: u32) -> Result { let sk = connection.query_row( "SELECT sk FROM accounts WHERE id_account = ?1", [id], |row| { let sk: Option = row.get(0)?; Ok(sk.unwrap_or(String::new())) }, )?; Ok(sk) } pub fn update_account_name(connection: &Connection, id: u32, name: &str) -> Result<()> { connection.execute( "UPDATE accounts SET name = ?2 WHERE id_account = ?1", params![id, name], )?; Ok(()) } pub fn get_balances(connection: &Connection, id: u32, confirmed_height: u32) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let shielded = connection.query_row( "SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND spent IS NULL", params![id], |row| { let value: Option = row.get(0)?; Ok(value.unwrap_or(0) as u64) }, )?; // funds not spent yet let unconfirmed_spent = connection.query_row( "SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND spent = 0", params![id], |row| { let value: Option = row.get(0)?; Ok(value.unwrap_or(0) as u64) }, )?; // funds used in unconfirmed tx let balance = shielded + unconfirmed_spent; let under_confirmed = connection.query_row( "SELECT SUM(value) AS value FROM received_notes WHERE account = ?1 AND spent IS NULL AND height > ?2", params![id, confirmed_height], |row| { let value: Option = row.get(0)?; Ok(value.unwrap_or(0) as u64) })?; // funds received but not old enough let excluded = connection.query_row( "SELECT SUM(value) FROM received_notes WHERE account = ?1 AND spent IS NULL \ AND height <= ?2 AND excluded", params![id, confirmed_height], |row| { let value: Option = row.get(0)?; Ok(value.unwrap_or(0) as u64) }, )?; // funds excluded from spending let sapling = connection.query_row( "SELECT SUM(value) FROM received_notes WHERE account = ?1 AND spent IS NULL AND orchard = 0 AND height <= ?2", params![id, confirmed_height], |row| { let value: Option = row.get(0)?; Ok(value.unwrap_or(0) as u64) })?; let orchard = connection.query_row( "SELECT SUM(value) FROM received_notes WHERE account = ?1 AND spent IS NULL AND orchard = 1 AND height <= ?2", params![id, confirmed_height], |row| { let value: Option = row.get(0)?; Ok(value.unwrap_or(0) as u64) })?; let balance = Balance::create( &mut builder, &BalanceArgs { shielded, unconfirmed_spent, balance, under_confirmed, excluded, sapling, orchard, }, ); builder.finish(balance, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn get_db_height(connection: &Connection) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let height = connection .query_row( "SELECT height, timestamp FROM blocks WHERE height = (SELECT MAX(height) FROM blocks)", [], |row| { let height: u32 = row.get(0)?; let timestamp: u32 = row.get(1)?; let height = Height::create(&mut builder, &HeightArgs { height, timestamp }); Ok(height) }, ) .optional()?; let data = height .map(|h| { builder.finish(h, None); builder.finished_data().to_vec() }) .unwrap_or(vec![]); Ok(data) } pub fn get_notes(connection: &Connection, id: u32) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let mut stmt = connection.prepare( "SELECT n.id_note, n.height, n.value, t.timestamp, n.orchard, n.excluded, n.spent FROM received_notes n, transactions t \ WHERE n.account = ?1 AND (n.spent IS NULL OR n.spent = 0) \ AND n.tx = t.id_tx ORDER BY n.height DESC")?; let rows = stmt.query_map(params![id], |row| { let id: u32 = row.get("id_note")?; let height: u32 = row.get("height")?; let value: i64 = row.get("value")?; let timestamp: u32 = row.get("timestamp")?; let orchard: u8 = row.get("orchard")?; let excluded: Option = row.get("excluded")?; let spent: Option = row.get("spent")?; let note = ShieldedNote::create( &mut builder, &ShieldedNoteArgs { id, height, value: value as u64, timestamp, orchard: orchard == 1, excluded: excluded.unwrap_or(false), spent: spent.is_some(), }, ); Ok(note) })?; let mut notes = vec![]; for r in rows { notes.push(r?); } let notes = builder.create_vector(¬es); let notes = ShieldedNoteVec::create(&mut builder, &ShieldedNoteVecArgs { notes: Some(notes) }); builder.finish(notes, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn get_txs(network: &Network, connection: &Connection, id: u32) -> Result { let addresses = resolve_addresses(network, connection)?; let mut stmt = connection.prepare( "SELECT id_tx, txid, height, timestamp, t.address, value, memo FROM transactions t \ WHERE account = ?1 ORDER BY height DESC", )?; let rows = stmt.query_map(params![id], |row| { let id_tx: u32 = row.get("id_tx")?; let height: u32 = row.get("height")?; let mut tx_id: Vec = row.get("txid")?; tx_id.reverse(); let tx_id = hex::encode(&tx_id); let short_tx_id = tx_id[..8].to_string(); let timestamp: u32 = row.get("timestamp")?; let address: Option = row.get("address")?; let value: i64 = row.get("value")?; let memo: Option = row.get("memo")?; let name = address.as_ref().and_then(|a| addresses.get(a)).cloned(); let tx = ShieldedTxT { id: id_tx, height, tx_id: Some(tx_id), short_tx_id: Some(short_tx_id), timestamp, name, value: value as u64, address, memo, }; Ok(tx) })?; let mut txs = vec![]; for r in rows { txs.push(r?); } let txs = ShieldedTxVecT { txs: Some(txs) }; Ok(txs) } pub fn get_messages(network: &Network, connection: &Connection, id: u32) -> Result { let addresses = resolve_addresses(network, connection)?; let mut stmt = connection.prepare( "SELECT m.id, m.id_tx, m.timestamp, m.sender, m.recipient, m.incoming, \ subject, body, height, read FROM messages m \ WHERE account = ?1 ORDER BY timestamp DESC", )?; let rows = stmt.query_map(params![id], |row| { let id_msg: u32 = row.get("id")?; let id_tx: Option = row.get("id_tx")?; let timestamp: u32 = row.get("timestamp")?; let height: u32 = row.get("height")?; let sender: Option = row.get("sender")?; let recipient: Option = row.get("recipient")?; let subject: String = row.get("subject")?; let body: String = row.get("body")?; let read: bool = row.get("read")?; let incoming: bool = row.get("incoming")?; let id_tx = id_tx.unwrap_or(0); let from = sender.map(|s| addresses.get(&s).cloned().unwrap_or(String::new())); let to = recipient.map(|s| addresses.get(&s).cloned().unwrap_or(String::new())); let message = MessageT { id_msg, id_tx, height, timestamp, from, to, subject: Some(subject), body: Some(body), read, incoming, }; Ok(message) })?; let mut messages = vec![]; for r in rows { messages.push(r?); } let messages = MessageVecT { messages: Some(messages), }; Ok(messages) } pub fn get_prev_next_message( connection: &Connection, subject: &str, height: u32, account: u32, ) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let prev = connection .query_row( "SELECT MAX(id) FROM messages WHERE subject = ?1 AND height < ?2 and account = ?3", params![subject, height, account], |row| { let id: Option = row.get(0)?; Ok(id) }, )? .unwrap_or(0); let next = connection .query_row( "SELECT MIN(id) FROM messages WHERE subject = ?1 AND height > ?2 and account = ?3", params![subject, height, account], |row| { let id: Option = row.get(0)?; Ok(id) }, )? .unwrap_or(0); let prev_next = PrevNext::create(&mut builder, &PrevNextArgs { prev, next }); builder.finish(prev_next, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn get_templates(connection: &Connection) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let mut stmt = connection.prepare( "SELECT id_send_template, title, address, amount, fiat_amount, fee_included, fiat, include_reply_to, subject, body FROM send_templates")?; let rows = stmt.query_map([], |row| { let id_msg: u32 = row.get("id_send_template")?; let title: String = row.get("title")?; let address: String = row.get("address")?; let amount: i64 = row.get("amount")?; let fiat_amount: f64 = row.get("fiat_amount")?; let fee_included: bool = row.get("fee_included")?; let fiat: Option = row.get("fiat")?; let include_reply_to: bool = row.get("include_reply_to")?; let subject: String = row.get("subject")?; let body: String = row.get("body")?; let title = builder.create_string(&title); let address = builder.create_string(&address); let fiat = fiat.map(|fiat| builder.create_string(&fiat)); let subject = builder.create_string(&subject); let body = builder.create_string(&body); let template = SendTemplate::create( &mut builder, &SendTemplateArgs { id: id_msg, title: Some(title), address: Some(address), amount: amount as u64, fiat_amount, fee_included, fiat, include_reply_to, subject: Some(subject), body: Some(body), }, ); Ok(template) })?; let mut templates = vec![]; for r in rows { templates.push(r?); } let templates = builder.create_vector(&templates); let templates = SendTemplateVec::create( &mut builder, &SendTemplateVecArgs { templates: Some(templates), }, ); builder.finish(templates, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn get_contacts(connection: &Connection) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let mut stmt = connection .prepare("SELECT id, name, address FROM contacts WHERE address <> '' ORDER BY name")?; let rows = stmt.query_map([], |row| { let id: u32 = row.get("id")?; let name: String = row.get("name")?; let address: String = row.get("address")?; let name = builder.create_string(&name); let address = builder.create_string(&address); let contact = Contact::create( &mut builder, &ContactArgs { id, name: Some(name), address: Some(address), }, ); Ok(contact) })?; let mut contacts = vec![]; for r in rows { contacts.push(r?); } let contacts = builder.create_vector(&contacts); let contacts = ContactVec::create( &mut builder, &ContactVecArgs { contacts: Some(contacts), }, ); builder.finish(contacts, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn get_pnl_txs(connection: &Connection, id: u32, timestamp: u32) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let mut stmt = connection.prepare( "SELECT timestamp, value FROM transactions WHERE timestamp >= ?2 AND account = ?1 ORDER BY timestamp DESC")?; let rows = stmt.query_map([id, timestamp], |row| { let timestamp: u32 = row.get(0)?; let value: i64 = row.get(1)?; let tx = TxTimeValue::create( &mut builder, &TxTimeValueArgs { timestamp, value: value as u64, }, ); Ok(tx) })?; let mut txs = vec![]; for r in rows { txs.push(r?); } let txs = builder.create_vector(&txs); let txs = TxTimeValueVec::create(&mut builder, &TxTimeValueVecArgs { values: Some(txs) }); builder.finish(txs, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn get_historical_prices( connection: &Connection, timestamp: u32, currency: &str, ) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let mut stmt = connection.prepare( "SELECT timestamp, price FROM historical_prices WHERE timestamp >= ?2 AND currency = ?1", )?; let rows = stmt.query_map(params![currency, timestamp], |row| { let timestamp: u32 = row.get(0)?; let price: f64 = row.get(1)?; let quote = Quote::create(&mut builder, &QuoteArgs { timestamp, price }); Ok(quote) })?; let mut quotes = vec![]; for r in rows { quotes.push(r?); } let quotes = builder.create_vector("es); let quotes = QuoteVec::create( &mut builder, &QuoteVecArgs { values: Some(quotes), }, ); builder.finish(quotes, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn get_spendings(connection: &Connection, id: u32, timestamp: u32) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let mut stmt = connection.prepare( "SELECT SUM(value) as v, t.address, c.name FROM transactions t LEFT JOIN contacts c ON t.address = c.address \ WHERE account = ?1 AND timestamp >= ?2 AND value < 0 GROUP BY t.address ORDER BY v ASC LIMIT 5")?; let rows = stmt.query_map([id, timestamp], |row| { let value: i64 = row.get(0)?; let address: Option = row.get(1)?; let name: Option = row.get(2)?; let recipient = name.or(address); let recipient = recipient.unwrap_or(String::new()); let recipient = builder.create_string(&recipient); let spending = Spending::create( &mut builder, &SpendingArgs { recipient: Some(recipient), amount: (-value) as u64, }, ); Ok(spending) })?; let mut spendings = vec![]; for r in rows { spendings.push(r?); } let spendings = builder.create_vector(&spendings); let spendings = SpendingVec::create( &mut builder, &SpendingVecArgs { values: Some(spendings), }, ); builder.finish(spendings, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn update_excluded(connection: &Connection, id: u32, excluded: bool) -> Result<()> { connection.execute( "UPDATE received_notes SET excluded = ?2 WHERE id_note = ?1", params![id, excluded], )?; Ok(()) } pub fn invert_excluded(connection: &Connection, id: u32) -> Result<()> { connection.execute( "UPDATE received_notes SET excluded = NOT(COALESCE(excluded, 0)) WHERE account = ?1", [id], )?; Ok(()) } pub fn get_checkpoints(connection: &Connection) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); let mut stmt = connection.prepare("SELECT height, timestamp FROM blocks ORDER by height")?; let rows = stmt.query_map([], |row| { let height: u32 = row.get(0)?; let timestamp: u32 = row.get(1)?; let checkpoint = Checkpoint::create(&mut builder, &CheckpointArgs { height, timestamp }); Ok(checkpoint) })?; let mut checkpoints = vec![]; for r in rows { checkpoints.push(r?); } let checkpoints = builder.create_vector(&checkpoints); let checkpoints = CheckpointVec::create( &mut builder, &CheckpointVecArgs { values: Some(checkpoints), }, ); builder.finish(checkpoints, None); let data = builder.finished_data().to_vec(); Ok(data) } pub fn resolve_addresses( network: &Network, connection: &Connection, ) -> Result> { let mut addresses: HashMap = HashMap::new(); let mut stmt = connection.prepare("SELECT name, address FROM contacts WHERE address <> ''")?; let rows = stmt.query_map([], |row| { let name: String = row.get(0)?; let address: String = row.get(1)?; let ra = RecipientAddress::decode(network, &address); if let Some(ra) = ra { match ra { RecipientAddress::Unified(ua) => { if let Some(ta) = ua.transparent() { addresses.insert(ta.encode(network), name.clone()); } if let Some(pa) = ua.sapling() { addresses.insert(pa.encode(network), name.clone()); } if let Some(oa) = ua.orchard() { let oa = orchard_as_unified(network, oa); addresses.insert(oa.encode(), name.clone()); } } _ => { addresses.insert(address, name); } } } Ok(()) })?; for r in rows { r?; } let mut stmt = connection.prepare( "SELECT a.name, a.address, t.address, o.fvk FROM accounts a LEFT JOIN taddrs t ON a.id_account = t.account \ LEFT JOIN orchard_addrs o ON a.id_account = o.account", )?; let rows = stmt.query_map([], |row| { let name: String = row.get(0)?; let z_addr: String = row.get(1)?; let t_addr: Option = row.get(2)?; let o_fvk: Option> = row.get(3)?; addresses.insert(z_addr, name.clone()); if let Some(t_addr) = t_addr { addresses.insert(t_addr, name.clone()); } if let Some(o_fvk) = o_fvk { let o_fvk = FullViewingKey::from_bytes(&o_fvk.try_into().unwrap()).unwrap(); let o_addr = o_fvk.address_at(0usize, Scope::External); let o_addr = orchard_as_unified(network, &o_addr); addresses.insert(o_addr.encode(), name.clone()); } Ok(()) })?; for r in rows { r?; } Ok(addresses) } pub fn get_property(connection: &Connection, name: &str) -> anyhow::Result { let url = connection .query_row( "SELECT value FROM properties WHERE name = ?1", [name], |row| { let url: String = row.get(0)?; Ok(url) }, ) .optional()?; Ok(url.unwrap_or(String::new())) } pub fn set_property(connection: &Connection, name: &str, value: &str) -> anyhow::Result<()> { connection.execute( "INSERT INTO properties(name, value) VALUES (?1, ?2) ON CONFLICT (name) \ DO UPDATE SET value = excluded.value", params![name, value], )?; Ok(()) } pub fn get_available_addrs(connection: &Connection, account: u32) -> anyhow::Result { let has_transparent = connection .query_row( "SELECT 1 FROM taddrs WHERE account = ?1", [account], |_row| Ok(()), ) .optional()? .is_some(); let has_sapling = true; let has_orchard = connection .query_row( "SELECT 1 FROM orchard_addrs WHERE account = ?1", [account], |_row| Ok(()), ) .optional()? .is_some(); let res = if has_transparent { 1 } else { 0 } | if has_sapling { 2 } else { 0 } | if has_orchard { 4 } else { 0 }; Ok(res) } pub fn get_account_by_address(connection: &Connection, address: &str) -> Result> { let id = connection .query_row( "SELECT id_account FROM accounts WHERE address = ?1", [address], |row| row.get::<_, u32>(0), ) .optional()?; Ok(id) }