riftenlabs-indexer/src/electrum.rs

106 lines
3.7 KiB
Rust
Raw Normal View History

2026-01-21 12:34:59 +01:00
// Copyright (C) 2024-2026 Whiterun LLC
2024-02-14 11:11:16 +01:00
//
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
2024-02-16 09:11:40 +01:00
use std::{collections::HashSet, str::FromStr};
2024-02-14 11:11:16 +01:00
use anyhow::{Context, Result};
2024-02-16 09:11:40 +01:00
use bitcoin_hashes::hex::ToHex;
use bitcoincash::{consensus::deserialize, BlockHeader, Transaction, Txid};
use electrum_client_netagnostic::{Client, ElectrumApi, Param};
2024-02-16 09:11:40 +01:00
use log::info;
use riftenlabs_defi::{
cauldron::V2_CONTRACT_TEMPLATE,
delphi::{v2::DELPHI_V2_REDEEM_SCRIPT_BODY, DELPHI_REDEEM_SCRIPT},
};
2024-02-16 09:11:40 +01:00
use serde_json::{json, Value};
2024-02-14 11:11:16 +01:00
/// Fetch blockchain tip from electrum server
pub fn electrum_get_tip(client: &Client) -> Result<(BlockHeader, u64)> {
let tip: Value =
serde_json::from_str(&client.raw_call("blockchain.headers.tip", [])?.to_string())?;
let height = tip
.get("height")
.context("no height")?
.as_i64()
.context("no int")?;
let header = tip
.get("hex")
.context("no hex in header")?
.as_str()
.context("hex not str")?;
Ok((deserialize(&hex::decode(header)?)?, height as u64))
}
2024-02-16 09:11:40 +01:00
/// Fetch cauldron mempool transactions
pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet<Txid>, HashSet<Txid>)> {
let cauldron_filter = json!({
"scriptsig": hex::encode(&V2_CONTRACT_TEMPLATE[(V2_CONTRACT_TEMPLATE.len() - 43)..]), // cauldron spends
"scriptpubkey": hex::encode([0x6a /* op_return */, 0x06 /* push */, b'S', b'U', b'M', b'M', b'O', b'N']), // new pools (potentially)
"operation": "union"
2024-02-16 09:11:40 +01:00
});
// v1 oracle filter: matches the cashc 0.10.5 redeem-script template.
let oracle_v1_filter = json!({
"scriptsig": hex::encode(DELPHI_REDEEM_SCRIPT),
});
// v2 oracle filter: matches the cashc 0.12.1 redeem-script body. The
// body is identical across deployments regardless of the constructor
// args, so this catches v2 spends without needing to know the deployed
// token ids.
let oracle_v2_filter = json!({
"scriptsig": hex::encode(DELPHI_V2_REDEEM_SCRIPT_BODY),
});
let fetch_txs = |filter: Value| -> Result<HashSet<Txid>> {
let response = client.raw_call("mempool.get", [Param::Value(filter)])?;
2024-02-16 09:11:40 +01:00
let txs = response
.get("transactions")
.context("no txs in mempool get")?
.as_array()
.context("txs not array")?;
Ok(txs
.iter()
.filter_map(|txid| match txid.as_str() {
Some(txid_hex) => match Txid::from_str(txid_hex) {
Ok(txid) => Some(txid),
Err(e) => {
2025-07-16 09:31:14 +02:00
info!("Txid not hex: {e}");
None
}
},
None => {
2025-07-16 09:31:14 +02:00
info!("Failed to read txid from electrum response {txid:?}");
2024-02-16 09:11:40 +01:00
None
}
})
.collect())
};
let cauldron_txs = fetch_txs(cauldron_filter)?;
let mut oracle_txs = fetch_txs(oracle_v1_filter)?;
oracle_txs.extend(fetch_txs(oracle_v2_filter)?);
Ok((cauldron_txs, oracle_txs))
2024-02-16 09:11:40 +01:00
}
/// Fetch blockchain tip from electrum server
pub fn electrum_get_tx(client: &Client, txid: &Txid) -> Result<Transaction> {
let tx: Value = serde_json::from_str(
&client
.raw_call("blockchain.transaction.get", [Param::String(txid.to_hex())])?
.to_string(),
)?;
let tx: Transaction = deserialize(
&hex::decode(tx.as_str().context("no tx in response")?).context("failed to decode tx")?,
)?;
Ok(tx)
}