diff --git a/openapi.yaml b/openapi.yaml index c152fdc..5847bd8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -711,6 +711,19 @@ paths: application/json: schema: $ref: '#/components/schemas/NodeInfoResponse' + /nodestate: + get: + tags: + - Other + summary: Get node state + description: Get the current state of the RGB Lightning Node + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/NodeStateResponse' /openchannel: post: tags: @@ -1824,6 +1837,25 @@ components: network_channels: type: integer example: 7812821 + NodeState: + type: string + enum: + - None + - Locked + - Running + - Changing + example: Running + description: | + The current state of the RGB Lightning Node: + * `None` - Node is not initialized (no mnemonic file exists) + * `Locked` - Node is initialized but locked (needs to be unlocked) + * `Running` - Node is unlocked and running (can perform all operations) + * `Changing` - Node is in the process of changing state (wait for completion) + NodeStateResponse: + type: object + properties: + state: + $ref: '#/components/schemas/NodeState' OpenChannelRequest: type: object properties: diff --git a/src/main.rs b/src/main.rs index ff0e7ec..dda8ddc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,7 +45,7 @@ use crate::routes::{ 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, + maker_execute, maker_init, network_info, node_info, node_state, open_channel, post_asset_media, refresh_transfers, restore, rgb_invoice, send_asset, send_btc, send_onion_message, send_payment, shutdown, sign_message, sync, taker, unlock, }; @@ -141,6 +141,7 @@ pub(crate) async fn app(args: LdkUserInfo) -> Result<(Router, Arc), Ap .route("/makerinit", post(maker_init)) .route("/networkinfo", get(network_info)) .route("/nodeinfo", get(node_info)) + .route("/nodestate", get(node_state)) .route("/openchannel", post(open_channel)) .route("/refreshtransfers", post(refresh_transfers)) .route("/restore", post(restore)) diff --git a/src/routes.rs b/src/routes.rs index e00c46e..9e2ccb0 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -791,6 +791,19 @@ pub(crate) struct NodeInfoResponse { pub(crate) network_channels: usize, } +#[derive(Deserialize, Serialize)] +pub(crate) enum NodeState { + None, + Locked, + Running, + Changing, +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct NodeStateResponse { + pub(crate) state: NodeState, +} + #[derive(Deserialize, Serialize)] pub(crate) struct OpenChannelRequest { pub(crate) peer_pubkey_and_opt_addr: String, @@ -2821,6 +2834,33 @@ pub(crate) async fn node_info( })) } +pub(crate) async fn node_state( + State(state): State>, +) -> Result, APIError> { + let mnemonic_path = get_mnemonic_path(&state.static_state.storage_dir_path); + if check_already_initialized(&mnemonic_path).is_ok() { + return Ok(Json(NodeStateResponse { + state: NodeState::None, + })); + } + + if *state.get_changing_state() { + return Ok(Json(NodeStateResponse { + state: NodeState::Changing, + })); + } + + if (state.check_locked().await).is_ok() { + return Ok(Json(NodeStateResponse { + state: NodeState::Locked, + })); + } + + Ok(Json(NodeStateResponse { + state: NodeState::Running, + })) +} + pub(crate) async fn open_channel( State(state): State>, WithRejection(Json(payload), _): WithRejection, APIError>, diff --git a/src/test/mod.rs b/src/test/mod.rs index fb72334..2761de1 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -32,11 +32,11 @@ use crate::routes::{ 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, + NetworkInfoResponse, NodeInfoResponse, NodeStateResponse, 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}; @@ -1019,6 +1019,20 @@ async fn node_info(node_address: SocketAddr) -> NodeInfoResponse { .unwrap() } +async fn node_state(node_address: SocketAddr) -> NodeStateResponse { + println!("getting node state for {node_address}"); + let res = reqwest::Client::new() + .get(format!("http://{}/nodestate", node_address)) + .send() + .await + .unwrap(); + _check_response_is_ok(res) + .await + .json::() + .await + .unwrap() +} + async fn open_channel( node_address: SocketAddr, dest_peer_pubkey: &str, @@ -1676,6 +1690,7 @@ mod issue; mod lock_unlock_changepassword; mod multi_hop; mod multi_open_close; +mod node_state_test; mod open_after_double_send; mod openchannel_fail; mod openchannel_optional_addr; diff --git a/src/test/node_state_test.rs b/src/test/node_state_test.rs new file mode 100644 index 0000000..16d6381 --- /dev/null +++ b/src/test/node_state_test.rs @@ -0,0 +1,62 @@ +use crate::routes::NodeState; + +use super::*; + +const TEST_DIR_BASE: &str = "tmp/node_state_test/"; + +#[serial_test::serial] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[traced_test] +async fn success() { + initialize(); + + let test_dir_node1 = format!("{TEST_DIR_BASE}node1"); + + if std::path::Path::new(&test_dir_node1).exists() { + std::fs::remove_dir_all(&test_dir_node1).unwrap(); + } + + let node1_addr = start_daemon(&test_dir_node1, NODE1_PEER_PORT).await; + let state_response = node_state(node1_addr).await; + assert!(matches!(state_response.state, NodeState::None)); + + let password = format!("{test_dir_node1}.{NODE1_PEER_PORT}"); + let payload = InitRequest { + password: password.clone(), + }; + let res = reqwest::Client::new() + .post(format!("http://{}/init", node1_addr)) + .json(&payload) + .send() + .await + .unwrap(); + _check_response_is_ok(res) + .await + .json::() + .await + .unwrap(); + + let state_response = node_state(node1_addr).await; + assert!(matches!(state_response.state, NodeState::Locked)); + + unlock(node1_addr, &password).await; + + let state_response = node_state(node1_addr).await; + assert!(matches!(state_response.state, NodeState::Running)); + + lock(node1_addr).await; + + let state_response = node_state(node1_addr).await; + assert!(matches!(state_response.state, NodeState::Locked)); + + unlock(node1_addr, &password).await; + let state_response = node_state(node1_addr).await; + assert!(matches!(state_response.state, NodeState::Running)); + + let node_info_response = node_info(node1_addr).await; + assert!(!node_info_response.pubkey.is_empty()); + assert_eq!(node_info_response.num_channels, 0); + assert_eq!(node_info_response.num_peers, 0); + + println!("Node state test completed successfully"); +}