diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index 862d23e35..9e9f1941a 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -186,10 +186,19 @@ dictionary NodeState { u64 max_receivable_msat; u64 max_single_payment_amount_msat; u64 max_chan_reserve_msats; - sequence connected_peers; + sequence connected_peers; u64 inbound_liquidity_msats; }; +dictionary ConnectedPeer { + string id; + PeerFeatures features; +}; + +dictionary PeerFeatures { + boolean trampoline; +}; + dictionary ConfigureNodeRequest { string? close_to_address; }; diff --git a/libs/sdk-bindings/src/uniffi_binding.rs b/libs/sdk-bindings/src/uniffi_binding.rs index 7aada9bd0..a8d193b46 100644 --- a/libs/sdk-bindings/src/uniffi_binding.rs +++ b/libs/sdk-bindings/src/uniffi_binding.rs @@ -8,18 +8,18 @@ use breez_sdk_core::{ BackupFailedData, BackupStatus, BitcoinAddressData, BreezEvent, BreezServices, BuyBitcoinProvider, BuyBitcoinRequest, BuyBitcoinResponse, ChannelState, CheckMessageRequest, CheckMessageResponse, ClosedChannelPaymentDetails, Config, ConfigureNodeRequest, - ConnectRequest, CurrencyInfo, EnvironmentType, EventListener, FeeratePreset, FiatCurrency, - GreenlightCredentials, GreenlightDeviceCredentials, GreenlightNodeConfig, HealthCheckStatus, - InputType, InvoicePaidDetails, LNInvoice, ListPaymentsRequest, LnPaymentDetails, - LnUrlAuthError, LnUrlAuthRequestData, LnUrlCallbackStatus, LnUrlErrorData, LnUrlPayError, - LnUrlPayErrorData, LnUrlPayRequest, LnUrlPayRequestData, LnUrlWithdrawError, + ConnectRequest, ConnectedPeer, CurrencyInfo, EnvironmentType, EventListener, FeeratePreset, + FiatCurrency, GreenlightCredentials, GreenlightDeviceCredentials, GreenlightNodeConfig, + HealthCheckStatus, InputType, InvoicePaidDetails, LNInvoice, ListPaymentsRequest, + LnPaymentDetails, LnUrlAuthError, LnUrlAuthRequestData, LnUrlCallbackStatus, LnUrlErrorData, + LnUrlPayError, LnUrlPayErrorData, LnUrlPayRequest, LnUrlPayRequestData, LnUrlWithdrawError, LnUrlWithdrawRequest, LnUrlWithdrawRequestData, LnUrlWithdrawResult, LnUrlWithdrawSuccessData, LocaleOverrides, LocalizedName, LogEntry, LogStream, LspInformation, MaxReverseSwapAmountResponse, MessageSuccessActionData, MetadataFilter, MetadataItem, Network, NodeConfig, NodeCredentials, NodeState, OnchainPaymentLimitsResponse, OpenChannelFeeRequest, OpenChannelFeeResponse, OpeningFeeParams, OpeningFeeParamsMenu, PayOnchainRequest, PayOnchainResponse, Payment, PaymentDetails, PaymentFailedData, PaymentStatus, PaymentType, - PaymentTypeFilter, PrepareOnchainPaymentRequest, PrepareOnchainPaymentResponse, + PaymentTypeFilter, PeerFeatures, PrepareOnchainPaymentRequest, PrepareOnchainPaymentResponse, PrepareRedeemOnchainFundsRequest, PrepareRedeemOnchainFundsResponse, PrepareRefundRequest, PrepareRefundResponse, Rate, ReceiveOnchainRequest, ReceivePaymentRequest, ReceivePaymentResponse, RecommendedFees, RedeemOnchainFundsRequest, RedeemOnchainFundsResponse, diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index abd300299..6325ac497 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -1202,7 +1202,7 @@ impl BreezServices { self.persister.set_lsp_id(lsp.id)?; self.persister.set_lsp_pubkey(lsp.pubkey.clone())?; - let node_state = match self.node_info() { + let mut node_state = match self.node_info() { Ok(node_state) => node_state, Err(_) => return Ok(()), }; @@ -1212,15 +1212,21 @@ impl BreezServices { let lsp_connected = node_state .connected_peers .iter() - .any(|e| e == node_id.as_str()); + .any(|peer| peer.id == node_id); if !lsp_connected { debug!("connecting to lsp {}@{}", node_id.clone(), address.clone()); - self.node_api + let features = self + .node_api .connect_peer(node_id.clone(), address.clone()) .await .map_err(|e| SdkError::ServiceConnectivity { err: format!("(LSP: {node_id}) Failed to connect: {e}"), })?; + node_state.connected_peers.push(ConnectedPeer { + id: node_id.clone(), + features, + }); + self.persister.set_node_state(&node_state)?; debug!("connected to lsp {node_id}@{address}"); } @@ -3217,7 +3223,10 @@ pub(crate) mod tests { max_receivable_msat: 4_000_000_000, max_single_payment_amount_msat: 1_000, max_chan_reserve_msats: 0, - connected_peers: vec!["1111".to_string()], + connected_peers: vec![ConnectedPeer { + id: "1111".to_string(), + features: PeerFeatures::default(), + }], inbound_liquidity_msats: 2_000, } } diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index 25738c38b..29213a742 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -41,6 +41,7 @@ use crate::models::ClosedChannelPaymentDetails; use crate::models::Config; use crate::models::ConfigureNodeRequest; use crate::models::ConnectRequest; +use crate::models::ConnectedPeer; use crate::models::EnvironmentType; use crate::models::GreenlightCredentials; use crate::models::GreenlightDeviceCredentials; @@ -66,6 +67,7 @@ use crate::models::PaymentDetails; use crate::models::PaymentStatus; use crate::models::PaymentType; use crate::models::PaymentTypeFilter; +use crate::models::PeerFeatures; use crate::models::PrepareOnchainPaymentRequest; use crate::models::PrepareOnchainPaymentResponse; use crate::models::PrepareRedeemOnchainFundsRequest; @@ -1506,6 +1508,22 @@ impl rust2dart::IntoIntoDart for Config { } } +impl support::IntoDart for ConnectedPeer { + fn into_dart(self) -> support::DartAbi { + vec![ + self.id.into_into_dart().into_dart(), + self.features.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl support::IntoDartExceptPrimitive for ConnectedPeer {} +impl rust2dart::IntoIntoDart for ConnectedPeer { + fn into_into_dart(self) -> Self { + self + } +} + impl support::IntoDart for mirror_CurrencyInfo { fn into_dart(self) -> support::DartAbi { vec![ @@ -2238,6 +2256,18 @@ impl rust2dart::IntoIntoDart for PaymentType { } } +impl support::IntoDart for PeerFeatures { + fn into_dart(self) -> support::DartAbi { + vec![self.trampoline.into_into_dart().into_dart()].into_dart() + } +} +impl support::IntoDartExceptPrimitive for PeerFeatures {} +impl rust2dart::IntoIntoDart for PeerFeatures { + fn into_into_dart(self) -> Self { + self + } +} + impl support::IntoDart for PrepareOnchainPaymentResponse { fn into_dart(self) -> support::DartAbi { vec![ diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index 6f19f77b5..ced2e875b 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -1,5 +1,5 @@ use std::cmp::{min, Reverse}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::iter::Iterator; use std::pin::Pin; use std::str::FromStr; @@ -456,7 +456,7 @@ impl Greenlight { ) -> NodeResult<( Vec, Vec, - Vec, + Vec, u64, )> { let (mut all_channels, mut opened_channels, mut connected_peers, mut channels_balance) = @@ -491,24 +491,28 @@ impl Greenlight { ) -> NodeResult<( Vec, Vec, - Vec, + Vec, u64, )> { // list all channels + let peers = cln_client + .list_peers(cln::ListpeersRequest::default()) + .await? + .into_inner(); let peerchannels = cln_client .list_peer_channels(cln::ListpeerchannelsRequest::default()) .await? .into_inner(); // filter only connected peers - let connected_peers: Vec = peerchannels - .channels + let connected_peers: Vec = peers + .peers .iter() - .filter(|channel| channel.peer_connected()) - .filter_map(|channel| channel.peer_id.clone()) - .map(hex::encode) - .collect::>() - .into_iter() + .filter(|peer| peer.connected) + .map(|peer| ConnectedPeer { + id: hex::encode(&peer.id), + features: peer.features().to_vec().into(), + }) .collect(); // filter only opened channels @@ -1333,15 +1337,16 @@ impl NodeAPI for Greenlight { } } - async fn connect_peer(&self, id: String, addr: String) -> NodeResult<()> { + /// Connects to a remote node and returns the remote node's features. + async fn connect_peer(&self, id: String, addr: String) -> NodeResult { let mut client = self.get_node_client().await?; let connect_req = cln::ConnectRequest { id: format!("{id}@{addr}"), host: None, port: None, }; - client.connect_peer(connect_req).await?; - Ok(()) + let resp = client.connect_peer(connect_req).await?.into_inner(); + Ok(resp.features.into()) } async fn sign_message(&self, message: &str) -> NodeResult { diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index a1f9455c6..32c6a8452 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -30,6 +30,8 @@ use crate::swap_out::error::{ReverseSwapError, ReverseSwapResult}; pub const SWAP_PAYMENT_FEE_EXPIRY_SECONDS: u32 = 60 * 60 * 24 * 2; // 2 days pub const INVOICE_PAYMENT_FEE_EXPIRY_SECONDS: u32 = 60 * 60; // 60 minutes +const FEATURE_TRAMPOLINE: usize = 427; + /// Different types of supported payments #[derive(Clone, PartialEq, Eq, Debug, EnumString, Display, Deserialize, Serialize, Hash)] pub enum PaymentType { @@ -625,10 +627,40 @@ pub struct NodeState { pub max_receivable_msat: u64, pub max_single_payment_amount_msat: u64, pub max_chan_reserve_msats: u64, - pub connected_peers: Vec, + pub connected_peers: Vec, pub inbound_liquidity_msats: u64, } +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct ConnectedPeer { + pub id: String, + pub features: PeerFeatures, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Default)] +pub struct PeerFeatures { + pub trampoline: bool, +} + +impl From> for PeerFeatures { + fn from(value: Vec) -> Self { + PeerFeatures { + trampoline: has_feature(&value, FEATURE_TRAMPOLINE), + } + } +} + +/// Checks whether the given features vector contains the specified feature. +fn has_feature(features: &[u8], feature: usize) -> bool { + if features.is_empty() || features.len() * 8 - 1 < feature { + return false; + } + + let byte_index = features.len() - feature / 8 - 1; + let bit_index = feature % 8; + ((features[byte_index] >> bit_index) & 1) == 1 +} + /// Internal response to a [crate::node_api::NodeAPI::pull_changed] call pub struct SyncResponse { pub node_state: NodeState, @@ -1587,7 +1619,7 @@ mod tests { use sdk_common::grpc; use crate::test_utils::{get_test_ofp, rand_vec_u8}; - use crate::{OpeningFeeParams, PaymentPath, PaymentPathEdge}; + use crate::{OpeningFeeParams, PaymentPath, PaymentPathEdge, PeerFeatures}; #[test] fn test_route_fees() -> Result<()> { @@ -1747,4 +1779,18 @@ mod tests { ofp.valid_until = "2023-08-03T00:30:35.117Z".to_string(); ofp.valid_until_date().map(|_| ()) } + + #[test] + fn test_trampoline_feature_bit() { + let features_hex = + "08000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000"; + let features = hex::decode(features_hex).unwrap(); + let peer_features = PeerFeatures::from(features); + assert!(peer_features.trampoline); + + let features = Vec::new(); + let peer_features = PeerFeatures::from(features); + assert!(!peer_features.trampoline); + } } diff --git a/libs/sdk-core/src/node_api.rs b/libs/sdk-core/src/node_api.rs index c32837f38..69a23c9ed 100644 --- a/libs/sdk-core/src/node_api.rs +++ b/libs/sdk-core/src/node_api.rs @@ -8,12 +8,7 @@ use tonic::Streaming; use sdk_common::prelude::*; use crate::{ - bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}, - lightning_invoice::RawBolt11Invoice, - persist::error::PersistError, - CustomMessage, LspInformation, MaxChannelAmount, NodeCredentials, Payment, PaymentResponse, - PrepareRedeemOnchainFundsRequest, PrepareRedeemOnchainFundsResponse, RouteHint, RouteHintHop, - SyncResponse, TlvEntry, LnUrlAuthError + bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}, lightning_invoice::RawBolt11Invoice, persist::error::PersistError, CustomMessage, LnUrlAuthError, LspInformation, MaxChannelAmount, NodeCredentials, Payment, PaymentResponse, PeerFeatures, PrepareRedeemOnchainFundsRequest, PrepareRedeemOnchainFundsResponse, RouteHint, RouteHintHop, SyncResponse, TlvEntry }; pub type NodeResult = Result; @@ -165,7 +160,9 @@ pub trait NodeAPI: Send + Sync { ) -> NodeResult; async fn start_signer(&self, shutdown: mpsc::Receiver<()>); async fn start_keep_alive(&self, shutdown: watch::Receiver<()>); - async fn connect_peer(&self, node_id: String, addr: String) -> NodeResult<()>; + + /// Connects to a remote node and returns the remote node's features. + async fn connect_peer(&self, node_id: String, addr: String) -> NodeResult; fn sign_invoice(&self, invoice: RawBolt11Invoice) -> NodeResult; async fn close_peer_channels(&self, node_id: String) -> NodeResult>; async fn stream_incoming_payments( diff --git a/libs/sdk-core/src/persist/migrations.rs b/libs/sdk-core/src/persist/migrations.rs index 91b92d4af..02155c998 100644 --- a/libs/sdk-core/src/persist/migrations.rs +++ b/libs/sdk-core/src/persist/migrations.rs @@ -432,7 +432,8 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { ALTER TABLE channels ADD COLUMN local_balance_msat INTEGER; UPDATE channels SET local_balance_msat = spendable_msat; ", - "DELETE FROM cached_items WHERE key = 'gl_credentials'" + "DELETE FROM cached_items WHERE key = 'gl_credentials'", + "DELETE FROM cached_items WHERE key = 'node_state'" ] } diff --git a/libs/sdk-core/src/test_utils.rs b/libs/sdk-core/src/test_utils.rs index 9a4dd4671..3a49734fe 100644 --- a/libs/sdk-core/src/test_utils.rs +++ b/libs/sdk-core/src/test_utils.rs @@ -45,7 +45,7 @@ use crate::swap_out::boltzswap::{BoltzApiCreateReverseSwapResponse, BoltzApiReve use crate::swap_out::error::{ReverseSwapError, ReverseSwapResult}; use crate::{ parse_invoice, Config, CustomMessage, LNInvoice, MaxChannelAmount, NodeCredentials, - OpeningFeeParamsMenu, PaymentResponse, PrepareRedeemOnchainFundsRequest, + OpeningFeeParamsMenu, PaymentResponse, PeerFeatures, PrepareRedeemOnchainFundsRequest, PrepareRedeemOnchainFundsResponse, ReceivePaymentRequest, ReverseSwapPairInfo, RouteHint, RouteHintHop, SwapInfo, }; @@ -426,8 +426,8 @@ impl NodeAPI for MockNodeAPI { async fn start_keep_alive(&self, _shutdown: watch::Receiver<()>) {} - async fn connect_peer(&self, _node_id: String, _addr: String) -> NodeResult<()> { - Ok(()) + async fn connect_peer(&self, _node_id: String, _addr: String) -> NodeResult { + Ok(PeerFeatures::default()) } async fn sign_message(&self, _message: &str) -> NodeResult { diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index ddd06d9ed..80bc41170 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -583,6 +583,16 @@ class ConnectRequest { }); } +class ConnectedPeer { + final String id; + final PeerFeatures features; + + const ConnectedPeer({ + required this.id, + required this.features, + }); +} + class CurrencyInfo { final String name; final int fractionSize; @@ -1110,7 +1120,7 @@ class NodeState { final int maxReceivableMsat; final int maxSinglePaymentAmountMsat; final int maxChanReserveMsats; - final List connectedPeers; + final List connectedPeers; final int inboundLiquidityMsats; const NodeState({ @@ -1300,6 +1310,14 @@ enum PaymentTypeFilter { ClosedChannel, } +class PeerFeatures { + final bool trampoline; + + const PeerFeatures({ + required this.trampoline, + }); +} + /// See [ReverseSwapFeesRequest] class PrepareOnchainPaymentRequest { /// Depending on `amount_type`, this may be the desired send amount or the desired receive amount. @@ -3323,6 +3341,15 @@ class BreezSdkCoreImpl implements BreezSdkCore { ); } + ConnectedPeer _wire2api_connected_peer(dynamic raw) { + final arr = raw as List; + if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return ConnectedPeer( + id: _wire2api_String(arr[0]), + features: _wire2api_peer_features(arr[1]), + ); + } + CurrencyInfo _wire2api_currency_info(dynamic raw) { final arr = raw as List; if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); @@ -3437,6 +3464,10 @@ class BreezSdkCoreImpl implements BreezSdkCore { ); } + List _wire2api_list_connected_peer(dynamic raw) { + return (raw as List).map(_wire2api_connected_peer).toList(); + } + List _wire2api_list_fiat_currency(dynamic raw) { return (raw as List).map(_wire2api_fiat_currency).toList(); } @@ -3753,7 +3784,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { maxReceivableMsat: _wire2api_u64(arr[7]), maxSinglePaymentAmountMsat: _wire2api_u64(arr[8]), maxChanReserveMsats: _wire2api_u64(arr[9]), - connectedPeers: _wire2api_StringList(arr[10]), + connectedPeers: _wire2api_list_connected_peer(arr[10]), inboundLiquidityMsats: _wire2api_u64(arr[11]), ); } @@ -3916,6 +3947,14 @@ class BreezSdkCoreImpl implements BreezSdkCore { return PaymentType.values[raw as int]; } + PeerFeatures _wire2api_peer_features(dynamic raw) { + final arr = raw as List; + if (arr.length != 1) throw Exception('unexpected arr length: expect 1 but see ${arr.length}'); + return PeerFeatures( + trampoline: _wire2api_bool(arr[0]), + ); + } + PrepareOnchainPaymentResponse _wire2api_prepare_onchain_payment_response(dynamic raw) { final arr = raw as List; if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); diff --git a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt index fa4c04fff..716231d87 100644 --- a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt +++ b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt @@ -514,6 +514,42 @@ fun asConnectRequestList(arr: ReadableArray): List { return list } +fun asConnectedPeer(connectedPeer: ReadableMap): ConnectedPeer? { + if (!validateMandatoryFields( + connectedPeer, + arrayOf( + "id", + "features", + ), + ) + ) { + return null + } + val id = connectedPeer.getString("id")!! + val features = connectedPeer.getMap("features")?.let { asPeerFeatures(it) }!! + return ConnectedPeer( + id, + features, + ) +} + +fun readableMapOf(connectedPeer: ConnectedPeer): ReadableMap = + readableMapOf( + "id" to connectedPeer.id, + "features" to readableMapOf(connectedPeer.features), + ) + +fun asConnectedPeerList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toArrayList()) { + when (value) { + is ReadableMap -> list.add(asConnectedPeer(value)!!) + else -> throw SdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } + } + return list +} + fun asCurrencyInfo(currencyInfo: ReadableMap): CurrencyInfo? { if (!validateMandatoryFields( currencyInfo, @@ -1756,7 +1792,7 @@ fun asNodeState(nodeState: ReadableMap): NodeState? { val maxReceivableMsat = nodeState.getDouble("maxReceivableMsat").toULong() val maxSinglePaymentAmountMsat = nodeState.getDouble("maxSinglePaymentAmountMsat").toULong() val maxChanReserveMsats = nodeState.getDouble("maxChanReserveMsats").toULong() - val connectedPeers = nodeState.getArray("connectedPeers")?.let { asStringList(it) }!! + val connectedPeers = nodeState.getArray("connectedPeers")?.let { asConnectedPeerList(it) }!! val inboundLiquidityMsats = nodeState.getDouble("inboundLiquidityMsats").toULong() return NodeState( id, @@ -2173,6 +2209,38 @@ fun asPaymentFailedDataList(arr: ReadableArray): List { return list } +fun asPeerFeatures(peerFeatures: ReadableMap): PeerFeatures? { + if (!validateMandatoryFields( + peerFeatures, + arrayOf( + "trampoline", + ), + ) + ) { + return null + } + val trampoline = peerFeatures.getBoolean("trampoline") + return PeerFeatures( + trampoline, + ) +} + +fun readableMapOf(peerFeatures: PeerFeatures): ReadableMap = + readableMapOf( + "trampoline" to peerFeatures.trampoline, + ) + +fun asPeerFeaturesList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toArrayList()) { + when (value) { + is ReadableMap -> list.add(asPeerFeatures(value)!!) + else -> throw SdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } + } + return list +} + fun asPrepareOnchainPaymentRequest(prepareOnchainPaymentRequest: ReadableMap): PrepareOnchainPaymentRequest? { if (!validateMandatoryFields( prepareOnchainPaymentRequest, @@ -4447,6 +4515,7 @@ fun pushToArray( ) { when (value) { null -> array.pushNull() + is ConnectedPeer -> array.pushMap(readableMapOf(value)) is FiatCurrency -> array.pushMap(readableMapOf(value)) is LocaleOverrides -> array.pushMap(readableMapOf(value)) is LocalizedName -> array.pushMap(readableMapOf(value)) diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift index 9da47264a..a9adcb26f 100644 --- a/libs/sdk-react-native/ios/BreezSDKMapper.swift +++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift @@ -577,6 +577,45 @@ enum BreezSDKMapper { return connectRequestList.map { v -> [String: Any?] in dictionaryOf(connectRequest: v) } } + static func asConnectedPeer(connectedPeer: [String: Any?]) throws -> ConnectedPeer { + guard let id = connectedPeer["id"] as? String else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "id", typeName: "ConnectedPeer")) + } + guard let featuresTmp = connectedPeer["features"] as? [String: Any?] else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "features", typeName: "ConnectedPeer")) + } + let features = try asPeerFeatures(peerFeatures: featuresTmp) + + return ConnectedPeer( + id: id, + features: features + ) + } + + static func dictionaryOf(connectedPeer: ConnectedPeer) -> [String: Any?] { + return [ + "id": connectedPeer.id, + "features": dictionaryOf(peerFeatures: connectedPeer.features), + ] + } + + static func asConnectedPeerList(arr: [Any]) throws -> [ConnectedPeer] { + var list = [ConnectedPeer]() + for value in arr { + if let val = value as? [String: Any?] { + var connectedPeer = try asConnectedPeer(connectedPeer: val) + list.append(connectedPeer) + } else { + throw SdkError.Generic(message: errUnexpectedType(typeName: "ConnectedPeer")) + } + } + return list + } + + static func arrayOf(connectedPeerList: [ConnectedPeer]) -> [Any] { + return connectedPeerList.map { v -> [String: Any?] in dictionaryOf(connectedPeer: v) } + } + static func asCurrencyInfo(currencyInfo: [String: Any?]) throws -> CurrencyInfo { guard let name = currencyInfo["name"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "name", typeName: "CurrencyInfo")) @@ -1999,9 +2038,11 @@ enum BreezSDKMapper { guard let maxChanReserveMsats = nodeState["maxChanReserveMsats"] as? UInt64 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "maxChanReserveMsats", typeName: "NodeState")) } - guard let connectedPeers = nodeState["connectedPeers"] as? [String] else { + guard let connectedPeersTmp = nodeState["connectedPeers"] as? [[String: Any?]] else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "connectedPeers", typeName: "NodeState")) } + let connectedPeers = try asConnectedPeerList(arr: connectedPeersTmp) + guard let inboundLiquidityMsats = nodeState["inboundLiquidityMsats"] as? UInt64 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "inboundLiquidityMsats", typeName: "NodeState")) } @@ -2034,7 +2075,7 @@ enum BreezSDKMapper { "maxReceivableMsat": nodeState.maxReceivableMsat, "maxSinglePaymentAmountMsat": nodeState.maxSinglePaymentAmountMsat, "maxChanReserveMsats": nodeState.maxChanReserveMsats, - "connectedPeers": nodeState.connectedPeers, + "connectedPeers": arrayOf(connectedPeerList: nodeState.connectedPeers), "inboundLiquidityMsats": nodeState.inboundLiquidityMsats, ] } @@ -2496,6 +2537,38 @@ enum BreezSDKMapper { return paymentFailedDataList.map { v -> [String: Any?] in dictionaryOf(paymentFailedData: v) } } + static func asPeerFeatures(peerFeatures: [String: Any?]) throws -> PeerFeatures { + guard let trampoline = peerFeatures["trampoline"] as? Bool else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "trampoline", typeName: "PeerFeatures")) + } + + return PeerFeatures( + trampoline: trampoline) + } + + static func dictionaryOf(peerFeatures: PeerFeatures) -> [String: Any?] { + return [ + "trampoline": peerFeatures.trampoline, + ] + } + + static func asPeerFeaturesList(arr: [Any]) throws -> [PeerFeatures] { + var list = [PeerFeatures]() + for value in arr { + if let val = value as? [String: Any?] { + var peerFeatures = try asPeerFeatures(peerFeatures: val) + list.append(peerFeatures) + } else { + throw SdkError.Generic(message: errUnexpectedType(typeName: "PeerFeatures")) + } + } + return list + } + + static func arrayOf(peerFeaturesList: [PeerFeatures]) -> [Any] { + return peerFeaturesList.map { v -> [String: Any?] in dictionaryOf(peerFeatures: v) } + } + static func asPrepareOnchainPaymentRequest(prepareOnchainPaymentRequest: [String: Any?]) throws -> PrepareOnchainPaymentRequest { guard let amountSat = prepareOnchainPaymentRequest["amountSat"] as? UInt64 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "amountSat", typeName: "PrepareOnchainPaymentRequest")) diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts index d6b455bd0..81aa1439f 100644 --- a/libs/sdk-react-native/src/index.ts +++ b/libs/sdk-react-native/src/index.ts @@ -92,6 +92,11 @@ export interface ConnectRequest { restoreOnly?: boolean } +export interface ConnectedPeer { + id: string + features: PeerFeatures +} + export interface CurrencyInfo { name: string fractionSize: number @@ -288,7 +293,7 @@ export interface NodeState { maxReceivableMsat: number maxSinglePaymentAmountMsat: number maxChanReserveMsats: number - connectedPeers: string[] + connectedPeers: ConnectedPeer[] inboundLiquidityMsats: number } @@ -349,6 +354,10 @@ export interface PaymentFailedData { label?: string } +export interface PeerFeatures { + trampoline: boolean +} + export interface PrepareOnchainPaymentRequest { amountSat: number amountType: SwapAmountType