From 7c9913e3491ca6fa4a8c2bd00a495cf43f91892c Mon Sep 17 00:00:00 2001 From: Evan Kaloudis Date: Fri, 19 Jan 2024 19:27:29 -0500 Subject: [PATCH] Lightning Node Connect: LSP support --- backends/LightningNodeConnect.ts | 2 +- ios/Podfile.lock | 4 +- ios/zeus.xcodeproj/project.pbxproj | 2 + lndmobile/LndMobileInjection.ts | 7 +++ lndmobile/index.ts | 22 ++++++++ stores/LSPStore.ts | 87 +++++++++++++++++++++--------- stores/Stores.ts | 6 ++- 7 files changed, 100 insertions(+), 30 deletions(-) diff --git a/backends/LightningNodeConnect.ts b/backends/LightningNodeConnect.ts index 6e89cf5a57..2cb3cc9627 100644 --- a/backends/LightningNodeConnect.ts +++ b/backends/LightningNodeConnect.ts @@ -388,7 +388,7 @@ export default class LightningNodeConnect { supportsAddressTypeSelection = () => true; supportsTaproot = () => this.supports('v0.15.0'); supportsBumpFee = () => true; - supportsLSPs = () => false; + supportsLSPs = () => true; supportsNetworkInfo = () => false; supportsSimpleTaprootChannels = () => this.supports('v0.17.0'); supportsCustomPreimages = () => true; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d19f1cf5ed..989b21494e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -520,7 +520,7 @@ PODS: - RNVectorIcons (7.1.0): - React - SocketRocket (0.6.1) - - SwiftProtobuf (1.23.0) + - SwiftProtobuf (1.25.2) - TcpSockets (4.0.0): - React - Yoga (1.14.0) @@ -830,7 +830,7 @@ SPEC CHECKSUMS: RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 RNVectorIcons: bc69e6a278b14842063605de32bec61f0b251a59 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - SwiftProtobuf: b70d65f419fbfe61a2d58003456ca5da58e337d6 + SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 TcpSockets: 4ef55305239923b343ed0a378b1fac188b1373b0 Yoga: 86fed2e4d425ee4c6eab3813ba1791101ee153c6 diff --git a/ios/zeus.xcodeproj/project.pbxproj b/ios/zeus.xcodeproj/project.pbxproj index cf025e61d6..17ebd3d04e 100644 --- a/ios/zeus.xcodeproj/project.pbxproj +++ b/ios/zeus.xcodeproj/project.pbxproj @@ -2134,6 +2134,7 @@ "-ld_classic", "-Wl", "-ld_classic", + "-Wl -ld_classic ", ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -2211,6 +2212,7 @@ "-ld_classic", "-Wl", "-ld_classic", + "-Wl -ld_classic ", ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; diff --git a/lndmobile/LndMobileInjection.ts b/lndmobile/LndMobileInjection.ts index c63a745e00..9798ef63ba 100644 --- a/lndmobile/LndMobileInjection.ts +++ b/lndmobile/LndMobileInjection.ts @@ -36,6 +36,7 @@ import { listPayments, listInvoices, subscribeChannelGraph, + channelAcceptorAnswer, sendKeysendPaymentV2 } from './index'; import { @@ -196,6 +197,11 @@ export interface ILndMobileInjections { ) => Promise; listPayments: () => Promise; subscribeChannelGraph: () => Promise; + channelAcceptorAnswer: ( + pending_chan_id: Uint8Array, + zero_conf: boolean, + accept: boolean + ) => void; sendKeysendPaymentV2: ({ amt, max_shard_size_msat, @@ -382,6 +388,7 @@ export default { listPayments, listInvoices, subscribeChannelGraph, + channelAcceptorAnswer, sendKeysendPaymentV2 }, channel: { diff --git a/lndmobile/index.ts b/lndmobile/index.ts index ab8dc1585d..84fddd67e0 100644 --- a/lndmobile/index.ts +++ b/lndmobile/index.ts @@ -841,6 +841,28 @@ export const subscribeChannelGraph = async (): Promise => { return response; }; +/** + * @throws + */ +export const channelAcceptorAnswer = async ( + pending_chan_id: Uint8Array, + zero_conf: boolean, + accept: boolean +) => { + await sendStreamCommand< + lnrpc.IChannelAcceptResponse, + lnrpc.ChannelAcceptResponse + >({ + request: lnrpc.ChannelAcceptResponse, + method: 'ChannelAcceptor', + options: { + pending_chan_id, + zero_conf, + accept + } + }); +}; + export type IReadLndLogResponse = string[]; /** * @throws diff --git a/stores/LSPStore.ts b/stores/LSPStore.ts index 6b15c14a25..4e675366f6 100644 --- a/stores/LSPStore.ts +++ b/stores/LSPStore.ts @@ -1,14 +1,15 @@ import { action, observable } from 'mobx'; import ReactNativeBlobUtil from 'react-native-blob-util'; +import { NativeEventEmitter, NativeModules } from 'react-native'; +import NodeInfoStore from './NodeInfoStore'; import SettingsStore from './SettingsStore'; import ChannelsStore from './ChannelsStore'; import stores from './Stores'; import lndMobile from '../lndmobile/LndMobileInjection'; -const { channel } = lndMobile; +const { channel, index } = lndMobile; -import Base64Utils from '../utils/Base64Utils'; import { LndMobileEventEmitter } from '../utils/LndMobileUtils'; import { localeString } from '../utils/LocaleUtils'; @@ -21,10 +22,16 @@ export default class LSPStore { @observable public showLspSettings: boolean = false; @observable public channelAcceptor: any; + nodeInfoStore: NodeInfoStore; settingsStore: SettingsStore; channelsStore: ChannelsStore; - constructor(settingsStore: SettingsStore, channelsStore: ChannelsStore) { + constructor( + nodeInfoStore: NodeInfoStore, + settingsStore: SettingsStore, + channelsStore: ChannelsStore + ) { + this.nodeInfoStore = nodeInfoStore; this.settingsStore = settingsStore; this.channelsStore = channelsStore; } @@ -146,17 +153,9 @@ export default class LSPStore { handleChannelAcceptorEvent = async (channelAcceptRequest: any) => { try { - const requestPubkey = Base64Utils.bytesToHex( - channelAcceptRequest.node_pubkey - ); - - // Only allow 0-conf chans from LSP or whitelisted peers + // only allow zero conf chans from the LSP const isZeroConfAllowed = - this.info?.pubkey === requestPubkey || - (this.settingsStore?.settings?.zeroConfPeers && - this.settingsStore?.settings?.zeroConfPeers.includes( - requestPubkey - )); + channelAcceptRequest.node_pubkey === this.info.pub_key; await channel.channelAcceptorResponse( channelAcceptRequest.pending_chan_id, @@ -170,22 +169,58 @@ export default class LSPStore { @action public initChannelAcceptor = async () => { + const { implementation } = this.settingsStore; if (this.channelAcceptor) return; - this.channelAcceptor = LndMobileEventEmitter.addListener( - 'ChannelAcceptor', - async (event: any) => { - try { - const channelAcceptRequest = - channel.decodeChannelAcceptRequest(event.data); - - await this.handleChannelAcceptorEvent(channelAcceptRequest); - } catch (error: any) { - console.error('channel acceptance error: ' + error.message); + + if (implementation === 'embedded-lnd') { + this.channelAcceptor = LndMobileEventEmitter.addListener( + 'ChannelAcceptor', + async (event: any) => { + try { + const result = channel.decodeChannelAcceptRequest( + event.data + ); + await this.handleChannelAcceptorEvent(result); + } catch (error: any) { + console.error( + 'channelAcceptorEvent embedded-lnd error:', + error.message + ); + } } - } - ); + ); + + await channel.channelAcceptor(); + } - await channel.channelAcceptor(); + if (implementation === 'lightning-node-connect') { + const { LncModule } = NativeModules; + const eventEmitter = new NativeEventEmitter(LncModule); + this.channelAcceptor = eventEmitter.addListener( + 'lnrpc.Lightning.ChannelAcceptor', + async (event: any) => { + if (event.result) { + try { + const result = JSON.parse(event.result); + // only allow zero conf chans from the LSP + const isZeroConfAllowed = + result.node_pubkey === this.info.pub_key; + + index.channelAcceptorAnswer( + result.pending_chan_id, + !result.wants_zero_conf || isZeroConfAllowed, + isZeroConfAllowed + ); + } catch (error: any) { + console.error( + 'channelAcceptorEvent lightning-node-connect error:', + error.message + ); + } + } + } + ); + } }; @action diff --git a/stores/Stores.ts b/stores/Stores.ts index a70ffaa4bc..2d009a630c 100644 --- a/stores/Stores.ts +++ b/stores/Stores.ts @@ -54,7 +54,11 @@ class Stores { this.channelsStore, this.settingsStore ); - this.lspStore = new LSPStore(this.settingsStore, this.channelsStore); + this.lspStore = new LSPStore( + this.nodeInfoStore, + this.settingsStore, + this.channelsStore + ); this.lightningAddressStore = new LightningAddressStore( this.nodeInfoStore, this.settingsStore