Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14864425
D18087.id54046.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
27 KB
Subscribers
None
D18087.id54046.diff
View Options
diff --git a/chronik/chronik-db/src/group.rs b/chronik/chronik-db/src/group.rs
--- a/chronik/chronik-db/src/group.rs
+++ b/chronik/chronik-db/src/group.rs
@@ -109,6 +109,10 @@
/// Serialize the given member.
fn ser_member(&self, member: &Self::Member<'_>) -> Self::MemberSer;
+ /// Whether this group supports ser_hash_member. If false the
+ /// ser_hash_member function will panic and should not be called.
+ fn is_hash_member_supported(&self) -> bool;
+
/// Hash the given member.
/// This is currently only used for ScriptGroup to create a
/// scripthash to script index for the ElectrumX API.
diff --git a/chronik/chronik-db/src/groups/lokad_id.rs b/chronik/chronik-db/src/groups/lokad_id.rs
--- a/chronik/chronik-db/src/groups/lokad_id.rs
+++ b/chronik/chronik-db/src/groups/lokad_id.rs
@@ -74,6 +74,10 @@
*member
}
+ fn is_hash_member_supported(&self) -> bool {
+ false
+ }
+
fn ser_hash_member(&self, _member: &Self::Member<'_>) -> [u8; 32] {
unimplemented!("There is no use case for hashing LokadIdGroup")
}
diff --git a/chronik/chronik-db/src/groups/script.rs b/chronik/chronik-db/src/groups/script.rs
--- a/chronik/chronik-db/src/groups/script.rs
+++ b/chronik/chronik-db/src/groups/script.rs
@@ -110,6 +110,10 @@
compress_script_variant(&member.variant())
}
+ fn is_hash_member_supported(&self) -> bool {
+ true
+ }
+
fn ser_hash_member(&self, member: &Self::Member<'_>) -> [u8; 32] {
Sha256::digest(member.bytecode()).to_be_bytes()
}
diff --git a/chronik/chronik-db/src/groups/token_id.rs b/chronik/chronik-db/src/groups/token_id.rs
--- a/chronik/chronik-db/src/groups/token_id.rs
+++ b/chronik/chronik-db/src/groups/token_id.rs
@@ -103,6 +103,10 @@
member.to_be_bytes()
}
+ fn is_hash_member_supported(&self) -> bool {
+ false
+ }
+
fn ser_hash_member(&self, _member: &Self::Member<'_>) -> [u8; 32] {
unimplemented!("There is no use case for hashing TokenIdGroup")
}
diff --git a/chronik/chronik-db/src/plugins/group.rs b/chronik/chronik-db/src/plugins/group.rs
--- a/chronik/chronik-db/src/plugins/group.rs
+++ b/chronik/chronik-db/src/plugins/group.rs
@@ -119,6 +119,10 @@
member.to_vec()
}
+ fn is_hash_member_supported(&self) -> bool {
+ false
+ }
+
fn ser_hash_member(&self, _member: &Self::Member<'_>) -> [u8; 32] {
unimplemented!("There is no known use case for hashing PluginsGroup")
}
diff --git a/chronik/chronik-db/src/test/value_group.rs b/chronik/chronik-db/src/test/value_group.rs
--- a/chronik/chronik-db/src/test/value_group.rs
+++ b/chronik/chronik-db/src/test/value_group.rs
@@ -61,6 +61,10 @@
ser_value(*sats)
}
+ fn is_hash_member_supported(&self) -> bool {
+ false
+ }
+
fn ser_hash_member(&self, _member: &Self::Member<'_>) -> [u8; 32] {
unimplemented!()
}
diff --git a/chronik/chronik-http/src/electrum.rs b/chronik/chronik-http/src/electrum.rs
--- a/chronik/chronik-http/src/electrum.rs
+++ b/chronik/chronik-http/src/electrum.rs
@@ -13,11 +13,13 @@
};
use bytes::Bytes;
use chronik_bridge::ffi;
-use chronik_db::group::GroupMember;
+use chronik_db::{group::GroupMember, io::BlockHeight};
use chronik_indexer::{
+ indexer::ChronikIndexer,
merkle::MerkleTree,
query::{QueryBlocks, MAX_HISTORY_PAGE_SIZE},
subs::BlockMsgType,
+ subs_group::TxMsgType,
};
use chronik_proto::proto::Tx;
use chronik_util::log_chronik;
@@ -142,7 +144,7 @@
message::Notification {
jsonrpc: message::JSONRPC_VERSION.to_string(),
method: nt.method,
- params: Some(Value::Array(vec![nt.result])),
+ params: Some(nt.result),
}
}
@@ -532,6 +534,150 @@
(confirmed, unconfirmed)
}
+async fn has_unconfirmed_parents(
+ tx: &Tx,
+ indexer: &tokio::sync::RwLockReadGuard<'_, ChronikIndexer>,
+ node: &NodeRef,
+) -> Result<bool, RPCError> {
+ for input in tx.inputs.iter() {
+ let prev_txid = match &input.prev_out {
+ Some(prev_out) => TxId::try_from(prev_out.txid.as_slice())
+ .map_err(|_| RPCError::InternalError)?,
+ // If a tx has no prev_out it's a coinbase, so it can't be missing
+ // parents.
+ None => return Ok(false),
+ };
+ // This should never fail
+ let tx = indexer
+ .txs(node)
+ .tx_by_id(prev_txid)
+ .map_err(|_| RPCError::InternalError)?;
+ if tx.block.is_none() {
+ return Ok(true);
+ }
+ }
+
+ Ok(false)
+}
+
+async fn get_scripthash_status(
+ script_hash: Sha256,
+ indexer: ChronikIndexerRef,
+ node: NodeRef,
+ max_history: u32,
+) -> Result<Option<String>, RPCError> {
+ let script_hash_hex = hex::encode(script_hash.to_be_bytes());
+
+ // First we gather the history in the order required for the status
+ // computation.
+ let indexer = indexer.read().await;
+ let script_history = indexer
+ .script_history(&node)
+ .map_err(|_| RPCError::InternalError)?;
+
+ let mut page = 0;
+ let mut num_pages = 1;
+ let mut tx_history: Vec<(String, BlockHeight)> = vec![];
+
+ while page < num_pages {
+ // Return the history in ascending block height order, with txs ordered
+ // as they appear in the block
+ let history = script_history
+ .confirmed_txs(
+ GroupMember::MemberHash(script_hash).as_ref(),
+ page as usize,
+ MAX_HISTORY_PAGE_SIZE,
+ )
+ .map_err(|_| RPCError::InternalError)?;
+
+ if history.num_txs > max_history {
+ return Err(RPCError::CustomError(
+ 1,
+ format!(
+ "transaction history for scripthash {script_hash_hex} \
+ exceeds limit ({0})",
+ max_history
+ ),
+ ));
+ }
+
+ for tx in history.txs.iter() {
+ tx_history.push((
+ hex::encode(tx.txid.iter().copied().rev().collect::<Vec<u8>>()),
+ tx.block.as_ref().unwrap().height,
+ ));
+ }
+
+ num_pages = history.num_pages;
+ page += 1;
+ }
+
+ // Return the mempool txs in the time first seen order (most recent first).
+ // We revert this ordering for ElectrumX compliance.
+ // Note that there is currently no pagination for the mempool.
+ let history = script_history
+ .unconfirmed_txs(GroupMember::MemberHash(script_hash).as_ref())
+ .map_err(|_| RPCError::InternalError)?;
+
+ if history.num_txs + (tx_history.len() as u32) > max_history {
+ return Err(RPCError::CustomError(
+ 1,
+ format!(
+ "transaction history for scripthash {script_hash_hex} exceeds \
+ limit ({0})",
+ max_history
+ ),
+ ));
+ }
+
+ let mut unconfirmed_tx_history: Vec<(String, BlockHeight)> = vec![];
+
+ for tx in history.txs.iter() {
+ let block_height =
+ if has_unconfirmed_parents(tx, &indexer, &node).await? {
+ -1
+ } else {
+ 0
+ };
+ unconfirmed_tx_history.push((
+ hex::encode(tx.txid.iter().copied().rev().collect::<Vec<u8>>()),
+ block_height,
+ ));
+ }
+
+ unconfirmed_tx_history.sort_by(|a, b| {
+ let (a_txid, a_height) = a;
+ let (b_txid, b_height) = b;
+
+ if a_height != b_height {
+ // Warning: reverse order! We place txs with no unconfirmed parents
+ // first (height = 0) then txs with unconfirmed parents
+ // (height = -1).
+ return b_height.cmp(a_height);
+ }
+
+ a_txid.cmp(b_txid)
+ });
+
+ tx_history.append(&mut unconfirmed_tx_history);
+
+ if tx_history.is_empty() {
+ return Ok(None);
+ }
+
+ // Then compute the status
+ let mut status_parts = Vec::<String>::new();
+
+ for tx in tx_history {
+ let (tx_hash, height) = tx;
+ status_parts.push(format!("{tx_hash}:{height}:"));
+ }
+
+ let status_string = status_parts.join("");
+
+ Ok(Some(Sha256::digest(status_string.as_bytes()).hex_le()))
+}
+
fn get_tx_fee(tx: &Tx) -> i64 {
let mut fee: i64 = 0;
for inp in tx.inputs.iter() {
@@ -543,10 +689,10 @@
fee
}
-async fn header_json_from_height(
+async fn header_hex_from_height(
blocks: &QueryBlocks<'_>,
height: i32,
-) -> Result<Value, RPCError> {
+) -> Result<String, RPCError> {
let header = blocks.header(height.to_string(), 0).await.map_err(|_| {
RPCError::CustomError(
1,
@@ -554,10 +700,14 @@
)
})?;
- Ok(json!({
- "height": height,
- "hex": hex::encode(header.raw_header),
- }))
+ Ok(hex::encode(header.raw_header))
+}
+
+fn script_hash_to_sub_id(script_hash: Sha256) -> u32 {
+ let script_hash_bytes: [u8; 32] = script_hash.into();
+ let id_bytes: [u8; 4] = script_hash_bytes[..4].try_into().unwrap();
+
+ u32::from_le_bytes(id_bytes)
}
#[rpc_pubsub_impl(name = "blockchain")]
@@ -604,15 +754,22 @@
let blocks: chronik_indexer::query::QueryBlocks<'_> =
indexer.blocks(&node_clone);
- match header_json_from_height(&blocks, block_msg.height)
+ match header_hex_from_height(&blocks, block_msg.height)
.await
{
Err(err) => {
log_chronik!("{err}\n");
break;
}
- Ok(header_json) => {
- if sub.notify(header_json).await.is_err() {
+ Ok(header_hex) => {
+ if sub
+ .notify(json!([{
+ "height": block_msg.height,
+ "hex": header_hex,
+ }]))
+ .await
+ .is_err()
+ {
// Don't log, it's likely a client
// unsubscription or disconnection
break;
@@ -630,7 +787,12 @@
.map_err(|_| RPCError::InternalError)?
.tip_height;
- header_json_from_height(&blocks, tip_height).await
+ let header_hex = header_hex_from_height(&blocks, tip_height).await?;
+
+ Ok(json!({
+ "height": tip_height,
+ "hex": header_hex,
+ }))
}
#[rpc_method(name = "headers.unsubscribe")]
@@ -642,6 +804,129 @@
) -> Result<Value, RPCError> {
let sub_id: message::SubscriptionID = 0;
let success = chan.remove_subscription(&sub_id).await.is_ok();
+ Ok(json!(success))
+ }
+
+ #[rpc_method(name = "scripthash.subscribe")]
+ async fn scripthash_subscribe(
+ &self,
+ chan: Arc<Channel>,
+ method: String,
+ params: Value,
+ ) -> Result<Value, RPCError> {
+ let script_hash_hex = match get_param!(params, 0, "scripthash")? {
+ Value::String(v) => Ok(v),
+ _ => {
+ Err(RPCError::CustomError(1, "Invalid scripthash".to_string()))
+ }
+ }?;
+
+ let script_hash =
+ Sha256::from_be_hex(&script_hash_hex).map_err(|_| {
+ RPCError::CustomError(1, "Invalid scripthash".to_string())
+ })?;
+
+ let indexer = self.indexer.read().await;
+ let mut subs: tokio::sync::RwLockWriteGuard<
+ '_,
+ chronik_indexer::subs::Subs,
+ > = indexer.subs().write().await;
+ let script_subs = subs.subs_script_mut();
+
+ let mut recv =
+ script_subs.subscribe_to_hash_member(&script_hash.to_be_bytes());
+
+ let indexer_clone = self.indexer.clone();
+ let node_clone = self.node.clone();
+ let max_history = self.max_history;
+
+ let sub_id = script_hash_to_sub_id(script_hash);
+ if let Ok(sub) = chan.new_subscription(&method, Some(sub_id)).await {
+ tokio::spawn(async move {
+ log_chronik!("Subscription to electrum scripthash\n");
+
+ let mut last_status = None;
+
+ loop {
+ let Ok(tx_msg) = recv.recv().await else {
+ // Error, disconnect
+ break;
+ };
+
+ // We want all the events except finalization (this might
+ // change in the future):
+ // - added to mempool
+ // - removed from mempool
+ // - confirmed
+ if tx_msg.msg_type == TxMsgType::Finalized {
+ continue;
+ }
+
+ if let Ok(status) = get_scripthash_status(
+ script_hash,
+ indexer_clone.clone(),
+ node_clone.clone(),
+ max_history,
+ )
+ .await
+ {
+ if last_status == status {
+ continue;
+ }
+ last_status = status.clone();
+
+ if sub
+ .notify(json!([
+ hex::encode(script_hash.to_be_bytes()),
+ status,
+ ]))
+ .await
+ .is_err()
+ {
+ // Don't log, it's likely a client
+ // unsubscription or
+ // disconnection
+ break;
+ }
+ }
+ }
+
+ log_chronik!("Unsubscription from electrum scripthash\n");
+ });
+ }
+
+ let status = get_scripthash_status(
+ script_hash,
+ self.indexer.clone(),
+ self.node.clone(),
+ max_history,
+ )
+ .await?;
+
+ Ok(serde_json::json!(status))
+ }
+
+ #[rpc_method(name = "scripthash.unsubscribe")]
+ async fn scripthash_unsubscribe(
+ &self,
+ chan: Arc<Channel>,
+ _method: String,
+ params: Value,
+ ) -> Result<Value, RPCError> {
+ let script_hash_hex = match get_param!(params, 0, "scripthash")? {
+ Value::String(v) => Ok(v),
+ _ => {
+ Err(RPCError::CustomError(1, "Invalid scripthash".to_string()))
+ }
+ }?;
+
+ let script_hash =
+ Sha256::from_be_hex(&script_hash_hex).map_err(|_| {
+ RPCError::CustomError(1, "Invalid scripthash".to_string())
+ })?;
+
+ let sub_id = script_hash_to_sub_id(script_hash);
+ let success = chan.remove_subscription(&sub_id).await.is_ok();
Ok(serde_json::json!(success))
}
}
diff --git a/chronik/chronik-indexer/src/subs_group.rs b/chronik/chronik-indexer/src/subs_group.rs
--- a/chronik/chronik-indexer/src/subs_group.rs
+++ b/chronik/chronik-indexer/src/subs_group.rs
@@ -71,6 +71,22 @@
}
}
+ /// Subscribe to updates about the given group hash member.
+ pub fn subscribe_to_hash_member(
+ &mut self,
+ hash_member: &[u8; 32],
+ ) -> broadcast::Receiver<TxMsg> {
+ match self.subs.get(hash_member.as_ref()) {
+ Some(sender) => sender.subscribe(),
+ None => {
+ let (sender, receiver) =
+ broadcast::channel(GROUP_CHANNEL_CAPACITY);
+ self.subs.insert(hash_member.as_ref().to_vec(), sender);
+ receiver
+ }
+ }
+ }
+
/// Cleanly unsubscribe from a member. This will try to deallocate the
/// memory used by a subscriber.
pub fn unsubscribe_from_member(&mut self, member: &G::Member<'_>) {
@@ -98,7 +114,14 @@
txid: tx.txid(),
};
let mut already_notified = HashSet::new();
+ let mut already_notified_hash = HashSet::new();
for member in tx_members_for_group(&self.group, query, aux) {
+ let hash_member = if self.group.is_hash_member_supported() {
+ Some(self.group.ser_hash_member(&member))
+ } else {
+ None
+ };
+
if !already_notified.contains(&member) {
let member_ser = self.group.ser_member(&member);
if let Some(sender) = self.subs.get(member_ser.as_ref()) {
@@ -109,6 +132,24 @@
}
already_notified.insert(member);
}
+
+ match hash_member {
+ // What is below is only for ScriptGroup
+ None => continue,
+ Some(hash_member) => {
+ if !already_notified_hash.contains(&hash_member) {
+ if let Some(sender) =
+ self.subs.get(hash_member.as_ref())
+ {
+ // Unclean unsubscribe
+ if sender.send(msg.clone()).is_err() {
+ self.subs.remove(hash_member.as_ref());
+ }
+ }
+ already_notified_hash.insert(hash_member);
+ }
+ }
+ }
}
}
diff --git a/test/functional/chronik_electrum_blockchain.py b/test/functional/chronik_electrum_blockchain.py
--- a/test/functional/chronik_electrum_blockchain.py
+++ b/test/functional/chronik_electrum_blockchain.py
@@ -25,7 +25,12 @@
)
from test_framework.script import OP_RETURN, OP_TRUE, CScript
from test_framework.test_framework import BitcoinTestFramework
-from test_framework.util import assert_equal, chronikelectrum_port, hex_to_be_bytes
+from test_framework.util import (
+ assert_equal,
+ assert_greater_than,
+ chronikelectrum_port,
+ hex_to_be_bytes,
+)
from test_framework.wallet import MiniWallet
COINBASE_TX_HEX = (
@@ -67,6 +72,7 @@
self.test_block_header()
self.test_scripthash()
self.test_headers_subscribe()
+ self.test_scripthash_subscribe()
def test_invalid_params(self):
# Invalid params type
@@ -825,6 +831,11 @@
sorted(utxos, key=utxo_sorting_key),
)
+ # Remove the history limit for the next tests
+ self.restart_node(0)
+ self.client = self.node.get_chronik_electrum_client()
+ self.wallet.rescan_utxos()
+
def test_headers_subscribe(self):
self.log.info("Test the blockchain.headers.subscribe endpoint")
@@ -860,7 +871,7 @@
for client in clients:
notification = client.wait_for_notification(
"blockchain.headers.subscribe"
- )
+ )[0]
assert_equal(notification["height"], height)
assert_equal(notification["hex"], header_hex)
@@ -918,6 +929,186 @@
unsub_message = client2.blockchain.headers.unsubscribe()
assert_equal(unsub_message.result, False)
+ # Unsubscribe all the clients so we don't mess with other tests
+ unsub_message = self.client.blockchain.headers.unsubscribe()
+ assert_equal(unsub_message.result, True)
+ unsub_message = client3.blockchain.headers.unsubscribe()
+ assert_equal(unsub_message.result, True)
+
+ def test_scripthash_subscribe(self):
+ self.log.info("Test the blockchain.scripthash.subscribe endpoint")
+
+ self.generate(self.wallet, 10)
+
+ # Subscribing to an address with no history returns null as a status
+ sub_message = self.client.blockchain.scripthash.subscribe("0" * 64)
+ result_no_history = sub_message.result
+ assert_equal(result_no_history, None)
+
+ # Subscribing to an address with some history returns a hash as a status
+ scripthash = hex_be_sha256(self.wallet.get_scriptPubKey())
+ assert_greater_than(
+ len(self.client.blockchain.scripthash.get_history(scripthash).result), 0
+ )
+ sub_message = self.client.blockchain.scripthash.subscribe(scripthash)
+ result_history = sub_message.result
+ assert result_history is not None
+ assert_equal(len(result_history), 64)
+
+ # Subscribing again is a no-op and returns the same result
+ for _ in range(3):
+ assert_equal(
+ self.client.blockchain.scripthash.subscribe("0" * 64).result,
+ result_no_history,
+ )
+ assert_equal(
+ self.client.blockchain.scripthash.subscribe(scripthash).result,
+ result_history,
+ )
+
+ # Generate a few wallet transactions so we get notifications
+ chain_length = 3
+ self.wallet.send_self_transfer_chain(
+ from_node=self.node, chain_length=chain_length
+ )
+
+ def check_notification(clients, scripthash, last_status=None):
+ ret_status = None
+ for client in clients:
+ notification = client.wait_for_notification(
+ "blockchain.scripthash.subscribe", timeout=1
+ )
+ # We should have exactly 2 items, the scripthash and the status
+ assert_equal(len(notification), 2)
+ (ret_scripthash, status) = notification
+ assert_equal(ret_scripthash, scripthash)
+ # Status is some hash
+ assert_equal(len(status), 64)
+
+ # The status should be the same for all clients
+ if ret_status:
+ assert_equal(status, ret_status)
+ ret_status = status
+
+ assert ret_status != last_status
+ return ret_status
+
+ # We should get a notification of each tx in the chain. Each tx causes
+ # the status to change so the status should be different for each
+ # notification.
+ last_status = None
+ for _ in range(chain_length):
+ last_status = check_notification([self.client], scripthash, last_status)
+
+ # Mine a block: the 3 previously unconfirmed txs are confirmed. We get 2
+ # notification: 1 for the confirmation of the 3 mempool txs, and 1 for
+ # the new coinbase tx
+ assert_equal(len(self.node.getrawmempool()), 3)
+ self.generate(self.wallet, 1)
+ assert_equal(len(self.node.getrawmempool()), 0)
+
+ # Here the confirmation happens for all the txs at the same time, so the
+ # status is the same across all the notifications (there is no such
+ # thing as one tx enters the block, then another one etc.).
+ # But this will differ from the previously saved status because the txs
+ # now have a non zero block height (and there is a new coinbase tx).
+ last_status = check_notification([self.client], scripthash, last_status)
+
+ # Let's add some clients
+ client2 = self.node.get_chronik_electrum_client()
+ assert_equal(
+ client2.blockchain.scripthash.subscribe(scripthash).result, last_status
+ )
+ client3 = self.node.get_chronik_electrum_client()
+ assert_equal(
+ client3.blockchain.scripthash.subscribe(scripthash).result, last_status
+ )
+
+ # Add a few more txs: all clients get notified. The status changes
+ # everytime, see the above rationale
+ self.wallet.send_self_transfer_chain(
+ from_node=self.node, chain_length=chain_length
+ )
+ for _ in range(chain_length):
+ last_status = check_notification(
+ [self.client, client2, client3], scripthash, last_status
+ )
+
+ # Mine the block to confirm the transactions
+ assert_equal(len(self.node.getrawmempool()), 3)
+ self.generate(self.wallet, 1)
+ assert_equal(len(self.node.getrawmempool()), 0)
+ last_status = check_notification(
+ [self.client, client2, client3], scripthash, last_status
+ )
+
+ # Unsubscribe client 2, the other clients are still notified
+ assert_equal(client2.blockchain.scripthash.unsubscribe(scripthash).result, True)
+
+ self.generate(self.wallet, 1)
+ last_status = check_notification(
+ [self.client, client3], scripthash, last_status
+ )
+
+ try:
+ client2.wait_for_notification("blockchain.scripthash.subscribe", timeout=1)
+ assert False, "Received an unexpected scripthash notification"
+ except TimeoutError:
+ pass
+
+ # Unsubscribing again is a no-op
+ for _ in range(3):
+ assert_equal(
+ client2.blockchain.scripthash.unsubscribe(scripthash).result, False
+ )
+
+ # Subscribe the first client to another hash
+ scriptpubkey = CScript(
+ bytes.fromhex("76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac")
+ )
+ other_scripthash = hex_be_sha256(scriptpubkey)
+ # This script has some history from the previous tests
+ sub_message = self.client.blockchain.scripthash.subscribe(other_scripthash)
+ assert_equal(len(sub_message.result), 64)
+
+ # We're sending from the originally subscribed address to the newly
+ # subscribed one so we also get the change output
+ self.wallet.send_to(
+ from_node=self.node,
+ scriptPubKey=scriptpubkey,
+ amount=1000,
+ )
+ check_notification([self.client], other_scripthash)
+ last_status = check_notification(
+ [self.client, client3], scripthash, last_status
+ )
+
+ # Unsubscribe the first client from the first scripthash
+ assert_equal(
+ self.client.blockchain.scripthash.unsubscribe(scripthash).result, True
+ )
+
+ # Now only client3 gets notified for the original scripthash
+ self.generate(self.wallet, 1)
+ last_status = check_notification([client3], scripthash, last_status)
+
+ # The other tx get confirmed
+ check_notification([self.client], other_scripthash)
+ # But that's the only notification
+ try:
+ self.client.wait_for_notification(
+ "blockchain.scripthash.subscribe", timeout=1
+ )
+ assert False, "Received an unexpected scripthash notification"
+ except TimeoutError:
+ pass
+
+ # Unsubscribe to everything
+ assert_equal(
+ self.client.blockchain.scripthash.unsubscribe(other_scripthash).result, True
+ )
+ assert_equal(client3.blockchain.scripthash.unsubscribe(scripthash).result, True)
+
if __name__ == "__main__":
ChronikElectrumBlockchain().main()
diff --git a/test/functional/test_framework/jsonrpctools.py b/test/functional/test_framework/jsonrpctools.py
--- a/test/functional/test_framework/jsonrpctools.py
+++ b/test/functional/test_framework/jsonrpctools.py
@@ -7,7 +7,7 @@
import socket
from typing import Any, Optional
-from .util import assert_equal
+from .util import assert_equal, assert_greater_than
class OversizedResponseError(Exception):
@@ -160,8 +160,8 @@
assert "id" not in json_reply
assert_equal(json_reply.get("method"), method)
assert "params" in json_reply
- assert_equal(len(json_reply["params"]), 1)
+ assert_greater_than(len(json_reply["params"]), 0)
# The "result" is within a "params" field. There is no point returning
# a JsonRpcResponse here as we only care about the result
- return json_reply["params"][0]
+ return json_reply["params"]
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, May 20, 19:35 (8 h, 22 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865795
Default Alt Text
D18087.id54046.diff (27 KB)
Attached To
D18087: [chronik] Add the blockchain.scripthash.subscribe endpoint
Event Timeline
Log In to Comment