diff --git a/Cargo.toml b/Cargo.toml index a7ee2c1..5933f12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ name = "warp_api_ffi" crate-type = ["rlib"] [dependencies] -dotenv = "0.15.0" env_logger = "0.9.0" anyhow = "1.0.40" thiserror = "1.0.25" @@ -79,12 +78,13 @@ allo-isolate = { version = "0.1", optional = true } once_cell = { version = "1.8.0", optional = true } android_logger = { version = "0.10.0", optional = true } rocket = { version = "0.5.0-rc.2", features = ["json"], optional = true } +dotenv = { version = "0.15.0", optional = true } [features] ledger = ["ledger-apdu", "hmac", "ed25519-bip32", "ledger-transport-hid"] ledger_sapling = ["ledger"] dart_ffi = ["allo-isolate", "once_cell", "android_logger"] -rpc = ["rocket"] +rpc = ["rocket", "dotenv"] # librustzcash synced to 35023ed8ca2fb1061e78fd740b640d4eefcc5edd diff --git a/Rocket.toml b/Rocket.toml new file mode 100644 index 0000000..3413d43 --- /dev/null +++ b/Rocket.toml @@ -0,0 +1,3 @@ +[default] +allow_backup = true +allow_send = true diff --git a/src/api/account.rs b/src/api/account.rs index c880bd0..9295a1b 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -1,6 +1,6 @@ // Account creation -use crate::coinconfig::{CoinConfig, ACTIVE_COIN}; +use crate::coinconfig::CoinConfig; use crate::key2::decode_key; use anyhow::anyhow; use bip39::{Language, Mnemonic}; diff --git a/src/api/sync.rs b/src/api/sync.rs index 93865ea..9b766b4 100644 --- a/src/api/sync.rs +++ b/src/api/sync.rs @@ -34,10 +34,10 @@ async fn coin_sync_impl( c.coin_type, chunk_size, get_tx, - &c.db_path, + &c.db_path.as_ref().unwrap(), target_height_offset, progress_callback, - &c.lwd_url, + &c.lwd_url.as_ref().unwrap(), ) .await?; Ok(()) @@ -50,6 +50,12 @@ pub async fn get_latest_height() -> anyhow::Result { Ok(last_height) } +pub fn get_synced_height() -> anyhow::Result { + let c = CoinConfig::get_active(); + let db = c.db()?; + db.get_last_sync_height().map(|h| h.unwrap_or(0)) +} + pub async fn skip_to_last_height(coin: u8) -> anyhow::Result<()> { let c = if coin == 0xFF { CoinConfig::get_active() diff --git a/src/coinconfig.rs b/src/coinconfig.rs index 082021d..d8802e4 100644 --- a/src/coinconfig.rs +++ b/src/coinconfig.rs @@ -3,6 +3,7 @@ use lazy_static::lazy_static; use lazycell::AtomicLazyCell; use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::{Arc, Mutex, MutexGuard}; +use anyhow::anyhow; use tonic::transport::Channel; use zcash_params::coin::{get_coin_chain, CoinChain, CoinType}; use zcash_params::{OUTPUT_PARAMS, SPEND_PARAMS}; @@ -34,7 +35,7 @@ pub fn set_active_account(coin: u8, id: u32) { pub fn set_coin_lwd_url(coin: u8, lwd_url: &str) { let mut c = COIN_CONFIG[coin as usize].lock().unwrap(); - c.lwd_url = lwd_url.to_string(); + c.lwd_url = Some(lwd_url.to_string()); } pub fn init_coin(coin: u8, db_path: &str) -> anyhow::Result<()> { @@ -49,8 +50,8 @@ pub struct CoinConfig { pub coin_type: CoinType, pub id_account: u32, pub height: u32, - pub lwd_url: String, - pub db_path: String, + pub lwd_url: Option, + pub db_path: Option, pub mempool: Arc>, pub db: Option>>, pub chain: &'static (dyn CoinChain + Send), @@ -64,8 +65,8 @@ impl CoinConfig { coin_type, id_account: 0, height: 0, - lwd_url: String::new(), - db_path: String::new(), + lwd_url: None, + db_path: None, db: None, mempool: Arc::new(Mutex::new(MemPool::new(coin))), chain, @@ -73,8 +74,8 @@ impl CoinConfig { } pub fn set_db_path(&mut self, db_path: &str) -> anyhow::Result<()> { - self.db_path = db_path.to_string(); - let db = DbAdapter::new(self.coin_type, &self.db_path)?; + self.db_path = Some(db_path.to_string()); + let db = DbAdapter::new(self.coin_type, &db_path)?; db.init_db()?; self.db = Some(Arc::new(Mutex::new(db))); Ok(()) @@ -108,7 +109,12 @@ impl CoinConfig { } pub async fn connect_lwd(&self) -> anyhow::Result> { - connect_lightwalletd(&self.lwd_url).await + if let Some(lwd_url) = &self.lwd_url { + connect_lightwalletd(lwd_url).await + } + else { + Err(anyhow!("LWD URL Not set")) + } } } diff --git a/src/db.rs b/src/db.rs index 469bf4a..0fb9647 100644 --- a/src/db.rs +++ b/src/db.rs @@ -590,8 +590,8 @@ impl DbAdapter { params![account], |row| { let seed: Option = row.get(0)?; - let sk: Option = row.get(0)?; - let ivk: String = row.get(0)?; + let sk: Option = row.get(1)?; + let ivk: String = row.get(2)?; Ok((seed, sk, ivk)) }, )?; @@ -938,6 +938,28 @@ impl DbAdapter { Ok(()) } + pub fn get_txs(&self, account: u32) -> anyhow::Result> { + let mut s = self.connection.prepare("SELECT txid, height, timestamp, value, address, memo FROM transactions WHERE account = ?1")?; + let tx_rec = s.query_map(params![account], |row| { + let mut txid: Vec = row.get(0)?; + txid.reverse(); + let txid = hex::encode(txid); + let height: u32 = row.get(1)?; + let timestamp: u32 = row.get(2)?; + let value: i64 = row.get(3)?; + let address: String = row.get(4)?; + let memo: String = row.get(5)?; + Ok(TxRec { + txid, height, timestamp, value, address, memo + }) + })?; + let mut txs = vec![]; + for row in tx_rec { + txs.push(row?); + } + Ok(txs) + } + fn network(&self) -> &'static Network { let chain = get_coin_chain(self.coin_type); chain.network() @@ -967,6 +989,16 @@ impl ZMessage { } } +#[derive(Serialize)] +pub struct TxRec { + txid: String, + height: u32, + timestamp: u32, + value: i64, + address: String, + memo: String, +} + #[cfg(test)] mod tests { use crate::db::{DbAdapter, ReceivedNote, DEFAULT_DB_PATH}; diff --git a/src/lib.rs b/src/lib.rs index f65ce1b..f42cfc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,7 +66,7 @@ pub use crate::chain::{ ChainError, DecryptNode, }; pub use crate::commitment::{CTree, Witness}; -pub use crate::db::DbAdapter; +pub use crate::db::{DbAdapter, TxRec}; pub use crate::hash::pedersen_hash; pub use crate::key::{generate_random_enc_key, KeyHelpers}; pub use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient; diff --git a/src/main/rpc.rs b/src/main/rpc.rs index 0f0733e..52d5ee1 100644 --- a/src/main/rpc.rs +++ b/src/main/rpc.rs @@ -1,34 +1,148 @@ #[macro_use] extern crate rocket; -use rocket::serde::{Deserialize, json::Json}; -use warp_api_ffi::CoinConfig; +use rocket::fairing::AdHoc; +use rocket::serde::{Serialize, Deserialize, json::Json}; +use rocket::State; +use warp_api_ffi::{CoinConfig, TxRec}; +use warp_api_ffi::api::payment::{Recipient, RecipientMemo}; #[rocket::main] async fn main() -> anyhow::Result<()> { - warp_api_ffi::init_coin(0, "/tmp/zec.db")?; + dotenv::dotenv()?; + warp_api_ffi::init_coin(0, &dotenv::var("ZEC_DB_PATH").unwrap_or("/tmp/zec.db".to_string()))?; + warp_api_ffi::set_coin_lwd_url(0, &dotenv::var("ZEC_LWD_URL").unwrap_or("https://mainnet.lightwalletd.com:9067".to_string())); let _ = rocket::build() .mount( "/", routes![ set_lwd, + set_active, new_account, sync, - // get_address, - // sync, - // rewind, - // balance, - // pay, - // tx_history + rewind, + get_latest_height, + get_backup, + get_balance, + get_address, + get_tx_history, + pay, ], ) + .attach(AdHoc::config::()) .launch() .await?; Ok(()) } +#[post("/set_lwd?&")] +pub fn set_lwd(coin: u8, lwd_url: String) { + warp_api_ffi::set_coin_lwd_url(coin, &lwd_url); +} + +#[post("/set_active?&")] +pub fn set_active(coin: u8, id_account: u32) { + warp_api_ffi::set_active_account(coin, id_account); +} + +#[post("/new_account", format = "application/json", data="")] +pub fn new_account(seed: Json) -> String { + let id_account = warp_api_ffi::api::account::new_account(seed.coin, &seed.name, seed.key.clone(), seed.index).unwrap(); + warp_api_ffi::set_active_account(seed.coin, id_account); + id_account.to_string() +} + +#[post("/sync?")] +pub async fn sync(offset: Option) { + let coin = CoinConfig::get_active(); + let _ = warp_api_ffi::api::sync::coin_sync(coin.coin, true, offset.unwrap_or(0), |_| {}).await; +} + +#[post("/rewind?")] +pub async fn rewind(height: u32) { + let _ = warp_api_ffi::api::sync::rewind_to_height(height).await; +} + +#[get("/latest_height")] +pub async fn get_latest_height() -> Json { + let latest = warp_api_ffi::api::sync::get_latest_height().await.unwrap(); + let synced = warp_api_ffi::api::sync::get_synced_height().unwrap(); + Json(Heights { latest, synced }) +} + +#[get("/address")] +pub fn get_address() -> String { + let c = CoinConfig::get_active(); + let db = c.db().unwrap(); + db.get_address(c.id_account).unwrap() +} + +#[get("/backup")] +pub fn get_backup(config: &State) -> Result, String> { + if !config.allow_backup { + Err("Backup API not enabled".to_string()) + } + else { + let c = CoinConfig::get_active(); + let db = c.db().unwrap(); + let (seed, sk, fvk) = db.get_backup(c.id_account).unwrap(); + Ok(Json(Backup { + seed, + sk, + fvk + })) + } +} + +#[get("/tx_history")] +pub fn get_tx_history() -> Json> { + let c = CoinConfig::get_active(); + let db = c.db().unwrap(); + let txs = db.get_txs(c.id_account).unwrap(); + Json(txs) +} + +#[get("/balance")] +pub fn get_balance() -> String { + let c = CoinConfig::get_active(); + let db = c.db().unwrap(); + let balance = db.get_balance(c.id_account).unwrap(); + balance.to_string() +} + +#[post("/pay", data="")] +pub async fn pay(payment: Json, config: &State) -> Result { + if !config.allow_send { + Err("Backup API not enabled".to_string()) + } + else { + let c = CoinConfig::get_active(); + let latest = warp_api_ffi::api::sync::get_latest_height().await.unwrap(); + let from = { + let db = c.db().unwrap(); + db.get_address(c.id_account).unwrap() + }; + let recipients: Vec<_> = payment.recipients.iter().map(|p| RecipientMemo::from_recipient(&from, p)).collect(); + let txid = warp_api_ffi::api::payment::build_sign_send_multi_payment( + latest, + &recipients, + false, + payment.confirmations, + Box::new(|_| {}) + ).await.unwrap(); + Ok(txid) + } +} + +#[derive(Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct Config { + allow_backup: bool, + allow_send: bool, +} + #[derive(Deserialize)] #[serde(crate = "rocket::serde")] pub struct AccountSeed { @@ -38,24 +152,24 @@ pub struct AccountSeed { index: Option, } -#[post("/set_lwd?&")] -pub fn set_lwd(coin: u8, lwd_url: String) { - warp_api_ffi::set_coin_lwd_url(coin, &lwd_url); +#[derive(Serialize)] +#[serde(crate = "rocket::serde")] +pub struct Heights { + latest: u32, + synced: u32, } -#[post("/new_account", format = "application/json", data="")] -pub fn new_account(seed: Json) -> std::result::Result { - let id_account = warp_api_ffi::api::account::new_account(seed.coin, &seed.name, seed.key.clone(), seed.index); - id_account.map(|v| v.to_string()).map_err(|e| e.to_string()) +#[derive(Serialize)] +#[serde(crate = "rocket::serde")] +pub struct Backup { + seed: Option, + sk: Option, + fvk: String, } -#[post("/sync?")] -pub async fn sync(offset: Option) { - let coin = CoinConfig::get_active(); - let _ = warp_api_ffi::api::sync::coin_sync(coin.coin, true, offset.unwrap_or(0), |_| {}).await; -} - - -pub fn get_backup(id_account: u32) { - +#[derive(Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct Payment { + recipients: Vec, + confirmations: u32, } diff --git a/src/mempool.rs b/src/mempool.rs index 8219435..fd9263e 100644 --- a/src/mempool.rs +++ b/src/mempool.rs @@ -5,7 +5,6 @@ use tonic::transport::Channel; use tonic::Request; use crate::coinconfig::CoinConfig; -use zcash_params::coin::CoinChain; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::sapling::note_encryption::try_sapling_compact_note_decryption; use zcash_primitives::sapling::SaplingIvk; diff --git a/src/taddr.rs b/src/taddr.rs index a4dec1c..a12254c 100644 --- a/src/taddr.rs +++ b/src/taddr.rs @@ -1,6 +1,6 @@ use crate::coinconfig::CoinConfig; use crate::{ - AddressList, CompactTxStreamerClient, DbAdapter, GetAddressUtxosArg, GetAddressUtxosReply, + AddressList, CompactTxStreamerClient, GetAddressUtxosArg, GetAddressUtxosReply, }; use bip39::{Language, Mnemonic, Seed}; use ripemd::{Digest, Ripemd160};