diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 995dafc0..3bcea265 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -302,7 +302,7 @@ PODS: - React-jsinspector (0.70.6) - React-logger (0.70.6): - glog - - react-native-ldk (0.0.109): + - react-native-ldk (0.0.111): - React - react-native-randombytes (3.6.1): - React-Core @@ -593,7 +593,7 @@ SPEC CHECKSUMS: React-jsiexecutor: b4a65947391c658450151275aa406f2b8263178f React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0 - react-native-ldk: 3c1cd457a2372ef3eda9fbe144f4cdf6bc1fd6c3 + react-native-ldk: 20bafd3ad8ea69c33841d7b4895379b6eb8cf9f6 react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-tcp-socket: c1b7297619616b4c9caae6889bcb0aba78086989 React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595 diff --git a/example/tests/lnd.ts b/example/tests/lnd.ts index 95370b58..0d1d306f 100644 --- a/example/tests/lnd.ts +++ b/example/tests/lnd.ts @@ -559,7 +559,7 @@ describe('LND', function () { // - force close channel from LDK // - check everything is ok - let fees = { highPriority: 3, normal: 2, background: 1 }; + let fees = { highPriority: 3, normal: 2, background: 1, mempoolMinimum: 1 }; const lmStart = await lm.start({ ...profile.getStartParams(), @@ -655,8 +655,8 @@ describe('LND', function () { EEventTypes.broadcast_transaction, ); - // set hight fees and restart LDK so it catches up - fees = { highPriority: 30, normal: 20, background: 10 }; + // set height fees and restart LDK so it catches up + fees = { highPriority: 30, normal: 20, background: 10, mempoolMinimum: 1 }; const syncRes0 = await lm.syncLdk(); await lm.setFees(); if (syncRes0.isErr()) { @@ -701,7 +701,7 @@ describe('LND', function () { if (Platform.OS === 'android') { // @ts-ignore claimableBalances1.value = claimableBalances1.value.filter( - ({ claimable_amount_satoshis }) => claimable_amount_satoshis > 0, + ({ amount_satoshis }) => amount_satoshis > 0, ); } expect(claimableBalances1.value).to.have.length(1); diff --git a/example/yarn.lock b/example/yarn.lock index 9e0ded90..c781ef67 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1428,7 +1428,7 @@ "@sinonjs/commons" "^1.7.0" "@synonymdev/react-native-ldk@../lib": - version "0.0.109" + version "0.0.111" dependencies: bitcoinjs-lib "^6.0.2" @@ -2934,9 +2934,9 @@ decamelize@^4.0.0: integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== decode-uri-component@^0.4.1: version "0.4.1" @@ -3909,9 +3909,9 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.1.3" @@ -5262,9 +5262,9 @@ json-stable-stringify-without-jsonify@^1.0.1: integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json5@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^2.1.0: version "2.4.0" diff --git a/lib/package.json b/lib/package.json index b20db50e..9d366a82 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.109", + "version": "0.0.111", "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 1035aceb..a7507b1b 100644 --- a/lib/src/lightning-manager.ts +++ b/lib/src/lightning-manager.ts @@ -1,5 +1,5 @@ import ldk from './ldk'; -import { err, ok, Result } from './utils/result'; +import { Err, err, ok, Result } from './utils/result'; import { DefaultLdkDataShape, DefaultTransactionDataShape, @@ -62,6 +62,7 @@ import { findOutputsFromRawTxs, parseData, promiseTimeout, + sleep, startParamCheck, } from './utils/helpers'; import * as bitcoin from 'bitcoinjs-lib'; @@ -136,6 +137,10 @@ class LightningManager { paymentFailedSubscription: EmitterSubscription | undefined; paymentSentSubscription: EmitterSubscription | undefined; + private isSyncing: boolean = false; + private forceSync: boolean = false; + private pendingSyncPromises: Array<(result: Result) => void> = []; + constructor() { // Step 0: Subscribe to all events ldk.onEvent(EEventTypes.native_log, (line) => { @@ -498,20 +503,62 @@ class LightningManager { /** * 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. + * @param {number} [timeout] Timeout to set for each async function in this method. Potential overall timeout may be greater. + * @param {number} [retryAttempts] Will attempt to sync LDK a given number of times before giving up. + * @param {boolean} [force] In the event a sync is underway, this will force another sync once the current sync is complete. * @returns {Promise>} */ - async syncLdk(): Promise> { + async syncLdk({ + timeout = 5000, + retryAttempts = 1, + force = false, + }: { + timeout?: number; + retryAttempts?: number; + force?: boolean; + } = {}): Promise> { + // Check that the getBestBlock method has been provided. if (!this.getBestBlock) { - return err('No getBestBlock method provided.'); + return this.handleSyncError(err('No getBestBlock method provided.')); } - const bestBlock = await this.getBestBlock(); - const height = bestBlock?.height; - //Don't update unnecessarily + if (force && this.isSyncing && !this.forceSync) { + // If syncing is already underway and force is true, set forceSync to true. + this.forceSync = true; + } + if (this.isSyncing) { + // If isSyncing, push to pendingSyncPromises to resolve when the current sync completes. + return new Promise>((resolve) => { + this.pendingSyncPromises.push(resolve); + }); + } + this.isSyncing = true; + + const bestBlock = await promiseTimeout( + timeout, + this.getBestBlock(), + ); + if (!bestBlock?.height) { + return this.retrySyncOrReturnError({ + timeout, + retryAttempts, + e: err('Unable to get best block in syncLdk method.'), + }); + } + const height = bestBlock.height; + + // Don't update unnecessarily if (this.currentBlock.hash !== bestBlock?.hash) { - const syncToTip = await ldk.syncToTip(bestBlock); + const syncToTip = await promiseTimeout>( + timeout, + ldk.syncToTip(bestBlock), + ); if (syncToTip.isErr()) { - return syncToTip; + return this.retrySyncOrReturnError({ + timeout, + retryAttempts, + e: syncToTip, + }); } this.currentBlock = bestBlock; @@ -520,29 +567,147 @@ class LightningManager { let channels: TChannel[] = []; if (this.watchTxs.length > 0) { // Get fresh array of channels. - const listChannelsResponse = await ldk.listChannels(); + const listChannelsResponse = await promiseTimeout>( + timeout, + ldk.listChannels(), + ); if (listChannelsResponse.isOk()) { channels = listChannelsResponse.value; } } // Iterate over watch transactions/outputs and set whether they are confirmed or unconfirmed. - await this.checkWatchTxs(this.watchTxs, channels, bestBlock); - await this.checkWatchOutputs(this.watchOutputs); - await this.checkUnconfirmedTransactions(); + const watchTxsRes = await promiseTimeout>( + timeout, + this.checkWatchTxs(this.watchTxs, channels, bestBlock), + ); + if (watchTxsRes.isErr()) { + return this.retrySyncOrReturnError({ + timeout, + retryAttempts, + e: watchTxsRes, + }); + } + const watchOutputsRes = await promiseTimeout>( + timeout, + this.checkWatchOutputs(this.watchOutputs), + ); + if (watchOutputsRes.isErr()) { + return this.retrySyncOrReturnError({ + timeout, + retryAttempts, + e: watchOutputsRes, + }); + } + const unconfirmedTxsRes = await promiseTimeout>( + timeout, + this.checkUnconfirmedTransactions(), + ); + if (unconfirmedTxsRes.isErr()) { + return this.retrySyncOrReturnError({ + timeout, + retryAttempts, + e: unconfirmedTxsRes, + }); + } + + this.isSyncing = false; - return ok(`Synced to block ${height}`); + // Handle force sync if needed. + if (this.forceSync) { + return this.handleForceSync({ timeout, retryAttempts }); + } + const result = ok(`Synced to block ${height}`); + this.resolveAllPendingSyncPromises(result); + return result; } + /** + * Resolves all pending sync promises with the provided result. + * @private + * @param {Result} result + * @returns {void} + */ + private resolveAllPendingSyncPromises(result: Result): void { + while (this.pendingSyncPromises.length > 0) { + const resolve = this.pendingSyncPromises.shift(); + if (resolve) { + resolve(result); + } + } + } + + /** + * Sets forceSync to false and re-runs the sync method. + * @private + * @param {number} timeout + * @param {number} retryAttempts + * @returns {Promise>} + */ + private handleForceSync = async ({ + timeout, + retryAttempts, + }: { + timeout: number; + retryAttempts: number; + }): Promise> => { + this.forceSync = false; + return this.syncLdk({ + timeout, + retryAttempts, + }); + }; + + /** + * Attempts to retry the syncLdk method. Otherwise, the error gets passed to handleSyncError. + * @private + * @param {number} [timeout] + * @param {number} retryAttempts + * @param {Err} e + * @returns {Promise>} + */ + private retrySyncOrReturnError = async ({ + timeout = 5000, + retryAttempts, + e, + }: { + timeout?: number; + retryAttempts: number; + e: Err; + }): Promise> => { + this.isSyncing = false; + if (retryAttempts > 0) { + await sleep(); + return this.syncLdk({ + timeout, + retryAttempts: retryAttempts - 1, + }); + } else { + return this.handleSyncError(e); + } + }; + + /** + * Sets isSyncing & forceSync to false and returns error. + * @private + * @param {Err} e + * @returns {Promise>} + */ + private handleSyncError = (e: Err): Result => { + this.isSyncing = false; + this.forceSync = false; + this.resolveAllPendingSyncPromises(e); + return e; + }; + checkWatchTxs = async ( watchTxs: TRegisterTxEvent[], channels: TChannel[], bestBlock: THeader, - ): Promise => { + ): Promise> => { const height = bestBlock?.height; if (!height) { - console.log('No height provided'); - return; + return err('No height provided'); } await Promise.all( watchTxs.map(async (watchTxData) => { @@ -557,7 +722,7 @@ class LightningManager { //Watch TX was never confirmed so there's no need to unconfirm it. return; } - if (!txData.transaction) { + if (!txData?.transaction) { console.log( 'Unable to retrieve transaction data from the getTransactionData method.', ); @@ -588,11 +753,12 @@ class LightningManager { } }), ); + return ok('Watch transactions checked'); }; checkWatchOutputs = async ( watchOutputs: TRegisterOutputEvent[], - ): Promise => { + ): Promise> => { await Promise.all( watchOutputs.map(async ({ index, script_pubkey }) => { const transactions = await this.getScriptPubKeyHistory(script_pubkey); @@ -654,6 +820,7 @@ class LightningManager { ); }), ); + return ok('Watch outputs checked'); }; /** @@ -1206,7 +1373,7 @@ class LightningManager { } }; - checkUnconfirmedTransactions = async (): Promise => { + checkUnconfirmedTransactions = async (): Promise> => { let needsToSync = false; let newUnconfirmedTxs: TLdkUnconfirmedTransactions = []; await Promise.all( @@ -1248,8 +1415,9 @@ class LightningManager { await this.updateUnconfirmedTxs(newUnconfirmedTxs); if (needsToSync) { - await this.syncLdk(); + await this.syncLdk({ force: true }); } + return ok('Unconfirmed transactions checked'); }; /** @@ -1405,18 +1573,15 @@ class LightningManager { paymentHash: string, ): Promise => { const invoices = await this.getBolt11Invoices(); - let invoice: TInvoice | undefined; for (let index = 0; index < invoices.length; index++) { const paymentRequest = invoices[index]; const invoiceRes = await ldk.decode({ paymentRequest }); if (invoiceRes.isOk()) { if (invoiceRes.value.payment_hash === paymentHash) { - invoice = invoiceRes.value; + return invoiceRes.value; } } } - - return invoice; }; private getLdkSpendableOutputs = async (): Promise => { @@ -1908,7 +2073,7 @@ class LightningManager { ): Promise { // Payment Received/Invoice Paid. console.log(`onChannelManagerPaymentClaimed: ${JSON.stringify(res)}`); - this.syncLdk().then(); + this.syncLdk({ force: true }).then(); } /** diff --git a/lib/src/utils/helpers.ts b/lib/src/utils/helpers.ts index eff9d8c8..47ac918c 100644 --- a/lib/src/utils/helpers.ts +++ b/lib/src/utils/helpers.ts @@ -296,3 +296,14 @@ export const findOutputsFromRawTxs = ( return result; }; + +/** + * Pauses execution of a function. + * @param {number} ms The time to wait in milliseconds. + * @returns {Promise} + */ +export const sleep = (ms = 1000): Promise => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}; diff --git a/lib/yarn.lock b/lib/yarn.lock index 25d897c6..69332058 100644 --- a/lib/yarn.lock +++ b/lib/yarn.lock @@ -1471,9 +1471,9 @@ json-stable-stringify-without-jsonify@^1.0.1: integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -1555,9 +1555,9 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: brace-expansion "^1.1.7" minimist@^1.2.0, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== ms@2.0.0: version "2.0.0" @@ -1843,14 +1843,14 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== semver@^6.1.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.3.2, semver@^7.3.5, semver@^7.3.7: - version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" @@ -2084,9 +2084,9 @@ wif@^2.0.1: bs58check "<3.0.0" word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== wrappy@1: version "1.0.2"