1265 lines
46 KiB
TypeScript
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;
|
|
}
|
|
};
|
|
}
|