Files
app.zeusln.zeus/stores/ChannelsStore.ts
T

1265 lines
46 KiB
TypeScript

import { action, observable, reaction, runInAction } from 'mobx';
import BigNumber from 'bignumber.js';
// leave as is, do not do tree-shaking
import { chain } from 'lodash';
import { randomBytes } from 'react-native-randombytes';
import Channel from '../models/Channel';
import ClosedChannel from '../models/ClosedChannel';
import ChannelInfo, { RoutingPolicy } from '../models/ChannelInfo';
import FundedPsbt from '../models/FundedPsbt';
import Peer from '../models/Peer';
import OpenChannelRequest from '../models/OpenChannelRequest';
import CloseChannelRequest from '../models/CloseChannelRequest';
import SettingsStore from './SettingsStore';
import BackendUtils from '../utils/BackendUtils';
import { localeString } from '../utils/LocaleUtils';
import { errorToUserFriendly } from '../utils/ErrorUtils';
interface ChannelInfoIndex {
[key: string]: ChannelInfo;
}
interface PendingHTLC {
incoming: boolean;
amount: number | string;
hash_lock: string;
expiration_height: number;
htlc_index?: number;
forwarding_channel?: number;
forwarding_htlc_index?: number;
}
export enum ChannelsType {
Open = 0,
Pending = 1,
Closed = 2
}
export enum ChannelsView {
Channels = 'channels',
Peers = 'peers'
}
interface aliases {
[index: string]: string;
}
const fixedAliases: aliases = {
'031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581':
'Olympus by ZEUS'
};
ChannelsView;
export default class ChannelsStore {
@observable public loading = false;
@observable public error = false;
@observable public errorPeerConnect = false;
@observable public errorMsgChannel: string | null;
@observable public errorMsgPeer: string | null;
@observable public errorListPeers: string | null;
@observable public errorDisconnectPeer: string | null;
@observable public nodes: any = {};
@observable public aliasesByChannelId: aliases = {};
@observable public aliasesByPubkey: aliases = { ...fixedAliases };
@observable public channels: Array<Channel> = [];
@observable public pendingChannels: Array<Channel> = [];
@observable public closedChannels: Array<Channel> = [];
@observable public output_index: number | null;
@observable public funding_txid_str: string | null;
@observable public openingChannel = false;
@observable public connectingToPeer = false;
@observable public errorOpenChannel = false;
@observable public peerSuccess = false;
@observable public channelSuccess = false;
@observable public closeChannelErr: string | null;
@observable public closingChannel = false;
@observable channelRequest: any;
@observable public peers: Array<Peer> = [];
// redesign
@observable public largestChannelSats = 0;
@observable public totalOutbound = 0;
@observable public totalInbound = 0;
@observable public totalOffline = 0;
@observable public chanInfo: ChannelInfoIndex = {};
@observable public channelsType = ChannelsType.Open;
@observable public channelsView: ChannelsView = ChannelsView.Channels;
// enriched
@observable public enrichedChannels: Array<Channel> = [];
@observable public enrichedPendingChannels: Array<Channel> = [];
@observable public enrichedClosedChannels: Array<Channel> = [];
// search and sort
@observable public search: string = '';
@observable public filteredChannels: Array<Channel> = [];
@observable public filteredPendingChannels: Array<Channel> = [];
@observable public filteredClosedChannels: Array<Channel> = [];
@observable public filteredPeers: Array<Peer> = [];
@observable public filterOptions: Array<string> = [];
@observable public sort = {
param: 'channelCapacity',
dir: 'DESC',
type: 'numeric'
};
@observable public showChannelsSearch: boolean = false;
@observable public showPeersSearch: boolean = false;
// aliasMap
@observable public aliasMap = observable.map({});
// external account funding
@observable public funded_psbt: string = '';
@observable public pending_chan_ids: Array<string>;
// pending HTLCs
@observable public pendingHTLCs: Array<PendingHTLC>;
@observable public haveAnnouncedChannels = false;
settingsStore: SettingsStore;
constructor(settingsStore: SettingsStore) {
this.settingsStore = settingsStore;
reaction(
() => this.channelRequest,
() => {
if (this.channelRequest) {
const chanReq = new OpenChannelRequest(this.channelRequest);
this.openChannel(chanReq);
}
}
);
reaction(
() => this.channels,
async () => {
if (this.channels) {
this.enrichedChannels = await this.enrichChannels(
this.channels,
true
);
this.filterChannels();
}
}
);
reaction(
() => this.pendingChannels,
async () => {
if (this.pendingChannels) {
this.enrichedPendingChannels = await this.enrichChannels(
this.pendingChannels
);
this.filterPendingChannels();
}
}
);
reaction(
() => this.closedChannels,
async () => {
if (this.closedChannels) {
this.enrichedClosedChannels = await this.enrichChannels(
this.closedChannels
);
this.filterClosedChannels();
}
}
);
}
@action
public resetOpenChannel = (silent?: boolean) => {
this.loading = false;
this.error = false;
if (!silent) {
this.errorMsgPeer = null;
this.errorPeerConnect = false;
this.connectingToPeer = false;
this.peerSuccess = false;
}
this.errorMsgChannel = null;
this.output_index = null;
this.funding_txid_str = null;
this.openingChannel = false;
this.errorOpenChannel = false;
this.channelSuccess = false;
this.channelRequest = null;
this.funded_psbt = '';
this.pending_chan_ids = [];
};
public clearCloseChannelErr = () => {
this.closeChannelErr = null;
};
@action
public reset = () => {
this.resetOpenChannel();
this.haveAnnouncedChannels = false;
this.nodes = {};
this.channels = [];
this.pendingChannels = [];
this.closedChannels = [];
this.enrichedChannels = [];
this.enrichedPendingChannels = [];
this.enrichedClosedChannels = [];
this.filteredChannels = [];
this.filteredPendingChannels = [];
this.filteredClosedChannels = [];
this.filteredPeers = [];
this.largestChannelSats = 0;
this.totalOutbound = 0;
this.totalInbound = 0;
this.totalOffline = 0;
this.channelsType = ChannelsType.Open;
this.pendingHTLCs = [];
this.peers = [];
};
@action
public setSearch = (query: string) => {
this.search = query;
this.filterChannels();
this.filterPendingChannels();
this.filterClosedChannels();
this.filterPeers();
};
@action
public setSort = (value: any) => {
this.sort = value;
if (this.channelsView === ChannelsView.Channels) {
this.filterChannels();
this.filterPendingChannels();
this.filterClosedChannels();
} else {
this.filterPeers();
}
};
@action
public setFilterOptions = (options: string[]) => {
this.filterOptions = options;
this.filterChannels();
this.filterPendingChannels();
this.filterClosedChannels();
};
private filter = (
items: Array<Channel | Peer>,
type: 'channel' | 'peer'
) => {
const query = this.search.toLocaleLowerCase();
let filtered = items;
if (type === 'channel') {
filtered = (items as Channel[])
.filter(
(channel) =>
channel.alias?.toLowerCase().includes(query) ||
channel.remotePubkey?.toLowerCase().includes(query) ||
channel.channelId
?.toString()
?.toLowerCase()
.includes(query)
)
.filter(
(channel) =>
(!this.filterOptions?.includes('unannounced') &&
!this.filterOptions?.includes('announced')) ||
this.filterOptions?.includes(
channel.private ? 'unannounced' : 'announced'
)
)
.filter(
(channel) =>
(!this.filterOptions?.includes('online') &&
!this.filterOptions?.includes('offline')) ||
this.filterOptions?.includes(
channel.active ? 'online' : 'offline'
)
);
}
if (type === 'peer') {
filtered = (items as Peer[]).filter(
(peer) =>
this.nodes[peer.pubkey]?.alias
?.toLowerCase()
.includes(query) ||
peer.alias?.toLowerCase().includes(query) ||
peer.pubkey?.toLowerCase().includes(query)
);
}
const sorted = filtered.sort((a: any, b: any) => {
const param = this.sort?.param;
const dir = this.sort?.dir;
const sortType = this.sort?.type;
const isPeerType = type === 'peer';
if (!param || !dir || !sortType) return 0;
const getSortValue = (item: any) => {
if (isPeerType && param === 'alias') {
return (
this.nodes[item.pubkey]?.alias ||
item.alias ||
item.pubkey ||
''
)
.toString()
.toLowerCase();
}
return item[param]?.toString().toLowerCase() || '';
};
const aVal = getSortValue(a);
const bVal = getSortValue(b);
if (sortType === 'numeric') {
const aNum = Number(aVal) || 0;
const bNum = Number(bVal) || 0;
return dir === 'DESC' ? bNum - aNum : aNum - bNum;
} else {
if (!aVal && !bVal) return 0;
if (!aVal) return dir === 'DESC' ? -1 : 1;
if (!bVal) return dir === 'DESC' ? 1 : -1;
return dir === 'DESC'
? bVal.localeCompare(aVal)
: aVal.localeCompare(bVal);
}
});
return sorted;
};
private filterChannels = () => {
this.filteredChannels = this.filter(
this.enrichedChannels,
'channel'
) as Channel[];
};
private filterPendingChannels = () => {
this.filteredPendingChannels = this.filter(
this.enrichedPendingChannels,
'channel'
) as Channel[];
};
private filterClosedChannels = () => {
this.filteredClosedChannels = this.filter(
this.enrichedClosedChannels,
'channel'
) as Channel[];
};
private filterPeers = () => {
this.filteredPeers = this.filter(this.peers, 'peer') as Peer[];
};
public getNodeInfo = (pubkey: string) => {
this.loading = true;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), 10000);
});
return Promise.race([
timeoutPromise,
BackendUtils.getNodeInfo([pubkey])
])
.then((data: any) => {
runInAction(() => {
this.loading = false;
if (data?.node?.alias)
this.aliasMap.set(pubkey, data.node.alias);
});
return data.node;
})
.catch(() => {
this.loading = false;
this.aliasMap.set(pubkey, localeString('general.unknown'));
});
};
private enrichChannels = async (
channels: Array<Channel>,
setPendingHtlcs?: boolean
): Promise<Channel[]> => {
if (channels.length === 0) return [];
const channelsWithMissingAliases = channels?.filter(
(c) =>
c.channelId != null &&
this.aliasesByChannelId[c.channelId] == null
);
const channelsWithMissingNodeInfos = channels?.filter(
(c) => this.nodes[c.remotePubkey] == null
);
const publicKeysOfToBeLoadedNodeInfos = chain(
channelsWithMissingAliases.concat(channelsWithMissingNodeInfos)
)
.map((c) => c.remotePubkey)
.uniq()
.value();
this.loading = true;
await Promise.all(
publicKeysOfToBeLoadedNodeInfos.map(
async (remotePubKey: string) => {
if (this.settingsStore.implementation !== 'cln-rest') {
const nodeInfo = await this.getNodeInfo(remotePubKey);
if (!nodeInfo) return;
this.nodes[remotePubKey] = nodeInfo;
}
}
)
);
runInAction(() => {
for (const channel of channelsWithMissingAliases) {
const nodeInfo = this.nodes[channel.remotePubkey];
const alias =
nodeInfo?.alias || fixedAliases[channel.remotePubkey];
if (alias) {
this.aliasesByChannelId[channel.channelId!] = alias;
this.aliasesByPubkey[channel.remotePubkey] = alias;
}
}
if (setPendingHtlcs) this.pendingHTLCs = [];
let haveAnnouncedChannels = false;
for (const channel of channels) {
if (
!haveAnnouncedChannels &&
!channel.private &&
channel.isActive
) {
haveAnnouncedChannels = true;
}
if (channel.alias == null) {
channel.alias =
this.nodes[channel.remotePubkey]?.alias ||
this.aliasesByChannelId[channel.channelId!];
}
channel.displayName =
channel.alias ||
channel.remotePubkey ||
channel.channelId ||
localeString('models.Channel.unknownId');
if (BackendUtils.isLNDBased() && setPendingHtlcs) {
channel.pending_htlcs?.forEach((htlc: any) => {
htlc.channelDisplayName = channel.displayName;
});
this.pendingHTLCs.push(...channel.pending_htlcs);
}
}
this.haveAnnouncedChannels = haveAnnouncedChannels;
if (this.pendingHTLCs.length > 0) {
console.log('Pending HTLCs', this.pendingHTLCs);
}
this.loading = false;
});
return channels;
};
@action
private getChannelsError = () => {
this.channels = [];
this.error = true;
this.loading = false;
this.closingChannel = false;
};
@action
public getChannels = () => {
this.loading = true;
this.channels = [];
this.largestChannelSats = 0;
this.totalOutbound = 0;
this.totalInbound = 0;
this.totalOffline = 0;
const loadPromises = [
BackendUtils.getChannels().then((data: any) => {
const channels = data.channels.map(
(channel: any) => new Channel(channel)
);
channels.forEach((channel: Channel) => {
const channelRemoteBalance = new BigNumber(
channel.receivingCapacity
);
const channelLocalBalance = new BigNumber(
channel.sendingCapacity
);
const channelTotal =
channelRemoteBalance.plus(channelLocalBalance);
if (channelTotal.gt(this.largestChannelSats))
this.largestChannelSats = channelTotal.toNumber();
if (!channel.isActive) {
this.totalOffline = new BigNumber(this.totalOffline)
.plus(channelTotal)
.toNumber();
} else {
this.totalInbound = new BigNumber(this.totalInbound)
.plus(channelRemoteBalance)
.toNumber();
this.totalOutbound = new BigNumber(this.totalOutbound)
.plus(channelLocalBalance)
.toNumber();
}
});
this.channels = channels;
})
];
if (BackendUtils.supportsClosedChannels()) {
loadPromises.push(
BackendUtils.getClosedChannels().then((data: any) => {
const closedChannels = data.channels.map(
(channel: any) => new ClosedChannel(channel)
);
this.closedChannels = closedChannels;
})
);
}
if (BackendUtils.supportsPendingChannels()) {
loadPromises.push(
BackendUtils.getPendingChannels().then((data: any) => {
const pendingOpenChannels = data.pending_open_channels.map(
(pending: any) => {
pending.channel.pendingOpen = true;
return new Channel(pending.channel);
}
);
const pendingCloseChannels =
data.pending_closing_channels.map((pending: any) => {
pending.channel.pendingClose = true;
pending.channel.closing_txid = pending.closing_txid;
return new Channel(pending.channel);
});
const forceCloseChannels =
data.pending_force_closing_channels.map(
(pending: any) => {
pending.channel.blocks_til_maturity =
pending.blocks_til_maturity;
pending.channel.forceClose = true;
pending.channel.closing_txid =
pending.closing_txid;
return new Channel(pending.channel);
}
);
const waitCloseChannels = data.waiting_close_channels.map(
(pending: any) => {
pending.channel.closing = true;
return new Channel(pending.channel);
}
);
this.pendingChannels = pendingOpenChannels
.concat(pendingCloseChannels)
.concat(forceCloseChannels)
.concat(waitCloseChannels);
})
);
}
return Promise.all(loadPromises)
.then(() => {
runInAction(() => {
this.loading = false;
this.error = false;
});
return;
})
.catch(() => this.getChannelsError());
};
// For embedded LND: polls getChannels until at least one channel is online
// This is useful because channels may take time to come online after node startup
@action
public getChannelsWithPolling = async (
pollingIntervalMs: number = 3000,
maxAttempts: number = 20
): Promise<void> => {
let attempts = 0;
const pollChannels = async (): Promise<void> => {
attempts++;
await this.getChannels();
// If no open channels, no need to poll
if (this.channels.length === 0) {
return;
}
// Check if at least one channel is active (online)
const hasActiveChannel = this.channels.some(
(channel) => channel.isActive
);
if (hasActiveChannel) {
return;
}
// If we've exceeded max attempts, stop polling
if (attempts >= maxAttempts) {
console.log(
'ChannelsStore: Max polling attempts reached, stopping poll'
);
return;
}
// Wait and try again
await new Promise((resolve) =>
setTimeout(resolve, pollingIntervalMs)
);
return pollChannels();
};
return pollChannels();
};
@action
public closeChannel = async (
channelPoint?: CloseChannelRequest,
channelId?: string,
satPerVbyte?: string,
forceClose?: boolean | string,
deliveryAddress?: string
) => {
this.closeChannelErr = null;
this.closingChannel = true;
let urlParams: Array<any> = [];
const implementation = this.settingsStore.implementation;
if (implementation === 'cln-rest') {
// Core Lightning
urlParams = [channelId, forceClose];
} else if (channelPoint) {
// lnd
const { funding_txid_str, output_index } = channelPoint;
urlParams = [
funding_txid_str,
output_index,
forceClose,
satPerVbyte,
deliveryAddress
];
}
if (implementation === 'lightning-node-connect') {
return BackendUtils.closeChannel(urlParams);
} else {
let resolved = false;
return await Promise.race([
new Promise((resolve) => {
BackendUtils.closeChannel(urlParams)
.then(() => {
this.handleChannelClose();
resolved = true;
resolve(true);
})
.catch((error: Error) => {
this.handleChannelCloseError(error);
resolved = true;
resolve(true);
});
}),
// LND REST call tends to time out, so let's put a 6 second
// forced resolution, as true errors will typically throw
// before that time
new Promise(async (resolve) => {
await new Promise((res) => setTimeout(res, 6000));
if (!resolved) this.handleChannelClose();
resolve(true);
})
]);
}
};
@action
public handleChannelClose = () => {
this.closeChannelErr = null;
this.error = false;
this.closingChannel = false;
};
@action
public handleChannelCloseError = (error: Error) => {
this.closeChannelErr = errorToUserFriendly(error);
this.getChannelsError();
};
@action
public connectPeer = async (
request: OpenChannelRequest,
perm?: boolean,
connectPeerOnly?: boolean,
silent?: boolean
) => {
this.resetOpenChannel(silent);
this.channelRequest = undefined;
if (!silent) this.connectingToPeer = true;
if (!request.host) {
return await new Promise((resolve) => {
this.channelRequest = request;
resolve(true);
});
}
// connect to additional channel peers
if (request.additionalChannels) {
for (let i = 0; i < request.additionalChannels?.length; i++) {
const channel = request.additionalChannels[i];
await BackendUtils.connectPeer({
addr: {
pubkey: channel.node_pubkey_string,
host: channel.host
},
perm
}).catch(() => {});
}
}
return await new Promise((resolve, reject) => {
BackendUtils.connectPeer({
addr: {
pubkey: request.node_pubkey_string,
host: request.host
},
perm
})
.then(async () => {
runInAction(() => {
if (!silent) {
this.errorPeerConnect = false;
this.connectingToPeer = false;
this.errorMsgPeer = null;
this.peerSuccess = true;
}
if (!connectPeerOnly) this.channelRequest = request;
});
resolve(true);
})
.catch((error: Error) => {
runInAction(() => {
if (!silent) {
this.connectingToPeer = false;
this.peerSuccess = false;
}
this.channelSuccess = false;
// handle error
if (
error &&
error.toString() &&
error.toString().includes('already')
) {
if (!connectPeerOnly) {
this.channelRequest = request;
} else {
if (!silent) {
this.errorMsgPeer =
errorToUserFriendly(error);
this.errorPeerConnect = true;
}
}
resolve(true);
} else {
if (!silent) {
this.errorMsgPeer = errorToUserFriendly(error);
this.errorPeerConnect = true;
}
reject(this.errorMsgPeer);
}
});
});
});
};
@action
public getPeers = async () => {
this.loading = true;
this.error = false;
this.errorListPeers = null;
try {
const response = await BackendUtils.listPeers();
runInAction(() => {
this.peers = response.map((peerData: any) => {
return new Peer(peerData);
});
this.filterPeers();
this.loading = false;
});
} catch (error: any) {
runInAction(() => {
this.error = true;
this.errorListPeers =
error.message ??
localeString('views.ChannelsPane.peersFetchFailed');
this.loading = false;
});
}
};
@action
public disconnectPeer = async (pubkey: string) => {
this.loading = true;
this.error = false;
this.errorDisconnectPeer = null;
try {
const isPeer = this.peers.find(
(peer: Peer) => peer.pubkey === pubkey
);
if (!isPeer) {
throw new Error(
localeString('views.ChannelsPane.peerNotFound')
);
}
const res = await BackendUtils.disconnectPeer(pubkey);
if (!res) {
throw new Error(
localeString('views.ChannelsPane.peerNotConnected')
);
}
runInAction(() => {
this.peers = this.peers.filter(
(peer: Peer) => peer.pubkey !== pubkey
);
this.filterPeers();
this.loading = false;
});
return true;
} catch (error: any) {
this.error = true;
this.errorDisconnectPeer =
error?.message?.message ||
error?.message ||
localeString('views.ChannelsPane.disconnectPeerFailed');
this.loading = false;
return false;
}
};
private handleChannelOpen = (request: any, outputs?: any) => {
const { account, sat_per_vbyte, utxos } = request;
const inputs: any = [];
if (utxos) {
utxos.forEach((input: any) => {
const [txid_str, output_index] = input.split(':');
inputs.push({
txid_str,
output_index: Number(output_index)
});
});
}
const fundPsbtRequest = {
raw: {
outputs,
inputs
},
sat_per_vbyte: Number(sat_per_vbyte),
spend_unconfirmed: true,
account
};
BackendUtils.fundPsbt(fundPsbtRequest)
.then(async (data: any) => {
let funded_psbt: string = new FundedPsbt(
data.funded_psbt
).getFormatted();
await BackendUtils.signPsbt({ funded_psbt })
.then((data: any) => {
if (data.signed_psbt) funded_psbt = data.signed_psbt;
return;
})
.catch((e: any) => {
console.log('signPsbt err', e);
return;
});
for (let i = 0; i < this.pending_chan_ids.length - 1; i++) {
const pending_chan_id = this.pending_chan_ids[i];
await BackendUtils.fundingStateStep({
psbt_verify: {
funded_psbt,
pending_chan_id,
skip_finalize: true
}
})
.then((data: any) => {
console.log(`fundingStateStep - data ${i}`, data);
return;
})
.catch((e: any) => {
console.log(`fundingStateStep - err ${i}`, e);
return;
});
}
BackendUtils.fundingStateStep({
psbt_verify: {
funded_psbt,
pending_chan_id:
this.pending_chan_ids[
this.pending_chan_ids.length - 1
]
}
})
.then((data: any) => {
if (data.publish_error) {
const errorMsg = errorToUserFriendly(
data.publish_error
);
runInAction(() => {
this.errorMsgChannel = errorMsg;
this.output_index = null;
this.funding_txid_str = null;
this.errorOpenChannel = true;
this.openingChannel = false;
this.channelRequest = null;
this.peerSuccess = false;
this.channelSuccess = false;
});
} else {
const formattedPsbt = new FundedPsbt(
funded_psbt
).getFormatted();
// Attempt finalize here
BackendUtils.fundingStateStep({
psbt_finalize: {
signed_psbt: formattedPsbt,
pending_chan_id:
this.pending_chan_ids[
this.pending_chan_ids.length - 1
]
}
})
.then((data: any) => {
runInAction(() => {
if (data.publish_error) {
this.funded_psbt = formattedPsbt;
this.output_index = null;
this.funding_txid_str = null;
this.errorOpenChannel = true;
this.openingChannel = false;
this.channelRequest = null;
this.peerSuccess = false;
this.channelSuccess = false;
} else {
// success case
this.errorOpenChannel = false;
this.openingChannel = false;
this.errorMsgChannel = null;
this.channelRequest = null;
this.channelSuccess = true;
}
});
})
.catch(() =>
runInAction(() => {
this.funded_psbt = formattedPsbt;
this.output_index = null;
this.funding_txid_str = null;
this.errorOpenChannel = true;
this.openingChannel = false;
this.channelRequest = null;
this.peerSuccess = false;
this.channelSuccess = false;
})
);
}
})
.catch((error: any) => {
const errorMsg = errorToUserFriendly(error);
runInAction(() => {
this.errorMsgChannel = errorMsg;
this.output_index = null;
this.funding_txid_str = null;
this.errorOpenChannel = true;
this.openingChannel = false;
this.channelRequest = null;
this.peerSuccess = false;
this.channelSuccess = false;
});
});
})
.catch((error: any) => {
const errorMsg = errorToUserFriendly(error);
runInAction(() => {
this.errorMsgChannel = errorMsg;
this.output_index = null;
this.funding_txid_str = null;
this.errorOpenChannel = true;
this.openingChannel = false;
this.channelRequest = null;
this.peerSuccess = false;
this.channelSuccess = false;
});
});
};
@action
private handleChannelOpenError = (error: Error) => {
this.errorMsgChannel = errorToUserFriendly(error);
this.output_index = null;
this.funding_txid_str = null;
this.errorOpenChannel = true;
this.openingChannel = false;
this.channelRequest = null;
this.peerSuccess = false;
this.channelSuccess = false;
this.connectingToPeer = false;
};
@action
private openChannel = (request: OpenChannelRequest) => {
const multipleChans =
request?.additionalChannels &&
request.additionalChannels?.length > 0;
delete request.host;
if (!multipleChans) delete request.additionalChannels;
this.peerSuccess = false;
this.channelSuccess = false;
this.openingChannel = true;
if (request?.account !== 'default' || multipleChans) {
const outputs: any = {};
if (multipleChans) {
let base_psbt = '';
request.funding_shim = {
psbt_shim: {
base_psbt,
pending_chan_id: randomBytes(32).toString('base64'),
no_publish: true
}
};
BackendUtils.openChannelStream(request)
.then((data: any) => {
const { psbt_fund, pending_chan_id } = data.result;
this.pending_chan_ids.push(pending_chan_id);
outputs[psbt_fund.funding_address] =
psbt_fund.funding_amount;
base_psbt = psbt_fund.psbt;
for (
let i = 0;
i < (request?.additionalChannels?.length || 0);
i++
) {
if (
request.additionalChannels &&
request.additionalChannels.length !== i + 1
) {
request.funding_shim = {
psbt_shim: {
base_psbt,
pending_chan_id:
randomBytes(32).toString('base64'),
no_publish: true
}
};
request.node_pubkey_string =
request.additionalChannels[
i
].node_pubkey_string;
request.local_funding_amount =
request.additionalChannels[
i
].satAmount.toString();
BackendUtils.openChannelStream(request)
.then((data: any) => {
const { psbt_fund, pending_chan_id } =
data.result;
this.pending_chan_ids.push(
pending_chan_id
);
outputs[psbt_fund.funding_address] =
psbt_fund.funding_amount;
base_psbt = psbt_fund.psbt;
})
.catch((error: Error) => {
this.handleChannelOpenError(error);
});
} else if (request.additionalChannels) {
// final chan
request.funding_shim = {
psbt_shim: {
base_psbt,
pending_chan_id:
randomBytes(32).toString('base64'),
no_publish: false
}
};
request.node_pubkey_string =
request.additionalChannels[
i
].node_pubkey_string;
request.local_funding_amount =
request.additionalChannels[
i
].satAmount.toString();
BackendUtils.openChannelStream(request)
.then((data: any) => {
const { psbt_fund, pending_chan_id } =
data.result;
this.pending_chan_ids.push(
pending_chan_id
);
outputs[psbt_fund.funding_address] =
psbt_fund.funding_amount;
this.handleChannelOpen(
request,
outputs
);
})
.catch((error: Error) => {
this.handleChannelOpenError(error);
});
}
}
})
.catch((error: Error) => {
this.handleChannelOpenError(error);
});
} else {
request.funding_shim = {
psbt_shim: {
base_psbt: '',
pending_chan_id: randomBytes(32).toString('base64')
}
};
BackendUtils.openChannelStream(request)
.then((data: any) => {
const { psbt_fund, pending_chan_id } = data.result;
this.pending_chan_ids.push(pending_chan_id);
if (psbt_fund?.funding_address) {
outputs[psbt_fund.funding_address] =
psbt_fund.funding_amount;
}
this.handleChannelOpen(request, outputs);
})
.catch((error: Error) => {
this.handleChannelOpenError(error);
});
}
} else {
BackendUtils.openChannelSync(request)
.then((data: any) =>
runInAction(() => {
this.output_index = data.output_index;
this.funding_txid_str = data.funding_txid_str;
this.errorOpenChannel = false;
this.openingChannel = false;
this.errorMsgChannel = null;
this.channelRequest = null;
this.channelSuccess = true;
this.connectingToPeer = false;
})
)
.catch((error: Error) => {
const errorMsg = errorToUserFriendly(error);
runInAction(() => {
this.errorMsgChannel = errorMsg;
this.output_index = null;
this.funding_txid_str = null;
this.errorOpenChannel = true;
this.openingChannel = false;
this.connectingToPeer = false;
this.channelRequest = null;
this.peerSuccess = false;
this.channelSuccess = false;
});
});
}
};
@action
public loadChannelInfo = (chanId: string, deleteBeforeLoading = false) => {
this.loading = true;
if (deleteBeforeLoading) {
if (this.chanInfo[chanId]) delete this.chanInfo[chanId];
}
BackendUtils.getChannelInfo(chanId)
.then((data: any) => {
const rawChannels = data.channels || data;
const channels = BackendUtils.isLNDBased()
? rawChannels
: rawChannels[0];
runInAction(() => {
this.chanInfo[chanId] = new ChannelInfo(channels);
this.loading = false;
});
})
.catch((error: any) => {
runInAction(() => {
this.errorMsgPeer = error.toString();
if (this.chanInfo[chanId]) delete this.chanInfo[chanId];
this.loading = false;
});
});
};
// for CLNRest nodes, because the backend does not return the node policy.
public getNodePolicy = (chanId: string): RoutingPolicy => {
return {
time_lock_delta: this.chanInfo[chanId].delay,
min_htlc: this.chanInfo[chanId].htlc_minimum_msat.toString(),
fee_base_msat:
this.chanInfo[chanId].base_fee_millisatoshi.toString(),
fee_rate_milli_msat:
this.chanInfo[chanId].fee_per_millionth.toString(),
disabled: false,
max_htlc_msat: this.chanInfo[chanId].htlc_maximum_msat.toString(),
last_update: Number(this.chanInfo[chanId].last_update?.toString())
};
};
public setChannelsType = (type: ChannelsType) => {
this.channelsType = type;
};
public setChannelsView = (view: ChannelsView) => {
this.channelsView = view;
};
public toggleSearch = (type: ChannelsView) => {
if (type === ChannelsView.Channels) {
this.showChannelsSearch = !this.showChannelsSearch;
} else {
this.showPeersSearch = !this.showPeersSearch;
}
};
}