diff --git a/README.md b/README.md index b6e57778..e7f88327 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,7 @@ The node currently exposes the following APIs: - `/getassetmedia` (POST) - `/getchannelid` (POST) - `/getpayment` (POST) +- `/getswap` (POST) - `/init` (POST) - `/invoicestatus` (POST) - `/issueassetcfa` (POST) diff --git a/openapi.yaml b/openapi.yaml index 5c1ac0df..dc7dc8eb 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -368,6 +368,24 @@ paths: application/json: schema: $ref: '#/components/schemas/GetPaymentResponse' + /getswap: + post: + tags: + - Swaps + summary: Get a swap + description: Get a swap by its payment hash + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GetSwapRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/GetSwapResponse' /init: post: tags: @@ -1433,6 +1451,20 @@ components: properties: payment: $ref: '#/components/schemas/Payment' + GetSwapRequest: + type: object + properties: + payment_hash: + type: string + example: 5ca5d81b482b4015e7b14df7a27fe0a38c226273604ffd3b008b752571811938 + taker: + type: boolean + example: false + GetSwapResponse: + type: object + properties: + swap: + $ref: '#/components/schemas/Swap' HTLCStatus: type: string enum: diff --git a/src/error.rs b/src/error.rs index b79178bc..ebf39972 100644 --- a/src/error.rs +++ b/src/error.rs @@ -246,6 +246,9 @@ pub enum APIError { #[error("Recipient ID already used")] RecipientIDAlreadyUsed, + #[error("Swap not found: {0}")] + SwapNotFound(String), + #[error("Sync needed")] SyncNeeded, @@ -459,6 +462,7 @@ impl IntoResponse for APIError { | APIError::OpenChannelInProgress | APIError::PaymentNotFound(_) | APIError::RecipientIDAlreadyUsed + | APIError::SwapNotFound(_) | APIError::SyncNeeded | APIError::TemporaryChannelIdAlreadyUsed | APIError::UnknownContractId diff --git a/src/main.rs b/src/main.rs index eebb4af7..ff0e7ec5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,7 @@ use crate::routes::{ address, asset_balance, asset_metadata, backup, btc_balance, change_password, check_indexer_url, check_proxy_endpoint, close_channel, connect_peer, create_utxos, decode_ln_invoice, decode_rgb_invoice, disconnect_peer, estimate_fee, fail_transfers, - get_asset_media, get_channel_id, get_payment, init, invoice_status, issue_asset_cfa, + get_asset_media, get_channel_id, get_payment, get_swap, init, invoice_status, issue_asset_cfa, issue_asset_nia, issue_asset_uda, keysend, list_assets, list_channels, list_payments, list_peers, list_swaps, list_transactions, list_transfers, list_unspents, ln_invoice, lock, maker_execute, maker_init, network_info, node_info, open_channel, post_asset_media, @@ -120,6 +120,7 @@ pub(crate) async fn app(args: LdkUserInfo) -> Result<(Router, Arc), Ap .route("/getassetmedia", post(get_asset_media)) .route("/getchannelid", post(get_channel_id)) .route("/getpayment", post(get_payment)) + .route("/getswap", post(get_swap)) .route("/init", post(init)) .route("/invoicestatus", post(invoice_status)) .route("/issueassetcfa", post(issue_asset_cfa)) diff --git a/src/routes.rs b/src/routes.rs index c7c42bc1..db90aa3c 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -543,6 +543,17 @@ pub(crate) struct GetPaymentResponse { pub(crate) payment: Payment, } +#[derive(Deserialize, Serialize)] +pub(crate) struct GetSwapRequest { + pub(crate) payment_hash: String, + pub(crate) taker: bool, +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct GetSwapResponse { + pub(crate) swap: Swap, +} + #[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] pub(crate) enum HTLCStatus { Pending, @@ -954,7 +965,7 @@ pub(crate) struct SignMessageResponse { pub(crate) signed_message: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] pub(crate) struct Swap { pub(crate) qty_from: u64, pub(crate) qty_to: u64, @@ -968,7 +979,7 @@ pub(crate) struct Swap { pub(crate) completed_at: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub(crate) enum SwapStatus { Waiting, Pending, @@ -2210,6 +2221,67 @@ pub(crate) async fn list_swaps( })) } +pub(crate) async fn get_swap( + State(state): State>, + WithRejection(Json(payload), _): WithRejection, APIError>, +) -> Result, APIError> { + let unlocked_state = state.check_unlocked().await?.clone().unwrap(); + + let payment_hash_vec = hex_str_to_vec(&payload.payment_hash); + if payment_hash_vec.is_none() || payment_hash_vec.as_ref().unwrap().len() != 32 { + return Err(APIError::InvalidPaymentHash(payload.payment_hash)); + } + let requested_ph = PaymentHash(payment_hash_vec.unwrap().try_into().unwrap()); + + let map_swap = |payment_hash: &PaymentHash, swap_data: &SwapData, taker: bool| { + let mut status = swap_data.status.clone(); + if status == SwapStatus::Waiting && get_current_timestamp() > swap_data.swap_info.expiry { + status = SwapStatus::Expired; + } else if status == SwapStatus::Pending + && get_current_timestamp() > swap_data.initiated_at.unwrap() + 86400 + { + status = SwapStatus::Failed; + } + if status != swap_data.status { + if taker { + unlocked_state.update_taker_swap_status(payment_hash, status.clone()); + } else { + unlocked_state.update_maker_swap_status(payment_hash, status.clone()); + } + } + Swap { + payment_hash: payment_hash.to_string(), + qty_from: swap_data.swap_info.qty_from, + qty_to: swap_data.swap_info.qty_to, + from_asset: swap_data.swap_info.from_asset.map(|c| c.to_string()), + to_asset: swap_data.swap_info.to_asset.map(|c| c.to_string()), + status, + requested_at: swap_data.requested_at, + initiated_at: swap_data.initiated_at, + expires_at: swap_data.swap_info.expiry, + completed_at: swap_data.completed_at, + } + }; + + if payload.taker { + let taker_swaps = unlocked_state.taker_swaps(); + if let Some(sd) = taker_swaps.get(&requested_ph) { + return Ok(Json(GetSwapResponse { + swap: map_swap(&requested_ph, sd, true), + })); + } + } else { + let maker_swaps = unlocked_state.maker_swaps(); + if let Some(sd) = maker_swaps.get(&requested_ph) { + return Ok(Json(GetSwapResponse { + swap: map_swap(&requested_ph, sd, false), + })); + } + } + + Err(APIError::SwapNotFound(payload.payment_hash)) +} + pub(crate) async fn list_transactions( State(state): State>, WithRejection(Json(payload), _): WithRejection, APIError>, diff --git a/src/test/mod.rs b/src/test/mod.rs index 3c4de56d..1192c18a 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -24,18 +24,19 @@ use crate::routes::{ DecodeLNInvoiceResponse, DecodeRGBInvoiceRequest, DecodeRGBInvoiceResponse, DisconnectPeerRequest, EmptyResponse, FailTransfersRequest, FailTransfersResponse, GetAssetMediaRequest, GetAssetMediaResponse, GetChannelIdRequest, GetChannelIdResponse, - GetPaymentRequest, GetPaymentResponse, HTLCStatus, InitRequest, InitResponse, InvoiceStatus, - InvoiceStatusRequest, InvoiceStatusResponse, IssueAssetCFARequest, IssueAssetCFAResponse, - IssueAssetNIARequest, IssueAssetNIAResponse, IssueAssetUDARequest, IssueAssetUDAResponse, - KeysendRequest, KeysendResponse, LNInvoiceRequest, LNInvoiceResponse, ListAssetsRequest, - ListAssetsResponse, ListChannelsResponse, ListPaymentsResponse, ListPeersResponse, - ListSwapsResponse, ListTransactionsRequest, ListTransactionsResponse, ListTransfersRequest, - ListTransfersResponse, ListUnspentsRequest, ListUnspentsResponse, MakerExecuteRequest, - MakerInitRequest, MakerInitResponse, NetworkInfoResponse, NodeInfoResponse, OpenChannelRequest, - OpenChannelResponse, Payment, Peer, PostAssetMediaResponse, RefreshRequest, RestoreRequest, - RgbInvoiceRequest, RgbInvoiceResponse, SendAssetRequest, SendAssetResponse, SendBtcRequest, - SendBtcResponse, SendPaymentRequest, SendPaymentResponse, SwapStatus, TakerRequest, - Transaction, Transfer, UnlockRequest, Unspent, + GetPaymentRequest, GetPaymentResponse, GetSwapRequest, GetSwapResponse, HTLCStatus, + InitRequest, InitResponse, InvoiceStatus, InvoiceStatusRequest, InvoiceStatusResponse, + IssueAssetCFARequest, IssueAssetCFAResponse, IssueAssetNIARequest, IssueAssetNIAResponse, + IssueAssetUDARequest, IssueAssetUDAResponse, KeysendRequest, KeysendResponse, LNInvoiceRequest, + LNInvoiceResponse, ListAssetsRequest, ListAssetsResponse, ListChannelsResponse, + ListPaymentsResponse, ListPeersResponse, ListSwapsResponse, ListTransactionsRequest, + ListTransactionsResponse, ListTransfersRequest, ListTransfersResponse, ListUnspentsRequest, + ListUnspentsResponse, MakerExecuteRequest, MakerInitRequest, MakerInitResponse, + NetworkInfoResponse, NodeInfoResponse, OpenChannelRequest, OpenChannelResponse, Payment, Peer, + PostAssetMediaResponse, RefreshRequest, RestoreRequest, RgbInvoiceRequest, RgbInvoiceResponse, + SendAssetRequest, SendAssetResponse, SendBtcRequest, SendBtcResponse, SendPaymentRequest, + SendPaymentResponse, Swap, SwapStatus, TakerRequest, Transaction, Transfer, UnlockRequest, + Unspent, }; use crate::utils::{hex_str_to_vec, ELECTRUM_URL_REGTEST, PROXY_ENDPOINT_LOCAL}; @@ -809,6 +810,26 @@ async fn list_swaps(node_address: SocketAddr) -> ListSwapsResponse { _check_response_is_ok(res).await.json().await.unwrap() } +async fn get_swap(node_address: SocketAddr, payment_hash: &str, taker: bool) -> Swap { + println!("getting swap with payment hash {payment_hash} for node {node_address}"); + let payload = GetSwapRequest { + payment_hash: payment_hash.to_string(), + taker, + }; + let res = reqwest::Client::new() + .post(format!("http://{}/getswap", node_address)) + .json(&payload) + .send() + .await + .unwrap(); + _check_response_is_ok(res) + .await + .json::() + .await + .unwrap() + .swap +} + async fn list_transactions(node_address: SocketAddr) -> Vec { println!("listing transactions for node {node_address}"); let payload = ListTransactionsRequest { skip_sync: false }; diff --git a/src/test/swap_roundtrip_assets.rs b/src/test/swap_roundtrip_assets.rs index 46e6cbbe..4c7f6478 100644 --- a/src/test/swap_roundtrip_assets.rs +++ b/src/test/swap_roundtrip_assets.rs @@ -93,6 +93,8 @@ async fn swap_roundtrip_assets() { assert_eq!(swap_maker.to_asset, Some(asset_id_1.clone())); assert_eq!(swap_maker.payment_hash, maker_init_response.payment_hash); assert_eq!(swap_maker.status, SwapStatus::Waiting); + let swap_maker_single = get_swap(maker_addr, &maker_init_response.payment_hash, false).await; + assert_eq!(swap_maker_single, *swap_maker); let swaps_taker = list_swaps(taker_addr).await; assert!(swaps_taker.maker.is_empty()); assert_eq!(swaps_taker.taker.len(), 1);