diff --git a/src/config.rs b/src/config.rs index ec4952a..78e8cad 100644 --- a/src/config.rs +++ b/src/config.rs @@ -66,6 +66,7 @@ fn read_secret_file(path: &std::path::Path) -> anyhow::Result { pub struct AppConfig { pub network: ZNetwork, pub datadir: PathBuf, + pub walletnotify: Option, pub default_wallet: String, pub wallets: BTreeMap, pub backend: BackendConfig, @@ -501,6 +502,7 @@ impl SpendConfig { struct ConfigFile { network: Option, datadir: Option, + walletnotify: Option, default_wallet: Option, #[serde(default)] wallets: BTreeMap, @@ -664,6 +666,10 @@ pub struct Cli { #[arg(long)] pub testnet: bool, + /// Command to execute when a wallet transaction comes in. + #[arg(long, value_name = "CMD")] + pub walletnotify: Option, + /// Use regtest - a local zebra regtest chain (overrides config `network`). #[arg(long)] pub regtest: bool, @@ -831,6 +837,8 @@ impl AppConfig { .clone() .unwrap_or_else(|| "default".to_string()); + let walletnotify = cli.walletnotify.clone().or(file.walletnotify); + // keys.toml location override (so the encrypted seed can be a mounted Secret, separate // from the disposable datadir). The global `[keys] keys_file` / `ZECD_KEYS_FILE` / // `--keys-file` applies to the default wallet; a per-wallet `[wallets.] keys_file` @@ -1094,6 +1102,7 @@ impl AppConfig { Ok(AppConfig { network, datadir, + walletnotify, default_wallet, wallets, backend, diff --git a/src/daemon.rs b/src/daemon.rs index a980ab7..5158994 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -140,6 +140,7 @@ pub async fn run(config: AppConfig) -> anyhow::Result<()> { transparent_allow_beyond_recovery_window: entry .transparent_allow_beyond_recovery_window, transparent_gap_warn_threshold: entry.transparent_gap_warn_threshold, + walletnotify: config.walletnotify.clone(), shutdown: shutdown_tx.subscribe(), }; match actor::spawn(actor_cfg).await { diff --git a/src/wallet/actor.rs b/src/wallet/actor.rs index 4538a4f..fad3037 100644 --- a/src/wallet/actor.rs +++ b/src/wallet/actor.rs @@ -318,6 +318,7 @@ pub struct ActorConfig { /// Warn when fewer than this many in-window transparent address slots remain. Only used when /// `transparent_enabled`. pub transparent_gap_warn_threshold: u32, + pub walletnotify: Option, /// Flips to `true` on Ctrl-C/`stop`; the actor exits its loop (between sync batches) /// so the `WalletDb` is dropped cleanly before the process ends. pub shutdown: watch::Receiver, @@ -358,6 +359,7 @@ struct WalletActor { /// Warn when fewer than this many in-window transparent address slots remain before generation /// would hit the gap limit. transparent_gap_warn_threshold: u32, + walletnotify: Option, /// The wallet's exposed transparent receiving + change addresses, as a membership set for the /// block-scan / mempool receive matcher. Transient (rebuilt from the DB, respects the stateless /// invariant). librustzcash never asks us to scan our *receiving* transparent addresses for @@ -695,6 +697,7 @@ pub async fn spawn( transparent_initial_scan: cfg.transparent_initial_scan, transparent_allow_beyond_recovery_window: cfg.transparent_allow_beyond_recovery_window, transparent_gap_warn_threshold: cfg.transparent_gap_warn_threshold, + walletnotify: cfg.walletnotify.clone(), transparent_scripts: None, transparent_set_dirty: true, transparent_preexposed: false, @@ -1533,6 +1536,7 @@ impl WalletActor { &tx, mined, )?; + self.trigger_walletnotify(&tx.txid().to_string()); } } } @@ -1575,6 +1579,7 @@ impl WalletActor { &tx, mined, )?; + self.trigger_walletnotify(&tx.txid().to_string()); } } } @@ -1869,6 +1874,9 @@ impl WalletActor { // decrypt stored it or we just recorded a transparent receive from it. let txid_hex = txid.to_string(); let ours = t_recorded > 0 || super::read::tx_exists(&self.wallet_dir, &txid_hex); + if ours { + self.trigger_walletnotify(&txid_hex); + } tracing::debug!( "[{}] processed mempool tx {txid} (ours={ours}, transparent_receives={t_recorded})", self.name @@ -2042,6 +2050,19 @@ impl WalletActor { Ok(outcome.worked) } + fn trigger_walletnotify(&self, txid_hex: &str) { + if let Some(cmd) = &self.walletnotify { + let replaced = cmd.replace("%s", txid_hex); + tracing::info!("[{}] walletnotify: executing {}", self.name, replaced); + std::thread::spawn(move || { + let _ = std::process::Command::new("sh") + .arg("-c") + .arg(&replaced) + .spawn(); + }); + } + } + /// Rebuild [`Self::transparent_scripts`] from the account's exposed transparent receivers /// (external + internal/change). Cheap relative to a sync batch and only run when the set may /// have changed (`transparent_set_dirty`), so an exchange with ~100k addresses pays the query