diff --git a/backends/CLightningREST.ts b/backends/CLightningREST.ts index dcab8c94ec..083bcab892 100644 --- a/backends/CLightningREST.ts +++ b/backends/CLightningREST.ts @@ -273,5 +273,6 @@ export default class CLightningREST extends LND { supportsCustomPreimages = () => false; supportsSweep = () => true; supportsOnchainBatching = () => false; + supportsChannelBatching = () => false; isLNDBased = () => false; } diff --git a/backends/Eclair.ts b/backends/Eclair.ts index d6477a6240..4eb5f42c46 100644 --- a/backends/Eclair.ts +++ b/backends/Eclair.ts @@ -505,6 +505,7 @@ export default class Eclair { supportsCustomPreimages = () => false; supportsSweep = () => false; supportsOnchainBatching = () => false; + supportsChannelBatching = () => false; isLNDBased = () => false; } diff --git a/backends/EmbeddedLND.ts b/backends/EmbeddedLND.ts index 30ae5f3e5b..41e4c3df95 100644 --- a/backends/EmbeddedLND.ts +++ b/backends/EmbeddedLND.ts @@ -41,6 +41,7 @@ const { verifyMessageNodePubkey, bumpFee, fundPsbt, + signPsbt, finalizePsbt, publishTransaction, listAccounts, @@ -205,6 +206,7 @@ export default class EmbeddedLND extends LND { // getForwardingHistory = () => N/A // // Coin Control fundPsbt = async (data: any) => await fundPsbt(data); + signPsbt = async (data: any) => await signPsbt(data); finalizePsbt = async (data: any) => await finalizePsbt(data); publishTransaction = async (data: any) => { if (data.tx_hex) data.tx_hex = Base64Utils.hexToBase64(data.tx_hex); @@ -212,16 +214,25 @@ export default class EmbeddedLND extends LND { }; fundingStateStep = async (data: any) => { // Verify - if (data.psbt_finalize?.funded_psbt) + if ( + data.psbt_finalize?.funded_psbt && + Base64Utils.isHex(data.psbt_finalize?.funded_psbt) + ) data.psbt_finalize.funded_psbt = Base64Utils.hexToBase64( data.psbt_finalize.funded_psbt ); // Finalize - if (data.psbt_finalize?.final_raw_tx) + if ( + data.psbt_finalize?.final_raw_tx && + Base64Utils.isHex(data.psbt_finalize?.final_raw_tx) + ) data.psbt_finalize.final_raw_tx = Base64Utils.hexToBase64( data.psbt_finalize.final_raw_tx ); - if (data.psbt_finalize?.signed_psbt) + if ( + data.psbt_finalize?.signed_psbt && + Base64Utils.isHex(data.psbt_finalize?.signed_psbt) + ) data.psbt_finalize.signed_psbt = Base64Utils.hexToBase64( data.psbt_finalize.signed_psbt ); @@ -268,5 +279,6 @@ export default class EmbeddedLND extends LND { supportsCustomPreimages = () => true; supportsSweep = () => true; supportsOnchainBatching = () => true; + supportsChannelBatching = () => true; isLNDBased = () => true; } diff --git a/backends/LND.ts b/backends/LND.ts index f56a0f4ce9..503f109159 100644 --- a/backends/LND.ts +++ b/backends/LND.ts @@ -490,6 +490,7 @@ export default class LND { }; // Coin Control fundPsbt = (data: any) => this.postRequest('/v2/wallet/psbt/fund', data); + signPsbt = (data: any) => this.postRequest('/v2/wallet/psbt/sign', data); finalizePsbt = (data: any) => this.postRequest('/v2/wallet/psbt/finalize', data); publishTransaction = (data: any) => { @@ -614,5 +615,6 @@ export default class LND { supportsCustomPreimages = () => true; supportsSweep = () => true; supportsOnchainBatching = () => true; + supportsChannelBatching = () => true; isLNDBased = () => true; } diff --git a/backends/LightningNodeConnect.ts b/backends/LightningNodeConnect.ts index ce2d048785..e13a37bb72 100644 --- a/backends/LightningNodeConnect.ts +++ b/backends/LightningNodeConnect.ts @@ -1,3 +1,5 @@ +import { NativeModules, NativeEventEmitter } from 'react-native'; + import LNC, { lnrpc, walletrpc } from '../zeus_modules/@lightninglabs/lnc-rn'; import stores from '../stores/Stores'; @@ -22,6 +24,7 @@ const ADDRESS_TYPES = [ export default class LightningNodeConnect { lnc: any; + listener: any; permOpenChannel: boolean; permSendCoins: boolean; @@ -223,7 +226,35 @@ export default class LightningNodeConnect { delete request.sat_per_vbyte; } - return this.lnc.lnd.lightning.openChannel(request); + const streamingCall = this.lnc.lnd.lightning.openChannel(request); + + const { LncModule } = NativeModules; + const eventEmitter = new NativeEventEmitter(LncModule); + return new Promise((resolve, reject) => { + this.listener = eventEmitter.addListener( + streamingCall, + (event: any) => { + if (event.result && event.result !== 'EOF') { + let result; + try { + result = JSON.parse(event.result); + + resolve({ result }); + this.listener.remove(); + } catch (e) { + try { + result = JSON.parse(event); + } catch (e2) { + result = event.result || event; + } + + reject(result); + this.listener.remove(); + } + } + } + ); + }); }; connectPeer = async (data: any) => await this.lnc.lnd.lightning @@ -340,6 +371,10 @@ export default class LightningNodeConnect { await this.lnc.lnd.walletKit .fundPsbt(req) .then((data: walletrpc.FundPsbtResponse) => snakeize(data)); + signPsbt = async (req: walletrpc.SignPsbtRequest) => + await this.lnc.lnd.walletKit + .signPsbt(req) + .then((data: walletrpc.SignPsbtResponse) => snakeize(data)); finalizePsbt = async (req: walletrpc.FinalizePsbtRequest) => await this.lnc.lnd.walletKit .finalizePsbt(req) @@ -351,20 +386,11 @@ export default class LightningNodeConnect { .then((data: walletrpc.PublishResponse) => snakeize(data)); }; fundingStateStep = async (req: lnrpc.FundingTransitionMsg) => { - // Verify - if (req.psbt_finalize?.funded_psbt) - req.psbt_finalize.funded_psbt = Base64Utils.hexToBase64( - req.psbt_finalize.funded_psbt - ); // Finalize if (req.psbt_finalize?.final_raw_tx) req.psbt_finalize.final_raw_tx = Base64Utils.hexToBase64( req.psbt_finalize.final_raw_tx ); - if (req.psbt_finalize?.signed_psbt) - req.psbt_finalize.signed_psbt = Base64Utils.hexToBase64( - req.psbt_finalize.signed_psbt - ); return await this.lnc.lnd.lightning .fundingStateStep(req) @@ -452,5 +478,6 @@ export default class LightningNodeConnect { supportsCustomPreimages = () => true; supportsSweep = () => true; supportsOnchainBatching = () => true; + supportsChannelBatching = () => true; isLNDBased = () => true; } diff --git a/backends/LndHub.ts b/backends/LndHub.ts index 6f858e451e..7f42128185 100644 --- a/backends/LndHub.ts +++ b/backends/LndHub.ts @@ -153,5 +153,6 @@ export default class LndHub extends LND { supportsCustomPreimages = () => false; supportsSweep = () => false; supportsOnchainBatching = () => false; + supportsChannelBatching = () => true; isLNDBased = () => false; } diff --git a/backends/Spark.ts b/backends/Spark.ts index dd3f888844..c5cd5d92d7 100644 --- a/backends/Spark.ts +++ b/backends/Spark.ts @@ -379,5 +379,6 @@ export default class Spark { supportsCustomPreimages = () => false; supportsSweep = () => false; supportsOnchainBatching = () => false; + supportsChannelBatching = () => true; isLNDBased = () => false; } diff --git a/ios/LndMobile/Lnd.swift b/ios/LndMobile/Lnd.swift index 4665084ce1..58ec8227d8 100644 --- a/ios/LndMobile/Lnd.swift +++ b/ios/LndMobile/Lnd.swift @@ -118,6 +118,7 @@ open class Lnd { "UnlockWallet": { bytes, cb in LndmobileUnlockWallet(bytes, cb) }, "WalletKitDeriveKey": { bytes, cb in LndmobileWalletKitDeriveKey(bytes, cb) }, "FundPsbt": { bytes, cb in LndmobileWalletKitFundPsbt(bytes, cb) }, + "SignPsbt": { bytes, cb in LndmobileWalletKitSignPsbt(bytes, cb) }, "FinalizePsbt": { bytes, cb in LndmobileWalletKitFinalizePsbt(bytes, cb) }, "PublishTransaction": { bytes, cb in LndmobileWalletKitPublishTransaction(bytes, cb) }, "ListAccounts": { bytes, cb in LndmobileWalletKitListAccounts(bytes, cb) }, diff --git a/lndmobile/LndMobileInjection.ts b/lndmobile/LndMobileInjection.ts index 5eac083fa7..aec8fc6294 100644 --- a/lndmobile/LndMobileInjection.ts +++ b/lndmobile/LndMobileInjection.ts @@ -80,6 +80,7 @@ import { signMessageNodePubkey, bumpFee, fundPsbt, + signPsbt, finalizePsbt, publishTransaction, listAccounts, @@ -370,15 +371,22 @@ export interface ILndMobileInjections { }) => Promise; fundPsbt: ({ account, + psbt, raw, spend_unconfirmed, sat_per_vbyte }: { account?: string; + psbt?: Uint8Array; raw: walletrpc.TxTemplate; spend_unconfirmed?: boolean; sat_per_vbyte?: Long; }) => Promise; + signPsbt: ({ + funded_psbt + }: { + funded_psbt?: Uint8Array; + }) => Promise; finalizePsbt: ({ funded_psbt }: { @@ -498,6 +506,7 @@ export default { signMessageNodePubkey, bumpFee, fundPsbt, + signPsbt, finalizePsbt, publishTransaction, listAccounts, diff --git a/lndmobile/wallet.ts b/lndmobile/wallet.ts index c0338597d9..660eab00a8 100644 --- a/lndmobile/wallet.ts +++ b/lndmobile/wallet.ts @@ -45,18 +45,21 @@ export const bumpFee = async ({ */ export const fundPsbt = async ({ account, + psbt, raw, spend_unconfirmed, sat_per_vbyte }: { account?: string; - raw: walletrpc.TxTemplate; + psbt?: Uint8Array; + raw?: walletrpc.TxTemplate; spend_unconfirmed?: boolean; sat_per_vbyte?: Long; }): Promise => { const options: walletrpc.IFundPsbtRequest = { account, raw, + psbt, spend_unconfirmed, sat_per_vbyte: sat_per_vbyte ? Long.fromValue(sat_per_vbyte) : undefined }; @@ -73,6 +76,30 @@ export const fundPsbt = async ({ return response; }; +/** + * @throws + */ +export const signPsbt = async ({ + funded_psbt +}: { + funded_psbt?: Uint8Array; +}): Promise => { + const options: walletrpc.ISignPsbtRequest = { + funded_psbt + }; + const response = await sendCommand< + walletrpc.ISignPsbtRequest, + walletrpc.SignPsbtRequest, + walletrpc.SignPsbtResponse + >({ + request: walletrpc.SignPsbtRequest, + response: walletrpc.SignPsbtResponse, + method: 'SignPsbt', + options + }); + return response; +}; + /** * @throws */ diff --git a/locales/en.json b/locales/en.json index bcd7bb1564..b4e0433005 100644 --- a/locales/en.json +++ b/locales/en.json @@ -286,6 +286,8 @@ "views.OpenChannel.openChannelToOlympus": "Open channel to Olympus", "views.OpenChannel.peerToOlympus": "Peer to Olympus", "views.OpenChannel.fundMax": "Use all possible funds", + "views.OpenChannel.openAdditionalChannel": "Open additional channel", + "views.OpenChannel.removeAdditionalChannel": "Remove additional channel", "views.Wallet.BalancePane.sync.title": "Finishing sync", "views.Wallet.BalancePane.sync.text": "Hang on tight! You will be ready to use Zeus soon.", "views.Wallet.BalancePane.backup.title": "Back up your funds", @@ -397,6 +399,7 @@ "views.Channel.SortButton.ascending": "ascending", "views.Channel.SortButton.descending": "descending", "views.Channel.channelId": "Channel ID", + "views.Channel.channelIds": "Channel IDs", "views.Channel.displayName": "Display name", "views.Channel.Total.outbound": "Total outbound", "views.Channel.Total.inbound": "Total inbound", @@ -485,12 +488,14 @@ "views.Intro.carousel4.text": "Go beyond a basic bitcoin wallet and manage your lightning channels, liquidity, and so much more.", "views.Intro.errorCreatingWallet": "Error creating wallet. Restart the app and try again.", "views.OpenChannel.openChannel": "Open Channel", + "views.OpenChannel.openChannels": "Open Channels", "views.OpenChannel.connectPeer": "Connect Peer", "views.OpenChannel.importText": "Detected the following Node URI in your clipboard", "views.OpenChannel.importPrompt": "Would you like to import it?", "views.OpenChannel.import": "Import", "views.OpenChannel.peerSuccess": "Successfully connected to peer", "views.OpenChannel.channelSuccess": "Successfully opened channel", + "views.OpenChannel.channelsSuccess": "Successfully opened channels", "views.OpenChannel.nodePubkey": "Node pubkey", "views.OpenChannel.host": "Host", "views.OpenChannel.hostPort": "Hostname:Port", @@ -1072,6 +1077,7 @@ "views.PSBT.scan": "Scan PSBT / TX Hex", "views.PSBT.finalizePsbtAndBroadcast": "Finalize PSBT and Broadcast", "views.PSBT.channelWarning": "DO NOT PUBLISH the finished transaction by yourself or with another tool. LND MUST publish it in the proper funding flow order OR THE FUNDS CAN BE LOST! Ensure you publish in ZEUS and only when the channel ID above is displayed.", + "views.PSBT.channelsWarning": "DO NOT PUBLISH the finished transaction by yourself or with another tool. LND MUST publish it in the proper funding flow order OR THE FUNDS CAN BE LOST! Ensure you publish in ZEUS and only when the channel IDs above are displayed.", "views.PSBT.input": "Input", "views.PSBT.inputCount": "Input count", "views.PSBT.output": "Output", diff --git a/models/OpenChannelRequest.ts b/models/OpenChannelRequest.ts index aaa8907f19..e9a68c58d0 100644 --- a/models/OpenChannelRequest.ts +++ b/models/OpenChannelRequest.ts @@ -1,5 +1,12 @@ import BaseModel from './BaseModel'; +export interface AdditionalChannel { + node_pubkey_string: string; + host: string; + local_funding_amount: string; + satAmount: string | number; +} + export default class OpenChannelRequest extends BaseModel { public min_confs?: number; public spend_unconfirmed?: boolean; @@ -27,8 +34,10 @@ export default class OpenChannelRequest extends BaseModel { psbt_shim: { pending_chan_id: any; base_psbt: string; + no_publish?: boolean; }; }; + public additionalChannels?: Array; constructor(data?: any) { super(data); diff --git a/stores/ChannelsStore.ts b/stores/ChannelsStore.ts index 0319381397..de173d4a3b 100644 --- a/stores/ChannelsStore.ts +++ b/stores/ChannelsStore.ts @@ -75,7 +75,7 @@ export default class ChannelsStore { @observable public aliasMap: any = observable.map({}); // external account funding @observable public funded_psbt: string = ''; - @observable public pending_chan_id: any; + @observable public pending_chan_ids: Array; settingsStore: SettingsStore; @@ -147,7 +147,7 @@ export default class ChannelsStore { this.channelSuccess = false; this.channelRequest = null; this.funded_psbt = ''; - this.pending_chan_id = null; + this.pending_chan_ids = []; }; @action @@ -536,6 +536,22 @@ export default class ChannelsStore { }); } + // 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((e: any) => { + console.log(`###${i} - e`, e); + }); + } + } + return await new Promise((resolve, reject) => { BackendUtils.connectPeer({ addr: { @@ -544,7 +560,7 @@ export default class ChannelsStore { }, perm }) - .then(() => { + .then(async () => { if (!silent) { this.errorPeerConnect = false; this.connectingToPeer = false; @@ -586,46 +602,85 @@ export default class ChannelsStore { }); }; - handleChannelOpen = (request: any, result: any) => { - const { psbt_fund, pending_chan_id } = result; - this.pending_chan_id = pending_chan_id; - + handleChannelOpen = (request: any, result: any, psbt?: string) => { + const { psbt_fund } = result; const { account, sat_per_vbyte, utxos } = request; - const inputs: any = []; - const outputs: any = { - [psbt_fund.funding_address]: Number(psbt_fund.funding_amount) - }; - - if (utxos) { - utxos.forEach((input: any) => { - const [txid_str, output_index] = input.split(':'); - inputs.push({ - txid_str, - output_index: Number(output_index) + let fundPsbtRequest; + + if (psbt) { + fundPsbtRequest = { + psbt, + sat_per_vbyte: Number(sat_per_vbyte), + spend_unconfirmed: true, + account + }; + } else { + const inputs: any = []; + const outputs: any = { + [psbt_fund.funding_address]: Number(psbt_fund.funding_amount) + }; + + if (utxos) { + utxos.forEach((input: any) => { + const [txid_str, output_index] = input.split(':'); + inputs.push({ + txid_str, + output_index: Number(output_index) + }); }); - }); + } + fundPsbtRequest = { + raw: { + outputs, + inputs + }, + sat_per_vbyte: Number(sat_per_vbyte), + spend_unconfirmed: true, + account + }; } - const fundPsbtRequest = { - raw: { - outputs, - inputs - }, - sat_per_vbyte: Number(sat_per_vbyte), - spend_unconfirmed: true, - account - }; BackendUtils.fundPsbt(fundPsbtRequest) - .then((data: any) => { - const funded_psbt: string = new FundedPsbt( + .then(async (data: any) => { + let funded_psbt: string = new FundedPsbt( data.funded_psbt ).getFormatted(); + BackendUtils.signPsbt({ funded_psbt }) + .then((data: any) => { + if (data.signed_psbt) funded_psbt = data.signed_psbt; + }) + .catch((e: any) => { + console.log('signPsbt err', e); + }); + + 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 + pending_chan_id: + this.pending_chan_ids[ + this.pending_chan_ids.length - 1 + ] } }) .then((data: any) => { @@ -641,14 +696,50 @@ export default class ChannelsStore { this.peerSuccess = false; this.channelSuccess = false; } else { - this.funded_psbt = funded_psbt; - this.output_index = null; - this.funding_txid_str = null; - this.errorOpenChannel = true; - this.openingChannel = false; - this.channelRequest = null; - this.peerSuccess = false; - this.channelSuccess = false; + 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) => { + 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(() => { + // handle 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; + }); } }) .catch((error: any) => { @@ -688,26 +779,128 @@ export default class ChannelsStore { }; 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') { - request.funding_shim = { - psbt_shim: { - base_psbt: '', - pending_chan_id: randomBytes(32).toString('base64') - } - }; - BackendUtils.openChannelStream(request) - .then((data: any) => { - this.handleChannelOpen(request, data.result); - }) - .catch((error: Error) => { - this.handleChannelOpenError(error); - }); + if (request?.account !== 'default' || multipleChans) { + 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); + + 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 + ); + 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 + ); + this.handleChannelOpen( + request, + data.result, + psbt_fund.psbt + ); + }) + .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) => { + this.handleChannelOpen(request, data.result); + }) + .catch((error: Error) => { + this.handleChannelOpenError(error); + }); + } } else { BackendUtils.openChannel(request) .then((data: any) => { diff --git a/stores/TransactionsStore.ts b/stores/TransactionsStore.ts index d5d57f2d6b..0639823b48 100644 --- a/stores/TransactionsStore.ts +++ b/stores/TransactionsStore.ts @@ -183,16 +183,16 @@ export default class TransactionsStore { }; @action - public finalizePsbtAndBroadcastChannel = ( + public finalizePsbtAndBroadcastChannel = async ( signed_psbt: string, - pending_chan_id: any + pending_chan_ids: Array ) => { this.loading = true; return BackendUtils.fundingStateStep({ psbt_finalize: { signed_psbt, - pending_chan_id + pending_chan_id: pending_chan_ids[pending_chan_ids.length - 1] } }) .then((data: any) => { @@ -226,16 +226,16 @@ export default class TransactionsStore { }; @action - public finalizeTxHexAndBroadcastChannel = ( + public finalizeTxHexAndBroadcastChannel = async ( tx_hex: string, - pending_chan_id: any + pending_chan_ids: Array ) => { this.loading = true; return BackendUtils.fundingStateStep({ psbt_finalize: { final_raw_tx: tx_hex, - pending_chan_id + pending_chan_id: pending_chan_ids[pending_chan_ids.length - 1] } }) .then((data: any) => { diff --git a/utils/BackendUtils.ts b/utils/BackendUtils.ts index a913389ba5..a85132a0e5 100644 --- a/utils/BackendUtils.ts +++ b/utils/BackendUtils.ts @@ -101,6 +101,7 @@ class BackendUtils { lnurlAuth = (...args: any[]) => this.call('lnurlAuth', args); fundPsbt = (...args: any[]) => this.call('fundPsbt', args); + signPsbt = (...args: any[]) => this.call('signPsbt', args); finalizePsbt = (...args: any[]) => this.call('finalizePsbt', args); publishTransaction = (...args: any[]) => this.call('publishTransaction', args); @@ -145,6 +146,7 @@ class BackendUtils { supportsCustomPreimages = () => this.call('supportsCustomPreimages'); supportsSweep = () => this.call('supportsSweep'); supportsOnchainBatching = () => this.call('supportsOnchainBatching'); + supportsChannelBatching = () => this.call('supportsChannelBatching'); isLNDBased = () => this.call('isLNDBased'); // LNC diff --git a/utils/Base64Utils.ts b/utils/Base64Utils.ts index 854b4e47fe..61a9322727 100644 --- a/utils/Base64Utils.ts +++ b/utils/Base64Utils.ts @@ -83,6 +83,11 @@ class Base64Utils { const byteArray = mfp.match(/.{2}/g) || []; return byteArray.reverse().join(''); }; + + isHex = (str: string) => { + const hexRegex = /^[0-9A-Fa-f]+$/g; + return hexRegex.test(str); + }; } const base64Utils = new Base64Utils(); diff --git a/views/OpenChannel.tsx b/views/OpenChannel.tsx index f40ece91ef..ee146d32b7 100644 --- a/views/OpenChannel.tsx +++ b/views/OpenChannel.tsx @@ -1,7 +1,5 @@ import * as React from 'react'; import { - NativeEventEmitter, - NativeModules, Platform, ScrollView, StyleSheet, @@ -12,7 +10,6 @@ import { import Clipboard from '@react-native-clipboard/clipboard'; import { inject, observer } from 'mobx-react'; import NfcManager, { NfcEvents, TagEvent } from 'react-native-nfc-manager'; -import { randomBytes } from 'react-native-randombytes'; import Amount from '../components/Amount'; import AmountInput from '../components/AmountInput'; @@ -45,6 +42,8 @@ import NodeInfoStore from '../stores/NodeInfoStore'; import SettingsStore from '../stores/SettingsStore'; import UTXOsStore from '../stores/UTXOsStore'; +import { AdditionalChannel } from '../models/OpenChannelRequest'; + import CaretDown from '../assets/images/SVG/Caret Down.svg'; import CaretRight from '../assets/images/SVG/Caret Right.svg'; import Scan from '../assets/images/SVG/Scan.svg'; @@ -79,6 +78,7 @@ interface OpenChannelState { advancedSettingsToggle: boolean; // external account funding account: string; + additionalChannels: Array; } @inject( @@ -99,6 +99,7 @@ export default class OpenChannel extends React.Component< super(props); this.state = { node_pubkey_string: '', + host: '', local_funding_amount: '', fundMax: false, satAmount: '', @@ -108,13 +109,13 @@ export default class OpenChannel extends React.Component< privateChannel: true, scidAlias: true, simpleTaprootChannel: false, - host: '', suggestImport: '', utxos: [], utxoBalance: 0, connectPeerOnly: false, advancedSettingsToggle: false, - account: 'default' + account: 'default', + additionalChannels: [] }; } @@ -272,36 +273,6 @@ export default class OpenChannel extends React.Component< }); }; - subscribeOpenChannelStream = (request: any, streamingCall: string) => { - const { handleChannelOpen, handleChannelOpenError } = - this.props.ChannelsStore; - const { LncModule } = NativeModules; - const eventEmitter = new NativeEventEmitter(LncModule); - this.listener = eventEmitter.addListener( - streamingCall, - (event: any) => { - if (event.result && event.result !== 'EOF') { - let result; - try { - result = JSON.parse(event.result); - - if (result?.psbt_fund) { - handleChannelOpen(request, result); - } - } catch (e) { - try { - result = JSON.parse(event); - } catch (e2) { - result = event.result || event; - } - - handleChannelOpenError(result); - } - } - } - ); - }; - render() { const { ChannelsStore, @@ -325,7 +296,8 @@ export default class OpenChannel extends React.Component< scidAlias, simpleTaprootChannel, connectPeerOnly, - advancedSettingsToggle + advancedSettingsToggle, + additionalChannels } = this.state; const { implementation } = SettingsStore; @@ -372,6 +344,10 @@ export default class OpenChannel extends React.Component< ? localeString( 'views.OpenChannel.connectPeer' ) + : additionalChannels.length > 0 + ? localeString( + 'views.OpenChannel.openChannels' + ) : localeString( 'views.OpenChannel.openChannel' ) @@ -435,9 +411,15 @@ export default class OpenChannel extends React.Component< )} {channelSuccess && ( 0 + ? localeString( + 'views.OpenChannel.channelsSuccess' + ) + : localeString( + 'views.OpenChannel.channelSuccess' + ) + } /> )} {(errorMsgPeer || errorMsgChannel) && ( @@ -523,46 +505,6 @@ export default class OpenChannel extends React.Component< {!connectPeerOnly && ( <> - {BackendUtils.supportsChannelCoinControl() && ( - - )} - - {BackendUtils.isLNDBased() && ( - <> - - {localeString( - 'views.OpenChannel.fundMax' - )} - - { - const newValue: boolean = - !fundMax; - this.setState({ - fundMax: newValue, - local_funding_amount: - newValue && - implementation === - 'c-lightning-REST' - ? 'all' - : '' - }); - }} - /> - - )} - {!fundMax && ( )} + {BackendUtils.isLNDBased() && + additionalChannels.length === 0 && ( + <> + + {localeString( + 'views.OpenChannel.fundMax' + )} + + { + const newValue: boolean = + !fundMax; + this.setState({ + fundMax: newValue, + local_funding_amount: + newValue && + implementation === + 'c-lightning-REST' + ? 'all' + : '' + }); + }} + /> + + )} + + {additionalChannels.map((channel, index) => { + return ( + + + {localeString( + 'views.OpenChannel.nodePubkey' + )} + + { + let newChannels = + additionalChannels; + + newChannels[ + index + ].node_pubkey_string = text; + + this.setState({ + additionalChannels: + newChannels + }); + }} + /> + + {localeString( + 'views.OpenChannel.host' + )} + + { + let newChannels = + additionalChannels; + + newChannels[index].host = + text; + + this.setState({ + additionalChannels: + newChannels + }); + }} + /> + { + let newChannels = + additionalChannels; + + newChannels[ + index + ].local_funding_amount = amount; + newChannels[ + index + ].satAmount = satAmount; + + this.setState({ + additionalChannels: + newChannels + }); + }} + /> + +