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>
169 lines
6.8 KiB
Rust
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)
|
|
}
|