1use axum::{
2 extract::{Json, Path, Query, State},
3 http::status::StatusCode,
4 response::IntoResponse,
5};
6use futures::TryFutureExt;
7use libp2p_identity::PeerId;
8use serde::{Deserialize, Serialize};
9use serde_with::{serde_as, DisplayFromStr};
10use std::sync::Arc;
11use tracing::warn;
12
13use hopr_crypto_types::types::Hash;
14use hopr_lib::{
15 errors::{HoprLibError, HoprStatusError},
16 Address, AsUnixTimestamp, Balance, BalanceType, ChainActionsError, ChannelEntry, ChannelStatus, Hopr, ToHex,
17};
18
19use crate::{
20 checksum_address_serializer,
21 types::{HoprIdentifier, PeerOrAddress},
22 ApiError, ApiErrorStatus, InternalState, BASE_PATH,
23};
24
25#[serde_as]
26#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
27#[serde(rename_all = "camelCase")]
28pub(crate) struct NodeChannel {
29 #[serde_as(as = "DisplayFromStr")]
30 #[schema(value_type = String)]
31 id: Hash,
32 #[serde(serialize_with = "checksum_address_serializer")]
33 #[schema(value_type = String)]
34 peer_address: Address,
35 #[serde_as(as = "DisplayFromStr")]
36 #[schema(value_type = String)]
37 status: ChannelStatus,
38 balance: String,
39}
40
41#[serde_as]
42#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
43#[schema(example = json!({
44 "balance": "10000000000000000000",
45 "channelEpoch": 1,
46 "channelId": "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f",
47 "closureTime": 0,
48 "destinationAddress": "0x188c4462b75e46f0c7262d7f48d182447b93a93c",
49 "destinationPeerId": "12D3KooWPWD5P5ZzMRDckgfVaicY5JNoo7JywGotoAv17d7iKx1z",
50 "sourceAddress": "0x07eaf07d6624f741e04f4092a755a9027aaab7f6",
51 "sourcePeerId": "12D3KooWJmLm8FnBfvYQ5BAZ5qcYBxQFFBzAAEYUBUNJNE8cRsYS",
52 "status": "Open",
53 "ticketIndex": 0
54 }))]
55#[serde(rename_all = "camelCase")]
56pub(crate) struct ChannelInfoResponse {
57 #[serde_as(as = "DisplayFromStr")]
58 #[schema(value_type = String)]
59 channel_id: Hash,
60 #[serde(serialize_with = "checksum_address_serializer")]
61 #[schema(value_type = String)]
62 source_address: Address,
63 #[serde(serialize_with = "checksum_address_serializer")]
64 #[schema(value_type = String)]
65 destination_address: Address,
66 source_peer_id: String,
67 destination_peer_id: String,
68 balance: String,
69 #[serde_as(as = "DisplayFromStr")]
70 #[schema(value_type = String)]
71 status: ChannelStatus,
72 ticket_index: u32,
73 channel_epoch: u32,
74 closure_time: u64,
75}
76
77#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
79#[schema(example = json!({
80 "all": [{
81 "channelId": "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f",
82 "sourceAddress": "0x07eaf07d6624f741e04f4092a755a9027aaab7f6",
83 "destinationAddress": "0x188c4462b75e46f0c7262d7f48d182447b93a93c",
84 "sourcePeerId": "12D3KooWJmLm8FnBfvYQ5BAZ5qcYBxQFFBzAAEYUBUNJNE8cRsYS",
85 "destinationPeerId": "12D3KooWPWD5P5ZzMRDckgfVaicY5JNoo7JywGotoAv17d7iKx1z",
86 "balance": "10000000000000000000",
87 "status": "Open",
88 "ticketIndex": 0,
89 "channelEpoch": 1,
90 "closureTime": 0
91 }],
92 "incoming": [],
93 "outgoing": [{
94 "balance": "10000000000000000010",
95 "id": "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f",
96 "peerAddress": "0x188c4462b75e46f0c7262d7f48d182447b93a93c",
97 "status": "Open"
98 }]
99 }))]
100pub(crate) struct NodeChannelsResponse {
101 incoming: Vec<NodeChannel>,
103 outgoing: Vec<NodeChannel>,
105 all: Vec<ChannelInfoResponse>,
107}
108
109async fn query_topology_info(channel: &ChannelEntry, node: &Hopr) -> Result<ChannelInfoResponse, HoprLibError> {
110 Ok(ChannelInfoResponse {
111 channel_id: channel.get_id(),
112 source_address: channel.source,
113 destination_address: channel.destination,
114 source_peer_id: node
115 .chain_key_to_peerid(&channel.source)
116 .await?
117 .map(|v| PeerId::to_string(&v))
118 .unwrap_or_else(|| {
119 warn!(address = %channel.source, "failed to map address to PeerId", );
120 "<FAILED_TO_MAP_THE_PEERID>".into()
121 }),
122 destination_peer_id: node
123 .chain_key_to_peerid(&channel.destination)
124 .await?
125 .map(|v| PeerId::to_string(&v))
126 .unwrap_or_else(|| {
127 warn!(address = %channel.destination, "failed to map address to PeerId", );
128 "<FAILED_TO_MAP_THE_PEERID>".into()
129 }),
130 balance: channel.balance.amount().to_string(),
131 status: channel.status,
132 ticket_index: channel.ticket_index.as_u32(),
133 channel_epoch: channel.channel_epoch.as_u32(),
134 closure_time: channel
135 .closure_time_at()
136 .map(|ct| ct.as_unix_timestamp().as_secs())
137 .unwrap_or_default(),
138 })
139}
140
141#[derive(Debug, Default, Copy, Clone, Deserialize, utoipa::IntoParams, utoipa::ToSchema)]
143#[into_params(parameter_in = Query)]
144#[serde(default, rename_all = "camelCase")]
145pub(crate) struct ChannelsQueryRequest {
146 #[schema(required = false)]
148 #[serde(default)]
149 including_closed: bool,
150 #[schema(required = false)]
152 #[serde(default)]
153 full_topology: bool,
154}
155
156#[utoipa::path(
159 get,
160 path = const_format::formatcp!("{BASE_PATH}/channels"),
161 params(ChannelsQueryRequest),
162 responses(
163 (status = 200, description = "Channels fetched successfully", body = NodeChannelsResponse),
164 (status = 401, description = "Invalid authorization token.", body = ApiError),
165 (status = 422, description = "Unknown failure", body = ApiError)
166 ),
167 security(
168 ("api_token" = []),
169 ("bearer_token" = [])
170 ),
171 tag = "Channels",
172 )]
173pub(super) async fn list_channels(
174 Query(query): Query<ChannelsQueryRequest>,
175 State(state): State<Arc<InternalState>>,
176) -> impl IntoResponse {
177 let hopr = state.hopr.clone();
178
179 if query.full_topology {
180 let hopr_clone = hopr.clone();
181 let topology = hopr
182 .all_channels()
183 .and_then(|channels| async move {
184 futures::future::try_join_all(channels.iter().map(|c| query_topology_info(c, hopr_clone.as_ref())))
185 .await
186 })
187 .await;
188
189 match topology {
190 Ok(all) => (
191 StatusCode::OK,
192 Json(NodeChannelsResponse {
193 incoming: vec![],
194 outgoing: vec![],
195 all,
196 }),
197 )
198 .into_response(),
199 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
200 }
201 } else {
202 let channels = hopr
203 .channels_to(&hopr.me_onchain())
204 .and_then(|incoming| async {
205 let outgoing = hopr.channels_from(&hopr.me_onchain()).await?;
206 Ok((incoming, outgoing))
207 })
208 .await;
209
210 match channels {
211 Ok((incoming, outgoing)) => {
212 let channel_info = NodeChannelsResponse {
213 incoming: incoming
214 .into_iter()
215 .filter(|c| query.including_closed || c.status != ChannelStatus::Closed)
216 .map(|c| NodeChannel {
217 id: c.get_id(),
218 peer_address: c.source,
219 status: c.status,
220 balance: c.balance.amount().to_string(),
221 })
222 .collect(),
223 outgoing: outgoing
224 .into_iter()
225 .filter(|c| query.including_closed || c.status != ChannelStatus::Closed)
226 .map(|c| NodeChannel {
227 id: c.get_id(),
228 peer_address: c.destination,
229 status: c.status,
230 balance: c.balance.amount().to_string(),
231 })
232 .collect(),
233 all: vec![],
234 };
235
236 (StatusCode::OK, Json(channel_info)).into_response()
237 }
238 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
239 }
240 }
241}
242
243#[serde_as]
244#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
245#[serde(rename_all = "camelCase")]
246#[schema(example = json!({
247 "amount": "10",
248 "destination": "0xa8194d36e322592d4c707b70dbe96121f5c74c64"
249 }))]
250pub(crate) struct OpenChannelBodyRequest {
251 #[serde_as(as = "DisplayFromStr")]
253 #[schema(value_type = String)]
254 destination: PeerOrAddress,
255 amount: String,
257}
258
259#[serde_as]
260#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
261#[schema(example = json!({
262 "channelId": "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f",
263 "transactionReceipt": "0x5181ac24759b8e01b3c932e4636c3852f386d17517a8dfc640a5ba6f2258f29c"
264 }))]
265#[serde(rename_all = "camelCase")]
266pub(crate) struct OpenChannelResponse {
267 #[serde_as(as = "DisplayFromStr")]
269 #[schema(value_type = String)]
270 channel_id: Hash,
271 #[serde_as(as = "DisplayFromStr")]
273 #[schema(value_type = String)]
274 transaction_receipt: Hash,
275}
276
277#[utoipa::path(
279 post,
280 path = const_format::formatcp!("{BASE_PATH}/channels"),
281 request_body(
282 content = OpenChannelBodyRequest,
283 description = "Open channel request specification: on-chain address of the counterparty and the initial HOPR token stake.",
284 content_type = "application/json"),
285 responses(
286 (status = 201, description = "Channel successfully opened.", body = OpenChannelResponse),
287 (status = 400, description = "Invalid counterparty address or stake amount.", body = ApiError),
288 (status = 401, description = "Invalid authorization token.", body = ApiError),
289 (status = 403, description = "Failed to open the channel because of insufficient HOPR balance or allowance.", body = ApiError),
290 (status = 409, description = "Failed to open the channel because the channel between this nodes already exists.", body = ApiError),
291 (status = 412, description = "The node is not ready."),
292 (status = 422, description = "Unknown failure", body = ApiError)
293 ),
294 security(
295 ("api_token" = []),
296 ("bearer_token" = [])
297 ),
298 tag = "Channels",
299 )]
300pub(super) async fn open_channel(
301 State(state): State<Arc<InternalState>>,
302 Json(open_req): Json<OpenChannelBodyRequest>,
303) -> impl IntoResponse {
304 let hopr = state.hopr.clone();
305
306 let address = match HoprIdentifier::new_with(open_req.destination, hopr.peer_resolver()).await {
307 Ok(destination) => destination.address,
308 Err(e) => return (StatusCode::UNPROCESSABLE_ENTITY, e).into_response(),
309 };
310
311 match hopr
312 .open_channel(&address, &Balance::new_from_str(&open_req.amount, BalanceType::HOPR))
313 .await
314 {
315 Ok(channel_details) => (
316 StatusCode::CREATED,
317 Json(OpenChannelResponse {
318 channel_id: channel_details.channel_id,
319 transaction_receipt: channel_details.tx_hash,
320 }),
321 )
322 .into_response(),
323 Err(HoprLibError::ChainError(ChainActionsError::BalanceTooLow)) => {
324 (StatusCode::FORBIDDEN, ApiErrorStatus::NotEnoughBalance).into_response()
325 }
326 Err(HoprLibError::ChainError(ChainActionsError::NotEnoughAllowance)) => {
327 (StatusCode::FORBIDDEN, ApiErrorStatus::NotEnoughAllowance).into_response()
328 }
329 Err(HoprLibError::ChainError(ChainActionsError::ChannelAlreadyExists)) => {
330 (StatusCode::CONFLICT, ApiErrorStatus::ChannelAlreadyOpen).into_response()
331 }
332 Err(HoprLibError::StatusError(HoprStatusError::NotThereYet(_, _))) => {
333 (StatusCode::PRECONDITION_FAILED, ApiErrorStatus::NotReady).into_response()
334 }
335 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
336 }
337}
338
339#[derive(Deserialize, utoipa::ToSchema)]
340#[schema(example = json!({
341 "channelId": "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f"
342}))]
343#[serde(rename_all = "camelCase")]
344pub(crate) struct ChannelIdParams {
345 channel_id: String,
346}
347
348#[utoipa::path(
350 get,
351 path = const_format::formatcp!("{BASE_PATH}/channels/{{channelId}}"),
352 params(
353 ("channelId" = String, Path, description = "ID of the channel.")
354 ),
355 responses(
356 (status = 200, description = "Channel fetched successfully", body = ChannelInfoResponse),
357 (status = 400, description = "Invalid channel id.", body = ApiError),
358 (status = 401, description = "Invalid authorization token.", body = ApiError),
359 (status = 404, description = "Channel not found.", body = ApiError),
360 (status = 422, description = "Unknown failure", body = ApiError)
361 ),
362 security(
363 ("api_token" = []),
364 ("bearer_token" = [])
365 ),
366 tag = "Channels",
367 )]
368pub(super) async fn show_channel(
369 Path(ChannelIdParams { channel_id }): Path<ChannelIdParams>,
370 State(state): State<Arc<InternalState>>,
371) -> impl IntoResponse {
372 let hopr = state.hopr.clone();
373
374 match Hash::from_hex(channel_id.as_str()) {
375 Ok(channel_id) => match hopr.channel_from_hash(&channel_id).await {
376 Ok(Some(channel)) => {
377 let info = query_topology_info(&channel, hopr.as_ref()).await;
378 match info {
379 Ok(info) => (StatusCode::OK, Json(info)).into_response(),
380 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
381 }
382 }
383 Ok(None) => (StatusCode::NOT_FOUND, ApiErrorStatus::ChannelNotFound).into_response(),
384 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
385 },
386 Err(_) => (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidChannelId).into_response(),
387 }
388}
389
390#[serde_as]
391#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
392#[schema(example = json!({
393 "receipt": "0xd77da7c1821249e663dead1464d185c03223d9663a06bc1d46ed0ad449a07118",
394 "channelStatus": "PendingToClose"
395 }))]
396#[serde(rename_all = "camelCase")]
397pub(crate) struct CloseChannelResponse {
398 #[serde_as(as = "DisplayFromStr")]
400 #[schema(value_type = String)]
401 receipt: Hash,
402 #[serde_as(as = "DisplayFromStr")]
404 #[schema(value_type = String)]
405 channel_status: ChannelStatus,
406}
407
408#[utoipa::path(
414 delete,
415 path = const_format::formatcp!("{BASE_PATH}/channels/{{channelId}}"),
416 params(
417 ("channelId" = String, Path, description = "ID of the channel.")
418 ),
419 responses(
420 (status = 200, description = "Channel closed successfully", body = CloseChannelResponse),
421 (status = 400, description = "Invalid channel id.", body = ApiError),
422 (status = 401, description = "Invalid authorization token.", body = ApiError),
423 (status = 404, description = "Channel not found.", body = ApiError),
424 (status = 412, description = "The node is not ready."),
425 (status = 422, description = "Unknown failure", body = ApiError)
426 ),
427 security(
428 ("api_token" = []),
429 ("bearer_token" = [])
430 ),
431 tag = "Channels",
432 )]
433pub(super) async fn close_channel(
434 Path(ChannelIdParams { channel_id }): Path<ChannelIdParams>,
435 State(state): State<Arc<InternalState>>,
436) -> impl IntoResponse {
437 let hopr = state.hopr.clone();
438
439 match Hash::from_hex(channel_id.as_str()) {
440 Ok(channel_id) => match hopr.close_channel_by_id(channel_id, false).await {
441 Ok(receipt) => (
442 StatusCode::OK,
443 Json(CloseChannelResponse {
444 channel_status: receipt.status,
445 receipt: receipt.tx_hash,
446 }),
447 )
448 .into_response(),
449 Err(HoprLibError::ChainError(ChainActionsError::ChannelDoesNotExist)) => {
450 (StatusCode::NOT_FOUND, ApiErrorStatus::ChannelNotFound).into_response()
451 }
452 Err(HoprLibError::ChainError(ChainActionsError::InvalidArguments(_))) => {
453 (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::UnsupportedFeature).into_response()
454 }
455 Err(HoprLibError::StatusError(HoprStatusError::NotThereYet(_, _))) => {
456 (StatusCode::PRECONDITION_FAILED, ApiErrorStatus::NotReady).into_response()
457 }
458 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
459 },
460 Err(_) => (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidChannelId).into_response(),
461 }
462}
463
464#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
466#[schema(example = json!({
467 "amount": "10000000000000000000"
468 }))]
469pub(crate) struct FundBodyRequest {
470 amount: String,
472}
473
474#[utoipa::path(
476 post,
477 path = const_format::formatcp!("{BASE_PATH}/channels/{{channelId}}/fund"),
478 params(
479 ("channelId" = String, Path, description = "ID of the channel.")
480 ),
481 request_body(
482 content = FundBodyRequest,
483 description = "Specifies the amount of HOPR tokens to fund a channel with.",
484 content_type = "application/json",
485 ),
486 responses(
487 (status = 200, description = "Channel funded successfully", body = String),
488 (status = 400, description = "Invalid channel id.", body = ApiError),
489 (status = 401, description = "Invalid authorization token.", body = ApiError),
490 (status = 403, description = "Failed to fund the channel because of insufficient HOPR balance or allowance.", body = ApiError),
491 (status = 404, description = "Channel not found.", body = ApiError),
492 (status = 412, description = "The node is not ready."),
493 (status = 422, description = "Unknown failure", body = ApiError)
494 ),
495 security(
496 ("api_token" = []),
497 ("bearer_token" = [])
498 ),
499 tag = "Channels",
500 )]
501pub(super) async fn fund_channel(
502 Path(ChannelIdParams { channel_id }): Path<ChannelIdParams>,
503 State(state): State<Arc<InternalState>>,
504 Json(fund_req): Json<FundBodyRequest>,
505) -> impl IntoResponse {
506 let hopr = state.hopr.clone();
507
508 let amount = Balance::new_from_str(&fund_req.amount, BalanceType::HOPR);
509
510 match Hash::from_hex(channel_id.as_str()) {
511 Ok(channel_id) => match hopr.fund_channel(&channel_id, &amount).await {
512 Ok(hash) => (StatusCode::OK, Json(hash.to_hex())).into_response(),
513 Err(HoprLibError::ChainError(ChainActionsError::ChannelDoesNotExist)) => {
514 (StatusCode::NOT_FOUND, ApiErrorStatus::ChannelNotFound).into_response()
515 }
516 Err(HoprLibError::ChainError(ChainActionsError::NotEnoughAllowance)) => {
517 (StatusCode::FORBIDDEN, ApiErrorStatus::NotEnoughAllowance).into_response()
518 }
519 Err(HoprLibError::ChainError(ChainActionsError::BalanceTooLow)) => {
520 (StatusCode::FORBIDDEN, ApiErrorStatus::NotEnoughBalance).into_response()
521 }
522 Err(HoprLibError::StatusError(HoprStatusError::NotThereYet(_, _))) => {
523 (StatusCode::PRECONDITION_FAILED, ApiErrorStatus::NotReady).into_response()
524 }
525 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
526 },
527 Err(_) => (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidChannelId).into_response(),
528 }
529}