diff --git a/backup-server/src/server.js b/backup-server/src/server.js index 12354309..b969e643 100644 --- a/backup-server/src/server.js +++ b/backup-server/src/server.js @@ -34,6 +34,7 @@ const ldkLabels = [ ]; const bitkitLabels = [ 'bitkit_settings', + 'bitkit_wallet', 'bitkit_widgets', 'bitkit_metadata', 'bitkit_blocktank_orders', diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 29629ba4..eb5623e8 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -316,7 +316,7 @@ PODS: - React-jsinspector (0.72.4) - React-logger (0.72.4): - glog - - react-native-ldk (0.0.147): + - react-native-ldk (0.0.148): - React - react-native-randombytes (3.6.1): - React-Core @@ -621,7 +621,7 @@ SPEC CHECKSUMS: React-jsiexecutor: c7f826e40fa9cab5d37cab6130b1af237332b594 React-jsinspector: aaed4cf551c4a1c98092436518c2d267b13a673f React-logger: da1ebe05ae06eb6db4b162202faeafac4b435e77 - react-native-ldk: 71275a0c18172fa1646bc2a38a62560ded090da5 + react-native-ldk: fda4d4381d40401bdc5c3a9965937d19b232ed08 react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-tcp-socket: c1b7297619616b4c9caae6889bcb0aba78086989 React-NativeModulesApple: edb5ace14f73f4969df6e7b1f3e41bef0012740f diff --git a/example/ldk/index.ts b/example/ldk/index.ts index c4369695..544bb221 100644 --- a/example/ldk/index.ts +++ b/example/ldk/index.ts @@ -150,7 +150,6 @@ export const setupLdk = async ( }, manually_accept_inbound_channels: true, }, - trustedZeroConfPeers: [peers.lnd.pubKey], skipRemoteBackups: !backupServerDetails, lspLogEvent: async (payload) => { console.log('Log event for LSP:', JSON.stringify(payload)); @@ -184,6 +183,15 @@ export const setupLdk = async ( return err(e.toString()); } + //Set trusted peers we should accept zero conf channels from + const trustedPeers = Object.values(peers).map((peer) => peer.pubKey); + const trustedPeersRes = await lm.setTrustedZeroConfPeerNodeIds( + trustedPeers, + ); + if (trustedPeersRes.isErr()) { + return err(trustedPeersRes.error.message); + } + const nodeIdRes = await ldk.nodeId(); if (nodeIdRes.isErr()) { return err(nodeIdRes.error.message); diff --git a/lib/android/src/main/java/com/reactnativeldk/Helpers.kt b/lib/android/src/main/java/com/reactnativeldk/Helpers.kt index b52706e2..1e68888b 100644 --- a/lib/android/src/main/java/com/reactnativeldk/Helpers.kt +++ b/lib/android/src/main/java/com/reactnativeldk/Helpers.kt @@ -123,6 +123,7 @@ val ChannelDetails.asJson: WritableMap result.putInt("balance_sat", (_balance_msat / 1000).toInt()) result.putHexString("counterparty_node_id", _counterparty._node_id) result.putHexString("funding_txid", _funding_txo?._txid?.reversed()?.toByteArray()) + _funding_txo?._index?.toInt()?.let { result.putInt("funding_output_index", it) } result.putHexString("channel_type", _channel_type?.write()) result.putString("user_channel_id", _user_channel_id.leBytes.hexEncodedString()) result.putInt("confirmations_required", (_confirmations_required as Option_u32Z.Some).some) diff --git a/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt b/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt index bc77c685..82fbc27b 100644 --- a/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt +++ b/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt @@ -1229,7 +1229,7 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod } @ReactMethod - fun spendRecoveredForceCloseOutputs(transaction: String, confirmationHeight: Double, changeDestinationScript: String, promise: Promise) { + fun spendRecoveredForceCloseOutputs(transaction: String, confirmationHeight: Double, changeDestinationScript: String, useInner: Boolean, promise: Promise) { if (channelStoragePath == "") { return handleReject(promise, LdkErrors.init_storage_path) } @@ -1271,13 +1271,23 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod continue } - val res = keysManager!!.spend_spendable_outputs( - descriptors, - emptyArray(), - changeDestinationScript.hexa(), - feeEstimator.onChainSweep, - Option_u32Z.none() - ) + val res = if (useInner) { + keysManager!!.inner.spend_spendable_outputs( + descriptors, + emptyArray(), + changeDestinationScript.hexa(), + feeEstimator.onChainSweep, + Option_u32Z.none() + ) + } else { + keysManager!!.spend_spendable_outputs( + descriptors, + emptyArray(), + changeDestinationScript.hexa(), + feeEstimator.onChainSweep, + Option_u32Z.none() + ) + } if (res.is_ok) { txs.pushHexString((res as Result_TransactionNoneZ.Result_TransactionNoneZ_OK).res) diff --git a/lib/ios/Helpers.swift b/lib/ios/Helpers.swift index a4d90fbe..89a82e1f 100644 --- a/lib/ios/Helpers.swift +++ b/lib/ios/Helpers.swift @@ -122,6 +122,7 @@ extension ChannelDetails { "balance_sat": getBalanceMsat() / 1000, "counterparty_node_id": Data(getCounterparty().getNodeId()).hexEncodedString(), "funding_txid": Data(getFundingTxo()?.getTxid()?.reversed() ?? []).hexEncodedString(), + "funding_output_index": getFundingTxo()?.getIndex() as Any, // Optional number "channel_type": Data(getChannelType()?.write() ?? []).hexEncodedString(), "user_channel_id": Data(getUserChannelId()).hexEncodedString(), //String "confirmations_required": getConfirmationsRequired() as Any, // Optional number diff --git a/lib/ios/Ldk.m b/lib/ios/Ldk.m index 34314708..fcc64328 100644 --- a/lib/ios/Ldk.m +++ b/lib/ios/Ldk.m @@ -176,6 +176,7 @@ @interface RCT_EXTERN_MODULE(Ldk, NSObject) RCT_EXTERN_METHOD(spendRecoveredForceCloseOutputs:(NSString *)transaction confirmationHeight:(NSInteger *)confirmationHeight changeDestinationScript:(NSString *)changeDestinationScript + useInner:(BOOL *)useInner resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(nodeSign:(NSString *)message diff --git a/lib/ios/Ldk.swift b/lib/ios/Ldk.swift index f3d742fe..66da9ee4 100644 --- a/lib/ios/Ldk.swift +++ b/lib/ios/Ldk.swift @@ -1393,7 +1393,7 @@ class Ldk: NSObject { } @objc - func spendRecoveredForceCloseOutputs(_ transaction: NSString, confirmationHeight: NSInteger, changeDestinationScript: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + func spendRecoveredForceCloseOutputs(_ transaction: NSString, confirmationHeight: NSInteger, changeDestinationScript: NSString, useInner: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { guard let channelStoragePath = Ldk.channelStoragePath, let keysManager, let channelManager else { return handleReject(reject, .init_storage_path) @@ -1439,7 +1439,14 @@ class Ldk: NSObject { continue } - let res = keysManager.spendSpendableOutputs( + let res = useInner ? keysManager.inner.spendSpendableOutputs( + descriptors: descriptors, + outputs: [], + changeDestinationScript: String(changeDestinationScript).hexaBytes, + feerateSatPer1000Weight: feeEstimator.getEstSatPer1000Weight(confirmationTarget: .OnChainSweep), + locktime: nil) + : + keysManager.spendSpendableOutputs( descriptors: descriptors, outputs: [], changeDestinationScript: String(changeDestinationScript).hexaBytes, diff --git a/lib/package.json b/lib/package.json index b8fc6cad..0aa9006d 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,7 +1,7 @@ { "name": "@synonymdev/react-native-ldk", "title": "React Native LDK", - "version": "0.0.147", + "version": "0.0.148", "description": "React Native wrapper for LDK", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/lib/src/ldk.ts b/lib/src/ldk.ts index ab6dae17..4d294357 100644 --- a/lib/src/ldk.ts +++ b/lib/src/ldk.ts @@ -1249,12 +1249,14 @@ class LDK { transaction, confirmationHeight, changeDestinationScript, + useInner, }: TSpendRecoveredForceCloseOutputsReq): Promise> { try { const res = await NativeLDK.spendRecoveredForceCloseOutputs( transaction, confirmationHeight, changeDestinationScript, + useInner, ); this.writeDebugToLog('spendRecoveredForceCloseOutputs', res); return ok(res); diff --git a/lib/src/lightning-manager.ts b/lib/src/lightning-manager.ts index 2b0e9426..a977a221 100644 --- a/lib/src/lightning-manager.ts +++ b/lib/src/lightning-manager.ts @@ -140,7 +140,6 @@ class LightningManager { minAllowedNonAnchorChannelRemoteFee: 5, onChainSweep: 5, }); - trustedZeroConfPeers: string[] = []; broadcastTransaction: TBroadcastTransaction = async (): Promise => {}; lspLogEvent: TLspLogEvent | undefined; pathFailedSubscription: EmitterSubscription | undefined; @@ -300,7 +299,6 @@ class LightningManager { scorerDownloadUrl = 'https://rapidsync.lightningdevkit.org/scoring/scorer.bin', forceCloseOnStartup, userConfig = defaultUserConfig, - trustedZeroConfPeers = [], skipParamCheck = false, skipRemoteBackups = false, lspLogEvent, @@ -378,7 +376,6 @@ class LightningManager { this.watchTxs = []; this.watchOutputs = []; this.confirmedWatchOutputs = await this.getConfirmedWatchOutputs(); - this.trustedZeroConfPeers = trustedZeroConfPeers; this.lspLogEvent = lspLogEvent; if (!this.baseStoragePath) { @@ -550,6 +547,37 @@ class LightningManager { return await ldk.updateFees(fees); } + /** + * Persists nodeIDs to disk that should be checked when accepting zero-conf channels. + * @param {string[]} nodeIds + * @param {boolean} [merge] If true, will merge provided nodeIds with existing nodeIds. Set false to overwrite entirely. + * @returns {Promise>} + */ + async setTrustedZeroConfPeerNodeIds( + nodeIds: string[], + merge: boolean = true, + ): Promise> { + let trustedNodeIds = nodeIds; + const accountPath = appendPath(this.baseStoragePath, this.account.name); + if (merge) { + const readRes = await ldk.readFromFile({ + fileName: ELdkFiles.trusted_peer_node_ids, + path: accountPath, + }); + if (readRes.isOk()) { + let currentIds = parseData(readRes.value.content, []); + trustedNodeIds = Array.from(new Set([...currentIds, ...nodeIds])); + } + } + + return await ldk.writeToFile({ + fileName: ELdkFiles.trusted_peer_node_ids, + path: accountPath, + content: JSON.stringify(trustedNodeIds), + remotePersist: false, + }); + } + /** * Fetches current best block and sends to LDK to update both channelManager and chainMonitor. * Also watches transactions and outputs for confirmed and unconfirmed transactions and updates LDK. @@ -1609,14 +1637,26 @@ class LightningManager { * Returns previously broadcasted transactions saved in storgare. * @returns {Promise} */ - async getLdkBroadcastedTxs(): Promise { + async getLdkBroadcastedTxs( + includeConfirmed: boolean = false, + ): Promise { + let txs: TLdkBroadcastedTransactions = []; const res = await ldk.readFromFile({ fileName: ELdkFiles.broadcasted_transactions, }); if (res.isOk()) { - return parseData(res.value.content, []); + txs = parseData(res.value.content, []); } - return []; + + if (includeConfirmed) { + const confirmedRes = await ldk.readFromFile({ + fileName: ELdkFiles.confirmed_broadcasted_transactions, + }); + if (confirmedRes.isOk()) { + txs = txs.concat(parseData(confirmedRes.value.content, [])); + } + } + return txs; } async cleanupBroadcastedTxs(): Promise { @@ -1738,33 +1778,41 @@ class LightningManager { return err('Unable to retrieve change_destination_script.'); } - const txs = await this.getLdkBroadcastedTxs(); + const txs = await this.getLdkBroadcastedTxs(true); if (!txs.length) { return ok('No outputs to reconstruct as no cached transactions found.'); } let txsToBroadcast = 0; - for (const hexTx of txs) { - const tx = bitcoin.Transaction.fromHex(hexTx); - const txData = await this.getTransactionData(tx.getId()); - const txsRes = await ldk.spendRecoveredForceCloseOutputs({ - transaction: hexTx, - confirmationHeight: txData?.height ?? 0, - changeDestinationScript, - }); + const processTxs = async (useInner: boolean): Promise => { + for (const hexTx of txs) { + const tx = bitcoin.Transaction.fromHex(hexTx); + const txData = await this.getTransactionData(tx.getId()); - if (txsRes.isErr()) { - await ldk.writeToLogFile('error', txsRes.error.message); - console.error(txsRes.error.message); - continue; - } + const txsRes = await ldk.spendRecoveredForceCloseOutputs({ + transaction: hexTx, + confirmationHeight: txData?.height ?? 0, + changeDestinationScript, + useInner, + }); + + if (txsRes.isErr()) { + await ldk.writeToLogFile('error', txsRes.error.message); + console.error(txsRes.error.message); + continue; + } - for (const createdTx of txsRes.value) { - txsToBroadcast++; - await this.broadcastTransaction(createdTx); + for (const createdTx of txsRes.value) { + txsToBroadcast++; + await this.broadcastTransaction(createdTx); + } } - } + }; + + //Process first using the inner (ldk keychain) and then using the custom keychain + await processTxs(true); + await processTxs(false); return ok(`Attempting to reconstruct ${txsToBroadcast} transactions.`); } @@ -2064,9 +2112,19 @@ class LightningManager { } = req; let trustedPeer0Conf = false; - const isTrustedPeer = - this.trustedZeroConfPeers.indexOf(counterparty_node_id) > -1; if (supports_zero_conf) { + let trustedZeroConfPeers: string[] = []; + const readRes = await ldk.readFromFile({ + fileName: ELdkFiles.trusted_peer_node_ids, + path: appendPath(this.baseStoragePath, this.account.name), + }); + if (readRes.isOk()) { + trustedZeroConfPeers = parseData(readRes.value.content, []); + } + + const isTrustedPeer = + trustedZeroConfPeers.indexOf(counterparty_node_id) > -1; + if (isTrustedPeer) { await ldk.writeToLogFile( 'info', diff --git a/lib/src/utils/types.ts b/lib/src/utils/types.ts index 9fa94a73..8cb617b1 100644 --- a/lib/src/utils/types.ts +++ b/lib/src/utils/types.ts @@ -150,6 +150,7 @@ export type TChannel = { balance_sat: number; counterparty_node_id: string; funding_txid?: string; + funding_output_index?: number; channel_type?: string; user_channel_id: string; confirmations_required?: number; @@ -481,6 +482,7 @@ export enum ELdkFiles { channel_manager = 'channel_manager.bin', //Serialised rust object channels = 'channels', //Path containing multiple files of serialised channels peers = 'peers.json', //File saved from JS + trusted_peer_node_ids = 'trusted_peer_node_ids.json', //File saved from JS unconfirmed_transactions = 'unconfirmed_transactions.json', broadcasted_transactions = 'broadcasted_transactions.json', confirmed_broadcasted_transactions = 'confirmed_broadcasted_transactions.json', @@ -571,7 +573,6 @@ export type TLdkStart = { scorerDownloadUrl?: string; forceCloseOnStartup?: TForceCloseOnStartup; userConfig?: TUserConfig; - trustedZeroConfPeers?: string[]; skipParamCheck?: boolean; skipRemoteBackups?: boolean; lspLogEvent?: TLspLogEvent; @@ -612,6 +613,7 @@ export type TSpendRecoveredForceCloseOutputsReq = { transaction: string; confirmationHeight: number; changeDestinationScript: string; + useInner: boolean; }; export type TBackupServerDetails = {