aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src/message.rs
diff options
context:
space:
mode:
authorLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2024-04-21 16:04:38 +0100
committerLibravatar sommerfeld <sommerfeld@sommerfeld.dev>2024-04-21 16:04:38 +0100
commit1ab6ecba6f509b7b76865d65c77ecebc51efd2d3 (patch)
treea9b92e15769d483560d5799569b14c985b9c3ea5 /src/message.rs
downloadsentrum-1ab6ecba6f509b7b76865d65c77ecebc51efd2d3.tar.gz
sentrum-1ab6ecba6f509b7b76865d65c77ecebc51efd2d3.tar.bz2
sentrum-1ab6ecba6f509b7b76865d65c77ecebc51efd2d3.zip
Initial commitv0.1.0
Diffstat (limited to 'src/message.rs')
-rw-r--r--src/message.rs203
1 files changed, 203 insertions, 0 deletions
diff --git a/src/message.rs b/src/message.rs
new file mode 100644
index 0000000..419bb4d
--- /dev/null
+++ b/src/message.rs
@@ -0,0 +1,203 @@
+extern crate chrono;
+extern crate strfmt;
+
+use anyhow::{bail, Context, Result};
+use bdk::{bitcoin::Network, TransactionDetails};
+use chrono::DateTime;
+use serde::Deserialize;
+use strfmt::strfmt;
+
+use crate::wallets::WalletInfo;
+
+pub struct MessageParams<'a, 'b> {
+ tx: &'a TransactionDetails,
+ wallet: &'b str,
+ total_balance: u64,
+ current_height: u32,
+ network: Network,
+}
+
+impl<'a, 'b> MessageParams<'a, 'b> {
+ pub fn new(tx: &'a TransactionDetails, wallet: &'b WalletInfo) -> Self {
+ Self {
+ tx,
+ wallet: wallet.name(),
+ total_balance: wallet.total_balance().unwrap_or_default(),
+ current_height: wallet.get_height().unwrap_or_default(),
+ network: wallet.get_network(),
+ }
+ }
+
+ fn tx_net(&self) -> i64 {
+ (self.tx.received as i64) - (self.tx.sent as i64)
+ }
+
+ fn tx_height(&self) -> Option<u32> {
+ self.tx.confirmation_time.as_ref().map(|x| x.height)
+ }
+
+ fn confs(&self) -> u32 {
+ let current_height = self.current_height;
+ self.tx_height()
+ .map(|h| {
+ if current_height >= h {
+ current_height - h
+ } else {
+ 0
+ }
+ })
+ .unwrap_or_default()
+ }
+
+ fn conf_timestamp(&self) -> String {
+ self.tx
+ .confirmation_time
+ .as_ref()
+ .map(|x| {
+ DateTime::from_timestamp(x.timestamp as i64, 0)
+ .unwrap_or_default()
+ .format("%Y-%m-%d %H:%M:%S UTC")
+ .to_string()
+ })
+ .unwrap_or_default()
+ }
+
+ fn txid(&self) -> String {
+ self.tx.txid.to_string()
+ }
+ fn txid_short(&self) -> String {
+ let txid = self.txid();
+ format!("{}...{}", &txid[..6], &txid[txid.len() - 6..])
+ }
+
+ fn tx(&self) -> &TransactionDetails {
+ self.tx
+ }
+
+ pub fn network(&self) -> Network {
+ self.network
+ }
+}
+
+#[derive(Deserialize, Debug, PartialEq, Copy, Clone)]
+pub enum MessageFormat {
+ Plain,
+ Markdown,
+ Html,
+}
+
+#[derive(Deserialize, Default, Debug)]
+pub struct BlockExplorers {
+ mainnet: Option<String>,
+ testnet: Option<String>,
+ signet: Option<String>,
+}
+
+impl BlockExplorers {
+ fn mainnet(&self) -> &str {
+ self.mainnet
+ .as_deref()
+ .unwrap_or("https://mempool.space/tx/{txid}")
+ }
+
+ fn testnet(&self) -> &str {
+ self.testnet
+ .as_deref()
+ .unwrap_or("https://mempool.space/testnet/tx/{txid}")
+ }
+
+ fn signet(&self) -> &str {
+ self.signet
+ .as_deref()
+ .unwrap_or("https://mempool.space/signet/tx/{txid}")
+ }
+
+ pub fn get_tx_url_template(&self, network: &Network) -> Result<&str> {
+ Ok(match network {
+ Network::Bitcoin => self.mainnet(),
+ Network::Testnet => self.testnet(),
+ Network::Signet => self.signet(),
+ _ => bail!("unsupported network"),
+ })
+ }
+ pub fn get_tx_url(&self, network: &Network, txid: &str) -> Result<String> {
+ let template = self.get_tx_url_template(network)?;
+ strfmt!(template, txid => txid.to_string())
+ .with_context(|| format!("bad block explorer URL template '{}'", template))
+ }
+}
+
+#[derive(Deserialize, Default, Debug)]
+pub struct MessageConfig {
+ subject: Option<String>,
+ body: Option<String>,
+ format: Option<MessageFormat>,
+ #[serde(default)]
+ block_explorers: BlockExplorers,
+}
+
+impl MessageConfig {
+ pub fn subject_template(&self) -> &str {
+ self.subject
+ .as_deref()
+ .unwrap_or("[{wallet}] new transaction")
+ }
+
+ pub fn body_template(&self) -> &str {
+ self.body
+ .as_deref()
+ .unwrap_or("net: {tx_net} sats, balance: {total_balance} sats, txid: {txid_short}")
+ }
+
+ pub fn replace_template_params(
+ &self,
+ template: &str,
+ params: &MessageParams,
+ ) -> Result<String> {
+ strfmt!(template,
+ tx_net => params.tx_net(),
+ wallet => params.wallet.to_string(),
+ total_balance => params.total_balance,
+ txid => params.txid(),
+ txid_short => params.txid_short(),
+ received => params.tx().received,
+ sent => params.tx().sent,
+ fee => params.tx().fee.unwrap_or_default(),
+ current_height => params.current_height,
+ tx_height => params.tx_height().unwrap_or_default(),
+ confs => params.confs(),
+ conf_timestamp => params.conf_timestamp(),
+ tx_url => self.get_tx_url(Some(params))?
+ )
+ .with_context(|| format!("invalid template '{}'", template))
+ }
+
+ pub fn subject(&self, params: Option<&MessageParams>) -> Result<String> {
+ match params {
+ Some(p) => self.replace_template_params(self.subject_template(), p),
+ None => Ok(self.subject_template().to_string()),
+ }
+ }
+
+ pub fn body(&self, params: Option<&MessageParams>) -> Result<String> {
+ match params {
+ Some(p) => self.replace_template_params(self.body_template(), p),
+ None => Ok(self.body_template().to_string()),
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn format(&self) -> &MessageFormat {
+ self.format.as_ref().unwrap_or(&MessageFormat::Plain)
+ }
+
+ pub fn get_tx_url(&self, params: Option<&MessageParams>) -> Result<String> {
+ match params {
+ Some(p) => self.block_explorers.get_tx_url(&p.network(), &p.txid()),
+ None => Ok(self
+ .block_explorers
+ .get_tx_url_template(&Network::Bitcoin)?
+ .to_string()),
+ }
+ }
+}