diff --git a/backends/CLNRest.ts b/backends/CLNRest.ts new file mode 100644 index 000000000..deadece7d --- /dev/null +++ b/backends/CLNRest.ts @@ -0,0 +1,384 @@ +import stores from '../stores/Stores'; +import TransactionRequest from '../models/TransactionRequest'; +import OpenChannelRequest from '../models/OpenChannelRequest'; +import VersionUtils from '../utils/VersionUtils'; +import Base64Utils from '../utils/Base64Utils'; +import { Hash as sha256Hash } from 'fast-sha256'; +import BigNumber from 'bignumber.js'; +import { + getBalance, + getChainTransactions, + getOffchainBalance, + listPeers +} from './CoreLightningRequestHandler'; +import { localeString } from '../utils/LocaleUtils'; +import ReactNativeBlobUtil from 'react-native-blob-util'; +import { doTorRequest, RequestMethod } from '../utils/TorUtils'; + +const calls = new Map>(); + +export default class CLNRest { + getHeaders = (rune: string): any => { + return { + Rune: rune + }; + }; + + supports = ( + minVersion: string, + eosVersion?: string, + minApiVersion?: string + ) => { + const { nodeInfo } = stores.nodeInfoStore; + const { version, api_version } = nodeInfo; + const { isSupportedVersion } = VersionUtils; + if (minApiVersion) { + return ( + isSupportedVersion(version, minVersion, eosVersion) && + isSupportedVersion(api_version, minApiVersion) + ); + } + return isSupportedVersion(version, minVersion, eosVersion); + }; + + clearCachedCalls = () => calls.clear(); + + restReq = async ( + headers: Headers | any, + url: string, + method: any, + data?: any, + certVerification?: boolean, + useTor?: boolean + ) => { + // use body data as an identifier too, we don't want to cancel when we + // are making multiples calls to get all the node names, for example + const id = data ? `${url}${JSON.stringify(data)}` : url; + if (calls.has(id)) { + return calls.get(id); + } + // API is a bit of a mess but + // If tor enabled in setting, start up the daemon here + if (useTor === true) { + calls.set( + id, + doTorRequest( + url, + method as RequestMethod, + JSON.stringify(data), + headers + ).then((response: any) => { + calls.delete(id); + return response; + }) + ); + } else { + calls.set( + id, + ReactNativeBlobUtil.config({ + trusty: !certVerification + }) + .fetch( + method, + url, + headers, + data ? JSON.stringify(data) : data + ) + .then((response: any) => { + calls.delete(id); + if (response.info().status < 300) { + // handle ws responses + if (response.data.includes('\n')) { + const split = response.data.split('\n'); + const length = split.length; + // last instance is empty + return JSON.parse(split[length - 2]); + } + return response.json(); + } else { + try { + const errorInfo = response.json(); + throw new Error( + (errorInfo.error && + errorInfo.error.message) || + errorInfo.message || + errorInfo.error + ); + } catch (e) { + if ( + response.data && + typeof response.data === 'string' + ) { + throw new Error(response.data); + } else { + throw new Error( + localeString( + 'backends.LND.restReq.connectionError' + ) + ); + } + } + } + }) + ); + } + + return await calls.get(id); + }; + + request = (route: string, method: string, data?: any, params?: any) => { + const { host, port, rune, certVerification, enableTor } = + stores.settingsStore; + + if (params) { + route = `${route}?${Object.keys(params) + .map((key: string) => key + '=' + params[key]) + .join('&')}`; + } + + const headers: any = this.getHeaders(rune); + headers['Content-Type'] = 'application/json'; + + const url = this.getURL(host, port, route); + + return this.restReq( + headers, + url, + method, + data, + certVerification, + enableTor + ); + }; + + getURL = ( + host: string, + port: string | number, + route: string, + ws?: boolean + ) => { + const hostPath = host.includes('://') ? host : `https://${host}`; + let baseUrl = `${hostPath}${port ? ':' + port : ''}`; + + if (ws) { + baseUrl = baseUrl.replace('https', 'wss').replace('http', 'ws'); + } + + if (baseUrl[baseUrl.length - 1] === '/') { + baseUrl = baseUrl.slice(0, -1); + } + + return `${baseUrl}${route}`; + }; + + postRequest = (route: string, data?: any) => + this.request(route, 'post', data); + + getNode = (data: any) => + this.postRequest('/v1/listnodes', { id: data.id }).then((res) => { + return res; + }); + getTransactions = async () => await getChainTransactions(); + getChannels = async () => { + const channels = await this.postRequest('/v1/listpeerchannels'); + return await listPeers(channels); + }; + getBlockchainBalance = () => + this.postRequest('/v1/listfunds').then((res) => { + return getBalance(res); + }); + getLightningBalance = () => + this.postRequest('/v1/listfunds').then((res) => { + return getOffchainBalance(res); + }); + sendCoins = (data: TransactionRequest) => { + let request: any; + if (data.utxos) { + request = { + destination: data.addr, + feerate: `${Number(data.sat_per_vbyte) * 1000}perkb`, + satoshi: data.amount, + utxos: data.utxos + }; + } else { + request = { + destination: data.addr, + feerate: `${Number(data.sat_per_vbyte) * 1000}perkb`, + satoshi: data.amount + }; + } + return this.postRequest('/v1/withdraw', request); + }; + getMyNodeInfo = () => this.postRequest('/v1/getinfo'); + getInvoices = () => this.postRequest('/v1/listinvoices'); + createInvoice = (data: any) => + this.postRequest('/v1/invoice', { + description: data.memo, + label: 'zeus.' + Math.random() * 1000000, + amount_msat: Number(data.value) * 1000, + expiry: Number(data.expiry), + exposeprivatechannels: true + }); + + getPayments = () => + this.postRequest('/v1/listpays').then((data: any) => ({ + payments: data.pays + })); + getNewAddress = () => this.postRequest('/v1/newaddr'); + openChannelSync = (data: OpenChannelRequest) => { + let request: any; + const feeRate = `${new BigNumber(data.sat_per_vbyte) + .times(1000) + .toString()}perkb`; + if (data.utxos && data.utxos.length > 0) { + request = { + id: data.id, + amount: data.satoshis, + feerate: feeRate, + announce: !data.privateChannel ? true : false, + minconf: data.min_confs, + utxos: data.utxos + }; + } else { + request = { + id: data.id, + amount: data.satoshis, + feerate: feeRate, + announce: !data.privateChannel ? true : false, + minconf: data.min_confs + }; + } + + return this.postRequest('/v1/fundchannel', request); + }; + connectPeer = (data: any) => { + const [host, port] = data.addr.host.split(':'); + + return this.postRequest('/v1/connect', { + id: data.addr.pubkey, + host, + port + }); + }; + decodePaymentRequest = (urlParams?: Array) => + this.postRequest('/v1/decode', { + string: urlParams && urlParams[0] + }); + + payLightningInvoice = (data: any) => + this.postRequest('/v1/pay', { + bolt11: data.payment_request, + amount_msat: Number(data.amt && data.amt * 1000), + maxfeepercent: data.max_fee_percent + }); + sendKeysend = (data: any) => { + return this.postRequest('/v1/keysend', { + destination: data.pubkey, + amount_msat: Number(data.amt && data.amt * 1000), + maxfeepercent: data.max_fee_percent + }); + }; + closeChannel = (urlParams?: Array) => { + const request = { + id: urlParams && urlParams[0], + unilateraltimeout: urlParams && urlParams[1] ? 2 : 0 + }; + return this.postRequest('/v1/close', request); + }; + getFees = () => + this.postRequest('/v1/getinfo').then((res: any) => ({ + total_fee_sum: res.fees_collected_msat / 1000 + })); + setFees = (data: any) => + this.postRequest('/v1/setchannel', { + id: data.global ? 'all' : data.channelId, + feebase: data.base_fee_msat, + feeppm: data.fee_rate + }); + getUTXOs = () => this.postRequest('/v1/listfunds'); + signMessage = (message: string) => + this.postRequest('/v1/signmessage', { + message + }); + verifyMessage = (data: any) => + this.postRequest('/v1/checkmessage', { + message: data.msg, + zbase: data.signature + }); + lnurlAuth = async (r_hash: string) => { + const signed = await this.signMessage(r_hash); + return { + signature: new sha256Hash() + .update(Base64Utils.stringToUint8Array(signed.signature)) + .digest() + }; + }; + + // BOLT 12 / Offers + listOffers = () => + this.postRequest('/v1/listoffers', { active_only: true }); + createOffer = ({ + description, + label, + singleUse + }: { + description?: string; + label?: string; + singleUse?: boolean; + }) => + this.postRequest('/v1/offer', { + amount: 'any', + description, + label, + single_use: singleUse || false + }); + disableOffer = ({ offer_id }: { offer_id: string }) => + this.postRequest('/v1/disableoffer', { offer_id }); + fetchInvoiceFromOffer = async (bolt12: string, amountSatoshis: string) => { + return await this.postRequest('/v1/fetchinvoice', { + offer: bolt12, + amount_msat: Number(amountSatoshis) * 1000, + timeout: 60 + }); + }; + + supportsMessageSigning = () => true; + supportsLnurlAuth = () => true; + supportsOnchainSends = () => true; + supportsOnchainReceiving = () => true; + supportsLightningSends = () => true; + supportsKeysend = () => true; + supportsChannelManagement = () => true; + supportsPendingChannels = () => false; + supportsMPP = () => false; + supportsAMP = () => false; + supportsCoinControl = () => true; + supportsChannelCoinControl = () => true; + supportsHopPicking = () => false; + supportsAccounts = () => false; + supportsRouting = () => true; + supportsNodeInfo = () => true; + singleFeesEarnedTotal = () => true; + supportsAddressTypeSelection = () => false; + supportsTaproot = () => false; + supportsBumpFee = () => false; + supportsLSPs = () => false; + supportsNetworkInfo = () => false; + supportsSimpleTaprootChannels = () => false; + supportsCustomPreimages = () => false; + supportsSweep = () => true; + supportsOnchainBatching = () => false; + supportsChannelBatching = () => false; + supportsLSPS1customMessage = () => false; + supportsLSPS1rest = () => true; + supportsOffers = async () => { + const { configs } = await this.postRequest('/v1/listconfigs'); + + const supportsOffers: boolean = configs['experimental-offers'] + ? true + : false; + + return supportsOffers; + }; + isLNDBased = () => false; +} diff --git a/backends/CLightningREST.ts b/backends/CLightningREST.ts index eace30738..85a4ca87d 100644 --- a/backends/CLightningREST.ts +++ b/backends/CLightningREST.ts @@ -212,10 +212,14 @@ export default class CLightningREST extends LND { amount: Number(data.amt && data.amt * 1000), maxfeepercent: data.max_fee_percent }); - closeChannel = (urlParams?: Array) => - this.deleteRequest( - `/v1/channel/closeChannel/${urlParams && urlParams[0]}/` + closeChannel = (urlParams?: Array) => { + const id = urlParams && urlParams[0]; + const unilateralTimeout = urlParams && urlParams[1] ? 2 : 0; + + return this.deleteRequest( + `/v1/channel/closeChannel/${id}?unilateralTimeout=${unilateralTimeout}` ); + }; getNodeInfo = () => this.getRequest('N/A'); getFees = () => this.getRequest('/v1/getFees/').then(({ feeCollected }: any) => ({ diff --git a/backends/CoreLightningRequestHandler.ts b/backends/CoreLightningRequestHandler.ts new file mode 100644 index 000000000..25272a449 --- /dev/null +++ b/backends/CoreLightningRequestHandler.ts @@ -0,0 +1,254 @@ +import CLNRest from './CLNRest'; + +const api = new CLNRest(); + +// Returns onchain balance of core-lightning node +export const getBalance = (data: any) => { + const opArray = data.outputs; + let confBalance = 0; + let unconfBalance = 0; + let totalBalance = 0; + + for (let i = 0; i < opArray.length; i++) { + if (opArray[i].status === 'confirmed') + confBalance = confBalance + opArray[i].amount_msat / 1000; + else if (opArray[i].status === 'unconfirmed') + unconfBalance = unconfBalance + opArray[i].amount_msat / 1000; + } + totalBalance = confBalance + unconfBalance; + return { + total_balance: totalBalance, + confirmed_balance: confBalance, + unconfirmed_balance: unconfBalance + }; +}; + +// Returns offchain balances of core-lightning node +export const getOffchainBalance = (data: any) => { + const chanArray = data.channels; + let localBalance = 0; + let remoteBalance = 0; + let pendingBalance = 0; + let inactiveBalance = 0; + + for (let i = 0; i < chanArray.length; i++) { + if ( + chanArray[i].state === 'CHANNELD_NORMAL' && + chanArray[i].connected === true + ) { + localBalance = localBalance + chanArray[i].our_amount_msat; + + remoteBalance = + remoteBalance + + chanArray[i].amount_msat - + chanArray[i].our_amount_msat; + } else if ( + chanArray[i].state === 'CHANNELD_NORMAL' && + chanArray[i].connected === false + ) { + inactiveBalance = inactiveBalance + chanArray[i].our_amount_msat; + } else if ( + chanArray[i].state === 'CHANNELD_AWAITING_LOCKIN' || + chanArray[i].state === 'DUALOPEND_AWAITING_LOCKIN' + ) { + pendingBalance = pendingBalance + chanArray[i].our_amount_msat; + } + } + + localBalance = localBalance / 1000; + remoteBalance = remoteBalance / 1000; + inactiveBalance = inactiveBalance / 1000; + pendingBalance = pendingBalance / 1000; + + return { + balance: localBalance, + remote_balance: remoteBalance, + inactive_balance: inactiveBalance, + pending_balance: pendingBalance + }; +}; + +// Get your peers and the channel info for core lightning node +export const listPeers = async (data: any) => { + const formattedChannels = data.channels + .map((peer: any) => { + if ( + peer.state === 'ONCHAIN' || + peer.state === 'CLOSED' || + peer.state === 'CHANNELD_AWAITING_LOCKIN' + ) { + return; + } + + return { + active: peer.peer_connected, + remote_pubkey: peer.peer_id, + channel_point: peer.funding_txid, + chan_id: peer.channel_id, + capacity: Number(peer.total_msat / 1000).toString(), + local_balance: Number(peer.to_us_msat / 1000).toString(), + remote_balance: Number( + (peer.total_msat - peer.to_us_msat) / 1000 + ).toString(), + total_satoshis_sent: Number( + peer.out_fulfilled_msat / 1000 + ).toString(), + total_satoshis_received: Number( + peer.in_fulfilled_msat / 1000 + ).toString(), + num_updates: ( + peer.in_payments_offered + peer.out_payments_offered + ).toString(), + csv_delay: peer.our_to_self_delay, + private: peer.private, + local_chan_reserve_sat: Number( + peer.our_reserve_msat / 1000 + ).toString(), + remote_chan_reserve_sat: Number( + peer.their_reserve_msat / 1000 + ).toString(), + close_address: peer.close_to_addr + }; + }) + .filter((n: any) => !!n); + + const channelsWithAliases = await Promise.all( + formattedChannels.map(async (n: any) => { + const { nodes } = await api.getNode({ id: n.remote_pubkey }); + + if (nodes.length) { + n.alias = nodes[0].alias || ''; + } else { + n.alias = ''; + } + + return n; + }) + ); + + return { channels: channelsWithAliases }; +}; + +// Get all chain transactions from your core-lightnig node +export const getChainTransactions = async () => { + const results = await Promise.allSettled([ + api.postRequest('/v1/bkpr-listaccountevents'), + api.postRequest('/v1/listtransactions'), + api.postRequest('/v1/getinfo') + ]); + + const [walletTxsResult, listTxsResult, getinfoResult] = results; + + // If getinfo fails, return blank txs + if (getinfoResult.status !== 'fulfilled') { + return { transactions: [] }; + } + + const getinfo = getinfoResult.value; + + const allTxs = + walletTxsResult.status === 'fulfilled' + ? walletTxsResult.value.events.filter( + (tx: any) => tx.type !== 'onchain_fee' + ) + : { events: [] }; + + const walletTxs = + walletTxsResult.status === 'fulfilled' + ? walletTxsResult.value.events.filter( + (tx: any) => tx.tag === 'deposit' && tx.account === 'wallet' + ) + : { events: [] }; + + const externalTxs = + walletTxsResult.status === 'fulfilled' + ? walletTxsResult.value.events.filter( + (tx: any) => tx.tag === 'deposit' && tx.account === 'external' + ) + : { events: [] }; + + const listTxs = + listTxsResult.status === 'fulfilled' + ? listTxsResult.value + : { transactions: [] }; + + const transactions = listTxs.transactions + .map((tx: any) => { + const withdrawal = externalTxs.find((n: any) => { + const txid = n.outpoint ? n.outpoint.split(':')[0] : null; + + // Check if the deposit is associated with a channel open or close + const isChannelChange = allTxs.find((c: any) => { + if (!c.outpoint) { + return undefined; + } + + return ( + c.outpoint.split(':')[0] === tx.hash && + (c.tag === 'channel_open' || c.tag === 'channel_close') + ); + }); + + if (isChannelChange) { + return undefined; + } + + return txid === tx.hash; + }); + + const deposit = walletTxs.find((n: any) => { + const txid = n.outpoint ? n.outpoint.split(':')[0] : null; + + // Check if the deposit is associated with a channel open or close + const isChannelChange = allTxs.find((c: any) => { + if (!c.outpoint) { + return undefined; + } + return ( + c.outpoint.split(':')[0] === tx.hash && + (c.tag === 'channel_open' || c.tag === 'channel_close') + ); + }); + + if (isChannelChange) { + return undefined; + } + + return txid === tx.hash; + }); + + if (withdrawal) { + return { + amount: -Math.abs(withdrawal.credit_msat) / 1000, + block_height: withdrawal.blockheight, + num_confirmations: + getinfo.blockheight - withdrawal.blockheight, + time_stamp: withdrawal.timestamp, + txid: tx.hash, + note: 'on-chain withdrawal' + }; + } + + if (deposit) { + return { + amount: deposit.credit_msat / 1000, + block_height: deposit.blockheight, + num_confirmations: + getinfo.blockheight - deposit.blockheight, + time_stamp: deposit.timestamp, + txid: tx.hash, + note: 'on-chain deposit' + }; + } + + return null; + }) + .filter((n: any) => !!n); + + // Sort the transactions array by the time_stamp field (descending order) + transactions.sort((a: any, b: any) => b.time_stamp - a.time_stamp); + + return { + transactions + }; +}; diff --git a/components/FeeLimit.tsx b/components/FeeLimit.tsx index 8fc291fe9..f6e6449f5 100644 --- a/components/FeeLimit.tsx +++ b/components/FeeLimit.tsx @@ -187,7 +187,9 @@ export default class FeeLimit extends React.Component< const { implementation } = SettingsStore; const isLnd: boolean = BackendUtils.isLNDBased(); - const isCLightning: boolean = implementation === 'c-lightning-REST'; + const isCLightning: boolean = + implementation === 'c-lightning-REST' || + implementation === 'cln-rest'; if (hide) return; diff --git a/components/SetFeesForm.tsx b/components/SetFeesForm.tsx index b290495be..4b3032b43 100644 --- a/components/SetFeesForm.tsx +++ b/components/SetFeesForm.tsx @@ -123,7 +123,8 @@ export default class SetFeesForm extends React.Component< }} > {`${localeString('components.SetFeesForm.feeRate')} (${ - implementation === 'c-lightning-REST' + implementation === 'c-lightning-REST' || + implementation === 'cln-rest' ? localeString( 'components.SetFeesForm.ppmMilliMsat' ) @@ -133,7 +134,9 @@ export default class SetFeesForm extends React.Component< { if ( - this.settingsStore.implementation !== 'c-lightning-REST' + this.settingsStore.implementation !== + 'c-lightning-REST' && + this.settingsStore.implementation !== 'cln-rest' ) { const nodeInfo = await this.getNodeInfo(remotePubKey); if (!nodeInfo) return; @@ -486,10 +488,17 @@ export default class ChannelsStore { this.closingChannel = true; let urlParams: Array = []; - if (channelId && !channelPoint) { + const implementation = this.settingsStore.implementation; + + if ( + implementation === 'c-lightning-REST' || + implementation === 'cln-rest' || + implementation === 'eclair' || + implementation === 'spark' + ) { // c-lightning, eclair urlParams = [channelId, forceClose]; - } else if (channelPoint) { + } else { // lnd const { funding_txid_str, output_index } = channelPoint; @@ -502,7 +511,7 @@ export default class ChannelsStore { ]; } - if (this.settingsStore.implementation === 'lightning-node-connect') { + if (implementation === 'lightning-node-connect') { return BackendUtils.closeChannel(urlParams); } else { let resolved = false; diff --git a/stores/SettingsStore.ts b/stores/SettingsStore.ts index 187e1d53f..2da1c8c10 100644 --- a/stores/SettingsStore.ts +++ b/stores/SettingsStore.ts @@ -16,6 +16,7 @@ export interface Node { port?: string; url?: string; macaroonHex?: string; + rune?: string; accessKey?: string; implementation?: string; certVerification?: boolean; @@ -205,12 +206,26 @@ export const INTERFACE_KEYS = [ { key: 'Embedded LND', value: 'embedded-lnd' }, { key: 'LND (REST)', value: 'lnd' }, { key: 'LND (Lightning Node Connect)', value: 'lightning-node-connect' }, - { key: 'Core Lightning (c-lightning-REST)', value: 'c-lightning-REST' }, + { key: 'Core Lightning (CLNRest)', value: 'cln-rest' }, { key: 'LNDHub', value: 'lndhub' }, + { + key: '[DEPRECATED] Core Lightning (c-lightning-REST)', + value: 'c-lightning-REST' + }, { key: '[DEPRECATED] Core Lightning (Sparko)', value: 'spark' }, { key: '[DEPRECATED] Eclair', value: 'eclair' } ]; +export type Implementations = + | 'embedded-lnd' + | 'lnd' + | 'lightning-node-connect' + | 'cln-rest' + | 'lndhub' + | 'c-lightning-REST' + | 'spark' + | 'eclair'; + export const EMBEDDED_NODE_NETWORK_KEYS = [ { key: 'Mainnet', translateKey: 'network.mainnet', value: 'mainnet' }, { key: 'Testnet', translateKey: 'network.testnet', value: 'testnet' } @@ -1105,8 +1120,9 @@ export default class SettingsStore { @observable port: string; @observable url: string; @observable macaroonHex: string; + @observable rune: string; @observable accessKey: string; - @observable implementation: string; + @observable implementation: Implementations; @observable certVerification: boolean | undefined; @observable public loggedIn = false; @observable public connecting = true; @@ -1450,6 +1466,7 @@ export default class SettingsStore { this.password = node.password; this.lndhubUrl = node.lndhubUrl; this.macaroonHex = node.macaroonHex; + this.rune = node.rune; this.accessKey = node.accessKey; this.implementation = node.implementation || 'lnd'; this.certVerification = node.certVerification || false; diff --git a/stores/TransactionsStore.ts b/stores/TransactionsStore.ts index f4605802f..6c2cd0da6 100644 --- a/stores/TransactionsStore.ts +++ b/stores/TransactionsStore.ts @@ -448,7 +448,8 @@ export default class TransactionsStore { // max fee percent for c-lightning if ( max_fee_percent && - this.settingsStore.implementation === 'c-lightning-REST' + (this.settingsStore.implementation === 'c-lightning-REST' || + this.settingsStore.implementation === 'cln-rest') ) { data.max_fee_percent = max_fee_percent; } @@ -460,6 +461,7 @@ export default class TransactionsStore { const payFunc = (this.settingsStore.implementation === 'c-lightning-REST' || + this.settingsStore.implementation === 'cln-rest' || this.settingsStore.implementation === 'embedded-lnd') && pubkey ? BackendUtils.sendKeysend diff --git a/utils/BackendUtils.ts b/utils/BackendUtils.ts index dae5ea476..f0c80fc67 100644 --- a/utils/BackendUtils.ts +++ b/utils/BackendUtils.ts @@ -5,6 +5,7 @@ import LightningNodeConnect from '../backends/LightningNodeConnect'; import EmbeddedLND from '../backends/EmbeddedLND'; // Core Lightning import CLightningREST from '../backends/CLightningREST'; +import CLNRest from '../backends/CLNRest'; import Spark from '../backends/Spark'; // Eclair import Eclair from '../backends/Eclair'; @@ -16,6 +17,7 @@ class BackendUtils { lightningNodeConnect: LightningNodeConnect; embeddedLND: EmbeddedLND; clightningREST: CLightningREST; + clnRest: CLNRest; spark: Spark; eclair: Eclair; lndHub: LndHub; @@ -24,6 +26,7 @@ class BackendUtils { this.lightningNodeConnect = new LightningNodeConnect(); this.embeddedLND = new EmbeddedLND(); this.clightningREST = new CLightningREST(); + this.clnRest = new CLNRest(); this.spark = new Spark(); this.eclair = new Eclair(); this.lndHub = new LndHub(); @@ -40,6 +43,8 @@ class BackendUtils { return this.embeddedLND; case 'c-lightning-REST': return this.clightningREST; + case 'cln-rest': + return this.clnRest; case 'spark': return this.spark; case 'eclair': diff --git a/views/OpenChannel.tsx b/views/OpenChannel.tsx index c22dacefe..5cfcf9455 100644 --- a/views/OpenChannel.tsx +++ b/views/OpenChannel.tsx @@ -565,8 +565,10 @@ export default class OpenChannel extends React.Component< fundMax: newValue, local_funding_amount: newValue && - implementation === - 'c-lightning-REST' + (implementation === + 'c-lightning-REST' || + implementation === + 'cln-rest') ? 'all' : '' }); diff --git a/views/PaymentRequest.tsx b/views/PaymentRequest.tsx index b02a9d903..0fe127c45 100644 --- a/views/PaymentRequest.tsx +++ b/views/PaymentRequest.tsx @@ -347,7 +347,9 @@ export default class PaymentRequest extends React.Component< const { enableTor, implementation } = SettingsStore; const isLnd: boolean = BackendUtils.isLNDBased(); - const isCLightning: boolean = implementation === 'c-lightning-REST'; + const isCLightning: boolean = + implementation === 'c-lightning-REST' || + implementation === 'cln-rest'; const isNoAmountInvoice: boolean = !requestAmount || requestAmount === 0; diff --git a/views/Send.tsx b/views/Send.tsx index c2aef6895..d8156a00c 100644 --- a/views/Send.tsx +++ b/views/Send.tsx @@ -343,7 +343,8 @@ export default class Send extends React.Component { utxos, utxoBalance, amount: - implementation === 'c-lightning-REST' + implementation === 'c-lightning-REST' || + implementation === 'cln-rest' ? 'all' : prevState.amount, account @@ -1242,34 +1243,35 @@ export default class Send extends React.Component { }} /> - {implementation !== 'c-lightning-REST' && ( - <> - - {`${localeString( - 'views.Send.message' - )} (${localeString( - 'general.optional' - )})`} - - - this.setState({ - message: text - }) - } - /> - - )} + {implementation !== 'c-lightning-REST' && + implementation !== 'cln-rest' && ( + <> + + {`${localeString( + 'views.Send.message' + )} (${localeString( + 'general.optional' + )})`} + + + this.setState({ + message: text + }) + } + /> + + )} { + onValueChange={(value: Implementations) => { this.setState({ implementation: value, saved: false, @@ -1347,6 +1357,75 @@ export default class NodeConfiguration extends React.Component< )} )} + {implementation === 'cln-rest' && ( + <> + + {localeString( + 'views.Settings.AddEditNode.host' + )} + + + this.setState({ + host: text.trim(), + saved: false + }) + } + locked={loading} + /> + + + {localeString( + 'views.Settings.AddEditNode.rune' + )} + + + this.setState({ + rune: text.trim(), + saved: false + }) + } + locked={loading} + /> + + + {localeString( + 'views.Settings.AddEditNode.restPort' + )} + + + this.setState({ + port: text.trim(), + saved: false + }) + } + locked={loading} + /> + + )} + {(implementation === 'lnd' || implementation === 'c-lightning-REST') && ( <> diff --git a/views/Settings/PaymentsSettings.tsx b/views/Settings/PaymentsSettings.tsx index 866cc469f..100641472 100644 --- a/views/Settings/PaymentsSettings.tsx +++ b/views/Settings/PaymentsSettings.tsx @@ -216,7 +216,8 @@ export default class PaymentsSettings extends React.Component< )} - {implementation === 'c-lightning-REST' && ( + {(implementation === 'c-lightning-REST' || + implementation === 'cln-rest') && ( { let request; request = - implementation === 'c-lightning-REST' + implementation === 'c-lightning-REST' || + implementation === 'cln-rest' ? { addr: destination, sat_per_vbyte: fee, diff --git a/views/Transaction.tsx b/views/Transaction.tsx index 2ba2370f9..14ac25cb8 100644 --- a/views/Transaction.tsx +++ b/views/Transaction.tsx @@ -82,6 +82,7 @@ export default class TransactionView extends React.Component< num_confirmations, time_stamp, destAddresses, + note, getFee, getFeePercentage, status, @@ -311,6 +312,21 @@ export default class TransactionView extends React.Component< {!!destAddresses && ( {addresses} )} + {!!note && ( + + {note} + + } + /> + )} {raw_tx_hex && ( <>