1use anyswap_core::{
47 api::{
48 CancelRequest, CreateSwapRequest, ErrorCode, InfoResponse, RevealClaimRequest,
49 RevealPreimageRequest, SwapListQuery, SwapListResponse, SwapState,
50 },
51 auth::{AuthCore, AuthRequest, IDENTITY_PUBKEY_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER},
52 utils::now,
53};
54use bitcoin::{
55 hashes::{Hash, sha256},
56 secp256k1::Keypair,
57};
58use reqwest::{
59 Method,
60 header::{CONTENT_TYPE, HeaderMap, HeaderValue},
61};
62use serde::Deserialize;
63use thiserror::Error;
64use uuid::Uuid;
65
66use crate::wallet::{Wallet, WalletError};
67
68#[derive(Clone, Debug, Error)]
69pub enum ClientError {
70 #[error("transport: {0}")]
73 Transport(String),
74
75 #[error("server ({status}): {message}")]
76 Api {
77 status: u16,
78 message: String,
79 code: Option<ErrorCode>,
81 },
82
83 #[error("invalid auth header value: {0}")]
84 Header(String),
85
86 #[error("json: {0}")]
87 Json(String),
88
89 #[error("missing identity private key")]
90 MissingPrivateKey,
91}
92
93impl From<reqwest::Error> for ClientError {
94 fn from(e: reqwest::Error) -> Self {
95 Self::Transport(e.to_string())
96 }
97}
98
99impl From<reqwest::header::InvalidHeaderValue> for ClientError {
100 fn from(e: reqwest::header::InvalidHeaderValue) -> Self {
101 Self::Header(e.to_string())
102 }
103}
104
105impl From<serde_json::Error> for ClientError {
106 fn from(e: serde_json::Error) -> Self {
107 Self::Json(e.to_string())
108 }
109}
110
111impl ClientError {
112 pub fn transition_rejected(&self) -> bool {
115 matches!(
116 self,
117 Self::Api {
118 code: Some(ErrorCode::TransitionRejected),
119 ..
120 }
121 )
122 }
123}
124
125#[derive(Clone)]
127pub struct HttpClient {
128 http: reqwest::Client,
129 base_url: String,
130 identity: Option<Keypair>,
131}
132
133impl HttpClient {
134 pub fn new(base_url: &str, wallet: Option<&Wallet>) -> Result<Self, WalletError> {
138 let identity = wallet
139 .map(Wallet::identity_privkey)
140 .transpose()?
141 .map(|secret| Keypair::from_secret_key(anyswap_core::secp(), &secret));
142 Ok(Self {
143 http: reqwest::Client::new(),
144 base_url: base_url.trim_end_matches('/').to_string(),
145 identity,
146 })
147 }
148
149 fn identity(&self) -> Result<&Keypair, ClientError> {
150 self.identity.as_ref().ok_or(ClientError::MissingPrivateKey)
151 }
152
153 pub fn subscribe_url(&self) -> Result<String, ClientError> {
155 let keypair = self.identity()?;
156 let pubkey = keypair.public_key();
157 let user_id = sha256::Hash::hash(&pubkey.serialize()).to_string();
158 let target = format!("/v1/subscribe/{user_id}");
159 let timestamp = now() as i64;
160 let signature = AuthCore::sign_request(
161 &AuthRequest {
162 method: Method::GET.as_str(),
163 target: &target,
164 body: &[],
165 swap_id: "",
166 timestamp,
167 },
168 keypair,
169 );
170 let base = self.base_url.replacen("http", "ws", 1);
171 Ok(format!(
172 "{base}{target}?{IDENTITY_PUBKEY_HEADER}={}&{TIMESTAMP_HEADER}={timestamp}&{SIGNATURE_HEADER}={}",
173 hex::encode(pubkey.serialize()),
174 hex::encode(signature.as_ref()),
175 ))
176 }
177
178 pub async fn get_info(&self) -> Result<InfoResponse, ClientError> {
181 let resp = self
182 .http
183 .get(format!("{}/v1/info", self.base_url))
184 .send()
185 .await?;
186 Ok(check(resp).await?.json().await?)
187 }
188
189 pub async fn create_swap(&self, req: &CreateSwapRequest) -> Result<(), ClientError> {
192 let path = "/v1/swap";
193 let body = serde_json::to_vec(req)?;
194 let headers = self.prepare_headers(
195 &req.swap_id.to_string(),
196 &Method::POST,
197 path,
198 &body,
199 self.identity()?,
200 )?;
201
202 let resp = self
203 .http
204 .post(format!("{}{}", self.base_url, path))
205 .headers(headers)
206 .header(CONTENT_TYPE, "application/json")
207 .body(body)
208 .send()
209 .await?;
210 check(resp).await?;
211 Ok(())
212 }
213
214 pub async fn get_swap(&self, swap_id: Uuid) -> Result<SwapState, ClientError> {
217 let path = format!("/v1/swap/{swap_id}");
218 let headers = self.prepare_headers(
219 &swap_id.to_string(),
220 &Method::GET,
221 &path,
222 &[],
223 self.identity()?,
224 )?;
225
226 let resp = self
227 .http
228 .get(format!("{}{}", self.base_url, path))
229 .headers(headers)
230 .send()
231 .await?;
232 Ok(check(resp).await?.json().await?)
233 }
234
235 pub async fn list_swaps(&self, query: &SwapListQuery) -> Result<SwapListResponse, ClientError> {
237 let path = "/v1/swap";
238 let identity = self.identity()?;
239
240 let mut request = self
241 .http
242 .get(format!("{}{}", self.base_url, path))
243 .query(query)
244 .build()?;
245 let target = match request.url().query() {
246 Some(query) => format!("{path}?{query}"),
247 None => path.to_owned(),
248 };
249 let headers = self.prepare_headers("", &Method::GET, &target, &[], identity)?;
250 request.headers_mut().extend(headers);
251
252 let resp = self.http.execute(request).await?;
253 Ok(check(resp).await?.json().await?)
254 }
255
256 pub async fn cancel(&self, swap_id: Uuid, req: &CancelRequest) -> Result<(), ClientError> {
260 self.post_protected(swap_id, "/cancel", req).await
261 }
262
263 pub async fn reveal_claim(
267 &self,
268 swap_id: Uuid,
269 req: &RevealClaimRequest,
270 ) -> Result<(), ClientError> {
271 self.post_protected(swap_id, "/reveal/claim", req).await
272 }
273
274 pub async fn reveal_preimage(
279 &self,
280 swap_id: Uuid,
281 req: &RevealPreimageRequest,
282 ) -> Result<(), ClientError> {
283 self.post_protected(swap_id, "/reveal/preimage", req).await
284 }
285
286 async fn post_protected(
287 &self,
288 swap_id: Uuid,
289 path_suffix: &str,
290 req: &impl serde::Serialize,
291 ) -> Result<(), ClientError> {
292 let body = serde_json::to_vec(req)?;
293 let path = format!("/v1/swap/{swap_id}{path_suffix}");
294 let headers = self.prepare_headers(
295 &swap_id.to_string(),
296 &Method::POST,
297 &path,
298 &body,
299 self.identity()?,
300 )?;
301
302 let resp = self
303 .http
304 .post(format!("{}{}", self.base_url, path))
305 .headers(headers)
306 .header(CONTENT_TYPE, "application/json")
307 .body(body)
308 .send()
309 .await?;
310 check(resp).await?;
311 Ok(())
312 }
313
314 fn prepare_headers(
316 &self,
317 swap_id: &str,
318 method: &Method,
319 target: &str,
320 raw_body: &[u8],
321 identity_keypair: &Keypair,
322 ) -> Result<HeaderMap, ClientError> {
323 let timestamp = now() as i64;
324 let pubkey = identity_keypair.public_key();
325 let signature = AuthCore::sign_request(
326 &AuthRequest {
327 method: method.as_str(),
328 target,
329 body: raw_body,
330 swap_id,
331 timestamp,
332 },
333 identity_keypair,
334 );
335
336 let mut headers = HeaderMap::new();
337 headers.insert(
338 IDENTITY_PUBKEY_HEADER,
339 HeaderValue::from_str(&hex::encode(pubkey.serialize()))?,
340 );
341 headers.insert(
342 TIMESTAMP_HEADER,
343 HeaderValue::from_str(×tamp.to_string())?,
344 );
345 headers.insert(
346 SIGNATURE_HEADER,
347 HeaderValue::from_str(&hex::encode(signature.as_ref()))?,
348 );
349
350 Ok(headers)
351 }
352}
353
354async fn check(resp: reqwest::Response) -> Result<reqwest::Response, ClientError> {
355 let status = resp.status();
356 if status.is_success() {
357 return Ok(resp);
358 }
359
360 #[derive(Deserialize)]
361 struct ErrorBody {
362 message: String,
363 #[serde(default)]
364 code: Option<String>,
365 }
366
367 let (message, code) = match resp.json::<ErrorBody>().await {
368 Ok(body) => (body.message, body.code.and_then(|c| c.parse().ok())),
369 Err(_) => (
370 status
371 .canonical_reason()
372 .unwrap_or("unknown error")
373 .to_string(),
374 None,
375 ),
376 };
377 Err(ClientError::Api {
378 status: status.as_u16(),
379 message,
380 code,
381 })
382}
383
384#[cfg(test)]
385mod tests {
386 use std::{
387 collections::HashMap,
388 io::{Read, Write},
389 net::{TcpListener, TcpStream},
390 time::Duration,
391 };
392
393 use anyswap_core::types::{Chain, SwapNetwork};
394 use bitcoin::{
395 hashes::{Hash, sha256},
396 secp256k1::{SecretKey, schnorr::Signature},
397 };
398
399 use super::*;
400
401 struct RecordedRequest {
402 method: String,
403 path: String,
404 headers: HashMap<String, String>,
405 body: Vec<u8>,
406 }
407
408 fn test_keypair() -> Keypair {
409 let secret = SecretKey::from_slice(&[1; 32]).unwrap();
410 Keypair::from_secret_key(anyswap_core::secp(), &secret)
411 }
412
413 fn read_request(stream: &mut TcpStream) -> RecordedRequest {
414 stream
415 .set_read_timeout(Some(Duration::from_secs(5)))
416 .unwrap();
417
418 let mut data = Vec::new();
419 let mut buf = [0; 1024];
420 let header_end = loop {
421 let n = stream.read(&mut buf).unwrap();
422 assert!(n > 0, "client closed connection before sending headers");
423 data.extend_from_slice(&buf[..n]);
424
425 if let Some(pos) = data.windows(4).position(|window| window == b"\r\n\r\n") {
426 break pos;
427 }
428 };
429
430 let headers = String::from_utf8(data[..header_end].to_vec()).unwrap();
431 let content_length = headers
432 .lines()
433 .find_map(|line| {
434 let (name, value) = line.split_once(':')?;
435 name.eq_ignore_ascii_case("content-length")
436 .then(|| value.trim().parse::<usize>().unwrap())
437 })
438 .unwrap_or(0);
439 let body_start = header_end + 4;
440 let total_len = body_start + content_length;
441
442 while data.len() < total_len {
443 let n = stream.read(&mut buf).unwrap();
444 assert!(n > 0, "client closed connection before sending body");
445 data.extend_from_slice(&buf[..n]);
446 }
447
448 let mut lines = headers.lines();
449 let request_line = lines.next().unwrap();
450 let mut request_parts = request_line.split_whitespace();
451 let method = request_parts.next().unwrap().to_string();
452 let path = request_parts.next().unwrap().to_string();
453 let headers = lines
454 .filter_map(|line| {
455 let (name, value) = line.split_once(':')?;
456 Some((name.to_ascii_lowercase(), value.trim().to_string()))
457 })
458 .collect();
459
460 RecordedRequest {
461 method,
462 path,
463 headers,
464 body: data[body_start..total_len].to_vec(),
465 }
466 }
467
468 fn auth_header<'a>(headers: &'a HashMap<String, String>, name: &str) -> &'a str {
469 headers.get(name).map(String::as_str).unwrap()
470 }
471
472 fn signature_and_timestamp(headers: &HeaderMap) -> (Signature, i64) {
473 let signature_hex = headers.get(SIGNATURE_HEADER).unwrap().to_str().unwrap();
474 let timestamp = headers.get(TIMESTAMP_HEADER).unwrap().to_str().unwrap();
475
476 (
477 Signature::from_slice(&hex::decode(signature_hex).unwrap()).unwrap(),
478 timestamp.parse().unwrap(),
479 )
480 }
481
482 #[test]
483 fn prepare_headers_signs_the_target_including_its_query() {
484 let client = HttpClient::new("http://localhost:8083", None).unwrap();
485 let keypair = test_keypair();
486 let pubkey = keypair.public_key();
487 let target = "/v1/swap?user_id=alice&limit=20";
488
489 let headers = client
490 .prepare_headers("", &Method::GET, target, &[], &keypair)
491 .unwrap();
492 let (signature, timestamp) = signature_and_timestamp(&headers);
493 let signed = AuthRequest {
494 method: Method::GET.as_str(),
495 target,
496 body: &[],
497 swap_id: "",
498 timestamp,
499 };
500
501 assert!(AuthCore::validate_signature(
502 &AuthCore::request_digest(&signed, &pubkey),
503 &signature,
504 &pubkey
505 ));
506
507 let widened = AuthRequest {
508 target: "/v1/swap?user_id=alice&limit=500",
509 ..signed
510 };
511 assert!(!AuthCore::validate_signature(
512 &AuthCore::request_digest(&widened, &pubkey),
513 &signature,
514 &pubkey
515 ));
516 }
517
518 #[test]
519 fn prepare_headers_sets_verifiable_signature() {
520 let client = HttpClient::new("http://localhost:8083", None).unwrap();
521 let keypair = test_keypair();
522 let pubkey = keypair.public_key();
523
524 let headers = client
525 .prepare_headers("swap-1", &Method::GET, "/v1/swap/swap-1", &[], &keypair)
526 .unwrap();
527
528 assert_eq!(
529 headers.get(IDENTITY_PUBKEY_HEADER).unwrap(),
530 hex::encode(pubkey.serialize()).as_str()
531 );
532 let (signature, timestamp) = signature_and_timestamp(&headers);
533 assert!(timestamp > 0);
534
535 let signed = AuthRequest {
536 method: Method::GET.as_str(),
537 target: "/v1/swap/swap-1",
538 body: &[],
539 swap_id: "swap-1",
540 timestamp,
541 };
542
543 assert!(AuthCore::validate_signature(
544 &AuthCore::request_digest(&signed, &pubkey),
545 &signature,
546 &pubkey
547 ));
548 }
549
550 #[tokio::test]
551 async fn create_swap_signature_verifies_against_body_swap_id() {
552 let swap_id = Uuid::from_u128(0x1234567890abcdef1234567890abcdef);
553 let (wallet, _) = Wallet::generate(SwapNetwork::Regtest).unwrap();
554 let pubkey =
555 Keypair::from_secret_key(anyswap_core::secp(), &wallet.identity_privkey().unwrap())
556 .public_key();
557 let req = CreateSwapRequest {
558 protocol_version: "3.0.0".into(),
559 swap_id,
560 user_id: sha256::Hash::hash(&pubkey.serialize()).to_string(),
561 source_chain: Some(Chain::Bitcoin),
562 source_asset_id: Some(anyswap_core::types::NATIVE_ASSET.to_string()),
563 dest_chain: None,
564 dest_asset_id: None,
565 receive_amount: 10_000,
566 send_amount: 11_000,
567 quote_id: None,
568 claim_invoice: Some("lnbc1test".to_string()),
569 source_refund_pubkey: Some(hex::encode(pubkey.serialize())),
570 source_blinding_pubkey: None,
571 dest_claim_pubkey: None,
572 dest_blinding_pubkey: None,
573 user_claim_address: None,
574 user_refund_address: None,
575 payment_hash: None,
576 webhook: None,
577 };
578 let expected_body = serde_json::to_vec(&req).unwrap();
579 let expected_user_id = req.user_id.clone();
580
581 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
582 let base_url = format!("http://{}", listener.local_addr().unwrap());
583 let server = std::thread::spawn(move || {
584 let (mut stream, _) = listener.accept().unwrap();
585 let request = read_request(&mut stream);
586
587 assert_eq!(request.method, Method::POST.as_str());
588 assert_eq!(request.path, "/v1/swap");
589 assert_eq!(request.body, expected_body);
590
591 let auth_params = anyswap_core::auth::AuthParams::from_fields(
592 auth_header(&request.headers, IDENTITY_PUBKEY_HEADER),
593 auth_header(&request.headers, TIMESTAMP_HEADER),
594 auth_header(&request.headers, SIGNATURE_HEADER),
595 )
596 .unwrap();
597 assert!(AuthCore::validate_pubkey(
598 &auth_params.pubkey,
599 &expected_user_id
600 ));
601
602 let signed = AuthRequest {
603 method: Method::POST.as_str(),
604 target: &request.path,
605 body: &request.body,
606 swap_id: &swap_id.to_string(),
607 timestamp: auth_params.timestamp,
608 };
609 assert!(AuthCore::validate_signature(
610 &AuthCore::request_digest(&signed, &auth_params.pubkey),
611 &auth_params.signature,
612 &auth_params.pubkey
613 ));
614
615 let wrong_swap_id = AuthRequest {
616 swap_id: "",
617 ..signed
618 };
619 assert!(!AuthCore::validate_signature(
620 &AuthCore::request_digest(&wrong_swap_id, &auth_params.pubkey),
621 &auth_params.signature,
622 &auth_params.pubkey
623 ));
624
625 stream
626 .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n")
627 .unwrap();
628 });
629
630 let client = HttpClient::new(&base_url, Some(&wallet)).unwrap();
631 let result = client.create_swap(&req).await;
632 let server_result = server.join();
633
634 assert!(result.is_ok(), "{result:?}");
635 server_result.unwrap();
636 }
637}