Skip to content

Commit

Permalink
fix(lightning): syncLdk Updates
Browse files Browse the repository at this point in the history
Prevents syncLdk from having overlapping calls, instead returning the outcome of the current sync.
Adds retry attempts to syncLdk if a network query times out.
  • Loading branch information
coreyphillips committed Sep 26, 2023
1 parent 5e6ce7f commit 6a7d814
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 28 deletions.
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions example/tests/lnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
210 changes: 189 additions & 21 deletions lib/src/lightning-manager.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -61,6 +61,7 @@ import {
findOutputsFromRawTxs,
parseData,
promiseTimeout,
sleep,
startParamCheck,
} from './utils/helpers';
import * as bitcoin from 'bitcoinjs-lib';
Expand Down Expand Up @@ -135,6 +136,10 @@ class LightningManager {
paymentFailedSubscription: EmitterSubscription | undefined;
paymentSentSubscription: EmitterSubscription | undefined;

private isSyncing: boolean = false;
private forceSync: boolean = false;
private pendingSyncPromises: Array<(result: Result<string>) => void> = [];

constructor() {
// Step 0: Subscribe to all events
ldk.onEvent(EEventTypes.native_log, (line) => {
Expand Down Expand Up @@ -474,20 +479,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<Result<string>>}
*/
async syncLdk(): Promise<Result<string>> {
async syncLdk({
timeout = 5000,
retryAttempts = 1,
force = false,
}: {
timeout?: number;
retryAttempts?: number;
force?: boolean;
} = {}): Promise<Result<string>> {
// 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<Result<string>>((resolve) => {
this.pendingSyncPromises.push(resolve);
});
}
this.isSyncing = true;

const bestBlock = await promiseTimeout<THeader>(
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<Result<string>>(
timeout,
ldk.syncToTip(bestBlock),
);
if (syncToTip.isErr()) {
return syncToTip;
return this.retrySyncOrReturnError({
timeout,
retryAttempts,
e: syncToTip,
});
}

this.currentBlock = bestBlock;
Expand All @@ -496,29 +543,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<Result<TChannel[]>>(
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<Result<string>>(
timeout,
this.checkWatchTxs(this.watchTxs, channels, bestBlock),
);
if (watchTxsRes.isErr()) {
return this.retrySyncOrReturnError({
timeout,
retryAttempts,
e: watchTxsRes,
});
}
const watchOutputsRes = await promiseTimeout<Result<string>>(
timeout,
this.checkWatchOutputs(this.watchOutputs),
);
if (watchOutputsRes.isErr()) {
return this.retrySyncOrReturnError({
timeout,
retryAttempts,
e: watchOutputsRes,
});
}
const unconfirmedTxsRes = await promiseTimeout<Result<string>>(
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<string>} result
* @returns {void}
*/
private resolveAllPendingSyncPromises(result: Result<string>): 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<Result<string>>}
*/
private handleForceSync = async ({
timeout,
retryAttempts,
}: {
timeout: number;
retryAttempts: number;
}): Promise<Result<string>> => {
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<string>} e
* @returns {Promise<Result<string>>}
*/
private retrySyncOrReturnError = async ({
timeout = 5000,
retryAttempts,
e,
}: {
timeout?: number;
retryAttempts: number;
e: Err<string>;
}): Promise<Result<string>> => {
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<string>} e
* @returns {Promise<Result<string>>}
*/
private handleSyncError = (e: Err<string>): Result<string> => {
this.isSyncing = false;
this.forceSync = false;
this.resolveAllPendingSyncPromises(e);
return e;
};

checkWatchTxs = async (
watchTxs: TRegisterTxEvent[],
channels: TChannel[],
bestBlock: THeader,
): Promise<void> => {
): Promise<Result<string>> => {
const height = bestBlock?.height;
if (!height) {
console.log('No height provided');
return;
return err('No height provided');
}
await Promise.all(
watchTxs.map(async (watchTxData) => {
Expand All @@ -533,7 +698,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.',
);
Expand Down Expand Up @@ -564,11 +729,12 @@ class LightningManager {
}
}),
);
return ok('Watch transactions checked');
};

checkWatchOutputs = async (
watchOutputs: TRegisterOutputEvent[],
): Promise<void> => {
): Promise<Result<string>> => {
await Promise.all(
watchOutputs.map(async ({ index, script_pubkey }) => {
const transactions = await this.getScriptPubKeyHistory(script_pubkey);
Expand Down Expand Up @@ -630,6 +796,7 @@ class LightningManager {
);
}),
);
return ok('Watch outputs checked');
};

/**
Expand Down Expand Up @@ -1127,7 +1294,7 @@ class LightningManager {
}
};

checkUnconfirmedTransactions = async (): Promise<void> => {
checkUnconfirmedTransactions = async (): Promise<Result<string>> => {
let needsToSync = false;
let newUnconfirmedTxs: TLdkUnconfirmedTransactions = [];
await Promise.all(
Expand Down Expand Up @@ -1169,8 +1336,9 @@ class LightningManager {

await this.updateUnconfirmedTxs(newUnconfirmedTxs);
if (needsToSync) {
await this.syncLdk();
await this.syncLdk({ force: true });
}
return ok('Unconfirmed transactions checked');
};

/**
Expand Down Expand Up @@ -1821,7 +1989,7 @@ class LightningManager {
): Promise<void> {
// Payment Received/Invoice Paid.
console.log(`onChannelManagerPaymentClaimed: ${JSON.stringify(res)}`);
this.syncLdk().then();
this.syncLdk({ force: true }).then();
}

/**
Expand Down
11 changes: 11 additions & 0 deletions lib/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>}
*/
export const sleep = (ms = 1000): Promise<void> => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};

0 comments on commit 6a7d814

Please sign in to comment.