mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 07:11:36 +02:00
Normalize line endings
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,875 +0,0 @@
|
||||
// Copyright 2021 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Controller for wallet.. instantiates and handles listeners (or single-run
|
||||
//! invocations) as needed.
|
||||
use crate::api::{self, ApiServer, BasicAuthMiddleware, ResponseFuture, Router, TLSConfig};
|
||||
use crate::config::TorConfig;
|
||||
use crate::keychain::Keychain;
|
||||
use crate::libwallet::{
|
||||
address, Error, NodeClient, NodeVersionInfo, Slate, SlatepackAddress, WalletInst,
|
||||
WalletLCProvider, GRIN_BLOCK_HEADER_VERSION,
|
||||
};
|
||||
use crate::util::secp::key::SecretKey;
|
||||
use crate::util::{from_hex, static_secp_instance, to_base64, Mutex};
|
||||
use futures::channel::oneshot;
|
||||
use grin_wallet_api::JsonId;
|
||||
use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig};
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use hyper::body;
|
||||
use hyper::header::HeaderValue;
|
||||
use hyper::{Body, Request, Response, StatusCode};
|
||||
use qr_code::QrCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::impls::tor::config as tor_config;
|
||||
use crate::impls::tor::process as tor_process;
|
||||
use crate::impls::tor::{bridge as tor_bridge, proxy as tor_proxy};
|
||||
|
||||
use crate::apiwallet::{
|
||||
EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign,
|
||||
ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc,
|
||||
};
|
||||
use easy_jsonrpc_mw;
|
||||
use easy_jsonrpc_mw::{Handler, MaybeReply};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref GRIN_OWNER_BASIC_REALM: HeaderValue =
|
||||
HeaderValue::from_str("Basic realm=GrinOwnerAPI").unwrap();
|
||||
}
|
||||
|
||||
fn check_middleware(
|
||||
name: ForeignCheckMiddlewareFn,
|
||||
node_version_info: Option<NodeVersionInfo>,
|
||||
slate: Option<&Slate>,
|
||||
) -> Result<(), Error> {
|
||||
match name {
|
||||
// allow coinbases to be built regardless
|
||||
ForeignCheckMiddlewareFn::BuildCoinbase => Ok(()),
|
||||
_ => {
|
||||
let mut bhv = 3;
|
||||
if let Some(n) = node_version_info {
|
||||
bhv = n.block_header_version;
|
||||
}
|
||||
if let Some(s) = slate {
|
||||
if bhv > 4 && s.version_info.block_header_version < GRIN_BLOCK_HEADER_VERSION {
|
||||
Err(Error::Compatibility(
|
||||
"Incoming Slate is not compatible with this wallet. \
|
||||
Please upgrade the node or use a different one."
|
||||
.into(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// initiate the tor listener
|
||||
fn init_tor_listener<L, C, K>(
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
addr: &str,
|
||||
bridge: TorBridgeConfig,
|
||||
tor_proxy: TorProxyConfig,
|
||||
) -> Result<(tor_process::TorProcess, SlatepackAddress), Error>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
let mut process = tor_process::TorProcess::new();
|
||||
let mask = keychain_mask.lock();
|
||||
// eventually want to read a list of service config keys
|
||||
let mut w_lock = wallet.lock();
|
||||
let lc = w_lock.lc_provider()?;
|
||||
let w_inst = lc.wallet_inst()?;
|
||||
let k = w_inst.keychain((&mask).as_ref())?;
|
||||
let parent_key_id = w_inst.parent_key_id();
|
||||
let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?);
|
||||
let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0)
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
let onion_address = OnionV3Address::from_private(&sec_key.0)
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
let sp_address = SlatepackAddress::try_from(onion_address.clone())?;
|
||||
|
||||
let mut hm_tor_bridge: HashMap<String, String> = HashMap::new();
|
||||
let mut tor_timeout = 20;
|
||||
if bridge.bridge_line.is_some() {
|
||||
tor_timeout = 40;
|
||||
let bridge_config = tor_bridge::TorBridge::try_from(bridge)
|
||||
.map_err(|e| Error::TorConfig(format!("{}", e).into()))?;
|
||||
hm_tor_bridge = bridge_config
|
||||
.to_hashmap()
|
||||
.map_err(|e| Error::TorConfig(format!("{}", e).into()))?;
|
||||
}
|
||||
|
||||
let mut hm_tor_poxy: HashMap<String, String> = HashMap::new();
|
||||
if tor_proxy.transport.is_some() || tor_proxy.allowed_port.is_some() {
|
||||
let proxy_config = tor_proxy::TorProxy::try_from(tor_proxy)
|
||||
.map_err(|e| Error::TorConfig(format!("{}", e).into()))?;
|
||||
hm_tor_poxy = proxy_config
|
||||
.to_hashmap()
|
||||
.map_err(|e| Error::TorConfig(format!("{}", e).into()))?;
|
||||
}
|
||||
|
||||
warn!(
|
||||
"Starting Tor Hidden Service for API listener at address {}, binding to {}",
|
||||
onion_address, addr
|
||||
);
|
||||
tor_config::output_tor_listener_config(
|
||||
&tor_dir,
|
||||
addr,
|
||||
&vec![sec_key],
|
||||
hm_tor_bridge,
|
||||
hm_tor_poxy,
|
||||
)
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e).into()))?;
|
||||
// Start TOR process
|
||||
process
|
||||
.torrc_path(&format!("{}/torrc", tor_dir))
|
||||
.working_dir(&tor_dir)
|
||||
.timeout(tor_timeout)
|
||||
.completion_percent(100)
|
||||
.launch()
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
Ok((process, sp_address))
|
||||
}
|
||||
|
||||
/// Instantiate wallet Owner API for a single-use (command line) call
|
||||
/// Return a function containing a loaded API context to call
|
||||
pub fn owner_single_use<L, F, C, K>(
|
||||
wallet: Option<Arc<Mutex<Box<dyn WalletInst<'static, L, C, K>>>>>,
|
||||
keychain_mask: Option<&SecretKey>,
|
||||
api_context: Option<&mut Owner<L, C, K>>,
|
||||
f: F,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
F: FnOnce(&mut Owner<L, C, K>, Option<&SecretKey>) -> Result<(), Error>,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
match api_context {
|
||||
Some(c) => f(c, keychain_mask)?,
|
||||
None => {
|
||||
let wallet = match wallet {
|
||||
Some(w) => w,
|
||||
None => {
|
||||
return Err(Error::GenericError(format!(
|
||||
"Instantiated wallet or Owner API context must be provided"
|
||||
)))
|
||||
}
|
||||
};
|
||||
f(&mut Owner::new(wallet, None), keychain_mask)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Instantiate wallet Foreign API for a single-use (command line) call
|
||||
/// Return a function containing a loaded API context to call
|
||||
pub fn foreign_single_use<'a, L, F, C, K>(
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
keychain_mask: Option<SecretKey>,
|
||||
f: F,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
L: WalletLCProvider<'a, C, K>,
|
||||
F: FnOnce(&mut Foreign<'a, L, C, K>) -> Result<(), Error>,
|
||||
C: NodeClient + 'a,
|
||||
K: Keychain + 'a,
|
||||
{
|
||||
f(&mut Foreign::new(
|
||||
wallet,
|
||||
keychain_mask,
|
||||
Some(check_middleware),
|
||||
false,
|
||||
))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Listener version, providing same API but listening for requests on a
|
||||
/// port and wrapping the calls
|
||||
/// Note keychain mask is only provided here in case the foreign listener is also being used
|
||||
/// in the same wallet instance
|
||||
pub fn owner_listener<L, C, K>(
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
addr: &str,
|
||||
api_secret: Option<String>,
|
||||
tls_config: Option<TLSConfig>,
|
||||
owner_api_include_foreign: Option<bool>,
|
||||
tor_config: Option<TorConfig>,
|
||||
test_mode: bool,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
let mut router = Router::new();
|
||||
if api_secret.is_some() {
|
||||
let api_basic_auth =
|
||||
"Basic ".to_string() + &to_base64(&("grin:".to_string() + &api_secret.unwrap()));
|
||||
let basic_auth_middleware = Arc::new(BasicAuthMiddleware::new(
|
||||
api_basic_auth,
|
||||
&GRIN_OWNER_BASIC_REALM,
|
||||
Some("/v2/foreign".into()),
|
||||
));
|
||||
router.add_middleware(basic_auth_middleware);
|
||||
}
|
||||
let mut running_foreign = false;
|
||||
if owner_api_include_foreign.unwrap_or(false) {
|
||||
running_foreign = true;
|
||||
}
|
||||
|
||||
let api_handler_v3 = OwnerAPIHandlerV3::new(
|
||||
wallet.clone(),
|
||||
keychain_mask.clone(),
|
||||
tor_config.clone(),
|
||||
running_foreign,
|
||||
);
|
||||
|
||||
router
|
||||
.add_route("/v3/owner", Arc::new(api_handler_v3))
|
||||
.map_err(|_| Error::GenericError("Router failed to add route".to_string()))?;
|
||||
|
||||
// If so configured, add the foreign API to the same port
|
||||
if running_foreign {
|
||||
warn!("Starting HTTP Foreign API on Owner server at {}.", addr);
|
||||
let foreign_api_handler_v2 =
|
||||
ForeignAPIHandlerV2::new(wallet, keychain_mask, test_mode, Mutex::new(tor_config));
|
||||
router
|
||||
.add_route("/v2/foreign", Arc::new(foreign_api_handler_v2))
|
||||
.map_err(|_| Error::GenericError("Router failed to add route".to_string()))?;
|
||||
}
|
||||
|
||||
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
|
||||
Box::leak(Box::new(oneshot::channel::<()>()));
|
||||
|
||||
let mut apis = ApiServer::new();
|
||||
warn!("Starting HTTP Owner API server at {}.", addr);
|
||||
let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address");
|
||||
let api_thread = apis
|
||||
.start(socket_addr, router, tls_config, api_chan)
|
||||
.map_err(|_| Error::GenericError("API thread failed to start".to_string()))?;
|
||||
warn!("HTTP Owner listener started.");
|
||||
api_thread
|
||||
.join()
|
||||
.map_err(|e| Error::GenericError(format!("API thread panicked :{:?}", e)))
|
||||
}
|
||||
|
||||
/// Listener version, providing same API but listening for requests on a
|
||||
/// port and wrapping the calls
|
||||
pub fn foreign_listener<L, C, K>(
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
addr: &str,
|
||||
tls_config: Option<TLSConfig>,
|
||||
use_tor: bool,
|
||||
test_mode: bool,
|
||||
tor_config: Option<TorConfig>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
// Check if wallet has been opened first
|
||||
{
|
||||
let mut w_lock = wallet.lock();
|
||||
let lc = w_lock.lc_provider()?;
|
||||
let _ = lc.wallet_inst()?;
|
||||
}
|
||||
|
||||
let (tor_bridge, tor_proxy) = match tor_config.clone() {
|
||||
Some(s) => (s.bridge, s.proxy),
|
||||
None => (TorBridgeConfig::default(), TorProxyConfig::default()),
|
||||
};
|
||||
|
||||
// need to keep in scope while the main listener is running
|
||||
let (_tor_process, address) = match use_tor {
|
||||
true => {
|
||||
match init_tor_listener(
|
||||
wallet.clone(),
|
||||
keychain_mask.clone(),
|
||||
addr,
|
||||
tor_bridge,
|
||||
tor_proxy,
|
||||
) {
|
||||
Ok((tp, addr)) => (Some(tp), Some(addr)),
|
||||
Err(e) => {
|
||||
warn!("Unable to start TOR listener; Check that TOR executable is installed and on your path");
|
||||
error!("Tor Error: {}", e);
|
||||
warn!("Listener will be available via HTTP only");
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
false => (None, None),
|
||||
};
|
||||
|
||||
let api_handler_v2 =
|
||||
ForeignAPIHandlerV2::new(wallet, keychain_mask, test_mode, Mutex::new(tor_config));
|
||||
let mut router = Router::new();
|
||||
|
||||
router
|
||||
.add_route("/v2/foreign", Arc::new(api_handler_v2))
|
||||
.map_err(|_| Error::GenericError("Router failed to add route".to_string()))?;
|
||||
|
||||
let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) =
|
||||
Box::leak(Box::new(oneshot::channel::<()>()));
|
||||
|
||||
let mut apis = ApiServer::new();
|
||||
warn!("Starting HTTP Foreign listener API server at {}.", addr);
|
||||
let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address");
|
||||
let api_thread = apis
|
||||
.start(socket_addr, router, tls_config, api_chan)
|
||||
.map_err(|_| Error::GenericError("API thread failed to start".to_string()))?;
|
||||
|
||||
warn!("HTTP Foreign listener started.");
|
||||
if let Some(a) = address {
|
||||
let qr_string = match QrCode::new(a.to_string()) {
|
||||
Ok(qr) => qr.to_string(false, 3),
|
||||
Err(_) => "Failed to generate QR code!".to_string(),
|
||||
};
|
||||
warn!("Slatepack Address is: {}\n{}", a, qr_string);
|
||||
}
|
||||
|
||||
api_thread
|
||||
.join()
|
||||
.map_err(|e| Error::GenericError(format!("API thread panicked :{:?}", e)))
|
||||
}
|
||||
|
||||
/// V3 API Handler/Wrapper for owner functions, which include a secure
|
||||
/// mode + lifecycle functions
|
||||
pub struct OwnerAPIHandlerV3<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// Wallet instance
|
||||
pub wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
|
||||
/// Handle to Owner API
|
||||
owner_api: Arc<Owner<L, C, K>>,
|
||||
|
||||
/// ECDH shared key
|
||||
pub shared_key: Arc<Mutex<Option<SecretKey>>>,
|
||||
|
||||
/// Keychain mask (to change if also running the foreign API)
|
||||
pub keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
|
||||
/// Whether we're running the foreign API on the same port, and therefore
|
||||
/// have to store the mask in-process
|
||||
pub running_foreign: bool,
|
||||
}
|
||||
|
||||
pub struct OwnerV3Helpers;
|
||||
|
||||
impl OwnerV3Helpers {
|
||||
/// Checks whether a request is to init the secure API
|
||||
pub fn is_init_secure_api(val: &serde_json::Value) -> bool {
|
||||
matches!(val["method"].as_str(), Some("init_secure_api"))
|
||||
}
|
||||
|
||||
/// Checks whether a request is to open the wallet
|
||||
pub fn is_open_wallet(val: &serde_json::Value) -> bool {
|
||||
matches!(val["method"].as_str(), Some("open_wallet"))
|
||||
}
|
||||
|
||||
/// Checks whether a request is an encrypted request
|
||||
pub fn is_encrypted_request(val: &serde_json::Value) -> bool {
|
||||
matches!(val["method"].as_str(), Some("encrypted_request_v3"))
|
||||
}
|
||||
|
||||
/// whether encryption is enabled
|
||||
pub fn encryption_enabled(key: Arc<Mutex<Option<SecretKey>>>) -> bool {
|
||||
let share_key_ref = key.lock();
|
||||
share_key_ref.is_some()
|
||||
}
|
||||
|
||||
/// If incoming is an encrypted request, check there is a shared key,
|
||||
/// Otherwise return an error value
|
||||
pub fn check_encryption_started(
|
||||
key: Arc<Mutex<Option<SecretKey>>>,
|
||||
) -> Result<(), serde_json::Value> {
|
||||
match OwnerV3Helpers::encryption_enabled(key) {
|
||||
true => Ok(()),
|
||||
false => Err(EncryptionErrorResponse::new(
|
||||
1,
|
||||
-32001,
|
||||
"Encryption must be enabled. Please call 'init_secure_api` first",
|
||||
)
|
||||
.as_json_value()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the statically held owner API shared key
|
||||
pub fn update_owner_api_shared_key(
|
||||
key: Arc<Mutex<Option<SecretKey>>>,
|
||||
val: &serde_json::Value,
|
||||
new_key: Option<SecretKey>,
|
||||
) {
|
||||
if let Some(_) = val["result"]["Ok"].as_str() {
|
||||
let mut share_key_ref = key.lock();
|
||||
*share_key_ref = new_key;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the shared mask, in case of foreign API being run
|
||||
pub fn update_mask(mask: Arc<Mutex<Option<SecretKey>>>, val: &serde_json::Value) {
|
||||
if let Some(key) = val["result"]["Ok"].as_str() {
|
||||
let key_bytes = match from_hex(key) {
|
||||
Ok(k) => k,
|
||||
Err(_) => return,
|
||||
};
|
||||
let secp_inst = static_secp_instance();
|
||||
let secp = secp_inst.lock();
|
||||
let sk = match SecretKey::from_slice(&secp, &key_bytes) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut shared_mask_ref = mask.lock();
|
||||
*shared_mask_ref = Some(sk);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt an encrypted request
|
||||
pub fn decrypt_request(
|
||||
key: Arc<Mutex<Option<SecretKey>>>,
|
||||
req: &serde_json::Value,
|
||||
) -> Result<(JsonId, serde_json::Value), serde_json::Value> {
|
||||
let share_key_ref = key.lock();
|
||||
let shared_key = share_key_ref.as_ref().unwrap();
|
||||
let enc_req: EncryptedRequest = serde_json::from_value(req.clone()).map_err(|e| {
|
||||
EncryptionErrorResponse::new(
|
||||
1,
|
||||
-32002,
|
||||
&format!("Encrypted request format error: {}", e),
|
||||
)
|
||||
.as_json_value()
|
||||
})?;
|
||||
let id = enc_req.id.clone();
|
||||
let res = enc_req.decrypt(&shared_key).map_err(|e| {
|
||||
EncryptionErrorResponse::new(1, -32002, &format!("Decryption error: {}", e))
|
||||
.as_json_value()
|
||||
})?;
|
||||
Ok((id, res))
|
||||
}
|
||||
|
||||
/// Encrypt a response
|
||||
pub fn encrypt_response(
|
||||
key: Arc<Mutex<Option<SecretKey>>>,
|
||||
id: &JsonId,
|
||||
res: &serde_json::Value,
|
||||
) -> Result<serde_json::Value, serde_json::Value> {
|
||||
let share_key_ref = key.lock();
|
||||
let shared_key = share_key_ref.as_ref().unwrap();
|
||||
let enc_res = EncryptedResponse::from_json(id, res, &shared_key).map_err(|e| {
|
||||
EncryptionErrorResponse::new(1, -32003, &format!("EncryptionError: {}", e))
|
||||
.as_json_value()
|
||||
})?;
|
||||
let res = enc_res.as_json_value().map_err(|e| {
|
||||
EncryptionErrorResponse::new(
|
||||
1,
|
||||
-32002,
|
||||
&format!("Encrypted response format error: {}", e),
|
||||
)
|
||||
.as_json_value()
|
||||
})?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// convert an internal error (if exists) as proper JSON-RPC
|
||||
pub fn check_error_response(val: &serde_json::Value) -> (bool, serde_json::Value) {
|
||||
// check for string first. This ensures that error messages
|
||||
// that are just strings aren't given weird formatting
|
||||
let err_string = if val["result"]["Err"].is_object() {
|
||||
let mut retval;
|
||||
let hashed: Result<HashMap<String, String>, serde_json::Error> =
|
||||
serde_json::from_value(val["result"]["Err"].clone());
|
||||
retval = match hashed {
|
||||
Err(e) => {
|
||||
debug!("Can't cast value to Hashmap<String> {}", e);
|
||||
None
|
||||
}
|
||||
Ok(h) => {
|
||||
let mut r = "".to_owned();
|
||||
for (k, v) in h.iter() {
|
||||
r = format!("{}: {}", k, v);
|
||||
}
|
||||
Some(r)
|
||||
}
|
||||
};
|
||||
// Otherwise, see if error message is a map that needs
|
||||
// to be stringified (and accept weird formatting)
|
||||
if retval.is_none() {
|
||||
let hashed: Result<HashMap<String, serde_json::Value>, serde_json::Error> =
|
||||
serde_json::from_value(val["result"]["Err"].clone());
|
||||
retval = match hashed {
|
||||
Err(e) => {
|
||||
debug!("Can't cast value to Hashmap<Value> {}", e);
|
||||
None
|
||||
}
|
||||
Ok(h) => {
|
||||
let mut r = "".to_owned();
|
||||
for (k, v) in h.iter() {
|
||||
r = format!("{}: {}", k, v);
|
||||
}
|
||||
Some(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
retval
|
||||
} else if val["result"]["Err"].is_string() {
|
||||
let parsed = serde_json::from_value::<String>(val["result"]["Err"].clone());
|
||||
match parsed {
|
||||
Ok(p) => Some(p),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match err_string {
|
||||
Some(s) => {
|
||||
return (
|
||||
true,
|
||||
serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": val["id"],
|
||||
"error": {
|
||||
"message": s,
|
||||
"code": -32099
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
None => (false, val.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, C, K> OwnerAPIHandlerV3<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K>,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// Create a new owner API handler for GET methods
|
||||
pub fn new(
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
tor_config: Option<TorConfig>,
|
||||
running_foreign: bool,
|
||||
) -> OwnerAPIHandlerV3<L, C, K> {
|
||||
let owner_api = Owner::new(wallet.clone(), None);
|
||||
owner_api.set_tor_config(tor_config);
|
||||
let owner_api = Arc::new(owner_api);
|
||||
OwnerAPIHandlerV3 {
|
||||
wallet,
|
||||
owner_api,
|
||||
shared_key: Arc::new(Mutex::new(None)),
|
||||
keychain_mask: keychain_mask,
|
||||
running_foreign,
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_api(
|
||||
req: Request<Body>,
|
||||
key: Arc<Mutex<Option<SecretKey>>>,
|
||||
mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
running_foreign: bool,
|
||||
api: Arc<Owner<L, C, K>>,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
let mut val: serde_json::Value = parse_body(req).await?;
|
||||
let mut is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val);
|
||||
let mut was_encrypted = false;
|
||||
let mut encrypted_req_id = JsonId::StrId(String::from(""));
|
||||
if !is_init_secure_api {
|
||||
if let Err(v) = OwnerV3Helpers::check_encryption_started(key.clone()) {
|
||||
return Ok(v);
|
||||
}
|
||||
let res = OwnerV3Helpers::decrypt_request(key.clone(), &val);
|
||||
match res {
|
||||
Err(e) => return Ok(e),
|
||||
Ok(v) => {
|
||||
encrypted_req_id = v.0.clone();
|
||||
val = v.1;
|
||||
}
|
||||
}
|
||||
was_encrypted = true;
|
||||
}
|
||||
// check again, in case it was an encrypted call to init_secure_api
|
||||
is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val);
|
||||
// also need to intercept open/close wallet requests
|
||||
let is_open_wallet = OwnerV3Helpers::is_open_wallet(&val);
|
||||
match <dyn OwnerRpc>::handle_request(&*api, val) {
|
||||
MaybeReply::Reply(mut r) => {
|
||||
let (_was_error, unencrypted_intercept) =
|
||||
OwnerV3Helpers::check_error_response(&r.clone());
|
||||
if is_open_wallet && running_foreign {
|
||||
OwnerV3Helpers::update_mask(mask, &r.clone());
|
||||
}
|
||||
if was_encrypted {
|
||||
let res = OwnerV3Helpers::encrypt_response(
|
||||
key.clone(),
|
||||
&encrypted_req_id,
|
||||
&unencrypted_intercept,
|
||||
);
|
||||
r = match res {
|
||||
Ok(v) => v,
|
||||
Err(v) => return Ok(v),
|
||||
}
|
||||
}
|
||||
// intercept init_secure_api response (after encryption,
|
||||
// in case it was an encrypted call to 'init_api_secure')
|
||||
if is_init_secure_api {
|
||||
OwnerV3Helpers::update_owner_api_shared_key(
|
||||
key.clone(),
|
||||
&unencrypted_intercept,
|
||||
api.shared_key.lock().clone(),
|
||||
);
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
MaybeReply::DontReply => {
|
||||
// Since it's http, we need to return something. We return [] because jsonrpc
|
||||
// clients will parse it as an empty batch response.
|
||||
Ok(serde_json::json!([]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_post_request(
|
||||
req: Request<Body>,
|
||||
key: Arc<Mutex<Option<SecretKey>>>,
|
||||
mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
running_foreign: bool,
|
||||
api: Arc<Owner<L, C, K>>,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
let res = Self::call_api(req, key, mask, running_foreign, api).await?;
|
||||
Ok(json_response_pretty(&res))
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, C, K> api::Handler for OwnerAPIHandlerV3<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
fn post(&self, req: Request<Body>) -> ResponseFuture {
|
||||
let key = self.shared_key.clone();
|
||||
let mask = self.keychain_mask.clone();
|
||||
let running_foreign = self.running_foreign;
|
||||
let api = self.owner_api.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
match Self::handle_post_request(req, key, mask, running_foreign, api).await {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) => {
|
||||
error!("Request Error: {:?}", e);
|
||||
Ok(create_error_response(e))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn options(&self, _req: Request<Body>) -> ResponseFuture {
|
||||
Box::pin(async { Ok(create_ok_response("{}")) })
|
||||
}
|
||||
}
|
||||
/// V2 API Handler/Wrapper for foreign functions
|
||||
pub struct ForeignAPIHandlerV2<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// Wallet instance
|
||||
pub wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
/// Keychain mask
|
||||
pub keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
/// run in doctest mode
|
||||
pub test_mode: bool,
|
||||
/// tor config
|
||||
pub tor_config: Mutex<Option<TorConfig>>,
|
||||
}
|
||||
|
||||
impl<L, C, K> ForeignAPIHandlerV2<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// Create a new foreign API handler for GET methods
|
||||
pub fn new(
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
test_mode: bool,
|
||||
tor_config: Mutex<Option<TorConfig>>,
|
||||
) -> ForeignAPIHandlerV2<L, C, K> {
|
||||
ForeignAPIHandlerV2 {
|
||||
wallet,
|
||||
keychain_mask,
|
||||
test_mode,
|
||||
tor_config,
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_api(
|
||||
req: Request<Body>,
|
||||
api: Foreign<'static, L, C, K>,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
let val: serde_json::Value = parse_body(req).await?;
|
||||
match <dyn ForeignRpc>::handle_request(&api, val) {
|
||||
MaybeReply::Reply(r) => Ok(r),
|
||||
MaybeReply::DontReply => {
|
||||
// Since it's http, we need to return something. We return [] because jsonrpc
|
||||
// clients will parse it as an empty batch response.
|
||||
Ok(serde_json::json!([]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_post_request(
|
||||
req: Request<Body>,
|
||||
mask: Option<SecretKey>,
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
test_mode: bool,
|
||||
tor_config: Option<TorConfig>,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
let api = Foreign::new(wallet, mask, Some(check_middleware), test_mode);
|
||||
api.set_tor_config(tor_config);
|
||||
let res = Self::call_api(req, api).await?;
|
||||
Ok(json_response_pretty(&res))
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, C, K> api::Handler for ForeignAPIHandlerV2<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
fn post(&self, req: Request<Body>) -> ResponseFuture {
|
||||
let mask = self.keychain_mask.lock().clone();
|
||||
let wallet = self.wallet.clone();
|
||||
let test_mode = self.test_mode;
|
||||
let tor_config = self.tor_config.lock().clone();
|
||||
|
||||
Box::pin(async move {
|
||||
match Self::handle_post_request(req, mask, wallet, test_mode, tor_config).await {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => {
|
||||
error!("Request Error: {:?}", e);
|
||||
Ok(create_error_response(e))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn options(&self, _req: Request<Body>) -> ResponseFuture {
|
||||
Box::pin(async { Ok(create_ok_response("{}")) })
|
||||
}
|
||||
}
|
||||
|
||||
// Utility to serialize a struct into JSON and produce a sensible Response
|
||||
// out of it.
|
||||
fn _json_response<T>(s: &T) -> Response<Body>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
match serde_json::to_string(s) {
|
||||
Ok(json) => response(StatusCode::OK, json),
|
||||
Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""),
|
||||
}
|
||||
}
|
||||
|
||||
// pretty-printed version of above
|
||||
fn json_response_pretty<T>(s: &T) -> Response<Body>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
match serde_json::to_string_pretty(s) {
|
||||
Ok(json) => response(StatusCode::OK, json),
|
||||
Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_error_response(e: Error) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header("access-control-allow-origin", "*")
|
||||
.header(
|
||||
"access-control-allow-headers",
|
||||
"Content-Type, Authorization",
|
||||
)
|
||||
.body(format!("{}", e).into())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn create_ok_response(json: &str) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("access-control-allow-origin", "*")
|
||||
.header(
|
||||
"access-control-allow-headers",
|
||||
"Content-Type, Authorization",
|
||||
)
|
||||
.header(hyper::header::CONTENT_TYPE, "application/json")
|
||||
.body(json.to_string().into())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Build a new hyper Response with the status code and body provided.
|
||||
///
|
||||
/// Whenever the status code is `StatusCode::OK` the text parameter should be
|
||||
/// valid JSON as the content type header will be set to `application/json'
|
||||
fn response<T: Into<Body>>(status: StatusCode, text: T) -> Response<Body> {
|
||||
let mut builder = Response::builder()
|
||||
.status(status)
|
||||
.header("access-control-allow-origin", "*")
|
||||
.header(
|
||||
"access-control-allow-headers",
|
||||
"Content-Type, Authorization",
|
||||
);
|
||||
|
||||
if status == StatusCode::OK {
|
||||
builder = builder.header(hyper::header::CONTENT_TYPE, "application/json");
|
||||
}
|
||||
|
||||
builder.body(text.into()).unwrap()
|
||||
}
|
||||
|
||||
async fn parse_body<T>(req: Request<Body>) -> Result<T, Error>
|
||||
where
|
||||
for<'de> T: Deserialize<'de> + Send + 'static,
|
||||
{
|
||||
let body = body::to_bytes(req.into_body())
|
||||
.await
|
||||
.map_err(|_| Error::GenericError("Failed to read request".to_string()))?;
|
||||
|
||||
serde_json::from_reader(&body[..])
|
||||
.map_err(|e| Error::GenericError(format!("Invalid request body: {}", e)))
|
||||
}
|
||||
@@ -1,630 +0,0 @@
|
||||
// Copyright 2021 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::core::core::FeeFields;
|
||||
use crate::core::core::{self, amount_to_hr_string};
|
||||
use crate::core::global;
|
||||
use crate::libwallet::{
|
||||
AcctPathMapping, Error, OutputCommitMapping, OutputStatus, TxLogEntry, ViewWallet, WalletInfo,
|
||||
};
|
||||
use crate::util::ToHex;
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use prettytable;
|
||||
use std::io::prelude::Write;
|
||||
use term;
|
||||
|
||||
/// Display outputs in a pretty way
|
||||
pub fn outputs(
|
||||
account: &str,
|
||||
cur_height: u64,
|
||||
validated: bool,
|
||||
outputs: Vec<OutputCommitMapping>,
|
||||
dark_background_color_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
let title = format!(
|
||||
"Wallet Outputs - Account '{}' - Block Height: {}",
|
||||
account, cur_height
|
||||
);
|
||||
println!();
|
||||
if term::stdout().is_none() {
|
||||
println!("Could not open terminal");
|
||||
return Ok(());
|
||||
}
|
||||
let mut t = term::stdout().unwrap();
|
||||
t.fg(term::color::MAGENTA).unwrap();
|
||||
writeln!(t, "{}", title).unwrap();
|
||||
t.reset().unwrap();
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
bMG->"Output Commitment",
|
||||
bMG->"MMR Index",
|
||||
bMG->"Block Height",
|
||||
bMG->"Locked Until",
|
||||
bMG->"Status",
|
||||
bMG->"Coinbase?",
|
||||
bMG->"# Confirms",
|
||||
bMG->"Value",
|
||||
bMG->"Tx"
|
||||
]);
|
||||
|
||||
for m in outputs {
|
||||
let commit = format!("{}", m.commit.as_ref().to_hex());
|
||||
let index = match m.output.mmr_index {
|
||||
None => "None".to_owned(),
|
||||
Some(t) => t.to_string(),
|
||||
};
|
||||
let height = format!("{}", m.output.height);
|
||||
let lock_height = format!("{}", m.output.lock_height);
|
||||
let is_coinbase = format!("{}", m.output.is_coinbase);
|
||||
|
||||
// Mark unconfirmed coinbase outputs as "Mining" instead of "Unconfirmed"
|
||||
let status = match m.output.status {
|
||||
OutputStatus::Unconfirmed if m.output.is_coinbase => "Mining".to_string(),
|
||||
_ => format!("{}", m.output.status),
|
||||
};
|
||||
|
||||
let num_confirmations = format!("{}", m.output.num_confirmations(cur_height));
|
||||
let value = format!("{}", core::amount_to_hr_string(m.output.value, false));
|
||||
let tx = match m.output.tx_log_entry {
|
||||
None => "".to_owned(),
|
||||
Some(t) => t.to_string(),
|
||||
};
|
||||
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFC->commit,
|
||||
bFB->index,
|
||||
bFB->height,
|
||||
bFB->lock_height,
|
||||
bFR->status,
|
||||
bFY->is_coinbase,
|
||||
bFB->num_confirmations,
|
||||
bFG->value,
|
||||
bFC->tx,
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFD->commit,
|
||||
bFB->index,
|
||||
bFB->height,
|
||||
bFB->lock_height,
|
||||
bFR->status,
|
||||
bFD->is_coinbase,
|
||||
bFB->num_confirmations,
|
||||
bFG->value,
|
||||
bFD->tx,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
|
||||
table.printstd();
|
||||
println!();
|
||||
|
||||
if !validated {
|
||||
println!(
|
||||
"\nWARNING: Wallet failed to verify data. \
|
||||
The above is from local cache and possibly invalid! \
|
||||
(is your `grin server` offline or broken?)"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display transaction log in a pretty way
|
||||
pub fn txs(
|
||||
account: &str,
|
||||
cur_height: u64,
|
||||
validated: bool,
|
||||
txs: &[TxLogEntry],
|
||||
include_status: bool,
|
||||
dark_background_color_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
let title = format!(
|
||||
"Transaction Log - Account '{}' - Block Height: {}",
|
||||
account, cur_height
|
||||
);
|
||||
println!();
|
||||
if term::stdout().is_none() {
|
||||
println!("Could not open terminal");
|
||||
return Ok(());
|
||||
}
|
||||
let mut t = term::stdout().unwrap();
|
||||
t.fg(term::color::MAGENTA).unwrap();
|
||||
writeln!(t, "{}", title).unwrap();
|
||||
t.reset().unwrap();
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
bMG->"Id",
|
||||
bMG->"Type",
|
||||
bMG->"Shared Transaction Id",
|
||||
bMG->"Creation Time",
|
||||
bMG->"TTL Cutoff Height",
|
||||
bMG->"Confirmed?",
|
||||
bMG->"Confirmation Time",
|
||||
bMG->"Num. \nInputs",
|
||||
bMG->"Num. \nOutputs",
|
||||
bMG->"Amount \nCredited",
|
||||
bMG->"Amount \nDebited",
|
||||
bMG->"Fee",
|
||||
bMG->"Net \nDifference",
|
||||
bMG->"Payment \nProof",
|
||||
bMG->"Kernel",
|
||||
bMG->"Tx \nData",
|
||||
]);
|
||||
|
||||
for t in txs {
|
||||
let id = format!("{}", t.id);
|
||||
let slate_id = match t.tx_slate_id {
|
||||
Some(m) => format!("{}", m),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let entry_type = format!("{}", t.tx_type);
|
||||
let creation_ts = format!("{}", t.creation_ts.format("%Y-%m-%d %H:%M:%S"));
|
||||
let ttl_cutoff_height = match t.ttl_cutoff_height {
|
||||
Some(b) => format!("{}", b),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let confirmation_ts = match t.confirmation_ts {
|
||||
Some(m) => format!("{}", m.format("%Y-%m-%d %H:%M:%S")),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let confirmed = format!("{}", t.confirmed);
|
||||
let num_inputs = format!("{}", t.num_inputs);
|
||||
let num_outputs = format!("{}", t.num_outputs);
|
||||
let amount_debited_str = core::amount_to_hr_string(t.amount_debited, true);
|
||||
let amount_credited_str = core::amount_to_hr_string(t.amount_credited, true);
|
||||
let fee = match t.fee {
|
||||
Some(f) => format!("{}", core::amount_to_hr_string(f.fee(), true)),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let net_diff = if t.amount_credited >= t.amount_debited {
|
||||
core::amount_to_hr_string(t.amount_credited - t.amount_debited, true)
|
||||
} else {
|
||||
format!(
|
||||
"-{}",
|
||||
core::amount_to_hr_string(t.amount_debited - t.amount_credited, true)
|
||||
)
|
||||
};
|
||||
let tx_data = match t.stored_tx {
|
||||
Some(_) => "Yes".to_owned(),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let kernel_excess = match t.kernel_excess {
|
||||
Some(e) => {
|
||||
let excess: &[u8] = e.0.as_ref();
|
||||
excess.to_hex()
|
||||
}
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let payment_proof = match t.payment_proof {
|
||||
Some(_) => "Yes".to_owned(),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFC->id,
|
||||
bFC->entry_type,
|
||||
bFC->slate_id,
|
||||
bFB->creation_ts,
|
||||
bFB->ttl_cutoff_height,
|
||||
bFC->confirmed,
|
||||
bFB->confirmation_ts,
|
||||
bFC->num_inputs,
|
||||
bFC->num_outputs,
|
||||
bFG->amount_credited_str,
|
||||
bFR->amount_debited_str,
|
||||
bFR->fee,
|
||||
bFY->net_diff,
|
||||
bfG->payment_proof,
|
||||
bFB->kernel_excess,
|
||||
bFb->tx_data,
|
||||
]);
|
||||
} else {
|
||||
if t.confirmed {
|
||||
table.add_row(row![
|
||||
bFD->id,
|
||||
bFb->entry_type,
|
||||
bFD->slate_id,
|
||||
bFB->creation_ts,
|
||||
bFg->confirmed,
|
||||
bFB->confirmation_ts,
|
||||
bFD->num_inputs,
|
||||
bFD->num_outputs,
|
||||
bFG->amount_credited_str,
|
||||
bFD->amount_debited_str,
|
||||
bFD->fee,
|
||||
bFG->net_diff,
|
||||
bfG->payment_proof,
|
||||
bFB->kernel_excess,
|
||||
bFB->tx_data,
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFD->id,
|
||||
bFb->entry_type,
|
||||
bFD->slate_id,
|
||||
bFB->creation_ts,
|
||||
bFR->confirmed,
|
||||
bFB->confirmation_ts,
|
||||
bFD->num_inputs,
|
||||
bFD->num_outputs,
|
||||
bFG->amount_credited_str,
|
||||
bFD->amount_debited_str,
|
||||
bFD->fee,
|
||||
bFG->net_diff,
|
||||
bfG->payment_proof,
|
||||
bFB->kernel_excess,
|
||||
bFB->tx_data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
|
||||
table.printstd();
|
||||
println!();
|
||||
|
||||
if !validated && include_status {
|
||||
println!(
|
||||
"\nWARNING: Wallet failed to verify data. \
|
||||
The above is from local cache and possibly invalid! \
|
||||
(is your `grin server` offline or broken?)"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn view_wallet_balance(w: ViewWallet, cur_height: u64, dark_background_color_scheme: bool) {
|
||||
println!(
|
||||
"\n____ View Wallet Summary Info - Block Height: {} ____\n Rewind Hash - {}\n",
|
||||
cur_height, w.rewind_hash
|
||||
);
|
||||
let mut table = table!();
|
||||
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFG->"Total Balance",
|
||||
FG->amount_to_hr_string(w.total_balance, false)
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFG->"Total Balance",
|
||||
FG->amount_to_hr_string(w.total_balance, false)
|
||||
]);
|
||||
};
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
table.printstd();
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn view_wallet_output(
|
||||
view_wallet: ViewWallet,
|
||||
cur_height: u64,
|
||||
dark_background_color_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
println!();
|
||||
let title = format!("View Wallet Outputs - Block Height: {}", cur_height);
|
||||
|
||||
if term::stdout().is_none() {
|
||||
println!("Could not open terminal");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut t = term::stdout().unwrap();
|
||||
t.fg(term::color::MAGENTA).unwrap();
|
||||
writeln!(t, "{}", title).unwrap();
|
||||
t.reset().unwrap();
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
bMG->"Output Commitment",
|
||||
bMG->"MMR Index",
|
||||
bMG->"Block Height",
|
||||
bMG->"Locked Until",
|
||||
bMG->"Coinbase?",
|
||||
bMG->"# Confirms",
|
||||
bMG->"Value",
|
||||
]);
|
||||
|
||||
for m in view_wallet.output_result {
|
||||
let commit = format!("{}", m.commit);
|
||||
let index = m.mmr_index;
|
||||
let height = format!("{}", m.height);
|
||||
let lock_height = format!("{}", m.lock_height);
|
||||
let is_coinbase = format!("{}", m.is_coinbase);
|
||||
let num_confirmations = format!("{}", m.num_confirmations(cur_height));
|
||||
let value = format!("{}", core::amount_to_hr_string(m.value, false));
|
||||
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFC->commit,
|
||||
bFB->index,
|
||||
bFB->height,
|
||||
bFB->lock_height,
|
||||
bFY->is_coinbase,
|
||||
bFB->num_confirmations,
|
||||
bFG->value,
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFD->commit,
|
||||
bFB->index,
|
||||
bFB->height,
|
||||
bFB->lock_height,
|
||||
bFD->is_coinbase,
|
||||
bFB->num_confirmations,
|
||||
bFG->value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
|
||||
table.printstd();
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display summary info in a pretty way
|
||||
pub fn info(
|
||||
account: &str,
|
||||
wallet_info: &WalletInfo,
|
||||
validated: bool,
|
||||
dark_background_color_scheme: bool,
|
||||
) {
|
||||
println!(
|
||||
"\n____ Wallet Summary Info - Account '{}' as of height {} ____\n",
|
||||
account, wallet_info.last_confirmed_height,
|
||||
);
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFG->"Confirmed Total",
|
||||
FG->amount_to_hr_string(wallet_info.total, false)
|
||||
]);
|
||||
if wallet_info.amount_reverted > 0 {
|
||||
table.add_row(row![
|
||||
Fr->format!("Reverted"),
|
||||
Fr->amount_to_hr_string(wallet_info.amount_reverted, false)
|
||||
]);
|
||||
}
|
||||
// Only dispay "Immature Coinbase" if we have related outputs in the wallet.
|
||||
// This row just introduces confusion if the wallet does not receive coinbase rewards.
|
||||
if wallet_info.amount_immature > 0 {
|
||||
table.add_row(row![
|
||||
bFY->format!("Immature Coinbase (< {})", global::coinbase_maturity()),
|
||||
FY->amount_to_hr_string(wallet_info.amount_immature, false)
|
||||
]);
|
||||
}
|
||||
table.add_row(row![
|
||||
bFY->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations),
|
||||
FY->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false)
|
||||
]);
|
||||
table.add_row(row![
|
||||
bFB->format!("Awaiting Finalization"),
|
||||
FB->amount_to_hr_string(wallet_info.amount_awaiting_finalization, false)
|
||||
]);
|
||||
table.add_row(row![
|
||||
Fr->"Locked by previous transaction",
|
||||
Fr->amount_to_hr_string(wallet_info.amount_locked, false)
|
||||
]);
|
||||
table.add_row(row![
|
||||
Fw->"--------------------------------",
|
||||
Fw->"-------------"
|
||||
]);
|
||||
table.add_row(row![
|
||||
bFG->"Currently Spendable",
|
||||
FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false)
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFG->"Total",
|
||||
FG->amount_to_hr_string(wallet_info.total, false)
|
||||
]);
|
||||
if wallet_info.amount_reverted > 0 {
|
||||
table.add_row(row![
|
||||
Fr->format!("Reverted"),
|
||||
Fr->amount_to_hr_string(wallet_info.amount_reverted, false)
|
||||
]);
|
||||
}
|
||||
// Only dispay "Immature Coinbase" if we have related outputs in the wallet.
|
||||
// This row just introduces confusion if the wallet does not receive coinbase rewards.
|
||||
if wallet_info.amount_immature > 0 {
|
||||
table.add_row(row![
|
||||
bFB->format!("Immature Coinbase (< {})", global::coinbase_maturity()),
|
||||
FB->amount_to_hr_string(wallet_info.amount_immature, false)
|
||||
]);
|
||||
}
|
||||
table.add_row(row![
|
||||
bFB->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations),
|
||||
FB->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false)
|
||||
]);
|
||||
table.add_row(row![
|
||||
Fr->"Locked by previous transaction",
|
||||
Fr->amount_to_hr_string(wallet_info.amount_locked, false)
|
||||
]);
|
||||
table.add_row(row![
|
||||
Fw->"--------------------------------",
|
||||
Fw->"-------------"
|
||||
]);
|
||||
table.add_row(row![
|
||||
bFG->"Currently Spendable",
|
||||
FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false)
|
||||
]);
|
||||
};
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
table.printstd();
|
||||
println!();
|
||||
if !validated {
|
||||
println!(
|
||||
"\nWARNING: Wallet failed to verify data against a live chain. \
|
||||
The above is from local cache and only valid up to the given height! \
|
||||
(is your `grin server` offline or broken?)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Display summary info in a pretty way
|
||||
pub fn estimate(
|
||||
amount: u64,
|
||||
strategies: Vec<(
|
||||
&str, // strategy
|
||||
u64, // total amount to be locked
|
||||
FeeFields, // fee
|
||||
)>,
|
||||
dark_background_color_scheme: bool,
|
||||
) {
|
||||
println!(
|
||||
"\nEstimation for sending {}:\n",
|
||||
amount_to_hr_string(amount, false)
|
||||
);
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
bMG->"Selection strategy",
|
||||
bMG->"Fee",
|
||||
bMG->"Will be locked",
|
||||
]);
|
||||
|
||||
for (strategy, total, fee_fields) in strategies {
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFC->strategy,
|
||||
FR->amount_to_hr_string(fee_fields.fee(), false), // apply fee mask past HF4
|
||||
FY->amount_to_hr_string(total, false),
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFD->strategy,
|
||||
FR->amount_to_hr_string(fee_fields.fee(), false), // apply fee mask past HF4
|
||||
FY->amount_to_hr_string(total, false),
|
||||
]);
|
||||
}
|
||||
}
|
||||
table.printstd();
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Display list of wallet accounts in a pretty way
|
||||
pub fn accounts(acct_mappings: Vec<AcctPathMapping>) {
|
||||
println!("\n____ Wallet Accounts ____\n",);
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
mMG->"Name",
|
||||
bMG->"Parent BIP-32 Derivation Path",
|
||||
]);
|
||||
for m in acct_mappings {
|
||||
table.add_row(row![
|
||||
bFC->m.label,
|
||||
bGC->m.path.to_bip_32_string(),
|
||||
]);
|
||||
}
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
table.printstd();
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Display individual Payment Proof
|
||||
pub fn payment_proof(tx: &TxLogEntry) -> Result<(), Error> {
|
||||
let title = format!("Payment Proof - Transaction '{}'", tx.id,);
|
||||
println!();
|
||||
if term::stdout().is_none() {
|
||||
println!("Could not open terminal");
|
||||
return Ok(());
|
||||
}
|
||||
let mut t = term::stdout().unwrap();
|
||||
t.fg(term::color::MAGENTA).unwrap();
|
||||
writeln!(t, "{}", title).unwrap();
|
||||
t.reset().unwrap();
|
||||
|
||||
let pp = match &tx.payment_proof {
|
||||
None => {
|
||||
writeln!(t, "None").unwrap();
|
||||
t.reset().unwrap();
|
||||
return Ok(());
|
||||
}
|
||||
Some(p) => p.clone(),
|
||||
};
|
||||
|
||||
t.fg(term::color::WHITE).unwrap();
|
||||
writeln!(t).unwrap();
|
||||
let receiver_signature = match pp.receiver_signature {
|
||||
Some(s) => {
|
||||
let sig_bytes = s.to_bytes();
|
||||
let sig_ref: &[u8] = sig_bytes.as_ref();
|
||||
sig_ref.to_hex()
|
||||
}
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let fee = match tx.fee {
|
||||
Some(f) => f.fee(), // apply fee mask past HF4
|
||||
None => 0,
|
||||
};
|
||||
let amount = if tx.amount_credited >= tx.amount_debited {
|
||||
core::amount_to_hr_string(tx.amount_credited - tx.amount_debited, true)
|
||||
} else {
|
||||
format!(
|
||||
"{}",
|
||||
core::amount_to_hr_string(tx.amount_debited - tx.amount_credited - fee, true)
|
||||
)
|
||||
};
|
||||
|
||||
let sender_signature = match pp.sender_signature {
|
||||
Some(s) => {
|
||||
let sig_bytes = s.to_bytes();
|
||||
let sig_ref: &[u8] = sig_bytes.as_ref();
|
||||
sig_ref.to_hex()
|
||||
}
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let kernel_excess = match tx.kernel_excess {
|
||||
Some(e) => {
|
||||
let excess: &[u8] = e.0.as_ref();
|
||||
excess.to_hex()
|
||||
}
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
|
||||
writeln!(
|
||||
t,
|
||||
"Receiver Address: {}",
|
||||
OnionV3Address::from_bytes(pp.receiver_address.to_bytes())
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(t, "Receiver Signature: {}", receiver_signature).unwrap();
|
||||
writeln!(t, "Amount: {}", amount).unwrap();
|
||||
writeln!(t, "Kernel Excess: {}", kernel_excess).unwrap();
|
||||
writeln!(
|
||||
t,
|
||||
"Sender Address: {}",
|
||||
OnionV3Address::from_bytes(pp.sender_address.to_bytes())
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(t, "Sender Signature: {}", sender_signature).unwrap();
|
||||
|
||||
t.reset().unwrap();
|
||||
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright 2021 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Implementation specific error types
|
||||
use crate::api;
|
||||
use crate::core::core::transaction;
|
||||
use crate::core::libtx;
|
||||
use crate::impls;
|
||||
use crate::keychain;
|
||||
use crate::libwallet;
|
||||
|
||||
/// Wallet errors, mostly wrappers around underlying crypto or I/O errors.
|
||||
#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// LibTX Error
|
||||
#[error("LibTx Error")]
|
||||
LibTX(#[from] libtx::Error),
|
||||
|
||||
/// Impls error
|
||||
#[error("Impls Error")]
|
||||
Impls(#[from] impls::Error),
|
||||
|
||||
/// LibWallet Error
|
||||
#[error("LibWallet Error: {0}")]
|
||||
LibWallet(#[from] libwallet::Error),
|
||||
|
||||
/// Keychain error
|
||||
#[error("Keychain error")]
|
||||
Keychain(#[from] keychain::Error),
|
||||
|
||||
/// Transaction Error
|
||||
#[error("Transaction error")]
|
||||
Transaction(#[from] transaction::Error),
|
||||
|
||||
/// Secp Error
|
||||
#[error("Secp error")]
|
||||
Secp,
|
||||
|
||||
/// Filewallet error
|
||||
#[error("Wallet data error: {0}")]
|
||||
FileWallet(&'static str),
|
||||
|
||||
/// Error when formatting json
|
||||
#[error("IO error")]
|
||||
IO,
|
||||
|
||||
/// Error when formatting json
|
||||
#[error("Serde JSON error")]
|
||||
Format,
|
||||
|
||||
/// Error when contacting a node through its API
|
||||
#[error("Node API error")]
|
||||
Node(#[from] api::Error),
|
||||
|
||||
/// Error originating from hyper.
|
||||
#[error("Hyper error")]
|
||||
Hyper,
|
||||
|
||||
/// Error originating from hyper uri parsing.
|
||||
#[error("Uri parsing error")]
|
||||
Uri,
|
||||
|
||||
/// Attempt to use duplicate transaction id in separate transactions
|
||||
#[error("Duplicate transaction ID error")]
|
||||
DuplicateTransactionId,
|
||||
|
||||
/// Wallet seed already exists
|
||||
#[error("Wallet seed file exists: {0}")]
|
||||
WalletSeedExists(String),
|
||||
|
||||
/// Wallet seed doesn't exist
|
||||
#[error("Wallet seed doesn't exist error")]
|
||||
WalletSeedDoesntExist,
|
||||
|
||||
/// Enc/Decryption Error
|
||||
#[error("Enc/Decryption error (check password?)")]
|
||||
Encryption,
|
||||
|
||||
/// BIP 39 word list
|
||||
#[error("BIP39 Mnemonic (word list) Error")]
|
||||
Mnemonic,
|
||||
|
||||
/// Command line argument error
|
||||
#[error("{0}")]
|
||||
ArgumentError(String),
|
||||
|
||||
/// Other
|
||||
#[error("Listener Startup Error")]
|
||||
ListenerError,
|
||||
|
||||
/// Other
|
||||
#[error("Generic error: {0}")]
|
||||
GenericError(String),
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2021 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Library module for the main wallet functionalities provided by Grin.
|
||||
|
||||
#[macro_use]
|
||||
extern crate prettytable;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
use grin_api as api;
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use grin_wallet_api as apiwallet;
|
||||
use grin_wallet_config as config;
|
||||
use grin_wallet_impls as impls;
|
||||
use grin_wallet_libwallet as libwallet;
|
||||
|
||||
pub mod command;
|
||||
pub mod controller;
|
||||
pub mod display;
|
||||
mod error;
|
||||
|
||||
pub use crate::error::Error;
|
||||
Reference in New Issue
Block a user