#![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::{sleep, JoinHandle};
use std::time::Duration;
use lwk_common::{
address_to_qr, address_to_text_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::amp2::Amp2;
use lwk_wollet::bitcoin::bip32::Fingerprint;
use lwk_wollet::bitcoin::XKeyIdentifier;
use lwk_wollet::clients::blocking::BlockchainBackend;
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, OutPoint, 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::registry::add_contracts;
use lwk_wollet::LiquidexProposal;
use lwk_wollet::Wollet;
use lwk_wollet::WolletDescriptor;
use serde_json::Value;
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 blockchain_client;
mod client;
mod config;
pub mod consts;
mod error;
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> {
log::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) => {
log::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(_) => {
log::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);
}
let (wollets_names, config) = {
let mut s = state_scanning.lock().expect("state lock poison");
s.interrupt_wait = false;
s.scan_loops_started += 1;
let wollets_names: Vec<_> = s.wollets.iter().map(|e| e.0.to_owned()).collect();
let config = s.config.clone();
(wollets_names, config)
};
match config.blockchain_client() {
Ok(mut blockchain_client) => {
for name in wollets_names {
let state = match state_scanning
.lock()
.expect("state lock poison")
.wollets
.get(&name)
{
Ok(w) => w.state(),
Err(_) => continue,
};
match blockchain_client.full_scan(&state) {
Ok(Some(update)) => {
let mut s = state_scanning.lock().expect("state lock poison");
let _ = match s.wollets.get_mut(&name) {
Ok(wollet) => wollet.apply_update(update),
Err(_) => continue,
};
}
Ok(None) => (),
Err(_) => continue,
}
}
}
Err(_) => {
log::info!(
"Cannot create an electrum client, are we conected? Retrying in one sec"
);
sleep(Duration::from_secs(1))
}
};
let mut s = state_scanning.lock().expect("state lock poison");
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> {
log::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()?;
if desc.is_mainnet() != s.config.is_mainnet() {
return Err(Error::Generic("Descriptor is for the wrong network".into()));
}
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.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_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()?
.as_ref()
.clone()
.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 builder = wollet
.tx_builder()
.set_unvalidated_recipients(&recipients)?
.fee_rate(r.fee_rate);
let mut tx = builder.finish()?;
add_contracts(&mut tx, s.registry_asset_data());
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.registry_asset_data());
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 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)?;
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::SignerDeriveBip85 => {
let r: request::SignerDeriveBip85 = serde_json::from_value(params)?;
let mut s = state.lock()?;
let signer = s.get_available_signer(&r.name)?;
let sw_signer = match signer {
AnySigner::Software(sw) => sw,
_ => {
return Err(Error::Generic(
"BIP85 derivation is only supported for software signers".to_string(),
))
}
};
let derived_mnemonic = sw_signer
.derive_bip85_mnemonic(r.index, r.word_count)
.map_err(|e| Error::Generic(format!("BIP85 derivation failed: {e}")))?;
Response::result(
request.id,
serde_json::to_value(response::SignerDeriveBip85 {
mnemonic: derived_mnemonic.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 blockchain_client = s.config.blockchain_client()?;
if !r.dry_run {
blockchain_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
.as_ref()
.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.fetch {
let client = s.config.blockchain_client()?;
let mut txs = client.get_transactions(&[txid])?;
txs.pop().ok_or(Error::WalletTxNotFound(r.txid, r.name))?
} 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::LiquidexMake => {
let r: request::LiquidexMake = serde_json::from_value(params)?;
let txid = Txid::from_str(&r.txid)?;
let vout = r.vout;
let asset = AssetId::from_str(&r.asset)?;
let satoshi = r.satoshi;
let mut s = state.lock()?;
let wollet = s.wollets.get_mut(&r.name)?;
let outpoint = OutPoint::new(txid, vout);
let addr = wollet.address(Some(0))?;
let receiving_address = addr.address().to_string();
let receiving_address = Address::from_str(&receiving_address)?;
let pset = wollet
.tx_builder()
.liquidex_make(outpoint, &receiving_address, satoshi, asset)?
.finish()?;
Response::result(
request.id,
serde_json::to_value(response::Pset {
pset: pset.to_string(),
})?,
)
}
Method::LiquidexTake => {
let r: request::LiquidexTake = serde_json::from_value(params)?;
let proposal = LiquidexProposal::from_str(&r.proposal)?;
let txid = proposal.needed_tx()?;
let client = {
let s = state.lock()?;
s.config.blockchain_client()?
};
let tx = client.get_transactions(&[txid])?.pop().expect("tx");
let proposal = proposal.validate(tx)?;
let mut s = state.lock()?;
let wollet: &mut Wollet = s.wollets.get_mut(&r.name)?;
let pset = wollet
.tx_builder()
.liquidex_take(vec![proposal])?
.finish()?;
Response::result(
request.id,
serde_json::to_value(response::Pset {
pset: pset.to_string(),
})?,
)
}
Method::LiquidexToProposal => {
let r: request::LiquidexToProposal = serde_json::from_value(params)?;
let pset = PartiallySignedTransaction::from_str(&r.pset).map_err(|e| e.to_string())?;
let proposal = LiquidexProposal::from_pset(&pset)?;
let proposal = serde_json::to_value(&proposal)?;
Response::result(
request.id,
serde_json::to_value(response::LiquidexProposal { proposal })?,
)
}
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.registry_asset_data());
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.registry_asset_data());
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::AssetFromRegistry => {
let r: request::AssetFromRegistry = 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 = lwk_wollet::registry::blocking::Registry::new(&s.config.registry_url)?;
let (contract, issuance_tx) =
registry.fetch_with_tx(asset_id, &s.config.blockchain_client()?)?;
s.insert_asset(asset_id, issuance_tx, 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))
};
log::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});
log::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(),
));
}
}
Method::Amp2Descriptor => {
let r: request::Amp2Descriptor = serde_json::from_value(params)?;
let mut s = state.lock()?;
if !matches!(s.config.network, lwk_wollet::ElementsNetwork::LiquidTestnet) {
return Err(Error::Generic(
"AMP2 methods are not available for this network".into(),
));
}
let signer = s.get_available_signer(&r.name)?;
let amp2 = Amp2::new_testnet();
let desc = amp2.descriptor_from_str(&2userkey(signer)?)?;
let descriptor = desc.descriptor().to_string();
Response::result(
request.id,
serde_json::to_value(response::Amp2Descriptor { descriptor })?,
)
}
Method::Amp2Register => {
let r: request::Amp2Register = serde_json::from_value(params)?;
let mut s = state.lock()?;
if !matches!(s.config.network, lwk_wollet::ElementsNetwork::LiquidTestnet) {
return Err(Error::Generic(
"AMP2 methods are not available for this network".into(),
));
}
let signer = s.get_available_signer(&r.name)?;
let amp2 = Amp2::new_testnet();
let desc = amp2.descriptor_from_str(&2userkey(signer)?)?;
let wid = amp2.blocking_register(desc)?.wid;
Response::result(
request.id,
serde_json::to_value(response::Amp2Register { wid })?,
)
}
Method::Amp2Cosign => {
let r: request::Amp2Cosign = serde_json::from_value(params)?;
let s = state.lock()?;
if !matches!(s.config.network, lwk_wollet::ElementsNetwork::LiquidTestnet) {
return Err(Error::Generic(
"AMP2 methods are not available for this network".into(),
));
}
let amp2 = Amp2::new_testnet();
let pset = PartiallySignedTransaction::from_str(&r.pset).map_err(|e| e.to_string())?;
let pset = amp2.blocking_cosign(&pset)?.pset.to_string();
Response::result(
request.id,
serde_json::to_value(response::Amp2Cosign { pset })?,
)
}
};
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 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(),
address: u.address.to_string(),
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,
}
}
fn amp2userkey(signer: &AnySigner) -> Result<String, Error> {
let bip = lwk_common::Bip::Bip87;
let is_mainnet = false;
Ok(signer.keyorigin_xpub(bip, is_mainnet)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::AppAsset;
use lwk_wollet::elements::pset::{elip100::PSET_HWW_PREFIX, PartiallySignedTransaction};
use lwk_wollet::elements::AssetId;
use lwk_wollet::{Contract, RegistryAssetData};
use std::collections::HashMap;
use std::net::TcpListener;
use std::str::FromStr;
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();
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();
}
#[test]
fn add_contracts_embeds_valid_metadata() {
let contract_json = r#"{"entity":{"domain":"example.com"},"issuer_pubkey":"020202020202020202020202020202020202020202020202020202020202020202","name":"MyCoin","precision":0,"ticker":"MYCO","version":0}"#;
let contract_value = serde_json::Value::from_str(contract_json).unwrap();
let contract = Contract::from_value(&contract_value).unwrap();
contract.validate().unwrap();
let contract_serialized = serde_json::to_string(&contract).unwrap();
assert_eq!(contract_serialized, contract_json);
let pset_str = include_str!("../test_data/issuance_pset.base64");
let mut pset = PartiallySignedTransaction::from_str(pset_str).unwrap();
let n_proprietary_keys = pset.global.proprietary.len();
pset.global
.proprietary
.retain(|key, _| !key.prefix.starts_with(PSET_HWW_PREFIX));
assert!(pset
.global
.proprietary
.keys()
.all(|key| !key.prefix.starts_with(PSET_HWW_PREFIX)));
let removed = n_proprietary_keys - pset.global.proprietary.len();
assert_eq!(
removed, 2,
"Expected to remove 2 proprietary keys with HWW prefix"
);
let tx = pset.extract_tx().unwrap();
let mut assets_map: HashMap<AssetId, AppAsset> = HashMap::new();
let asset_id =
AssetId::from_str("25e85efe02e5010a880ddb7c936e82896cd7fc493d2a5bc4422e8ec26100b00d")
.unwrap();
let asset_data = RegistryAssetData::new(asset_id, tx.clone(), contract.clone())
.expect("valid registry data");
let token_id = asset_data.reissuance_token();
assets_map.insert(asset_id, AppAsset::RegistryAsset(asset_data.clone()));
assets_map.insert(token_id, AppAsset::ReissuanceToken(asset_data.clone()));
assert_eq!(
asset_data.entropy().unwrap(),
[
246, 117, 58, 147, 49, 238, 254, 149, 30, 173, 173, 99, 251, 157, 220, 73, 16, 39,
72, 67, 153, 90, 118, 180, 236, 207, 166, 53, 173, 215, 201, 113
],
);
add_contracts(&mut pset, [&asset_data].into_iter());
assert_eq!(
pset.global.proprietary.len(),
n_proprietary_keys,
"Contract metadata was not fully restored"
);
let asset_meta = pset.get_asset_metadata(asset_id).unwrap().unwrap();
assert_eq!(
asset_meta.contract(),
contract_json,
"Invalid contract in asset metadata"
);
assert_eq!(
asset_meta.issuance_prevout(),
asset_data.issuance_prevout(),
"Invalid issuance prevout in asset metadata"
);
let token_meta = pset.get_token_metadata(token_id).unwrap().unwrap();
assert_eq!(
token_meta.asset_id(),
&asset_id,
"Invalid asset tag in reissuance token metadata"
);
assert!(
!token_meta.issuance_blinded(),
"Invalid issuance blinded flag in reissuance token metadata"
);
}
}