Skip to content

Commit e3c8563

Browse files
committed
feat: add LNSocket support for direct Core Lightning TCP connections via lnmessage
1 parent 1aee033 commit e3c8563

File tree

8 files changed

+618
-11
lines changed

8 files changed

+618
-11
lines changed

backends/LNSocket.ts

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import { Platform } from 'react-native';
2+
// @ts-ignore
3+
// eslint-disable-next-line import/no-duplicates
4+
import Lnmessage, { createReactNativeTcpSocket } from 'lnmessage';
5+
import TcpSocket from 'react-native-tcp-socket';
6+
7+
import TransactionRequest from './../models/TransactionRequest';
8+
import OpenChannelRequest from './../models/OpenChannelRequest';
9+
import { settingsStore } from '../stores/Stores';
10+
11+
const parseHostPort = (host: string, defaultPort: number = 9735) => {
12+
const parts = host.split(':');
13+
if (parts.length === 2) {
14+
return {
15+
host: parts[0],
16+
port: parseInt(parts[1], 10) || defaultPort
17+
};
18+
}
19+
return {
20+
host,
21+
port: defaultPort
22+
};
23+
};
24+
25+
export default class LnSocket {
26+
ln: any;
27+
28+
init = async () => {
29+
const { pubkey, host } = settingsStore;
30+
const { host: parsedHost, port } = parseHostPort(host || '', 9735);
31+
32+
let tcpSocket;
33+
if (Platform.OS !== 'web') {
34+
tcpSocket = createReactNativeTcpSocket(TcpSocket);
35+
}
36+
37+
this.ln = new Lnmessage({
38+
remoteNodePublicKey: pubkey,
39+
ip: parsedHost,
40+
port,
41+
// Use TCP socket adapter in React Native, undefined in Node.js/browser (will use WebSocket)
42+
tcpSocket,
43+
// TODO make proxy configurable or create our own
44+
wsProxy: 'wss://lnproxy.getalby.com',
45+
// Hex encoded private key string to use for the node local secret
46+
// Use same key for persistent identity across connections
47+
privateKey:
48+
'31e9739c0d6a2eba36168dd364841cc01d3f4fde221d256e6a781a4dd46715ea',
49+
logger: {
50+
info: console.log,
51+
warn: console.warn,
52+
error: console.error
53+
}
54+
});
55+
56+
return this.ln;
57+
};
58+
59+
connect = async () => await this.ln.connect();
60+
61+
disconnect = () => this.ln.disconnect();
62+
63+
rpc = async (method: string, params = {}) => {
64+
const { rune } = settingsStore;
65+
const res = await this.ln.commando({ method, params, rune });
66+
return res;
67+
};
68+
69+
getTransactions = () =>
70+
this.rpc('listfunds').then(({ outputs }: any) => ({
71+
transactions: outputs
72+
}));
73+
getChannels = () =>
74+
this.rpc('listpeers').then(({ peers }: any) => ({
75+
channels: peers
76+
.filter((peer: any) => peer.channels.length)
77+
.map((peer: any) => {
78+
const channel =
79+
peer.channels.find(
80+
(c: any) =>
81+
c.state !== 'ONCHAIN' && c.state !== 'CLOSED'
82+
) || peer.channels[0];
83+
84+
return {
85+
active: peer.connected,
86+
remote_pubkey: peer.id,
87+
channel_point: channel.funding_txid,
88+
chan_id: channel.channel_id,
89+
capacity: Number(
90+
channel.msatoshi_total / 1000
91+
).toString(),
92+
local_balance: Number(
93+
channel.msatoshi_to_us / 1000
94+
).toString(),
95+
remote_balance: Number(
96+
(channel.msatoshi_total - channel.msatoshi_to_us) /
97+
1000
98+
).toString(),
99+
total_satoshis_sent: Number(
100+
channel.out_msatoshi_fulfilled / 1000
101+
).toString(),
102+
total_satoshis_received: Number(
103+
channel.in_msatoshi_fulfilled / 1000
104+
).toString(),
105+
num_updates: (
106+
channel.in_payments_offered +
107+
channel.out_payments_offered
108+
).toString(),
109+
csv_delay: channel.our_to_self_delay,
110+
private: channel.private,
111+
local_chan_reserve_sat:
112+
channel.our_channel_reserve_satoshis.toString(),
113+
remote_chan_reserve_sat:
114+
channel.their_channel_reserve_satoshis.toString(),
115+
close_address: channel.close_to_addr
116+
};
117+
})
118+
}));
119+
getBlockchainBalance = () =>
120+
this.rpc('listfunds').then(({ outputs }: any) => {
121+
const unconf = outputs
122+
.filter((o: any) => o.status !== 'confirmed')
123+
.reduce((acc: any, o: any) => acc + o.value, 0);
124+
const conf = outputs
125+
.filter((o: any) => o.status === 'confirmed')
126+
.reduce((acc: any, o: any) => acc + o.value, 0);
127+
128+
return {
129+
total_balance: conf + unconf,
130+
confirmed_balance: conf,
131+
unconfirmed_balance: unconf
132+
};
133+
});
134+
getLightningBalance = () =>
135+
this.rpc('listfunds').then(({ channels }: any) => ({
136+
balance: channels
137+
.filter((o: any) => o.state === 'CHANNELD_NORMAL')
138+
.reduce((acc: any, o: any) => acc + o.channel_sat, 0),
139+
pending_open_balance: channels
140+
.filter((o: any) => o.state === 'CHANNELD_AWAITING_LOCKIN')
141+
.reduce((acc: any, o: any) => acc + o.channel_sat, 0)
142+
}));
143+
sendCoins = (data: TransactionRequest) =>
144+
this.rpc('withdraw', {
145+
desination: data.addr,
146+
feerate: `${Number(data.sat_per_byte) * 1000}perkb`,
147+
satoshi: data.amount
148+
});
149+
getMyNodeInfo = () => this.rpc('getinfo');
150+
getInvoices = () =>
151+
this.rpc('listinvoices', {}).then(({ invoices }: any) => ({
152+
invoices: invoices.map((inv: any) => ({
153+
memo: inv.description,
154+
r_preimage: inv.payment_preimage,
155+
r_hash: inv.payment_hash,
156+
value: inv.msatoshi / 1000,
157+
value_msat: inv.msatoshi,
158+
settled: inv.status === 'paid',
159+
creation_date: inv.expires_at,
160+
settle_date: inv.paid_at,
161+
payment_request: inv.bolt11,
162+
expiry: inv.expires_at,
163+
amt_paid: inv.msatoshi_received / 1000,
164+
amt_paid_sat: inv.msatoshi_received / 1000,
165+
amt_paid_msat: inv.msatoshi_received
166+
}))
167+
}));
168+
createInvoice = (data: any) =>
169+
this.rpc('invoice', {
170+
description: data.memo,
171+
label: 'zeus.' + Math.random() * 1000000,
172+
msatoshi: Number(data.value) * 1000,
173+
expiry: Math.round(Date.now() / 1000) + Number(data.expiry),
174+
exposeprivatechannels: true
175+
});
176+
getPayments = () =>
177+
this.rpc('listsendpays', {}).then(({ pays }: any) => ({
178+
payments: pays
179+
}));
180+
getNewAddress = () => this.rpc('newaddr');
181+
openChannel = (data: OpenChannelRequest) =>
182+
this.rpc('fundchannel', {
183+
id: data.node_pubkey_string,
184+
amount: data.satoshis,
185+
feerate: `${Number(data.sat_per_byte) * 1000}perkb`,
186+
announce: !data.privateChannel
187+
}).then(({ txid }: any) => ({ funding_txid_str: txid }));
188+
connectPeer = (data: any) =>
189+
this.rpc('connect', [data.addr.pubkey, data.addr.host]);
190+
decodePaymentRequest = (urlParams?: Array<string>) =>
191+
this.rpc('decodepay', [urlParams && urlParams[0]]);
192+
payLightningInvoice = (data: any) =>
193+
this.rpc('pay', {
194+
bolt11: data.payment_request,
195+
msatoshi: data.amt ? Number(data.amt * 1000) : undefined
196+
});
197+
closeChannel = (urlParams?: Array<string>) =>
198+
this.rpc('close', {
199+
id: urlParams && urlParams[0],
200+
unilateraltimeout: urlParams && urlParams[1] ? 60 : 0
201+
}).then(() => ({ chan_close: { success: true } }));
202+
getNodeInfo = (urlParams?: Array<string>) =>
203+
this.rpc('listnodes', [urlParams && urlParams[0]]).then(
204+
({ nodes }: any) => {
205+
const node = nodes[0];
206+
return {
207+
node: node && {
208+
last_update: node.last_timestamp,
209+
pub_key: node.nodeid,
210+
alias: node.alias,
211+
color: node.color,
212+
addresses: node.addresses.map((addr: any) => ({
213+
network: 'tcp',
214+
addr:
215+
addr.type === 'ipv6'
216+
? `[${addr.address}]:${addr.port}`
217+
: `${addr.address}:${addr.port}`
218+
}))
219+
}
220+
};
221+
}
222+
);
223+
getFees = async () => {
224+
const info = await this.rpc('getinfo');
225+
226+
const [listforwards, listpeers, listchannels] = await Promise.all([
227+
this.rpc('listforwards'),
228+
this.rpc('listpeers'),
229+
this.rpc('listchannels', { source: info.id })
230+
]);
231+
232+
let lastDay = 0,
233+
lastWeek = 0,
234+
lastMonth = 0;
235+
const now = new Date().getTime() / 1000;
236+
const oneDayAgo = now - 60 * 60 * 24;
237+
const oneWeekAgo = now - 60 * 60 * 24 * 7;
238+
const oneMonthAgo = now - 60 * 60 * 24 * 30;
239+
for (let i = listforwards.forwards.length - 1; i >= 0; i--) {
240+
const forward = listforwards.forwards[i];
241+
if (forward.status !== 'settled') {
242+
continue;
243+
}
244+
if (forward.resolved_time > oneDayAgo) {
245+
lastDay += forward.fee;
246+
lastWeek += forward.fee;
247+
lastMonth += forward.fee;
248+
} else if (forward.resolved_time > oneWeekAgo) {
249+
lastWeek += forward.fee;
250+
lastMonth += forward.fee;
251+
} else if (forward.resolved_time > oneMonthAgo) {
252+
lastMonth += forward.fee;
253+
} else {
254+
break;
255+
}
256+
}
257+
258+
const channelsMap: any = {};
259+
for (let i = 0; i < listchannels.channels.length; i++) {
260+
const channel = listchannels.channels[i];
261+
channelsMap[channel.short_channel_id] = {
262+
base_fee_msat: channel.base_fee_millisatoshi,
263+
fee_rate: channel.fee_per_millionth / 1000000
264+
};
265+
}
266+
267+
return {
268+
channel_fees: listpeers.peers
269+
.filter(({ channels }: any) => channels && channels.length)
270+
.filter(
271+
({ channels: [{ short_channel_id }] }: any) =>
272+
channelsMap[short_channel_id]
273+
)
274+
.map(
275+
({
276+
channels: [
277+
{ short_channel_id, channel_id, funding_txid }
278+
]
279+
}: any) => ({
280+
chan_id: channel_id,
281+
channel_point: funding_txid,
282+
base_fee_msat:
283+
channelsMap[short_channel_id].base_fee_msat,
284+
fee_rate: channelsMap[short_channel_id].fee_rate
285+
})
286+
),
287+
day_fee_sum: lastDay / 1000,
288+
week_fee_sum: lastWeek / 1000,
289+
month_fee_sum: lastMonth / 1000
290+
};
291+
};
292+
setFees = (data: any) =>
293+
this.rpc('setchannelfee', {
294+
id: data.global ? 'all' : data.channelId,
295+
base: data.base_fee_msat,
296+
ppm: data.fee_rate * 1000000
297+
});
298+
getRoutes = async (urlParams?: Array<string>) => {
299+
const msatoshi = Number(urlParams && urlParams[1]) * 1000;
300+
301+
const res = await this.rpc('getroute', {
302+
id: urlParams && urlParams[0],
303+
msatoshi,
304+
riskfactor: 2
305+
});
306+
307+
const route = res.route[0];
308+
309+
return {
310+
routes: [
311+
{
312+
total_fees: (route[0].msatoshi - msatoshi) / 1000
313+
}
314+
]
315+
};
316+
};
317+
318+
supportsMessageSigning = () => false;
319+
supportsOnchainSends = () => true;
320+
supportsOnchainReceiving = () => true;
321+
supportsKeysend = () => false;
322+
supportsChannelManagement = () => true;
323+
supportsMPP = () => false;
324+
supportsAMP = () => false;
325+
supportsCoinControl = () => false;
326+
supportsHopPicking = () => false;
327+
supportsAccounts = () => false;
328+
supportsRouting = () => true;
329+
supportsNodeInfo = () => true;
330+
singleFeesEarnedTotal = () => false;
331+
supportsAddressTypeSelection = () => false;
332+
supportsTaproot = () => false;
333+
isLNDBased = () => false;
334+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"identicon.js": "2.3.3",
9595
"js-lnurl": "0.5.1",
9696
"js-sha256": "0.9.0",
97+
"lnmessage": "^0.2.7",
9798
"lodash": "4.17.21",
9899
"long": "5.2.3",
99100
"lottie-react-native": "7.3.4",

stores/SettingsStore.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface Node {
3434
nickname?: string;
3535
dismissCustodialWarning: boolean;
3636
photo?: string;
37+
pubkey?: string;
3738
// LNC
3839
pairingPhrase?: string;
3940
mailboxServer?: string;
@@ -362,6 +363,7 @@ export const INTERFACE_KEYS: {
362363
{ key: 'LND (REST)', value: 'lnd' },
363364
{ key: 'LND (Lightning Node Connect)', value: 'lightning-node-connect' },
364365
{ key: 'Core Lightning (CLNRest)', value: 'cln-rest' },
366+
{ key: 'Core Lightning (LNSocket)', value: 'lnsocket' },
365367
{ key: 'Nostr Wallet Connect', value: 'nostr-wallet-connect' },
366368
{ key: 'LNDHub', value: 'lndhub' }
367369
];
@@ -372,7 +374,8 @@ export type Implementations =
372374
| 'lightning-node-connect'
373375
| 'cln-rest'
374376
| 'lndhub'
375-
| 'nostr-wallet-connect';
377+
| 'nostr-wallet-connect'
378+
| 'lnsocket';
376379

377380
export const EMBEDDED_NODE_NETWORK_KEYS = [
378381
{ key: 'Mainnet', translateKey: 'network.mainnet', value: 'mainnet' },
@@ -1523,6 +1526,7 @@ export default class SettingsStore {
15231526
@observable public customMailboxServer: string;
15241527
@observable public error = false;
15251528
@observable public errorMsg: string;
1529+
@observable public pubkey: string;
15261530
// Embedded lnd
15271531
@observable public seedPhrase: Array<string>;
15281532
@observable public walletPassword: string;
@@ -1684,6 +1688,7 @@ export default class SettingsStore {
16841688
this.implementation = node.implementation || 'lnd';
16851689
this.certVerification = node.certVerification || false;
16861690
this.enableTor = node.enableTor;
1691+
this.pubkey = node.pubkey;
16871692
// LNC
16881693
this.pairingPhrase = node.pairingPhrase;
16891694
this.mailboxServer = node.mailboxServer;

0 commit comments

Comments
 (0)