Skip to content

Refactor(electrum): Remove unwraps and expects#956

Open
vatsalkeshav wants to merge 6 commits into
getfloresta:masterfrom
vatsalkeshav:463-electrum-only
Open

Refactor(electrum): Remove unwraps and expects#956
vatsalkeshav wants to merge 6 commits into
getfloresta:masterfrom
vatsalkeshav:463-electrum-only

Conversation

@vatsalkeshav
Copy link
Copy Markdown

Description and Notes

This pr removes all .unwrap() and non-infallible .expect() calls in non-test code of floresta-electrum

contributes to issue #463

note to reviewers : added self.addresses_to_scan.extend(addresses) to avoid silent address drop in at get_block_height failure in rescan_with_block_filters fn

Contributor Checklist

  • I've followed the contribution guidelines
  • I've verified one of the following:
    • Ran just pcc (recommended but slower)
    • Ran just lint-features '-- -D warnings' && cargo test --release
    • Confirmed CI passed on my fork
  • I've linked any related issue(s) in the sections above

Finally, you are encouraged to sign all your commits (it proves authorship and guards against tampering—see How (and why) to sign Git commits and GitHub's guide to signing commits).

remove all .unwrap() and non-infallible .expect() calls in non-test code of /floresta-electrum/s/electrum_protocol.rs

contribute to issue getfloresta#463
@jaoleal jaoleal self-requested a review April 14, 2026 12:10
@jaoleal
Copy link
Copy Markdown
Member

jaoleal commented Apr 14, 2026

Did you tested this ? I dont think that the approach of just logging the error is efficient but i may be missing something about the electrum protocol...

Ill take a read about it and after that ill do a review...

Also, another strange thing are the unwrap_or calls, this default behavior is not desired, we need to know when this call fails.

Copy link
Copy Markdown
Member

@Davidson-Souza Davidson-Souza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass through the code


let position = self.address_cache.get_position(&prevout.txid).unwrap();
for (utxo, prevout) in utxos.into_iter() {
let height = self.address_cache.get_height(&prevout.txid).unwrap_or(0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getting a None means we don't have it cached, so we should send and empty reply

Copy link
Copy Markdown
Author

@vatsalkeshav vatsalkeshav Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.
I initially left unwrap_or(0) as electrum treats 0 as sentinel for unconfirmed tx - sorry for that

.chain
.get_block_hash(0)
.expect("Genesis block should be present");
.expect("genesis block is always in the chain store");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.expect("genesis block is always in the chain store");
.expect("Genesis block is always in the chain store");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +547 to +553
let unconfirmed = match self.address_cache.find_unconfirmed() {
Ok(txs) => txs,
Err(e) => {
error!("Could not fetch unconfirmed transactions for rebroadcast: {e}");
return;
}
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be propagated back. Our convension is to not handle errors in utility functions like this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

let Ok(blocks) =
cfilters.match_any(_addresses, start_height, stop_height, self.chain.clone())
else {
info!("Could not match block filters");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are errors, not info

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 722 to 735
match self.chain.get_height() {
Ok(chain_height) => {
if chain_height == height {
for client in &mut self.clients.values() {
let res = client.write(
serde_json::to_string(&result)
.expect("serde_json::Value is always serializable")
.as_bytes(),
);
if res.is_err() {
info!("Could not write to client {client:?}");
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please reduce nesting. I think you can use combinators here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +765 to +769
client.write(
serde_json::to_string(&res)
.expect("serde_json::Value is always serializable")
.as_bytes(),
)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
client.write(
serde_json::to_string(&res)
.expect("serde_json::Value is always serializable")
.as_bytes(),
)?;
let res = serde_json::to_string(&res)
.expect("serde_json::Value is always serializable");
client.write(
res.as_bytes(),
)?;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@Davidson-Souza Davidson-Souza added the reliability Related to runtime reliability, stability and production readiness label Apr 14, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in Floresta Apr 14, 2026
@Davidson-Souza Davidson-Souza moved this from Backlog to Needs review in Floresta Apr 14, 2026
@Davidson-Souza
Copy link
Copy Markdown
Member

Is this enough to forbid clippy::unwrap_used in floresta-electrum?

@vatsalkeshav
Copy link
Copy Markdown
Author

vatsalkeshav commented Apr 15, 2026

Is this enough to forbid clippy::unwrap_used in floresta-electrum?

yes it is

Thanks for the review!

@vatsalkeshav
Copy link
Copy Markdown
Author

Did you tested this ? I dont think that the approach of just logging the error is efficient but i may be missing something about the electrum protocol...

Ill take a read about it and after that ill do a review...

Also, another strange thing are the unwrap_or calls, this default behavior is not desired, we need to know when this call fails.

fixed unwrap_or calls after review

error!("main loop receiver dropped: {e:?}");
break;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer the a if let Err(e) = self.message_transmitter.send(Message::Message((self.client_id, line))) instead of a whole match.

Theres an example on line 80 in this same file

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +105 to +108
match self.message_transmitter.send(Message::Disconnect(self.client_id)) {
Ok(_) => {}
Err(e) => error!("main loop receiver dropped: {e:?}"),
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +113 to +116
match self.message_transmitter.send(Message::Disconnect(self.client_id)) {
Ok(_) => {}
Err(e) => error!("main loop receiver dropped: {e:?}"),
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Err(super::error::Error::InvalidParams)
let Some(proof) = self.address_cache.get_merkle_proof(&tx_id) else {
return Err(super::error::Error::InvalidParams);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Err(super::error::Error::InvalidParams);
return Err(Error::InvalidParams);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +503 to +505
let Some(height) = self.address_cache.get_height(&tx_id) else {
return Err(super::error::Error::InvalidParams);
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let Some(height) = self.address_cache.get_height(&tx_id) else {
return Err(super::error::Error::InvalidParams);
};
let Some(height) = self.address_cache.get_height(&tx_id) else {
return Err(Error::InvalidParams);
};

let unconfirmed = self
.address_cache
.find_unconfirmed()
.map_err(|e| super::error::Error::WatchOnly(Box::new(e)))?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map_err(|e| super::error::Error::WatchOnly(Box::new(e)))?;
.map_err(|e| Error::WatchOnly(Box::new(e)))?;


pub async fn rebroadcast_mempool_transactions(&self) {
let unconfirmed = self.address_cache.find_unconfirmed().unwrap();
pub async fn rebroadcast_mempool_transactions(&self) -> Result<(), super::error::Error> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub async fn rebroadcast_mempool_transactions(&self) -> Result<(), super::error::Error> {
pub async fn rebroadcast_mempool_transactions(&self) -> Result<(), Error> {

let Ok(blocks) =
cfilters.match_any(_addresses, start_height, stop_height, self.chain.clone())
else {
error!("Could not match block filters");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
error!("Could not match block filters");
error!("Could not find matching block filters");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +880 to +890
match message_transmitter
.send(Message::NewClient((client.client_id, client)))
.expect("Main loop is broken");
id_count += 1;
{
Ok(_) => {
id_count += 1;
}
Err(e) => {
error!("main loop receiver dropped: {e:?}");
break;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +898 to +906
match message_transmitter.send(Message::NewClient((client.client_id, client))) {
Ok(_) => {
id_count += 1;
}
Err(e) => {
error!("main loop receiver dropped: {e:?}");
break;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… instead of pattern matching, shorter erro path
Copy link
Copy Markdown
Member

@jaoleal jaoleal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading a little about the Electrum protocol, it appears to leave to the server implementation to decide the error handling, similar to how the jsonrpc server should handle errors, following the spec of course.

Maybe we should follow another implementation as a standard ? Should we define our own standards for that as were doing on #831 ?

cc @Davidson-Souza @csgui

Vec::from_hex(&tx).map_err(|_| super::error::Error::InvalidParams)?;
let tx: Transaction =
deserialize(&hex).map_err(|_| super::error::Error::InvalidParams)?;
let hex: Vec<_> = Vec::from_hex(&tx).map_err(|_| Error::InvalidParams)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let hex: Vec<_> = Vec::from_hex(&tx).map_err(|_| Error::InvalidParams)?;
let hex = Vec::from_hex(&tx).map_err(|_| Error::InvalidParams)?;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +346 to +351
let Some(height) = self.address_cache.get_height(&prevout.txid) else {
return json_rpc_res!(request, []);
};
let Some(position) = self.address_cache.get_position(&prevout.txid) else {
return json_rpc_res!(request, []);
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be returning errors here too no ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this isnt focused on error handling but it should replace unwraps with errors atleast right ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense
but None means not cached, so @davidsonsouza suggested sending []
I'll try forcing the error with electrum daemon to be sure though

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, if it was a recommendation from Davidson you can just forget this, thank you for telling me this.

Comment on lines +346 to +348
let Some(height) = self.address_cache.get_height(&prevout.txid) else {
return json_rpc_res!(request, []);
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to double check what happens inside this for loop and, specially, with the return statement.

What's the expected behavior when a UTXO is found in the address cache but its height is missing? For example, utxos list has 5 UTXOs and one of them (the last one of 5) has a cache inconsistency (no height) what should the client receive? An empty list? A partial list of the 4 valid ones? An error? What is your code doing?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i looked into AddressCache - the tx gets saved before the utxo is added, and not removed later. so the none case shouldn't happen. [] seems to be right for the case when address is not yet cached as suggested by @davidsonsouza . An error could be added if something changes in future from that side

Copy link
Copy Markdown
Member

@Davidson-Souza Davidson-Souza Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@csgui Would you prefer returning the ones we can get? Something like:

let final_utxos = utxos
      .into_iter()
      .flat_map(|(prev_output, outpoint)| {
             let height = self.address_cache.get_height(&outpoint.txid)?;
             let position = self.address_cache.get_position(&outpoint.txid)?;

             Some(json!({
                   "height": height,
                    "tx_pos": position,
                     "tx_hash": outpoint.1.txid,
                      "value": prev_output.value
                }))
             })
       .collect::<Vec<_>>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, keep it simple returning only the consistent ones.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the suggest
Done!

return json_rpc_res!(request, []);
};
let Some(position) = self.address_cache.get_position(&prevout.txid) else {
return json_rpc_res!(request, []);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@csgui
Copy link
Copy Markdown
Collaborator

csgui commented Apr 16, 2026

After reading a little about the Electrum protocol, it appears to leave to the server implementation to decide the error handling, similar to how the jsonrpc server should handle errors, following the spec of course.

Maybe we should follow another implementation as a standard ?

Yep, a protocol is designed around independent server implementations. So error handling is minimally specified, perhaps only as an envelope. Since Electrum is heavily based on JSON-RPC, I’d expect the error handling to be similar to a RPC server implementation.

@vatsalkeshav
Copy link
Copy Markdown
Author

I guess an issue be opened for error handling as a separate pr
or do that with this one?

After reading a little about the Electrum protocol, it appears to leave to the server implementation to decide the error handling, similar to how the jsonrpc server should handle errors, following the spec of course.
Maybe we should follow another implementation as a standard ?

Yep, a protocol is designed around independent server implementations. So error handling is minimally specified, perhaps only as an envelope. Since Electrum is heavily based on JSON-RPC, I’d expect the error handling to be similar to a RPC server implementation.

@jaoleal
Copy link
Copy Markdown
Member

jaoleal commented Apr 22, 2026

I guess an issue be opened for error handling as a separate pr
or do that with this one?

Surely a follow up

@csgui
Copy link
Copy Markdown
Collaborator

csgui commented May 8, 2026

@vatsalkeshav anything else holding this PR? Please refresh my mind :-)

@csgui csgui added this to the Q2/2026 milestone May 8, 2026
@vatsalkeshav
Copy link
Copy Markdown
Author

@vatsalkeshav anything else holding this PR? Please refresh my mind :-)

tested affected paths with electrum - good to go!

@csgui
Copy link
Copy Markdown
Collaborator

csgui commented May 14, 2026

@vatsalkeshav, the error-handling work is now scoped as part of a SoB/2026 effort. Would you mind handing this PR over to the folks working on that? Thanks for your work here, we really appreciate it.

@vatsalkeshav
Copy link
Copy Markdown
Author

@vatsalkeshav, the error-handling work is now scoped as part of a SoB/2026 effort. Would you mind handing this PR over to the folks working on that? Thanks for your work here, we really appreciate it.

Sure!

@moisesPompilio
Copy link
Copy Markdown
Collaborator

Need rebase

@csgui csgui removed this from the Q2/2026 milestone May 14, 2026
@csgui csgui added this to the SoB/2026 milestone May 14, 2026
@csgui csgui moved this from Needs review to Backlog in Floresta May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reliability Related to runtime reliability, stability and production readiness

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

5 participants