diff --git a/backup-server/src/server.js b/backup-server/src/server.js index fc19e850..69e2730e 100644 --- a/backup-server/src/server.js +++ b/backup-server/src/server.js @@ -28,7 +28,8 @@ const ldkLabels = [ 'payments_claimed', 'payments_sent', 'bolt11_invoices', - 'channel_opened_with_custom_keys_manager' + 'channel_opened_with_custom_keys_manager', + 'confirmed_watch_outputs', ]; const bitkitLabels = [ 'bitkit_settings', diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 8434decb..c4ce9057 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -375,7 +375,7 @@ PODS: - React-jsinspector (0.72.4) - React-logger (0.72.4): - glog - - react-native-ldk (0.0.132): + - react-native-ldk (0.0.134): - React - react-native-randombytes (3.6.1): - React-Core @@ -723,7 +723,7 @@ SPEC CHECKSUMS: React-jsiexecutor: c7f826e40fa9cab5d37cab6130b1af237332b594 React-jsinspector: aaed4cf551c4a1c98092436518c2d267b13a673f React-logger: da1ebe05ae06eb6db4b162202faeafac4b435e77 - react-native-ldk: 4bb809be74082223644931a3239323af56c7ee8f + react-native-ldk: e4971770e3773415fff4e5aa04df83f9d5485744 react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-tcp-socket: c1b7297619616b4c9caae6889bcb0aba78086989 React-NativeModulesApple: edb5ace14f73f4969df6e7b1f3e41bef0012740f diff --git a/lib/package.json b/lib/package.json index e7fe0a6d..3f822f18 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.132", + "version": "0.0.134", "description": "React Native wrapper for LDK", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/lib/src/lightning-manager.ts b/lib/src/lightning-manager.ts index 821de307..8aacf1b2 100644 --- a/lib/src/lightning-manager.ts +++ b/lib/src/lightning-manager.ts @@ -102,6 +102,7 @@ class LightningManager { unconfirmedTxs: TLdkUnconfirmedTransactions = []; watchTxs: TRegisterTxEvent[] = []; watchOutputs: TRegisterOutputEvent[] = []; + confirmedWatchOutputs: string[] = []; getBestBlock: TGetBestBlock = async (): Promise => ({ hex: '', hash: '', @@ -365,6 +366,7 @@ class LightningManager { this.unconfirmedTxs = await this.getLdkUnconfirmedTxs(); this.watchTxs = []; this.watchOutputs = []; + this.confirmedWatchOutputs = await this.getConfirmedWatchOutputs(); this.trustedZeroConfPeers = trustedZeroConfPeers; if (!this.baseStoragePath) { @@ -541,7 +543,7 @@ class LightningManager { * @returns {Promise>} */ async syncLdk({ - timeout = 5000, + timeout = 20000, retryAttempts = 1, force = false, }: { @@ -700,7 +702,7 @@ class LightningManager { * @returns {Promise>} */ private retrySyncOrReturnError = async ({ - timeout = 5000, + timeout = 20000, retryAttempts, e, }: { @@ -709,6 +711,9 @@ class LightningManager { e: Err; }): Promise> => { this.isSyncing = false; + if (e?.code === 'timed-out') { + return this.handleSyncError(e); + } if (retryAttempts > 0) { await sleep(); return this.syncLdk({ @@ -742,121 +747,166 @@ class LightningManager { if (!height) { return err('No height provided'); } - await Promise.all( - watchTxs.map(async (watchTxData) => { - const { txid, script_pubkey } = watchTxData; - let requiredConfirmations = 1; - const channel = channels.find((c) => c.funding_txid === txid); - if (channel && channel?.confirmations_required !== undefined) { - requiredConfirmations = channel.confirmations_required; - } - const txData = await this.getTransactionData(txid); - if (!txData) { - //Watch TX was never confirmed so there's no need to unconfirm it. - return; - } - if (!txData?.transaction) { - console.log( - 'Unable to retrieve transaction data from the getTransactionData method.', - ); - return; - } - const txConfirmations = - txData.height === 0 ? 0 : height - txData.height + 1; - if (txConfirmations >= requiredConfirmations) { - const pos = await this.getTransactionPosition({ - tx_hash: txid, + for (const watchTxData of watchTxs) { + const { txid, script_pubkey } = watchTxData; + let requiredConfirmations = 1; + const channel = channels.find((c) => c.funding_txid === txid); + if (channel && channel?.confirmations_required !== undefined) { + requiredConfirmations = channel.confirmations_required; + } + const txData = await this.getTransactionData(txid); + if (!txData) { + //Watch TX was never confirmed so there's no need to unconfirm it. + continue; + } + if (!txData?.transaction) { + console.log( + 'Unable to retrieve transaction data from the getTransactionData method.', + ); + continue; + } + const txConfirmations = + txData.height === 0 ? 0 : height - txData.height + 1; + if (txConfirmations >= requiredConfirmations) { + const pos = await this.getTransactionPosition({ + tx_hash: txid, + height: txData.height, + }); + if (pos >= 0) { + const setTxConfirmedRes = await ldk.setTxConfirmed({ + header: txData.header, height: txData.height, + txData: [{ transaction: txData.transaction, pos }], }); - if (pos >= 0) { - const setTxConfirmedRes = await ldk.setTxConfirmed({ - header: txData.header, - height: txData.height, - txData: [{ transaction: txData.transaction, pos }], + if (setTxConfirmedRes.isOk()) { + await this.saveUnconfirmedTx({ + ...txData, + txid, + script_pubkey, }); - if (setTxConfirmedRes.isOk()) { - await this.saveUnconfirmedTx({ - ...txData, - txid, - script_pubkey, - }); - this.watchTxs = this.watchTxs.filter((tx) => tx.txid !== txid); - } + this.watchTxs = this.watchTxs.filter((tx) => tx.txid !== txid); } } - }), - ); + } + } return ok('Watch transactions checked'); }; + /** + * Saves confirmed watch outputs to storage. + * @param {string} confirmedWatchOutput + * @returns {Promise>} + */ + private saveConfirmedWatchOutput = async ( + confirmedWatchOutput: string, + ): Promise> => { + if (!confirmedWatchOutput) { + return err( + 'No confirmedWatchOutput provided to saveConfirmedWatchOutput.', + ); + } + if (this.confirmedWatchOutputs.includes(confirmedWatchOutput)) { + return ok(true); + } + this.confirmedWatchOutputs.push(confirmedWatchOutput); + const accountPath = appendPath(this.baseStoragePath, this.account.name); + const writeRes = await ldk.writeToFile({ + fileName: ELdkFiles.confirmed_watch_outputs, + path: accountPath, + content: JSON.stringify(this.confirmedWatchOutputs), + remotePersist: true, + }); + if (writeRes.isErr()) { + return err(writeRes.error.message); + } + return ok(true); + }; + + getConfirmedWatchOutputs = async (): Promise => { + const accountPath = appendPath(this.baseStoragePath, this.account.name); + const writeRes = await ldk.readFromFile({ + fileName: ELdkFiles.confirmed_watch_outputs, + path: accountPath, + }); + if (writeRes.isOk()) { + return parseData(writeRes.value.content, []); + } + return []; + }; + checkWatchOutputs = async ( watchOutputs: TRegisterOutputEvent[], ): Promise> => { - await Promise.all( - watchOutputs.map(async ({ index, script_pubkey }) => { - const transactions = await this.getScriptPubKeyHistory(script_pubkey); - await Promise.all( - transactions.map(async ({ txid }) => { - const transactionData = await this.getTransactionData(txid); - if (!transactionData) { - //Watch Output was never confirmed so there's no need to unconfirm it. - return; - } - if ( - !transactionData?.height && - transactionData?.vout?.length < index + 1 - ) { - return; - } - - if (!transactionData?.vout[index]) { - return; - } - - const txs = await this.getScriptPubKeyHistory( - transactionData?.vout[index].hex, - ); + for (const { index, script_pubkey } of watchOutputs) { + if (this.confirmedWatchOutputs.includes(script_pubkey)) { + this.watchOutputs = this.watchOutputs.filter( + (o) => o.script_pubkey !== script_pubkey, + ); + continue; + } + const transactions = await this.getScriptPubKeyHistory(script_pubkey); + for (const { txid } of transactions) { + const transactionData = await this.getTransactionData(txid); + if (!transactionData) { + //Watch Output was never confirmed so there's no need to unconfirm it. + continue; + } + if ( + !transactionData?.height && + transactionData?.vout?.length < index + 1 + ) { + continue; + } - // We're looking for more than two transactions from this address. - if (txs.length <= 1) { - return; - } - - // We only need the second transaction. - const tx = txs[1]; - const txData = await this.getTransactionData(tx.txid); - if (!txData) { - //Watch Output was never confirmed so there's no need to unconfirm it. - return; - } - if (!txData?.height) { - return; - } - const pos = await this.getTransactionPosition({ - tx_hash: tx.txid, - height: txData.height, - }); - if (pos >= 0) { - const setTxConfirmedRes = await ldk.setTxConfirmed({ - header: txData.header, - height: txData.height, - txData: [{ transaction: txData.transaction, pos }], - }); - if (setTxConfirmedRes.isOk()) { - await this.saveUnconfirmedTx({ - ...txData, - txid: tx.txid, - script_pubkey, - }); - this.watchOutputs = this.watchOutputs.filter( - (o) => o.script_pubkey !== script_pubkey, - ); - } - } - }), + if (!transactionData?.vout[index]) { + continue; + } + + const txs = await this.getScriptPubKeyHistory( + transactionData?.vout[index].hex, ); - }), - ); + + if (txs.length < 1) { + continue; + } + + // TODO: Implement index fix + const tx = txs[1]; + if (!tx?.txid) { + continue; + } + const txData = await this.getTransactionData(tx.txid); + if (!txData) { + //Watch Output was never confirmed so there's no need to unconfirm it. + continue; + } + if (!txData?.height) { + continue; + } + const pos = await this.getTransactionPosition({ + tx_hash: tx.txid, + height: txData.height, + }); + if (pos >= 0) { + const setTxConfirmedRes = await ldk.setTxConfirmed({ + header: txData.header, + height: txData.height, + txData: [{ transaction: txData.transaction, pos }], + }); + if (setTxConfirmedRes.isOk()) { + await this.saveUnconfirmedTx({ + ...txData, + txid: tx.txid, + script_pubkey, + }); + await this.saveConfirmedWatchOutput(script_pubkey); + this.watchOutputs = this.watchOutputs.filter( + (o) => o.script_pubkey !== script_pubkey, + ); + } + } + } + } return ok('Watch outputs checked'); }; @@ -1247,42 +1297,40 @@ class LightningManager { checkUnconfirmedTransactions = async (): Promise> => { let needsToSync = false; let newUnconfirmedTxs: TLdkUnconfirmedTransactions = []; - await Promise.all( - this.unconfirmedTxs.map(async (unconfirmedTx) => { - const { txid, height } = unconfirmedTx; - const newTxData = await this.getTransactionData(txid); - - //Tx was removed from mempool. - if (!newTxData) { - await ldk.setTxUnconfirmed(txid); - needsToSync = true; - return; - } + for (const unconfirmedTx of this.unconfirmedTxs) { + const { txid, height } = unconfirmedTx; + const newTxData = await this.getTransactionData(txid); + + //Tx was removed from mempool. + if (!newTxData) { + await ldk.setTxUnconfirmed(txid); + needsToSync = true; + continue; + } - //Possible issue retrieving tx data from electrum. Keep current data. Try again later. - if (!newTxData?.header && newTxData?.height === 0) { - newUnconfirmedTxs.push(unconfirmedTx); - return; - } + //Possible issue retrieving tx data from electrum. Keep current data. Try again later. + if (!newTxData?.header && newTxData?.height === 0) { + newUnconfirmedTxs.push(unconfirmedTx); + continue; + } - //Transaction is fully confirmed. No need to add it back to the newUnconfirmedTxs array. - if (this.currentBlock.height - newTxData.height >= 6) { - return; - } + //Transaction is fully confirmed. No need to add it back to the newUnconfirmedTxs array. + if (this.currentBlock.height - newTxData.height >= 6) { + continue; + } - //If the tx is less than its previously known height or the height has fallen back to zero, run setTxUnconfirmed. - if (newTxData.height < height && newTxData.height === 0) { - await ldk.setTxUnconfirmed(txid); - needsToSync = true; - return; - } - newUnconfirmedTxs.push({ - ...newTxData, - txid, - script_pubkey: unconfirmedTx.script_pubkey, - }); - }), - ); + //If the tx is less than its previously known height or the height has fallen back to zero, run setTxUnconfirmed. + if (newTxData.height < height && newTxData.height === 0) { + await ldk.setTxUnconfirmed(txid); + needsToSync = true; + continue; + } + newUnconfirmedTxs.push({ + ...newTxData, + txid, + script_pubkey: unconfirmedTx.script_pubkey, + }); + } await this.updateUnconfirmedTxs(newUnconfirmedTxs); if (needsToSync) { @@ -1470,11 +1518,12 @@ class LightningManager { async rebroadcastAllKnownTransactions(): Promise { const broadcastedTransactions = await this.getLdkBroadcastedTxs(); - return await Promise.all( - broadcastedTransactions.map(async (tx) => { - return await this.broadcastTransaction(tx); - }), - ); + const results = []; + for (const tx of broadcastedTransactions) { + const result = await this.broadcastTransaction(tx); + results.push(result); + } + return results; } /** @@ -1670,7 +1719,10 @@ class LightningManager { const isDuplicate = this.watchTxs.some( (tx) => tx.script_pubkey === res.script_pubkey && tx.txid === res.txid, ); - if (!isDuplicate) { + if ( + !isDuplicate && + !this.confirmedWatchOutputs.includes(res.script_pubkey) + ) { this.watchTxs.push(res); } } diff --git a/lib/src/utils/types.ts b/lib/src/utils/types.ts index 05304069..32894bf3 100644 --- a/lib/src/utils/types.ts +++ b/lib/src/utils/types.ts @@ -478,6 +478,7 @@ export enum ELdkFiles { payments_claimed = 'payments_claimed.json', // Written in swift/kotlin and read from JS payments_sent = 'payments_sent.json', // Written in swift/kotlin and read from JS bolt11_invoices = 'bolt11_invoices.json', // Saved/read from JS + confirmed_watch_outputs = 'confirmed_watch_outputs.json', } export enum ELdkData {