riftenlabs-indexer/src/electrum.rs
Hossein Zoda cab3f49c35 feat: index token-token & token-BCH delegation pools with ORB admission
Re-index tokentoken for the delegation design and add a tokenbch
single-UTXO token-to-BCH pool indexer: mempool + block ingestion, reorg
undo, RPC (/tokenbch), and cauldron.db table migrations.

ORB PoolParams admission (src/db/cauldron/orb.rs): a pool creation is
admitted only if its tx carried the live PoolParams NFT and the pool
matches the pinned platformFeeRate + platformFeeNfth (logicHash derived
from the NFTH at admission, not a constant). Per-network params-NFT
categories are centralized in src/db/orbconstants.rs (pool-params
categories placeholder + fail-closed). Store platform_fee_nfth on both
pool tables and the tokenbch fee_paid_in_bch variant.

Cargo.toml: riftenlabs-defi 0.4.5 (published, no longer a local path),
bitcoincash 0.32.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 18:29:41 +03:00

169 lines
6.8 KiB
Rust

// Copyright (C) 2024-2026 Whiterun LLC
//
// 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
use std::{collections::HashSet, str::FromStr};
use anyhow::{Context, Result};
use bitcoincash::{
blockdata::block::Header as BlockHeader, consensus::deserialize, Transaction, Txid,
};
use electrum_client_netagnostic::{Client, ElectrumApi, Param};
use log::info;
use riftenlabs_defi::{
cauldron::V2_CONTRACT_TEMPLATE,
delphi::{v2::DELPHI_V2_REDEEM_SCRIPT_BODY, DELPHI_REDEEM_SCRIPT},
tokenbch::{
CARRIER_UNLOCK_PREFIX_HEX as TOKENBCH_CARRIER_UNLOCK_PREFIX_HEX,
THIN_MAIN_SUFFIX as TOKENBCH_THIN_MAIN_SUFFIX,
},
tokentoken::{
CARRIER_UNLOCK_PREFIX_HEX as TOKENTOKEN_CARRIER_UNLOCK_PREFIX_HEX,
SIBCODE as TOKENTOKEN_SIBCODE,
},
};
use serde_json::{json, Value};
use crate::bcmr::BCMR_PREFIX;
use crate::db::ido::{IDO_PREINIT_ANNOUNCEMENT_SIGNATURE, IDO_SIGNATURE};
/// 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))
}
/// Mempool transaction-id sets returned by [`electrum_fetch_mempool`], in order:
/// defi (cauldron + tokentoken + tokenbch), oracle, ido, bcmr.
type MempoolTxSets = (HashSet<Txid>, HashSet<Txid>, HashSet<Txid>, HashSet<Txid>);
/// Fetch defi (cauldron + tokentoken + tokenbch), oracle, ido and bcmr mempool transactions
pub fn electrum_fetch_mempool(client: &Client) -> Result<MempoolTxSets> {
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"
});
// TokenToken (token-A ⇄ token-B delegation) pools are bare-P2S, so a spend
// never reveals the pool script; recognition is output-driven. Every
// creation, swap, and platform-fee sweep recreates the pool at a <main,
// sibling> output pair; the per-pool storage sibling's locking ends in the
// fixed StorageSibling code — the scriptpubkey side catches those. An LP
// teardown recreates no pool output but presents the carrier covenant's logic
// blob in an input; its shared leading bytes (already hex) match on the
// scriptsig side.
let tokentoken_filter = json!({
"scriptsig": TOKENTOKEN_CARRIER_UNLOCK_PREFIX_HEX, // teardowns (carrier logic blob)
"scriptpubkey": hex::encode(TOKENTOKEN_SIBCODE), // pool siblings (create/swap/sweep)
"operation": "union"
});
// TokenBch (token ⇄ BCH delegation) pools are the single-UTXO sibling of the
// above; same bare-P2S, output-driven recognition. Every creation, swap, and
// platform-fee sweep recreates the pool output, whose locking ends in the
// fixed dispatcher suffix (variant- and NFTH-independent) — the scriptpubkey
// side catches those. An LP teardown presents the carrier logic blob in an
// input; its shared leading bytes match on the scriptsig side.
let tokenbch_filter = json!({
"scriptsig": TOKENBCH_CARRIER_UNLOCK_PREFIX_HEX, // teardowns (carrier logic blob)
"scriptpubkey": hex::encode(TOKENBCH_THIN_MAIN_SUFFIX), // pool outputs (create/swap/sweep)
"operation": "union"
});
// 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 ido_filter = json!({
"scriptsig": hex::encode(IDO_SIGNATURE), // ido state machine spends
"scriptpubkey": hex::encode(IDO_PREINIT_ANNOUNCEMENT_SIGNATURE), // preinit announcements (potentially)
"operation": "union"
});
// BCMR registrations carry an `OP_RETURN "BCMR"` output (genesis or auth
// chain update). Auth chain transfers without a BCMR output are only
// picked up once confirmed.
let bcmr_filter = json!({
"scriptpubkey": hex::encode(BCMR_PREFIX),
});
let fetch_txs = |filter: Value| -> Result<HashSet<Txid>> {
let response = client.raw_call("mempool.get", [Param::Value(filter)])?;
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) => {
info!("Txid not hex: {e}");
None
}
},
None => {
info!("Failed to read txid from electrum response {txid:?}");
None
}
})
.collect())
};
// Cauldron, tokentoken and tokenbch txs share one set: update_mempool diffs
// it against the stored mempool (tx table) to decide adds and deletes.
let mut defi_txs = fetch_txs(cauldron_filter)?;
defi_txs.extend(fetch_txs(tokentoken_filter)?);
defi_txs.extend(fetch_txs(tokenbch_filter)?);
let mut oracle_txs = fetch_txs(oracle_v1_filter)?;
oracle_txs.extend(fetch_txs(oracle_v2_filter)?);
let ido_txs = fetch_txs(ido_filter)?;
let bcmr_txs = fetch_txs(bcmr_filter)?;
Ok((defi_txs, oracle_txs, ido_txs, bcmr_txs))
}
/// 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_string())],
)?
.to_string(),
)?;
let tx: Transaction = deserialize(
&hex::decode(tx.as_str().context("no tx in response")?).context("failed to decode tx")?,
)?;
Ok(tx)
}