Skip to content

Commit

Permalink
Merge pull request #171 from synonymdev/sync-ldk-updates
Browse files Browse the repository at this point in the history
fix(lightning): syncLdk Updates
  • Loading branch information
Jasonvdb authored Sep 27, 2023
2 parents 5e6ce7f + 6a7d814 commit 8586e5f
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 8586e5f

Please sign in to comment.