Skip to content

Commit 39d262b

Browse files
committed
feat: Automatically negotiate protocol version on connect
Send `server.version` as the first message after establishing a connection, as required by Electrum protocol v1.6. The negotiated protocol version is stored in `RawClient` and can be retrieved via the new `protocol_version()` method. - Add `CLIENT_NAME`, `PROTOCOL_VERSION_MIN`, `PROTOCOL_VERSION_MAX` constants - Add `protocol_version` field to `RawClient` to store negotiated version - All constructors (`new`, `new_ssl`, `new_ssl_from_stream`, `new_proxy`) now automatically call `server.version` after connecting - The `server_version()` method also updates the stored version when called This is a breaking change as the constructors now return errors if protocol negotiation fails. Co-Authored-By: Claude Code AI
1 parent 4d23832 commit 39d262b

File tree

1 file changed

+63
-5
lines changed

1 file changed

+63
-5
lines changed

src/raw_client.rs

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ use crate::api::ElectrumApi;
4545
use crate::batch::Batch;
4646
use crate::types::*;
4747

48+
/// Client name sent to the server during protocol version negotiation.
49+
pub const CLIENT_NAME: &str = "rust-electrum-client";
50+
51+
/// Minimum protocol version supported by this client.
52+
pub const PROTOCOL_VERSION_MIN: &str = "1.4";
53+
54+
/// Maximum protocol version supported by this client.
55+
pub const PROTOCOL_VERSION_MAX: &str = "1.6";
56+
4857
macro_rules! impl_batch_call {
4958
( $self:expr, $data:expr, $call:ident ) => {{
5059
impl_batch_call!($self, $data, $call, )
@@ -142,6 +151,9 @@ where
142151
headers: Mutex<VecDeque<RawHeaderNotification>>,
143152
script_notifications: Mutex<HashMap<ScriptHash, VecDeque<ScriptStatus>>>,
144153

154+
/// The protocol version negotiated with the server via `server.version`.
155+
protocol_version: Mutex<Option<String>>,
156+
145157
#[cfg(feature = "debug-calls")]
146158
calls: AtomicUsize,
147159
}
@@ -163,6 +175,8 @@ where
163175
headers: Mutex::new(VecDeque::new()),
164176
script_notifications: Mutex::new(HashMap::new()),
165177

178+
protocol_version: Mutex::new(None),
179+
166180
#[cfg(feature = "debug-calls")]
167181
calls: AtomicUsize::new(0),
168182
}
@@ -173,6 +187,9 @@ where
173187
pub type ElectrumPlaintextStream = TcpStream;
174188
impl RawClient<ElectrumPlaintextStream> {
175189
/// Creates a new plaintext client and tries to connect to `socket_addr`.
190+
///
191+
/// Automatically negotiates the protocol version with the server using
192+
/// `server.version` as required by the Electrum protocol.
176193
pub fn new<A: ToSocketAddrs>(
177194
socket_addrs: A,
178195
timeout: Option<Duration>,
@@ -187,7 +204,9 @@ impl RawClient<ElectrumPlaintextStream> {
187204
None => TcpStream::connect(socket_addrs)?,
188205
};
189206

190-
Ok(stream.into())
207+
let client: Self = stream.into();
208+
client.negotiate_protocol_version()?;
209+
Ok(client)
191210
}
192211
}
193212

@@ -285,7 +304,9 @@ impl RawClient<ElectrumSslStream> {
285304
.connect(&domain, stream)
286305
.map_err(Error::SslHandshakeError)?;
287306

288-
Ok(stream.into())
307+
let client: Self = stream.into();
308+
client.negotiate_protocol_version()?;
309+
Ok(client)
289310
}
290311
}
291312

@@ -466,7 +487,9 @@ impl RawClient<ElectrumSslStream> {
466487
.map_err(Error::CouldNotCreateConnection)?;
467488
let stream = StreamOwned::new(session, tcp_stream);
468489

469-
Ok(stream.into())
490+
let client: Self = stream.into();
491+
client.negotiate_protocol_version()?;
492+
Ok(client)
470493
}
471494
}
472495

@@ -496,7 +519,9 @@ impl RawClient<ElectrumProxyStream> {
496519
stream.get_mut().set_read_timeout(timeout)?;
497520
stream.get_mut().set_write_timeout(timeout)?;
498521

499-
Ok(stream.into())
522+
let client: Self = stream.into();
523+
client.negotiate_protocol_version()?;
524+
Ok(client)
500525
}
501526

502527
#[cfg(any(
@@ -551,6 +576,35 @@ impl<S: Read + Write> RawClient<S> {
551576
// self._reader_thread(None).map(|_| ())
552577
// }
553578

579+
/// Negotiates the protocol version with the server.
580+
///
581+
/// This sends `server.version` as the first message and stores the negotiated
582+
/// protocol version. Called automatically by constructors.
583+
fn negotiate_protocol_version(&self) -> Result<(), Error> {
584+
let version_range = vec![
585+
PROTOCOL_VERSION_MIN.to_string(),
586+
PROTOCOL_VERSION_MAX.to_string(),
587+
];
588+
let req = Request::new_id(
589+
self.last_id.fetch_add(1, Ordering::SeqCst),
590+
"server.version",
591+
vec![
592+
Param::String(CLIENT_NAME.to_string()),
593+
Param::StringVec(version_range),
594+
],
595+
);
596+
let result = self.call(req)?;
597+
let response: ServerVersionRes = serde_json::from_value(result)?;
598+
599+
*self.protocol_version.lock()? = Some(response.protocol_version);
600+
Ok(())
601+
}
602+
603+
/// Returns the protocol version negotiated with the server, if available.
604+
pub fn protocol_version(&self) -> Result<Option<String>, Error> {
605+
Ok(self.protocol_version.lock()?.clone())
606+
}
607+
554608
fn _reader_thread(&self, until_message: Option<usize>) -> Result<serde_json::Value, Error> {
555609
let mut raw_resp = String::new();
556610
let resp = match self.buf_reader.try_lock() {
@@ -1220,8 +1274,12 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
12201274
],
12211275
);
12221276
let result = self.call(req)?;
1277+
let response: ServerVersionRes = serde_json::from_value(result)?;
12231278

1224-
Ok(serde_json::from_value(result)?)
1279+
// Store the negotiated protocol version
1280+
*self.protocol_version.lock()? = Some(response.protocol_version.clone());
1281+
1282+
Ok(response)
12251283
}
12261284

12271285
fn ping(&self) -> Result<(), Error> {

0 commit comments

Comments
 (0)