diff --git a/node/Cargo.toml b/node/Cargo.toml index 2d602aad2..ffb249f05 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -15,6 +15,7 @@ time = "^0.1" serde = { version = "1.0", features=["derive"] } hex = "^0.3" futures = "0.3" +futures-util = "0.3" tokio = {version = "0.2", features=["full"]} warp = "0.2" tera = "1" diff --git a/node/api.md b/node/api.md index fb3747fc6..94ae07efc 100644 --- a/node/api.md +++ b/node/api.md @@ -19,11 +19,11 @@ * [/network/tx/:id](#networktxid) * [Wallet API](#wallet-api) * [/wallet/new](#walletnew) - * [/wallet/:id/balance](#walletidbalance) - * [/wallet/:id/txs](#walletidtxs) - * [/wallet/:id/address](#walletidaddress) - * [/wallet/:id/receiver](#walletidreceiver) - * [/wallet/:id/buildtx](#walletidbuildtx) + * [/wallet/balance](#walletbalance) + * [/wallet/txs](#wallettxs) + * [/wallet/address](#walletaddress) + * [/wallet/receiver](#walletreceiver) + * [/wallet/buildtx](#walletbuildtx) Responses are listed in JSON for a time being, but we are also going to provide the API responses via XDR format. @@ -312,7 +312,7 @@ forming transactions and tracking the state of unspent outputs. ### /wallet/new -Creates a new wallet +Creates a new wallet. Successful submission returns 200 OK status. Request: @@ -325,21 +325,13 @@ struct NewWalletRequest { } ``` -Response: - -```rust -struct NewWalletResponse { - id: [u8; 32], -} -``` - -### /wallet/:id/balance +### /wallet/balance Returns wallet's balance. Request: -`GET /wallet/:id/balance` +`GET /wallet/balance` Response: @@ -350,13 +342,13 @@ struct Balance { ``` -### /wallet/:id/txs +### /wallet/txs Lists annotated transactions. Request: -`GET /wallet/:id/txs?cursor=[5786...]` +`GET /wallet/txs?cursor=[5786...]` Response: @@ -367,13 +359,13 @@ struct WalletTxs { } ``` -### /wallet/:id/address +### /wallet/address Generates a new address. Request: -`GET /wallet/:id/address` +`GET /wallet/address` Response: @@ -383,13 +375,13 @@ struct NewAddress { } ``` -### /wallet/:id/receiver +### /wallet/receiver Generates a new receiver. Request: -`POST /wallet/:id/receiver` +`POST /wallet/receiver` ```rust struct NewReceiverRequest { @@ -407,13 +399,13 @@ struct NewReceiverResponse { } ``` -### /wallet/:id/buildtx +### /wallet/buildtx Builds a transaction and returns the signing instructions. Request: -`POST /wallet/:id/buildtx` +`POST /wallet/buildtx` ```rust struct BuildTxRequest { diff --git a/node/src/api.rs b/node/src/api.rs index be61196b8..dcfe40c70 100644 --- a/node/src/api.rs +++ b/node/src/api.rs @@ -1,9 +1,17 @@ +mod network; +mod response; +pub(self) mod serde_utils; +mod types; +mod wallet; +mod warp_utils; + use std::net::SocketAddr; use warp::Filter; use crate::bc::BlockchainRef; use crate::config::Config; use crate::wallet_manager::WalletRef; +use std::convert::Infallible; /// Launches the API server. pub async fn launch(config: Config, bc: BlockchainRef, wallet: WalletRef) { @@ -11,14 +19,21 @@ pub async fn launch(config: Config, bc: BlockchainRef, wallet: WalletRef) { if conf.disabled { return; } - let echo = - warp::path!("v1" / "echo" / String).map(|thingy| format!("API v1 echo: {}!", thingy)); + let routes = routes(bc, wallet); + + eprintln!("API: http://{}", &conf.listen); + warp::serve(routes).run(conf.listen).await; +} + +fn routes( + bc: BlockchainRef, + wallet: WalletRef, +) -> impl Filter + Clone { + let wallet_routes = wallet::routes(wallet); + let network_routes = network::routes(bc); let not_found = warp::any() .map(|| warp::reply::with_status("Not found.", warp::http::StatusCode::NOT_FOUND)); - let routes = echo.or(not_found); - - eprintln!("API: http://{}", &conf.listen); - warp::serve(routes).run(conf.listen).await; + wallet_routes.or(network_routes).or(not_found) } diff --git a/node/src/api/network.rs b/node/src/api/network.rs new file mode 100644 index 000000000..78d4c4027 --- /dev/null +++ b/node/src/api/network.rs @@ -0,0 +1,56 @@ +mod handlers; +mod requests; +mod responses; + +use crate::api::types::{Cursor, HexId}; +use crate::api::warp_utils::{handle1, handle2}; +use crate::bc::BlockchainRef; +use std::convert::Infallible; +use warp::Filter; + +pub fn routes( + bc: BlockchainRef, +) -> impl Filter + Clone { + use warp::*; + + let status = path!("v1" / "network" / "status") + .and(get()) + .and(with_bc(bc.clone())) + .and_then(handle1(handlers::status)); + + let mempool = path!("v1" / "network" / "mempool") + .and(get()) + .and(query::()) + .and(with_bc(bc.clone())) + .and_then(handle2(handlers::mempool)); + + let blocks = path!("v1" / "network" / "blocks") + .and(get()) + .and(query::()) + .and(with_bc(bc.clone())) + .and_then(handle2(handlers::blocks)); + + let block = path!("v1" / "network" / "block" / HexId) + .and(get()) + .and(with_bc(bc.clone())) + .and_then(handle2(handlers::block)); + + let tx = path!("v1" / "network" / "tx" / HexId) + .and(get()) + .and(with_bc(bc.clone())) + .and_then(handle2(handlers::tx)); + + let submit = path!("v1" / "network" / "submit") + .and(post()) + .and(body::json()) + .and(with_bc(bc.clone())) + .and_then(handle2(handlers::submit)); + + status.or(mempool).or(blocks).or(block).or(tx).or(submit) +} + +fn with_bc( + bc: BlockchainRef, +) -> impl Filter + Clone { + warp::any().map(move || bc.clone()) +} diff --git a/node/src/api/network/handlers.rs b/node/src/api/network/handlers.rs new file mode 100644 index 000000000..c2550f27a --- /dev/null +++ b/node/src/api/network/handlers.rs @@ -0,0 +1,141 @@ +use crate::{ + api::{ + network::{requests, responses}, + response::{error, ResponseResult}, + types, + types::{Cursor, HexId}, + }, + bc::BlockchainRef, +}; +use blockchain::{BlockHeader, BlockchainState, Mempool}; +use std::convert::TryFrom; +use zkvm::Tx; + +pub(super) async fn status(bc: BlockchainRef) -> ResponseResult { + let bc_state = BlockchainState::make_initial(5, vec![]).0; + let mempool = &Mempool::new(bc_state.clone(), 5); + + let status = mempool_status(mempool); + let state = &bc_state; + let tip = state.tip.clone().into(); + let utreexo = [None; 64]; + + let state = types::State { tip, utreexo }; + + let peers = vec![]; + + Ok(responses::Status { + mempool: status, + state, + peers, + }) +} + +pub(super) async fn mempool( + cursor: types::Cursor, + bc: BlockchainRef, +) -> ResponseResult { + let bc_state = BlockchainState::make_initial(5, vec![]).0; + let mempool = &Mempool::new(bc_state.clone(), 5); + let txs_owned = Vec::::new(); + let txs = txs_owned.iter(); + + let offset = cursor + .cursor + .parse::() + .map_err(|_| error::invalid_cursor())?; + let elements = cursor.count() as usize; + + let status = mempool_status(mempool); + let txs = txs + .skip(offset) + .take(elements) + .map(|tx| types::Tx::try_from(tx.clone())) + .collect::, _>>() + .map_err(|_| error::tx_compute_error())?; + + Ok(responses::MempoolTxs { + cursor: (offset + elements).to_string(), + status, + txs, + }) +} + +pub(super) async fn blocks(cursor: Cursor, bc: BlockchainRef) -> ResponseResult { + let blocks_headers = Vec::::new(); + + let offset = cursor + .cursor + .parse::() + .map_err(|_| error::invalid_cursor())?; + let count = cursor.count() as usize; + + let headers = blocks_headers + .iter() + .skip(offset) + .take(count) + .map(|b| b.clone().into()) + .collect::>(); + Ok(responses::Blocks { + cursor: (offset + count).to_string(), + blocks: headers, + }) +} + +pub(super) async fn block(block_id: HexId, bc: BlockchainRef) -> ResponseResult { + let header = BlockHeader::make_initial(0, zkvm::Hash::default()); + let txs = Vec::::new(); + + Ok(responses::Block { + header: header.into(), + txs, + }) +} + +pub(super) async fn tx(tx_id: HexId, bc: BlockchainRef) -> ResponseResult { + let block_tx = blockchain::BlockTx { + tx: Tx { + header: zkvm::TxHeader { + version: 0, + mintime_ms: 0, + maxtime_ms: 0, + }, + program: vec![], + signature: musig::Signature { + s: Default::default(), + R: Default::default(), + }, + proof: zkvm::bulletproofs::r1cs::R1CSProof::from_bytes(&[0; 1 + 15 * 32]).unwrap(), + }, + proofs: vec![], + }; + + let tx = types::Tx::try_from(block_tx).map_err(|_| error::tx_compute_error())?; + + let status = responses::TxStatus { + confirmed: true, + block_height: 0, + block_id: [0; 32], + }; + + Ok(responses::TxResponse { status, tx }) +} + +pub(super) async fn submit( + raw_tx: requests::RawTx, + bc: BlockchainRef, +) -> ResponseResult { + unimplemented!() +} + +fn mempool_status(mempool: &Mempool) -> types::MempoolStatus { + let count = mempool.entries().count() as u64; + let size = mempool.len() as u64; + let feerate = 0; + + types::MempoolStatus { + count, + size, + feerate, + } +} diff --git a/node/src/api/network/requests.rs b/node/src/api/network/requests.rs new file mode 100644 index 000000000..eb7d13932 --- /dev/null +++ b/node/src/api/network/requests.rs @@ -0,0 +1,14 @@ +use serde::Deserialize; + +use crate::api::serde_utils::BigArray; +use crate::api::types::TxHeader; + +#[derive(Deserialize)] +pub struct RawTx { + pub header: TxHeader, + pub program: Vec, + #[serde(with = "BigArray")] + pub signature: [u8; 64], + pub r1cs_proof: Vec, + pub utreexo_proofs: Vec>, +} diff --git a/node/src/api/network/responses.rs b/node/src/api/network/responses.rs new file mode 100644 index 000000000..530ff3e0c --- /dev/null +++ b/node/src/api/network/responses.rs @@ -0,0 +1,47 @@ +use serde::Serialize; + +use crate::api::types::{BlockHeader, Cursor, MempoolStatus, Peer, State, Tx}; +use blockchain::BlockTx; +use zkvm::TxHeader; + +#[derive(Serialize)] +pub struct Status { + pub mempool: MempoolStatus, + pub state: State, + pub peers: Vec, +} + +#[derive(Serialize)] +pub struct MempoolTxs { + pub cursor: String, + pub status: MempoolStatus, + pub txs: Vec, +} + +#[derive(Serialize)] +pub struct Blocks { + pub cursor: String, + pub blocks: Vec, +} + +#[derive(Serialize)] +pub struct Block { + pub header: BlockHeader, + pub txs: Vec, +} + +#[derive(Serialize)] +pub struct TxResponse { + pub status: TxStatus, + pub tx: Tx, +} + +#[derive(Serialize)] +pub struct TxStatus { + pub confirmed: bool, + pub block_height: u64, + pub block_id: [u8; 32], +} + +#[derive(Serialize)] +pub struct Submit {} diff --git a/node/src/api/response.rs b/node/src/api/response.rs new file mode 100644 index 000000000..61dcf48b7 --- /dev/null +++ b/node/src/api/response.rs @@ -0,0 +1,127 @@ +use serde::Serialize; +use warp::reply::Json; +use warp::Reply; + +pub type ResponseResult = Result; + +#[derive(Debug, Serialize)] +pub struct Response { + ok: bool, + response: Option, + error: Option, +} + +#[derive(Debug, Serialize)] +pub struct ResponseError { + code: u16, + description: String, +} + +impl ResponseError { + pub fn new(code: u16, description: impl Into) -> Self { + ResponseError { + code, + description: description.into(), + } + } +} + +impl From> for Response { + fn from(res: Result) -> Self { + match res { + Ok(t) => Response::ok(t), + Err(e) => Response::err(e), + } + } +} + +impl Response { + pub fn ok(data: T) -> Self { + Self { + ok: true, + response: Some(data), + error: None, + } + } + pub fn err(err: ResponseError) -> Self { + Self { + ok: false, + response: None, + error: Some(err), + } + } +} + +#[cfg(test)] +impl Response { + pub fn unwrap_ok(self) -> T { + let Response { + ok, + response, + error, + } = self; + if let Some(err) = error { + panic!("Unwrap at err: {:?}", err); + } + response.unwrap() + } +} +#[cfg(test)] +impl Response { + pub fn unwrap_err(self) -> ResponseError { + let Response { + ok, + response, + error, + } = self; + if let Some(t) = response { + panic!("Unwrap err at ok: {:?}", t); + } + error.unwrap() + } +} + +impl Reply for Response { + fn into_response(self) -> warp::reply::Response { + warp::reply::json(&self).into_response() + } +} + +pub mod error { + use crate::api::response::{Response, ResponseError}; + use crate::wallet::WalletError; + + pub fn cannot_delete_file() -> ResponseError { + ResponseError::new(100, "Cannot delete file with wallet") + } + pub fn invalid_address_label() -> ResponseError { + ResponseError::new(101, "Invalid address label") + } + pub fn invalid_xpub() -> ResponseError { + ResponseError::new(102, "Invalid xpub") + } + pub fn wallet_does_not_exist() -> ResponseError { + ResponseError::new(103, "Wallet not exists") + } + pub fn wallet_updating_error() -> ResponseError { + ResponseError::new(104, "Something wrong when updating wallet") + } + pub fn tx_building_error() -> ResponseError { + ResponseError::new(105, "Something wrong when building tx") + } + pub fn wallet_error(error: WalletError) -> ResponseError { + let code = match &error { + WalletError::InsufficientFunds => 106, + WalletError::XprvMismatch => 107, + WalletError::AssetNotFound => 108, + WalletError::AddressLabelMismatch => 109, + }; + ResponseError::new(code, error.to_string()) + } + pub fn invalid_cursor() -> ResponseError { + ResponseError::new(110, "Something wrong when building tx") + } + pub fn tx_compute_error() -> ResponseError { + ResponseError::new(111, "Something wrong when computing tx") + } +} diff --git a/node/src/api/serde_utils.rs b/node/src/api/serde_utils.rs new file mode 100644 index 000000000..56ffdedc7 --- /dev/null +++ b/node/src/api/serde_utils.rs @@ -0,0 +1,67 @@ +use serde::de::{Deserialize, Deserializer, Error, SeqAccess, Visitor}; +use serde::ser::{Serialize, SerializeTuple, Serializer}; +use std::fmt; +use std::marker::PhantomData; + +pub(crate) trait BigArray<'de>: Sized { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer; + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>; +} + +macro_rules! big_array { + ($($len:expr,)+) => { + $( + impl<'de, T> BigArray<'de> for [T; $len] + where T: Default + Copy + Serialize + Deserialize<'de> + { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + let mut seq = serializer.serialize_tuple(self.len())?; + for elem in &self[..] { + seq.serialize_element(elem)?; + } + seq.end() + } + + fn deserialize(deserializer: D) -> Result<[T; $len], D::Error> + where D: Deserializer<'de> + { + struct ArrayVisitor { + element: PhantomData, + } + + impl<'de, T> Visitor<'de> for ArrayVisitor + where T: Default + Copy + Deserialize<'de> + { + type Value = [T; $len]; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(concat!("an array of length ", $len)) + } + + fn visit_seq(self, mut seq: A) -> Result<[T; $len], A::Error> + where A: SeqAccess<'de> + { + let mut arr = [T::default(); $len]; + for i in 0..$len { + arr[i] = seq.next_element()? + .ok_or_else(|| Error::invalid_length(i, &self))?; + } + Ok(arr) + } + } + + let visitor = ArrayVisitor { element: PhantomData }; + deserializer.deserialize_tuple($len, visitor) + } + } + )+ + } +} + +big_array! { 64, } diff --git a/node/src/api/types.rs b/node/src/api/types.rs new file mode 100644 index 000000000..76112503a --- /dev/null +++ b/node/src/api/types.rs @@ -0,0 +1,208 @@ +use serde::{Deserialize, Serialize}; + +use accounts::Receiver; + +use super::serde_utils::BigArray; +use blockchain::BlockTx; +use std::convert::TryFrom; +use std::str::FromStr; +use zkvm::encoding::Encodable; + +/// Stats about unconfirmed transactions. +#[derive(Serialize)] +pub struct MempoolStatus { + /// Total number of transactions + pub count: u64, + /// Total size of all transactions in the mempool + pub size: u64, + /// Lowest feerate for inclusing in the block + pub feerate: u64, +} + +#[derive(Serialize)] +pub struct BlockHeader { + pub version: u64, // Network version. + pub height: u64, // Serial number of the block, starting with 1. + pub prev: [u8; 32], // ID of the previous block. Initial block uses the all-zero string. + pub timestamp_ms: u64, // Integer timestamp of the block in milliseconds since the Unix epoch + pub txroot: [u8; 32], // 32-byte Merkle root of the transaction witness hashes (`BlockTx::witness_hash`) in the block. + pub utxoroot: [u8; 32], // 32-byte Merkle root of the Utreexo state. + pub ext: Vec, // Extra data for the future extensions. +} + +#[derive(Deserialize)] +pub struct TxHeader { + pub version: u64, + pub mintime_ms: u64, + pub maxtime_ms: u64, +} + +#[derive(Serialize)] +pub struct Block { + pub header: BlockHeader, + pub txs: Vec, +} + +#[derive(Serialize)] +pub struct Tx { + pub id: [u8; 32], // canonical tx id + pub wid: [u8; 32], // witness hash of the tx (includes signatures and proofs) + pub raw: String, + pub fee: u64, // fee paid by the tx + pub size: u64, // size in bytes of the encoded tx +} + +/// Description of the current blockchain state. +#[derive(Serialize)] +pub struct State { + // Block header + pub tip: BlockHeader, + // The utreexo state + #[serde(with = "BigArray")] + pub utreexo: [Option<[u8; 32]>; 64], +} + +/// Description of a connected peer. +#[derive(Serialize)] +pub struct Peer { + pub id: [u8; 32], + pub since: u64, + /// ipv6 address format + pub addr: [u8; 16], + pub priority: u64, +} + +#[derive(Serialize)] +pub enum AnnotatedAction { + Issue(IssueAction), + Spend(SpendAction), + Receive(ReceiveAction), + Retire(RetireAction), + Memo(MemoAction), +} + +#[derive(Serialize)] +pub struct IssueAction { + // Index of the txlog entry + pub entry: u32, + pub qty: u64, + pub flv: [u8; 32], +} + +#[derive(Serialize)] +pub struct SpendAction { + // Index of the txlog entry + pub entry: u32, + pub qty: u64, + pub flv: [u8; 32], + // Identifier of the account sending funds + pub account: [u8; 32], +} + +#[derive(Serialize)] +pub struct ReceiveAction { + // Index of the txlog entry + pub entry: u32, + pub qty: u64, + pub flv: [u8; 32], + // Identifier of the account receiving funds (if known) + pub account: Option<[u8; 32]>, +} + +#[derive(Serialize)] +pub struct RetireAction { + // Index of the txlog entry + pub entry: u32, + pub qty: u64, + pub flv: [u8; 32], +} + +#[derive(Serialize)] +pub struct MemoAction { + pub entry: u32, + pub data: Vec, +} + +/// Description of the current blockchain state. +#[derive(Serialize)] +pub struct AnnotatedTx { + /// Raw tx + pub tx: Tx, + pub actions: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum BuildTxAction { + IssueToAddress([u8; 32], u64, String), + IssueToReceiver(Receiver), + TransferToAddress([u8; 32], u64, String), + TransferToReceiver(Receiver), + Memo(Vec), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Cursor { + pub cursor: String, + pub count: Option, +} + +impl Cursor { + const DEFAULT_ELEMENTS_PER_PAGE: u32 = 20; + pub fn count(&self) -> u32 { + self.count.unwrap_or(Self::DEFAULT_ELEMENTS_PER_PAGE) + } +} + +#[derive(Deserialize)] +pub struct HexId([u8; 32]); + +impl FromStr for HexId { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + let array = serde_json::from_str(s)?; + Ok(Self(array)) + } +} + +impl From for BlockHeader { + fn from(header: blockchain::BlockHeader) -> Self { + let blockchain::BlockHeader { + version, + height, + prev, + timestamp_ms, + txroot, + utxoroot, + ext, + } = header; + Self { + version, + height, + prev: prev.0, + timestamp_ms, + txroot: txroot.0, + utxoroot: utxoroot.0, + ext, + } + } +} + +impl TryFrom for Tx { + type Error = zkvm::VMError; + + fn try_from(tx: BlockTx) -> Result { + let wid = tx.witness_hash().0; + let precomputed = tx.tx.precompute()?; + let id = (precomputed.id.0).0; + let fee = precomputed.feerate.fee(); + let size = precomputed.feerate.size() as u64; + Ok(Tx { + id, + wid, + raw: hex::encode(tx.encode_to_vec()), + fee, + size, + }) + } +} diff --git a/node/src/api/wallet.rs b/node/src/api/wallet.rs new file mode 100644 index 000000000..388a1a24c --- /dev/null +++ b/node/src/api/wallet.rs @@ -0,0 +1,154 @@ +mod handlers; +mod requests; +mod responses; + +use crate::api::response::{Response, ResponseResult}; +use crate::api::types::Cursor; +use crate::api::warp_utils::{handle1, handle2}; +use crate::wallet_manager::WalletRef; +use futures::future::NeverError; +use futures::{Future, FutureExt}; +use std::convert::Infallible; +use warp::filters::path::param; +use warp::{any, Filter}; + +pub fn routes( + wallet: WalletRef, +) -> impl Filter + Clone { + use warp::*; + + let new = path!("v1" / "wallet" / "new") + .and(post()) + .and(body::json()) + .and(with_wallet(wallet.clone())) + .and_then(handle2(handlers::new)); + + let balance = path!("v1" / "wallet" / "balance") + .and(get()) + .and(with_wallet(wallet.clone())) + .and_then(handle1(handlers::balance)); + + let txs = path!("v1" / "wallet" / "txs") + .and(query::()) + .and(get()) + .and(with_wallet(wallet.clone())) + .and_then(handle2(handlers::txs)); + + let address = path!("v1" / "wallet" / "address") + .and(get()) + .and(with_wallet(wallet.clone())) + .and_then(handle1(handlers::address)); + + let receiver = path!("v1" / "wallet" / "receiver") + .and(post()) + .and(body::json()) + .and(with_wallet(wallet.clone())) + .and_then(handle2(handlers::receiver)); + + let buildtx = path!("v1" / "wallet" / "buildtx") + .and(post()) + .and(body::json()) + .and(with_wallet(wallet.clone())) + .and_then(handle2(handlers::buildtx)); + + new.or(balance).or(txs).or(address).or(receiver).or(buildtx) +} + +fn with_wallet( + wallet: WalletRef, +) -> impl Filter + Clone { + any().map(move || wallet.clone()) +} +/* +TODO: there are no posibility to testing because impl warp::Reply not implement warp::test::inner::OneOrTuple +#[cfg(test)] +mod wallet_tests { + use super::*; + + use crate::config::Config; + use crate::wallet_manager::{WalletManager, WalletRef}; + use std::sync::Arc; + use tokio::sync::RwLock; + + fn prepare_wallet() -> WalletRef { + WalletManager::new(Config { + data: Default::default(), + path: Default::default(), + }) + .unwrap() + } + + async fn remove_wallet(wallet: WalletRef) { + let mut manager = wallet.write().await; + manager.clear_wallet(); + } + + #[tokio::test] + async fn test_new() { + let wallet = prepare_wallet(); + let routes = routes(wallet.clone()); + + let response: Response = warp::test::request() + .path("/v1/wallet/new") + .method("POST") + .json(&requests::NewWallet { + xpub: vec![0; 64], + label: "test_label".to_string(), + }) + .filter(&routes) + .await + .unwrap(); + + remove_wallet(wallet).await; + + response.unwrap_ok(); + } + + #[tokio::test] + #[should_panic( + expected = r#"Unwrap at err: ResponseError { code: 101, description: "Invalid address label" }"# + )] + async fn test_new_wrong_label() { + let wallet = prepare_wallet(); + let routes = routes(wallet.clone()); + + let response: Response = warp::test::request() + .path("/v1/wallet/new") + .method("POST") + .json(&requests::NewWallet { + xpub: vec![0; 64], + label: "invalid label".to_string(), + }) + .filter(&routes) + .await + .unwrap(); + + remove_wallet(wallet).await; + + response.unwrap_ok(); + } + + #[tokio::test] + #[should_panic( + expected = r#"Unwrap at err: ResponseError { code: 102, description: "Invalid xpub" }"# + )] + async fn test_new_invalid_xpub() { + let wallet = prepare_wallet(); + let routes = routes(wallet.clone()); + + let response: Response = warp::test::request() + .path("/v1/wallet/new") + .method("POST") + .json(&requests::NewWallet { + xpub: vec![0; 32], + label: "test_label".to_string(), + }) + .filter(&routes) + .await + .unwrap(); + + remove_wallet(wallet).await; + + response.unwrap_ok(); + } +}*/ diff --git a/node/src/api/wallet/handlers.rs b/node/src/api/wallet/handlers.rs new file mode 100644 index 000000000..a02fd7d59 --- /dev/null +++ b/node/src/api/wallet/handlers.rs @@ -0,0 +1,177 @@ +use std::convert::{Infallible, TryFrom}; + +use crate::{ + api::{ + response::{error, Response, ResponseError, ResponseResult}, + types, + types::Cursor, + wallet::{requests, responses}, + }, + wallet::{BuiltTx, TxAction, Wallet}, + wallet_manager::WalletRef, +}; +use accounts::{Address, AddressLabel}; +use curve25519_dalek::scalar::Scalar; +use keytree::{Xprv, Xpub}; + +/// Creates a new wallet +pub(super) async fn new( + request: requests::NewWallet, + wallet: WalletRef, +) -> ResponseResult { + let requests::NewWallet { xpub, label } = request; + let mut wallet_ref = wallet.write().await; + if wallet_ref.wallet_exists() { + if let Err(_) = wallet_ref.clear_wallet() { + return Err(error::cannot_delete_file()); + } + } + let label = AddressLabel::new(label).ok_or_else(|| error::invalid_address_label())?; + let xpub = Xpub::from_bytes(&xpub).ok_or_else(|| error::invalid_xpub())?; + + let new_wallet = Wallet::new(label, xpub); + wallet_ref + .initialize_wallet(new_wallet) + .expect("We previously deleted wallet, there are no other errors when initializing wallet"); + + Ok(responses::NewWallet) +} + +/// Returns wallet's balance. +pub(super) async fn balance(wallet: WalletRef) -> ResponseResult { + let mut wallet_ref = wallet.read().await; + let wallet = wallet_ref + .wallet_ref() + .map_err(|_| error::wallet_does_not_exist())?; + + let balances = wallet + .balances() + .map(|balance| (balance.flavor.to_bytes(), balance.total)) + .collect::>(); + + Ok(responses::Balance { balances }) +} + +/// Lists annotated transactions. +pub(super) async fn txs( + _cursor: Cursor, + _wallet: WalletRef, +) -> ResponseResult { + unimplemented!() +} + +/// Generates a new address. +pub(super) async fn address(wallet: WalletRef) -> ResponseResult { + let mut wallet_ref = wallet.write().await; + let update_wallet = |wallet: &mut Wallet| Ok(wallet.create_address()); + + match wallet_ref.update_wallet(update_wallet) { + Ok(address) => Ok(responses::NewAddress { + address: address.to_string(), + }), + Err(crate::Error::WalletNotInitialized) => Err(error::wallet_does_not_exist()), + _ => Err(error::wallet_updating_error()), + } +} + +/// Generates a new receiver. +pub(super) async fn receiver( + req: requests::NewReceiver, + wallet: WalletRef, +) -> ResponseResult { + let mut wallet_ref = wallet.write().await; + let update_wallet = |wallet: &mut Wallet| { + let requests::NewReceiver { flv, qty, exp } = req; // TODO: expiration time? + let receiver_value = zkvm::ClearValue { + qty, + flv: Scalar::from_bits(flv), + }; + let (_, receiver) = wallet.create_receiver(receiver_value); + Ok(receiver) + }; + match wallet_ref.update_wallet(update_wallet) { + Ok(receiver) => Ok(responses::NewReceiver { receiver }), + Err(crate::Error::WalletNotInitialized) => Err(error::wallet_does_not_exist()), + _ => Err(error::wallet_updating_error()), + } +} + +/// Generates a new receiver. +pub(super) async fn buildtx( + req: requests::BuildTx, + wallet: WalletRef, +) -> ResponseResult { + let mut wallet_ref = wallet.write().await; + let requests::BuildTx { actions } = req; + let actions = actions + .clone() + .into_iter() + .map(build_tx_action_to_tx_action) + .collect::, _>>()?; + + let mut err = None; + + let update_wallet = |wallet: &mut Wallet| -> Result { + let gens = zkvm::bulletproofs::BulletproofGens::new(256, 1); + let res = wallet.build_tx(&gens, |builder| { + for action in actions { + builder._add_action(action); + } + }); + match res { + Ok(tx) => Ok(tx), + Err(e) => { + err = Some(e); + // Dummy error to specify that giving error when update wallet + Err(crate::Error::WalletAlreadyExists) + } + } + }; + match wallet_ref.update_wallet(update_wallet) { + Ok(tx) => { + let xprv = wallet_ref + .read_xprv() + .map_err(|_| error::tx_building_error())?; + let block_tx = tx.sign(&xprv).map_err(|e| error::wallet_error(e))?; + + let tx = types::Tx::try_from(block_tx).map_err(|_| error::tx_compute_error())?; + + Ok(responses::BuiltTx { tx }) + } + Err(crate::Error::WalletNotInitialized) => Err(error::wallet_does_not_exist()), + // This means that we have error while updating wallet + Err(crate::Error::WalletAlreadyExists) => Err(match err { + Some(e) => error::wallet_error(e), + None => error::wallet_updating_error(), + }), + _ => Err(error::wallet_updating_error()), + } +} + +fn build_tx_action_to_tx_action(action: types::BuildTxAction) -> Result { + use crate::api::types::BuildTxAction::*; + + match action { + IssueToAddress(flv, qty, address) => { + let address = + Address::from_string(&address).ok_or_else(|| error::invalid_address_label())?; + let clr = zkvm::ClearValue { + qty, + flv: Scalar::from_bits(flv), + }; + Ok(TxAction::IssueToAddress(clr, address)) + } + IssueToReceiver(rec) => Ok(TxAction::IssueToReceiver(rec)), + TransferToAddress(flv, qty, address) => { + let address = + Address::from_string(&address).ok_or_else(|| error::invalid_address_label())?; + let clr = zkvm::ClearValue { + qty, + flv: Scalar::from_bits(flv), + }; + Ok(TxAction::TransferToAddress(clr, address)) + } + TransferToReceiver(rec) => Ok(TxAction::TransferToReceiver(rec)), + Memo(memo) => Ok(TxAction::Memo(memo)), + } +} diff --git a/node/src/api/wallet/requests.rs b/node/src/api/wallet/requests.rs new file mode 100644 index 000000000..ee058231b --- /dev/null +++ b/node/src/api/wallet/requests.rs @@ -0,0 +1,21 @@ +use super::super::serde_utils::BigArray; +use crate::api::types::BuildTxAction; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct NewWallet { + pub xpub: Vec, + pub label: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NewReceiver { + pub flv: [u8; 32], + pub qty: u64, + pub exp: u64, // expiration timestamp +} + +#[derive(Serialize, Deserialize)] +pub struct BuildTx { + pub actions: Vec, +} diff --git a/node/src/api/wallet/responses.rs b/node/src/api/wallet/responses.rs new file mode 100644 index 000000000..2b451eb55 --- /dev/null +++ b/node/src/api/wallet/responses.rs @@ -0,0 +1,33 @@ +use crate::api::types::{AnnotatedTx, Tx}; +use crate::wallet::SigntxInstruction; +use accounts::Receiver; +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct NewWallet; + +#[derive(Debug, Serialize)] +pub struct Balance { + pub balances: Vec<([u8; 32], u64)>, +} + +#[derive(Serialize)] +pub struct WalletTxs { + pub cursor: Vec, + pub txs: Vec, +} + +#[derive(Serialize)] +pub struct NewAddress { + pub address: String, +} + +#[derive(Serialize)] +pub struct NewReceiver { + pub receiver: Receiver, +} + +#[derive(Serialize)] +pub struct BuiltTx { + pub tx: Tx, +} diff --git a/node/src/api/warp_utils.rs b/node/src/api/warp_utils.rs new file mode 100644 index 000000000..4cd4b67dc --- /dev/null +++ b/node/src/api/warp_utils.rs @@ -0,0 +1,24 @@ +use crate::api::response::{Response, ResponseResult}; +use futures::{future::NeverError, Future, FutureExt}; + +// Combinator Fn(A) -> impl Future> into Fn(A) -> impl TryFuture, Error = Infallible> +pub fn handle1( + f: F, +) -> impl Fn(A) -> NeverError>> + Clone +where + F: Fn(A) -> Fut + 'static + Clone, + Fut: Future>, +{ + move |a| f(a).map_into().never_error() +} + +// Combinator Fn(A, B) -> impl Future> into Fn(A, B) -> impl TryFuture, Error = Infallible> +pub fn handle2( + f: F, +) -> impl Fn(A, B) -> NeverError>> + Clone +where + F: Fn(A, B) -> Fut + 'static + Clone, + Fut: Future>, +{ + move |a, b| f(a, b).map_into().never_error() +} diff --git a/node/src/wallet.rs b/node/src/wallet.rs index b02dc077f..080584769 100644 --- a/node/src/wallet.rs +++ b/node/src/wallet.rs @@ -128,7 +128,7 @@ pub enum SigntxInstruction { /// A high-level description of the tx action that /// will turn into specific ZkVM instructions under the hood. #[derive(Clone, Debug)] -enum TxAction { +pub enum TxAction { IssueToAddress(ClearValue, Address), IssueToReceiver(Receiver), TransferToAddress(ClearValue, Address), @@ -657,6 +657,10 @@ impl TxBuilder { pub fn memo(&mut self, memo: Vec) { self.actions.push(TxAction::Memo(memo)); } + + pub fn _add_action(&mut self, action: TxAction) { + self.actions.push(action) + } } impl BuiltTx { diff --git a/node/src/wallet_manager.rs b/node/src/wallet_manager.rs index 305b5650e..279e1cbcd 100644 --- a/node/src/wallet_manager.rs +++ b/node/src/wallet_manager.rs @@ -3,7 +3,7 @@ use super::errors::Error; use super::wallet::Wallet; use keytree::Xprv; use std::fs::{self, File}; -use std::io::Write; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::sync::RwLock; @@ -72,6 +72,18 @@ impl WalletManager { Ok(()) } + /// Reads + pub fn read_xprv(&self) -> Result { + let path = self.wallet_keypath(); + + let mut file = File::open(path)?; + let mut out = String::with_capacity(64); + file.read_to_string(&mut out)?; + let xprv = Xprv::from_bytes(out.as_bytes()) + .expect("We previously write Xprv by self so we expect that it must be valid"); + Ok(xprv) + } + /// Removes the wallet pub fn clear_wallet(&mut self) -> Result<(), Error> { fs::remove_file(self.wallet_filepath())?;