#![cfg_attr(not(test), deny(clippy::unwrap_used))]
use std::collections::{HashMap, HashSet};
use std::net::SocketAddr;
use std::num::NonZeroU8;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
use lwk_common::{
address_to_text_qr, address_to_uri_qr, keyorigin_xpub_from_str, multisig_desc, singlesig_desc,
InvalidBipVariant, InvalidBlindingKeyVariant, InvalidMultisigVariant, InvalidSinglesigVariant,
Signer,
};
use lwk_jade::derivation_path_to_vec;
use lwk_jade::get_receive_address::Variant;
use lwk_jade::register_multisig::{JadeDescriptor, RegisterMultisigParams};
use lwk_jade::Jade;
use lwk_signer::{AnySigner, SwSigner};
use lwk_tiny_jrpc::{tiny_http, JsonRpcServer, Request, Response};
use lwk_wollet::bitcoin::bip32::Fingerprint;
use lwk_wollet::bitcoin::XKeyIdentifier;
use lwk_wollet::elements::encode::serialize;
use lwk_wollet::elements::hex::{FromHex, ToHex};
use lwk_wollet::elements::pset::PartiallySignedTransaction;
use lwk_wollet::elements::{Address, AssetId, Txid};
use lwk_wollet::elements_miniscript::descriptor::{Descriptor, DescriptorType, WshInner};
use lwk_wollet::elements_miniscript::miniscript::decode::Terminal;
use lwk_wollet::elements_miniscript::{DescriptorPublicKey, ForEachKey};
use lwk_wollet::{full_scan_with_electrum_client, Wollet};
use lwk_wollet::{BlockchainBackend, WolletDescriptor};
use serde_json::Value;
use crate::explorer::{get_registry_data, get_tx};
use crate::method::Method;
use crate::state::{AppAsset, AppSigner, State};
use lwk_rpc_model::{request, response};
pub use client::Client;
pub use config::Config;
pub use error::Error;
pub use lwk_tiny_jrpc::RpcError;
mod client;
mod config;
pub mod consts;
mod error;
mod explorer;
pub mod method;
mod reqwest_transport;
mod state;
pub struct App {
rpc: Option<JsonRpcServer>,
config: Config,
is_scanning: Arc<AtomicBool>,
scanning_handle: Option<JoinHandle<()>>,
}
impl App {
pub fn new(config: Config) -> Result<App, Error> {
tracing::info!("Creating new app with config: {:?}", config);
Ok(App {
rpc: None,
config,
scanning_handle: None,
is_scanning: Arc::new(AtomicBool::new(false)),
})
}
fn apply_request(&self, client: &Client, line: &str) -> Result<(), Error> {
let r: Request = serde_json::from_str(line)?;
let method: Method = r.method.parse()?;
let _value: Value = client.make_request(method, r.params)?;
Ok(())
}
pub fn run(&mut self) -> Result<(), Error> {
if self.rpc.is_some() {
return Err(error::Error::AlreadyStarted);
}
let mut state = State {
config: self.config.clone(),
wollets: Default::default(),
signers: Default::default(),
assets: Default::default(),
tx_memos: Default::default(),
addr_memos: Default::default(),
do_persist: false,
scan_loops_started: 0,
scan_loops_completed: 0,
interrupt_wait: false,
};
state.insert_policy_asset();
let state = Arc::new(Mutex::new(state));
let server = tiny_http::Server::http(self.config.addr)
.map_err(|_| Error::ServerStart(self.config.addr.to_string()))?;
let config = lwk_tiny_jrpc::Config::builder()
.with_num_threads(NonZeroU8::new(1).expect("static"))
.build();
let rpc = lwk_tiny_jrpc::JsonRpcServer::new(server, config, state.clone(), method_handler);
let path = self.config.state_path()?;
match std::fs::read_to_string(&path) {
Ok(string) => {
tracing::info!(
"Loading previous state, {} elements",
string.lines().count()
);
let client = self.client()?;
for (n, line) in string.lines().enumerate() {
self.apply_request(&client, line).map_err(|err| {
Error::StartStateLoad(err.to_string(), n + 1, path.display().to_string())
})?
}
}
Err(_) => {
tracing::info!("There is no previous state at {path:?}");
}
}
state.lock().map_err(|e| e.to_string())?.do_persist = true;
self.rpc = Some(rpc);
self.is_scanning.store(true, Ordering::Relaxed);
let is_scanning = self.is_scanning.clone();
let state_scanning = state.clone();
let scanning_interval = self.config.scanning_interval;
let stop_interval = Duration::from_millis(100);
let mut interval = Duration::ZERO; let scanning_handle = std::thread::spawn(move || 'scan: loop {
'stop: loop {
if !is_scanning.load(Ordering::Relaxed) {
break 'scan;
}
if interval == Duration::ZERO
|| state_scanning
.lock()
.map(|s| s.interrupt_wait)
.unwrap_or(false)
{
interval = scanning_interval; break 'stop;
}
std::thread::sleep(stop_interval);
interval = interval.saturating_sub(stop_interval);
}
if let Ok(mut s) = state_scanning.lock() {
s.interrupt_wait = false;
s.scan_loops_started += 1;
if let Ok(mut electrum_client) = s.config.electrum_client() {
for (_name, wollet) in s.wollets.iter_mut() {
let _ = full_scan_with_electrum_client(wollet, &mut electrum_client);
}
}
s.scan_loops_completed += 1;
}
});
self.scanning_handle = Some(scanning_handle);
Ok(())
}
pub fn stop(&self) -> Result<(), Error> {
self.is_scanning.store(false, Ordering::Relaxed);
match self.rpc.as_ref() {
Some(rpc) => {
rpc.stop();
Ok(())
}
None => Err(error::Error::NotStarted),
}
}
pub fn is_running(&self) -> Result<bool, Error> {
match self.rpc.as_ref() {
Some(rpc) => Ok(rpc.is_running()),
None => Err(error::Error::NotStarted),
}
}
pub fn addr(&self) -> SocketAddr {
self.config.addr
}
pub fn join_threads(&mut self) -> Result<(), Error> {
self.rpc
.take()
.ok_or(error::Error::NotStarted)?
.join_threads();
if let Some(scanning_handle) = self.scanning_handle.take() {
let _ = scanning_handle.join();
}
Ok(())
}
fn client(&self) -> Result<Client, Error> {
Client::new(self.config.addr)
}
}
fn method_handler(
request: Request,
state: Arc<Mutex<State>>,
) -> Result<Response, lwk_tiny_jrpc::Error> {
Ok(inner_method_handler(request, state)?)
}
fn inner_method_handler(request: Request, state: Arc<Mutex<State>>) -> Result<Response, Error> {
tracing::debug!(
"method: {} params: {:?} ",
request.method.as_str(),
request.params
);
let method: Method = match request.method.as_str().parse() {
Ok(method) => method,
Err(e) => return Ok(Response::unimplemented(request.id, e.to_string())),
};
let params = request.params.clone().unwrap_or_default();
let response = match method {
Method::Schema => {
let r: request::Schema = serde_json::from_value(params)?;
let method: Method = r.method.parse()?;
Response::result(request.id, method.schema(r.direction)?)
}
Method::SignerGenerate => {
let (_signer, mnemonic) = SwSigner::random(state.lock()?.config.is_mainnet())?;
Response::result(
request.id,
serde_json::to_value(response::SignerGenerate {
mnemonic: mnemonic.to_string(),
})?,
)
}
Method::Version => {
let network = state.lock()?.config.network.as_str().to_string();
Response::result(
request.id,
serde_json::to_value(response::Version {
version: consts::APP_VERSION.into(),
network,
})?,
)
}
Method::WalletLoad => {
let r: request::WalletLoad = serde_json::from_value(params)?;
let mut s = state.lock()?;
let desc: WolletDescriptor = r.descriptor.parse()?;
let wollet = Wollet::with_fs_persist(s.config.network, desc, &s.config.datadir)?;
s.wollets.insert(&r.name, wollet)?;
s.persist(&request)?;
Response::result(
request.id,
serde_json::to_value(response::Wallet {
descriptor: r.descriptor,
name: r.name,
})?,
)
}
Method::WalletUnload => {
let r: request::WalletUnload = serde_json::from_value(params)?;
let mut s = state.lock()?;
let removed = s.wollets.remove(&r.name)?;
s.tx_memos.remove(&r.name);
s.addr_memos.remove(&r.name);
s.persist_all()?;
Response::result(
request.id,
serde_json::to_value(response::WalletUnload {
unloaded: response::Wallet {
name: r.name,
descriptor: removed.descriptor().to_string(),
},
})?,
)
}
Method::WalletList => {
let s = state.lock()?;
let wallets = s
.wollets
.iter()
.map(|(name, wollet)| response::Wallet {
descriptor: wollet.descriptor().to_string(),
name: name.clone(),
})
.collect();
let r = response::WalletList { wallets };
Response::result(request.id, serde_json::to_value(r)?)
}
Method::SignerLoadSoftware => {
let r: request::SignerLoadSoftware = serde_json::from_value(params)?;
let mut s = state.lock()?;
let signer = AppSigner::new_sw(&r.mnemonic, s.config.is_mainnet(), r.persist)?;
let resp: response::Signer = signer_response_from(&r.name, &signer)?;
s.signers.insert(&r.name, signer)?;
if r.persist {
s.persist(&request)?;
}
Response::result(request.id, serde_json::to_value(resp)?)
}
Method::SignerLoadJade => {
let r: request::SignerLoadJade = serde_json::from_value(params)?;
let mut s = state.lock()?;
let id = XKeyIdentifier::from_str(&r.id).map_err(|e| e.to_string())?; let signer = AppSigner::new_jade(id, r.emulator, s.config.jade_network())?;
let resp: response::Signer = signer_response_from(&r.name, &signer)?;
s.signers.insert(&r.name, signer)?;
s.persist(&request)?;
Response::result(request.id, serde_json::to_value(resp)?)
}
Method::SignerLoadExternal => {
let r: request::SignerLoadExternal = serde_json::from_value(params)?;
let mut s = state.lock()?;
let fingerprint =
Fingerprint::from_str(&r.fingerprint).map_err(|e| Error::Generic(e.to_string()))?;
let signer = AppSigner::new_external(fingerprint);
let resp: response::Signer = signer_response_from(&r.name, &signer)?;
s.signers.insert(&r.name, signer)?;
s.persist(&request)?;
Response::result(request.id, serde_json::to_value(resp)?)
}
Method::SignerUnload => {
let r: request::SignerUnload = serde_json::from_value(params)?;
let mut s = state.lock()?;
let removed = s.signers.remove(&r.name)?;
let signer: response::Signer = signer_response_from(&r.name, &removed)?;
s.persist_all()?;
Response::result(
request.id,
serde_json::to_value(response::SignerUnload { unloaded: signer })?,
)
}
Method::SignerDetails => {
let r: request::SignerDetails = serde_json::from_value(params)?;
let s = state.lock()?;
let signer = s.signers.get(&r.name)?;
let details = signer_details(&r.name, signer)?;
Response::result(request.id, serde_json::to_value(details)?)
}
Method::SignerList => {
let s = state.lock()?;
let signers: Result<Vec<_>, _> = s
.signers
.iter()
.map(|(name, signer)| signer_response_from(name, signer))
.collect();
let mut signers = signers?;
signers.sort();
let r = response::SignerList { signers };
Response::result(request.id, serde_json::to_value(r)?)
}
Method::WalletAddress => {
let r: request::WalletAddress = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let addr = wollet.address(r.index)?;
let definite_desc = wollet
.wollet_descriptor()
.definite_descriptor(lwk_wollet::Chain::External, addr.index())?;
let text_qr = r
.with_text_qr
.then(|| address_to_text_qr(addr.address()))
.transpose()?;
let uri_qr = r
.with_uri_qr
.map(|e| {
let pixel_per_module = (e != 0).then_some(e);
address_to_uri_qr(addr.address(), pixel_per_module)
})
.transpose()?;
if let Some(signer) = r.signer {
let signer = s.get_available_signer(&signer)?;
if let AnySigner::Jade(jade, _id) = signer {
let fingerprint = signer.fingerprint()?;
let mut paths: Vec<Vec<u32>> = vec![];
let mut full_path: Vec<u32> = vec![];
definite_desc.for_each_key(|k| {
if k.master_fingerprint() == fingerprint {
if let Some(path) = k.full_derivation_path() {
full_path = derivation_path_to_vec(&path);
}
}
if let DescriptorPublicKey::XPub(x) = k.as_descriptor_public_key() {
paths.push(derivation_path_to_vec(&x.derivation_path));
}
true
});
if full_path.is_empty() {
return Err(Error::Generic("Signer is not in wallet".into()));
}
let jade_addr = match paths.len() {
0 => return Err(Error::Generic("Unsupported signer or descriptor".into())),
1 => {
match definite_desc.desc_type() {
DescriptorType::Wpkh => {
jade.get_receive_address_single(Variant::Wpkh, full_path)?
}
DescriptorType::ShWpkh => {
jade.get_receive_address_single(Variant::ShWpkh, full_path)?
}
_ => {
return Err(Error::Generic(
"Unsupported signer or descriptor".into(),
))
}
}
}
_ => {
jade.get_receive_address_multi(&r.name, paths)?
}
};
if jade_addr != addr.address().to_string() {
return Err(Error::Generic(
"Mismatching addresses between wallet and jade".into(),
));
}
} else {
return Err(Error::Generic(
"Cannot display address with software signer".into(),
));
}
};
let address = addr.address();
let memos = s.addr_memos.for_wollet(&r.name);
let memo = memos.get(address).cloned().unwrap_or_default();
Response::result(
request.id,
serde_json::to_value(response::WalletAddress {
address: address.to_string(),
index: addr.index(),
memo,
text_qr,
uri_qr,
})?,
)
}
Method::WalletBalance => {
let r: request::WalletBalance = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let mut balance = wollet
.balance()?
.into_iter()
.map(|(k, v)| (k.to_string(), v as i64))
.collect();
if r.with_tickers {
balance = s.replace_id_with_ticker(balance);
}
Response::result(
request.id,
serde_json::to_value(response::WalletBalance { balance })?,
)
}
Method::WalletSendMany => {
let r: request::WalletSendMany = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet: &mut Wollet = s.wollets.get_mut(&r.name)?;
let recipients: Vec<_> = r
.addressees
.into_iter()
.map(unvalidated_addressee)
.collect();
let mut tx = wollet
.tx_builder()
.set_unvalidated_recipients(&recipients)?
.fee_rate(r.fee_rate)
.finish()?;
add_contracts(&mut tx, s.assets.iter());
Response::result(
request.id,
serde_json::to_value(response::Pset {
pset: tx.to_string(),
})?,
)
}
Method::WalletDrain => {
let r: request::WalletDrain = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet: &mut Wollet = s.wollets.get_mut(&r.name)?;
let address = Address::from_str(&r.address)?;
let mut tx = wollet
.tx_builder()
.drain_lbtc_wallet()
.drain_lbtc_to(address)
.fee_rate(r.fee_rate)
.finish()?;
add_contracts(&mut tx, s.assets.iter());
Response::result(
request.id,
serde_json::to_value(response::Pset {
pset: tx.to_string(),
})?,
)
}
Method::SignerSinglesigDescriptor => {
let r: request::SignerSinglesigDescriptor = serde_json::from_value(params)?;
let mut s = state.lock()?;
let is_mainnet = s.config.is_mainnet();
let signer = s.get_available_signer(&r.name)?;
let script_variant = r
.singlesig_kind
.parse()
.map_err(|e: InvalidSinglesigVariant| e.to_string())?;
let blinding_variant = r
.descriptor_blinding_key
.parse()
.map_err(|e: InvalidBlindingKeyVariant| e.to_string())?;
let descriptor = singlesig_desc(signer, script_variant, blinding_variant, is_mainnet)?;
Response::result(
request.id,
serde_json::to_value(response::SignerSinglesigDescriptor { descriptor })?,
)
}
Method::WalletMultisigDescriptor => {
let r: request::WalletMultisigDescriptor = serde_json::from_value(params)?;
let multisig_variant = r
.multisig_kind
.parse()
.map_err(|e: InvalidMultisigVariant| e.to_string())?;
let blinding_variant = r
.descriptor_blinding_key
.parse()
.map_err(|e: InvalidBlindingKeyVariant| e.to_string())?;
let mut keyorigin_xpubs = vec![];
for keyorigin_xpub in r.keyorigin_xpubs {
keyorigin_xpubs.push(
keyorigin_xpub_from_str(&keyorigin_xpub)
.map_err(|e| Error::Generic(e.to_string()))?,
);
}
let descriptor = multisig_desc(
r.threshold,
keyorigin_xpubs,
multisig_variant,
blinding_variant,
)?;
Response::result(
request.id,
serde_json::to_value(response::WalletMultisigDescriptor { descriptor })?,
)
}
Method::SignerRegisterMultisig => {
let r: request::SignerRegisterMultisig = serde_json::from_value(params)?;
let mut s = state.lock()?;
let network = s.config.jade_network();
let descriptor = s.wollets.get(&r.wallet)?.descriptor().clone();
let signer = s.get_available_signer(&r.name)?;
if let AnySigner::Jade(jade, _id) = signer {
let descriptor: JadeDescriptor = (&descriptor).try_into()?;
jade.register_multisig(RegisterMultisigParams {
network,
multisig_name: r.wallet,
descriptor,
})?;
}
Response::result(request.id, serde_json::to_value(response::Empty {})?)
}
Method::SignerXpub => {
let r: request::SignerXpub = serde_json::from_value(params)?;
let mut s = state.lock()?;
let is_mainnet = s.config.is_mainnet();
let signer = s.get_available_signer(&r.name)?;
let bip = r
.xpub_kind
.parse()
.map_err(|e: InvalidBipVariant| e.to_string())?;
let keyorigin_xpub = signer.keyorigin_xpub(bip, is_mainnet)?;
Response::result(
request.id,
serde_json::to_value(response::SignerXpub { keyorigin_xpub })?,
)
}
Method::SignerSign => {
let r: request::SignerSign = serde_json::from_value(params)?;
let mut s = state.lock()?;
let signer = s.get_available_signer(&r.name)?;
let mut pset =
PartiallySignedTransaction::from_str(&r.pset).map_err(|e| e.to_string())?;
signer.sign(&mut pset)?;
Response::result(
request.id,
serde_json::to_value(response::Pset {
pset: pset.to_string(),
})?,
)
}
Method::WalletBroadcast => {
let r: request::WalletBroadcast = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let mut pset =
PartiallySignedTransaction::from_str(&r.pset).map_err(|e| e.to_string())?;
let tx = wollet.finalize(&mut pset)?;
let electrum_client = s.config.electrum_client()?;
if !r.dry_run {
electrum_client.broadcast(&tx)?;
}
Response::result(
request.id,
serde_json::to_value(response::WalletBroadcast {
txid: tx.txid().to_string(),
})?,
)
}
Method::WalletDetails => {
let r: request::WalletDetails = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let descriptor = wollet.descriptor().to_string();
let type_ = match wollet.descriptor().descriptor.desc_type() {
DescriptorType::Wpkh => response::WalletType::Wpkh,
DescriptorType::ShWpkh => response::WalletType::ShWpkh,
_ => match &wollet.descriptor().descriptor {
Descriptor::Wsh(wsh) => match wsh.as_inner() {
WshInner::Ms(ms) => match &ms.node {
Terminal::Multi(threshold, pubkeys) => {
response::WalletType::WshMulti(*threshold, pubkeys.len())
}
_ => response::WalletType::Unknown,
},
_ => response::WalletType::Unknown,
},
_ => response::WalletType::Unknown,
},
};
let mut warnings: Vec<String> = vec![];
let has_unique_fingerprints = {
let mut hs = HashSet::new();
wollet.signers().into_iter().all(|f| hs.insert(f))
};
if !has_unique_fingerprints {
warnings.push("wallet has multiple signers with the same fingerprint".into());
}
let signers: Vec<_> = wollet
.signers()
.iter()
.map(|fingerprint| {
let name = s.signers.name_from_fingerprint(fingerprint, &mut warnings);
response::SignerShortDetails {
name,
fingerprint: fingerprint.to_string(),
}
})
.collect();
Response::result(
request.id,
serde_json::to_value(response::WalletDetails {
descriptor,
type_: type_.to_string(),
signers,
warnings: warnings.join(", "),
})?,
)
}
Method::WalletCombine => {
let r: request::WalletCombine = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let mut psets = vec![];
for pset in r.pset {
psets.push(PartiallySignedTransaction::from_str(&pset).map_err(|e| e.to_string())?);
}
let pset = wollet.combine(&psets)?;
Response::result(
request.id,
serde_json::to_value(response::WalletCombine {
pset: pset.to_string(),
})?,
)
}
Method::WalletPsetDetails => {
let r: request::WalletPsetDetails = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let pset = PartiallySignedTransaction::from_str(&r.pset).map_err(|e| e.to_string())?;
let details = wollet.get_details(&pset)?;
let mut warnings = vec![];
let has_signatures_from = details
.fingerprints_has()
.iter()
.map(|f| response::SignerShortDetails {
name: s.signers.name_from_fingerprint(f, &mut warnings),
fingerprint: f.to_string(),
})
.collect();
let missing_signatures_from = details
.fingerprints_missing()
.iter()
.map(|f| response::SignerShortDetails {
name: s.signers.name_from_fingerprint(f, &mut warnings),
fingerprint: f.to_string(),
})
.collect();
let mut balance: HashMap<String, i64> = details
.balance
.balances
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
if r.with_tickers {
balance = s.replace_id_with_ticker(balance);
}
let issuances = details
.issuances
.iter()
.enumerate()
.filter(|(_, e)| e.is_issuance())
.map(|(vin, e)| response::Issuance {
asset: e.asset().expect("issuance").to_string(),
token: e.token().expect("issuance").to_string(),
is_confidential: e.is_confidential(),
vin: vin as u32,
asset_satoshi: e.asset_satoshi().unwrap_or(0),
token_satoshi: e.token_satoshi().unwrap_or(0),
prev_txid: e.prev_txid().expect("issuance").to_string(),
prev_vout: e.prev_vout().expect("issuance"),
})
.collect();
let reissuances = details
.issuances
.iter()
.enumerate()
.filter(|(_, e)| e.is_reissuance())
.map(|(vin, e)| response::Reissuance {
asset: e.asset().expect("reissuance").to_string(),
token: e.token().expect("reissuance").to_string(),
is_confidential: e.is_confidential(),
vin: vin as u32,
asset_satoshi: e.asset_satoshi().unwrap_or(0),
})
.collect();
Response::result(
request.id,
serde_json::to_value(response::WalletPsetDetails {
has_signatures_from,
missing_signatures_from,
balance,
fee: details.balance.fee,
issuances,
reissuances,
warnings: warnings.join(", "),
})?,
)
}
Method::WalletUtxos => {
let r: request::WalletUtxos = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let utxos: Vec<response::Utxo> = wollet.utxos()?.iter().map(convert_utxo).collect();
Response::result(
request.id,
serde_json::to_value(response::WalletUtxos { utxos })?,
)
}
Method::WalletTxs => {
let r: request::WalletTxs = serde_json::from_value(params)?;
let mut s = state.lock()?;
let explorer_url = s.config.explorer_url.clone();
let memos = s.tx_memos.for_wollet(&r.name);
let wollet = s.wollets.get_mut(&r.name)?;
let mut txs: Vec<response::Tx> = wollet
.transactions()?
.iter()
.map(|tx| convert_tx(tx, &explorer_url, &memos))
.collect();
if r.with_tickers {
for tx in &mut txs {
tx.balance = s.replace_id_with_ticker(tx.balance.clone());
}
}
Response::result(
request.id,
serde_json::to_value(response::WalletTxs { txs })?,
)
}
Method::WalletTx => {
let r: request::WalletTx = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let txid = Txid::from_str(&r.txid)?;
let tx = if let Some(tx) = wollet.transaction(&txid)? {
tx.tx.clone()
} else if r.from_explorer {
get_tx(&s.config.esplora_api_url, &txid)?
} else {
return Err(Error::WalletTxNotFound(r.txid, r.name));
};
let tx = serialize(&tx).to_hex();
Response::result(request.id, serde_json::to_value(response::WalletTx { tx })?)
}
Method::WalletSetTxMemo => {
let r: request::WalletSetTxMemo = serde_json::from_value(params)?;
let mut s = state.lock()?;
let _wollet = s.wollets.get(&r.name)?;
let txid = Txid::from_str(&r.txid).map_err(|e| Error::Generic(e.to_string()))?;
s.tx_memos.set(&r.name, &txid, &r.memo)?;
s.persist(&request)?;
Response::result(request.id, serde_json::to_value(response::Empty {})?)
}
Method::WalletSetAddrMemo => {
let r: request::WalletSetAddrMemo = serde_json::from_value(params)?;
let mut s = state.lock()?;
let _wollet = s.wollets.get(&r.name)?;
let address =
Address::from_str(&r.address).map_err(|e| Error::Generic(e.to_string()))?;
s.addr_memos.set(&r.name, &address, &r.memo)?;
s.persist(&request)?;
Response::result(request.id, serde_json::to_value(response::Empty {})?)
}
Method::WalletIssue => {
let r: request::WalletIssue = serde_json::from_value(params)?;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let tx = wollet
.tx_builder()
.issue_asset(
r.satoshi_asset,
r.address_asset.map(|a| Address::from_str(&a)).transpose()?,
r.satoshi_token,
r.address_token.map(|a| Address::from_str(&a)).transpose()?,
r.contract
.map(|c| lwk_wollet::Contract::from_str(&c))
.transpose()?,
)?
.fee_rate(r.fee_rate)
.finish()?;
Response::result(
request.id,
serde_json::to_value(response::Pset {
pset: tx.to_string(),
})?,
)
}
Method::WalletReissue => {
let r: request::WalletReissue = serde_json::from_value(params)?;
let mut s = state.lock()?;
let asset_id = AssetId::from_str(&r.asset)?;
let issuance_tx = s.get_issuance_tx(&asset_id);
let wollet = s.wollets.get_mut(&r.name)?;
let mut pset = wollet
.tx_builder()
.reissue_asset(
asset_id,
r.satoshi_asset,
r.address_asset.map(|a| Address::from_str(&a)).transpose()?,
issuance_tx,
)?
.fee_rate(r.fee_rate)
.finish()?;
add_contracts(&mut pset, s.assets.iter());
Response::result(
request.id,
serde_json::to_value(response::Pset {
pset: pset.to_string(),
})?,
)
}
Method::WalletBurn => {
let r: request::WalletBurn = serde_json::from_value(params)?;
let mut s = state.lock()?;
let asset_id = AssetId::from_str(&r.asset)?;
let wollet = s.wollets.get_mut(&r.name)?;
let mut pset = wollet
.tx_builder()
.add_burn(r.satoshi_asset, asset_id)?
.fee_rate(r.fee_rate)
.finish()?;
add_contracts(&mut pset, s.assets.iter());
Response::result(
request.id,
serde_json::to_value(response::Pset {
pset: pset.to_string(),
})?,
)
}
Method::AssetContract => {
let r: request::AssetContract = serde_json::from_value(params)?;
let c = lwk_wollet::Contract {
entity: lwk_wollet::Entity::Domain(r.domain),
issuer_pubkey: Vec::<u8>::from_hex(&r.issuer_pubkey)?,
name: r.name,
precision: r.precision,
ticker: r.ticker,
version: r.version,
};
c.validate()?;
Response::result(request.id, serde_json::to_value(c)?)
}
Method::AssetDetails => {
let r: request::AssetDetails = serde_json::from_value(params)?;
let s = state.lock()?;
let asset_id = lwk_wollet::elements::AssetId::from_str(&r.asset_id)
.map_err(|e| Error::Generic(e.to_string()))?;
let asset = s.get_asset(&asset_id)?;
Response::result(
request.id,
serde_json::to_value(response::AssetDetails {
name: asset.name(),
ticker: asset.ticker(),
})?,
)
}
Method::AssetList => {
let s = state.lock()?;
let mut assets: Vec<_> = s
.assets
.iter()
.map(|(asset_id, asset)| response::Asset {
asset_id: asset_id.to_string(),
name: asset.name(),
})
.collect();
assets.sort();
let r = response::AssetList { assets };
Response::result(request.id, serde_json::to_value(r)?)
}
Method::AssetInsert => {
let r: request::AssetInsert = serde_json::from_value(params)?;
let mut s = state.lock()?;
let asset_id = lwk_wollet::elements::AssetId::from_str(&r.asset_id)
.map_err(|e| Error::Generic(e.to_string()))?;
let issuance_tx =
Vec::<u8>::from_hex(&r.issuance_tx).map_err(|e| Error::Generic(e.to_string()))?;
let issuance_tx = lwk_wollet::elements::encode::deserialize(&issuance_tx)
.map_err(|e| Error::Generic(e.to_string()))?;
let contract = serde_json::Value::from_str(&r.contract)?;
let contract = lwk_wollet::Contract::from_value(&contract)?;
s.insert_asset(asset_id, issuance_tx, contract)?;
s.persist(&request)?;
Response::result(request.id, serde_json::to_value(response::Empty {})?)
}
Method::AssetRemove => {
let r: request::AssetRemove = serde_json::from_value(params)?;
let mut s = state.lock()?;
let asset_id = lwk_wollet::elements::AssetId::from_str(&r.asset_id)
.map_err(|e| Error::Generic(e.to_string()))?;
s.remove_asset(&asset_id)?;
s.persist_all()?;
Response::result(request.id, serde_json::to_value(response::Empty {})?)
}
Method::AssetFromExplorer => {
let r: request::AssetFromExplorer = serde_json::from_value(params)?;
let mut s = state.lock()?;
let asset_id = AssetId::from_str(&r.asset_id)?;
if s.get_asset(&asset_id).is_ok() {
return Err(Error::AssetAlreadyInserted(r.asset_id));
}
let registry_data = get_registry_data(&s.config.registry_url, &asset_id)?;
let txid = Txid::from_str(®istry_data.issuance_txin.txid)?;
let issuance_tx = get_tx(&s.config.esplora_api_url, &txid)?;
s.insert_asset(asset_id, issuance_tx, registry_data.contract)?;
let asset_insert_request = s.get_asset(&asset_id)?.request().expect("asset");
s.persist(&asset_insert_request)?;
Response::result(request.id, serde_json::to_value(response::Empty {})?)
}
Method::SignerJadeId => {
let r: request::SignerJadeId = serde_json::from_value(params)?;
let (network, timeout) = {
let s = state.lock()?;
(s.config.jade_network(), Some(s.config.timeout))
};
tracing::debug!("jade network: {}", network);
let jade = match r.emulator {
Some(emulator) => Jade::from_socket(emulator, network)?,
#[cfg(not(feature = "serial"))]
None => {
let _timeout = timeout;
return Err(Error::FeatSerialDisabled);
}
#[cfg(feature = "serial")]
None => {
let mut jade = Jade::from_any_serial(network, timeout)
.into_iter()
.filter_map(|e| e.ok())
.next();
jade.take()
.ok_or(Error::Generic("no Jade available".to_string()))?
}
};
jade.unlock()?;
let identifier = jade.identifier()?.to_string();
Response::result(
request.id,
serde_json::to_value(response::JadeId { identifier })?,
)
}
Method::Scan => {
scan(&state)?;
Response::result(request.id, serde_json::to_value(response::Empty {})?)
}
Method::Stop => {
return Err(Error::Stop);
}
Method::AssetPublish => {
let r: request::AssetPublish = serde_json::from_value(params)?;
let asset_id =
AssetId::from_str(&r.asset_id).map_err(|e| Error::Generic(e.to_string()))?;
let s = state.lock()?;
let asset = s.get_asset(&asset_id)?;
if let AppAsset::RegistryAsset(asset) = asset {
let client = reqwest::blocking::Client::new();
let url = &s.config.registry_url;
let contract = asset.contract();
let data = serde_json::json!({"asset_id": asset_id, "contract": contract});
tracing::debug!("posting {data:?} as json to {url} ");
let response = client.post(url).json(&data).send()?;
let mut result = response.text()?;
if result.contains("failed verifying linked entity") {
let domain = contract.entity.domain();
result = format!("https://{domain}/.well-known/liquid-asset-proof-{asset_id} must contain the following 'Authorize linking the domain name {domain} to the Liquid asset {asset_id}'");
}
Response::result(
request.id,
serde_json::to_value(response::AssetPublish {
asset_id: asset_id.to_string(),
result,
})?,
)
} else {
return Err(Error::Generic(
"Can't publish a policy asset or a reissuance token".to_string(),
));
}
}
};
Ok(response)
}
fn scan(state: &Arc<Mutex<State>>) -> Result<(), Error> {
let required_scan_loops = {
let mut s = state.lock()?;
s.interrupt_wait = true;
let is_scanning = s.scan_loops_completed != s.scan_loops_started;
s.scan_loops_completed + is_scanning as u32
};
loop {
std::thread::sleep(std::time::Duration::from_millis(100));
let current_scan_loops = state.lock()?.scan_loops_completed;
if current_scan_loops > required_scan_loops {
break;
}
}
Ok(())
}
fn unvalidated_addressee(a: request::UnvalidatedAddressee) -> lwk_wollet::UnvalidatedRecipient {
lwk_wollet::UnvalidatedRecipient {
satoshi: a.satoshi,
address: a.address,
asset: a.asset,
}
}
fn signer_response_from(name: &str, signer: &AppSigner) -> Result<response::Signer, Error> {
Ok(response::Signer {
name: name.to_string(),
fingerprint: signer.fingerprint()?.to_string(),
})
}
fn signer_details(name: &str, signer: &AppSigner) -> Result<response::SignerDetails, Error> {
Ok(response::SignerDetails {
name: name.to_string(),
id: signer.id()?.map(|i| i.to_string()),
fingerprint: signer.fingerprint()?.to_string(),
xpub: signer.xpub()?.map(|x| x.to_string()),
mnemonic: signer.mnemonic(),
type_: signer.type_(),
})
}
fn add_contracts<'a>(
pset: &mut PartiallySignedTransaction,
assets: impl Iterator<Item = (&'a AssetId, &'a AppAsset)>,
) {
let assets_in_pset: HashSet<_> = pset.outputs().iter().filter_map(|o| o.asset).collect();
for (_, asset) in assets {
if let AppAsset::RegistryAsset(_) = asset {
let asset_id = asset.asset_id();
if assets_in_pset.contains(&asset_id) {
if let Some(metadata) = asset.asset_metadata() {
pset.add_asset_metadata(asset_id, &metadata);
}
}
}
}
}
fn convert_utxo(u: &lwk_wollet::WalletTxOut) -> response::Utxo {
response::Utxo {
txid: u.outpoint.txid.to_string(),
vout: u.outpoint.vout,
height: u.height,
script_pubkey: u.script_pubkey.to_hex(),
asset: u.unblinded.asset.to_string(),
value: u.unblinded.value,
}
}
fn convert_tx(
tx: &lwk_wollet::WalletTx,
explorer_url: &str,
memos: &HashMap<Txid, String>,
) -> response::Tx {
let unblinded_url = tx.unblinded_url(explorer_url);
let memo = memos.get(&tx.txid).cloned().unwrap_or_default();
response::Tx {
txid: tx.txid.to_string(),
height: tx.height,
balance: tx
.balance
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect(),
fee: tx.fee,
timestamp: tx.timestamp,
type_: tx.type_.clone(),
unblinded_url,
memo,
}
}
#[cfg(test)]
mod tests {
use std::net::TcpListener;
use super::*;
fn app_random_port() -> App {
let addr = TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let mut config = Config::default_testnet(tempdir.path().to_path_buf());
config.addr = addr;
let mut app = App::new(config).unwrap();
app.run().unwrap();
app
}
#[test]
fn version() {
let mut app = app_random_port();
let addr = app.addr();
let url = addr.to_string();
dbg!(&url);
let client = jsonrpc::Client::simple_http(&url, None, None).unwrap();
let request = client.build_request("version", None);
let response = client.send_request(request).unwrap();
let result = response.result.unwrap().to_string();
let actual: response::Version = serde_json::from_str(&result).unwrap();
assert_eq!(actual.version, consts::APP_VERSION);
app.stop().unwrap();
app.join_threads().unwrap();
}
}