Skip to content

Commit 38386fe

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 Signed-off-by: Elias Rohrer <dev@tnull.de>
1 parent 22ad6bc commit 38386fe

2 files changed

Lines changed: 87 additions & 5 deletions

File tree

src/raw_client.rs

Lines changed: 53 additions & 4 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,30 @@ 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+
554603
fn _reader_thread(&self, until_message: Option<usize>) -> Result<serde_json::Value, Error> {
555604
let mut raw_resp = String::new();
556605
let resp = match self.buf_reader.try_lock() {

src/types.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub enum Param {
3333
Bool(bool),
3434
/// Bytes array parameter
3535
Bytes(Vec<u8>),
36+
/// String array parameter
37+
StringVec(Vec<String>),
3638
}
3739

3840
#[derive(Serialize, Clone)]
@@ -202,7 +204,38 @@ pub struct ServerFeaturesRes {
202204
pub pruning: Option<i64>,
203205
}
204206

205-
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
207+
/// Response to a [`server_version`](../client/struct.Client.html#method.server_version) request.
208+
///
209+
/// This is returned as an array of two strings: `[server_software_version, protocol_version]`.
210+
#[derive(Clone, Debug)]
211+
pub struct ServerVersionRes {
212+
/// Server software version string (e.g., "ElectrumX 1.18.0").
213+
pub server_software_version: String,
214+
/// Negotiated protocol version (e.g., "1.6").
215+
pub protocol_version: String,
216+
}
217+
218+
impl<'de> Deserialize<'de> for ServerVersionRes {
219+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
220+
where
221+
D: de::Deserializer<'de>,
222+
{
223+
let arr: Vec<String> = Vec::deserialize(deserializer)?;
224+
let mut iter = arr.into_iter();
225+
let server_software_version = iter.next().ok_or_else(|| {
226+
de::Error::custom("expected server_software_version as first element")
227+
})?;
228+
let protocol_version = iter
229+
.next()
230+
.ok_or_else(|| de::Error::custom("expected protocol_version as second element"))?;
231+
Ok(ServerVersionRes {
232+
server_software_version,
233+
protocol_version,
234+
})
235+
}
236+
}
237+
238+
/// Response to a [`block_headers`](../client/struct.Client.html#method.block_headers) request.
206239
#[derive(Clone, Debug, Deserialize)]
207240
pub struct GetHeadersRes {
208241
/// Maximum number of headers returned in a single response.

0 commit comments

Comments
 (0)