Skip to content
This repository was archived by the owner on Feb 9, 2026. It is now read-only.

Commit 8586e5f

Browse files
authored
Merge pull request #171 from synonymdev/sync-ldk-updates
fix(lightning): syncLdk Updates
2 parents 5e6ce7f + 6a7d814 commit 8586e5f

5 files changed

Lines changed: 207 additions & 28 deletions

File tree

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ PODS:
302302
- React-jsinspector (0.70.6)
303303
- React-logger (0.70.6):
304304
- glog
305-
- react-native-ldk (0.0.109):
305+
- react-native-ldk (0.0.111):
306306
- React
307307
- react-native-randombytes (3.6.1):
308308
- React-Core
@@ -593,7 +593,7 @@ SPEC CHECKSUMS:
593593
React-jsiexecutor: b4a65947391c658450151275aa406f2b8263178f
594594
React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b
595595
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
596-
react-native-ldk: 3c1cd457a2372ef3eda9fbe144f4cdf6bc1fd6c3
596+
react-native-ldk: 20bafd3ad8ea69c33841d7b4895379b6eb8cf9f6
597597
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
598598
react-native-tcp-socket: c1b7297619616b4c9caae6889bcb0aba78086989
599599
React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595

example/tests/lnd.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ describe('LND', function () {
559559
// - force close channel from LDK
560560
// - check everything is ok
561561

562-
let fees = { highPriority: 3, normal: 2, background: 1 };
562+
let fees = { highPriority: 3, normal: 2, background: 1, mempoolMinimum: 1 };
563563

564564
const lmStart = await lm.start({
565565
...profile.getStartParams(),
@@ -655,8 +655,8 @@ describe('LND', function () {
655655
EEventTypes.broadcast_transaction,
656656
);
657657

658-
// set hight fees and restart LDK so it catches up
659-
fees = { highPriority: 30, normal: 20, background: 10 };
658+
// set height fees and restart LDK so it catches up
659+
fees = { highPriority: 30, normal: 20, background: 10, mempoolMinimum: 1 };
660660
const syncRes0 = await lm.syncLdk();
661661
await lm.setFees();
662662
if (syncRes0.isErr()) {
@@ -701,7 +701,7 @@ describe('LND', function () {
701701
if (Platform.OS === 'android') {
702702
// @ts-ignore
703703
claimableBalances1.value = claimableBalances1.value.filter(
704-
({ claimable_amount_satoshis }) => claimable_amount_satoshis > 0,
704+
({ amount_satoshis }) => amount_satoshis > 0,
705705
);
706706
}
707707
expect(claimableBalances1.value).to.have.length(1);

lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@synonymdev/react-native-ldk",
33
"title": "React Native LDK",
4-
"version": "0.0.109",
4+
"version": "0.0.111",
55
"description": "React Native wrapper for LDK",
66
"main": "./dist/index.js",
77
"types": "./dist/index.d.ts",

lib/src/lightning-manager.ts

Lines changed: 189 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ldk from './ldk';
2-
import { err, ok, Result } from './utils/result';
2+
import { Err, err, ok, Result } from './utils/result';
33
import {
44
DefaultLdkDataShape,
55
DefaultTransactionDataShape,
@@ -61,6 +61,7 @@ import {
6161
findOutputsFromRawTxs,
6262
parseData,
6363
promiseTimeout,
64+
sleep,
6465
startParamCheck,
6566
} from './utils/helpers';
6667
import * as bitcoin from 'bitcoinjs-lib';
@@ -135,6 +136,10 @@ class LightningManager {
135136
paymentFailedSubscription: EmitterSubscription | undefined;
136137
paymentSentSubscription: EmitterSubscription | undefined;
137138

139+
private isSyncing: boolean = false;
140+
private forceSync: boolean = false;
141+
private pendingSyncPromises: Array<(result: Result<string>) => void> = [];
142+
138143
constructor() {
139144
// Step 0: Subscribe to all events
140145
ldk.onEvent(EEventTypes.native_log, (line) => {
@@ -474,20 +479,62 @@ class LightningManager {
474479
/**
475480
* Fetches current best block and sends to LDK to update both channelManager and chainMonitor.
476481
* Also watches transactions and outputs for confirmed and unconfirmed transactions and updates LDK.
482+
* @param {number} [timeout] Timeout to set for each async function in this method. Potential overall timeout may be greater.
483+
* @param {number} [retryAttempts] Will attempt to sync LDK a given number of times before giving up.
484+
* @param {boolean} [force] In the event a sync is underway, this will force another sync once the current sync is complete.
477485
* @returns {Promise<Result<string>>}
478486
*/
479-
async syncLdk(): Promise<Result<string>> {
487+
async syncLdk({
488+
timeout = 5000,
489+
retryAttempts = 1,
490+
force = false,
491+
}: {
492+
timeout?: number;
493+
retryAttempts?: number;
494+
force?: boolean;
495+
} = {}): Promise<Result<string>> {
496+
// Check that the getBestBlock method has been provided.
480497
if (!this.getBestBlock) {
481-
return err('No getBestBlock method provided.');
498+
return this.handleSyncError(err('No getBestBlock method provided.'));
482499
}
483-
const bestBlock = await this.getBestBlock();
484-
const height = bestBlock?.height;
485500

486-
//Don't update unnecessarily
501+
if (force && this.isSyncing && !this.forceSync) {
502+
// If syncing is already underway and force is true, set forceSync to true.
503+
this.forceSync = true;
504+
}
505+
if (this.isSyncing) {
506+
// If isSyncing, push to pendingSyncPromises to resolve when the current sync completes.
507+
return new Promise<Result<string>>((resolve) => {
508+
this.pendingSyncPromises.push(resolve);
509+
});
510+
}
511+
this.isSyncing = true;
512+
513+
const bestBlock = await promiseTimeout<THeader>(
514+
timeout,
515+
this.getBestBlock(),
516+
);
517+
if (!bestBlock?.height) {
518+
return this.retrySyncOrReturnError({
519+
timeout,
520+
retryAttempts,
521+
e: err('Unable to get best block in syncLdk method.'),
522+
});
523+
}
524+
const height = bestBlock.height;
525+
526+
// Don't update unnecessarily
487527
if (this.currentBlock.hash !== bestBlock?.hash) {
488-
const syncToTip = await ldk.syncToTip(bestBlock);
528+
const syncToTip = await promiseTimeout<Result<string>>(
529+
timeout,
530+
ldk.syncToTip(bestBlock),
531+
);
489532
if (syncToTip.isErr()) {
490-
return syncToTip;
533+
return this.retrySyncOrReturnError({
534+
timeout,
535+
retryAttempts,
536+
e: syncToTip,
537+
});
491538
}
492539

493540
this.currentBlock = bestBlock;
@@ -496,29 +543,147 @@ class LightningManager {
496543
let channels: TChannel[] = [];
497544
if (this.watchTxs.length > 0) {
498545
// Get fresh array of channels.
499-
const listChannelsResponse = await ldk.listChannels();
546+
const listChannelsResponse = await promiseTimeout<Result<TChannel[]>>(
547+
timeout,
548+
ldk.listChannels(),
549+
);
500550
if (listChannelsResponse.isOk()) {
501551
channels = listChannelsResponse.value;
502552
}
503553
}
504554

505555
// Iterate over watch transactions/outputs and set whether they are confirmed or unconfirmed.
506-
await this.checkWatchTxs(this.watchTxs, channels, bestBlock);
507-
await this.checkWatchOutputs(this.watchOutputs);
508-
await this.checkUnconfirmedTransactions();
556+
const watchTxsRes = await promiseTimeout<Result<string>>(
557+
timeout,
558+
this.checkWatchTxs(this.watchTxs, channels, bestBlock),
559+
);
560+
if (watchTxsRes.isErr()) {
561+
return this.retrySyncOrReturnError({
562+
timeout,
563+
retryAttempts,
564+
e: watchTxsRes,
565+
});
566+
}
567+
const watchOutputsRes = await promiseTimeout<Result<string>>(
568+
timeout,
569+
this.checkWatchOutputs(this.watchOutputs),
570+
);
571+
if (watchOutputsRes.isErr()) {
572+
return this.retrySyncOrReturnError({
573+
timeout,
574+
retryAttempts,
575+
e: watchOutputsRes,
576+
});
577+
}
578+
const unconfirmedTxsRes = await promiseTimeout<Result<string>>(
579+
timeout,
580+
this.checkUnconfirmedTransactions(),
581+
);
582+
if (unconfirmedTxsRes.isErr()) {
583+
return this.retrySyncOrReturnError({
584+
timeout,
585+
retryAttempts,
586+
e: unconfirmedTxsRes,
587+
});
588+
}
589+
590+
this.isSyncing = false;
509591

510-
return ok(`Synced to block ${height}`);
592+
// Handle force sync if needed.
593+
if (this.forceSync) {
594+
return this.handleForceSync({ timeout, retryAttempts });
595+
}
596+
const result = ok(`Synced to block ${height}`);
597+
this.resolveAllPendingSyncPromises(result);
598+
return result;
511599
}
512600

601+
/**
602+
* Resolves all pending sync promises with the provided result.
603+
* @private
604+
* @param {Result<string>} result
605+
* @returns {void}
606+
*/
607+
private resolveAllPendingSyncPromises(result: Result<string>): void {
608+
while (this.pendingSyncPromises.length > 0) {
609+
const resolve = this.pendingSyncPromises.shift();
610+
if (resolve) {
611+
resolve(result);
612+
}
613+
}
614+
}
615+
616+
/**
617+
* Sets forceSync to false and re-runs the sync method.
618+
* @private
619+
* @param {number} timeout
620+
* @param {number} retryAttempts
621+
* @returns {Promise<Result<string>>}
622+
*/
623+
private handleForceSync = async ({
624+
timeout,
625+
retryAttempts,
626+
}: {
627+
timeout: number;
628+
retryAttempts: number;
629+
}): Promise<Result<string>> => {
630+
this.forceSync = false;
631+
return this.syncLdk({
632+
timeout,
633+
retryAttempts,
634+
});
635+
};
636+
637+
/**
638+
* Attempts to retry the syncLdk method. Otherwise, the error gets passed to handleSyncError.
639+
* @private
640+
* @param {number} [timeout]
641+
* @param {number} retryAttempts
642+
* @param {Err<string>} e
643+
* @returns {Promise<Result<string>>}
644+
*/
645+
private retrySyncOrReturnError = async ({
646+
timeout = 5000,
647+
retryAttempts,
648+
e,
649+
}: {
650+
timeout?: number;
651+
retryAttempts: number;
652+
e: Err<string>;
653+
}): Promise<Result<string>> => {
654+
this.isSyncing = false;
655+
if (retryAttempts > 0) {
656+
await sleep();
657+
return this.syncLdk({
658+
timeout,
659+
retryAttempts: retryAttempts - 1,
660+
});
661+
} else {
662+
return this.handleSyncError(e);
663+
}
664+
};
665+
666+
/**
667+
* Sets isSyncing & forceSync to false and returns error.
668+
* @private
669+
* @param {Err<string>} e
670+
* @returns {Promise<Result<string>>}
671+
*/
672+
private handleSyncError = (e: Err<string>): Result<string> => {
673+
this.isSyncing = false;
674+
this.forceSync = false;
675+
this.resolveAllPendingSyncPromises(e);
676+
return e;
677+
};
678+
513679
checkWatchTxs = async (
514680
watchTxs: TRegisterTxEvent[],
515681
channels: TChannel[],
516682
bestBlock: THeader,
517-
): Promise<void> => {
683+
): Promise<Result<string>> => {
518684
const height = bestBlock?.height;
519685
if (!height) {
520-
console.log('No height provided');
521-
return;
686+
return err('No height provided');
522687
}
523688
await Promise.all(
524689
watchTxs.map(async (watchTxData) => {
@@ -533,7 +698,7 @@ class LightningManager {
533698
//Watch TX was never confirmed so there's no need to unconfirm it.
534699
return;
535700
}
536-
if (!txData.transaction) {
701+
if (!txData?.transaction) {
537702
console.log(
538703
'Unable to retrieve transaction data from the getTransactionData method.',
539704
);
@@ -564,11 +729,12 @@ class LightningManager {
564729
}
565730
}),
566731
);
732+
return ok('Watch transactions checked');
567733
};
568734

569735
checkWatchOutputs = async (
570736
watchOutputs: TRegisterOutputEvent[],
571-
): Promise<void> => {
737+
): Promise<Result<string>> => {
572738
await Promise.all(
573739
watchOutputs.map(async ({ index, script_pubkey }) => {
574740
const transactions = await this.getScriptPubKeyHistory(script_pubkey);
@@ -630,6 +796,7 @@ class LightningManager {
630796
);
631797
}),
632798
);
799+
return ok('Watch outputs checked');
633800
};
634801

635802
/**
@@ -1127,7 +1294,7 @@ class LightningManager {
11271294
}
11281295
};
11291296

1130-
checkUnconfirmedTransactions = async (): Promise<void> => {
1297+
checkUnconfirmedTransactions = async (): Promise<Result<string>> => {
11311298
let needsToSync = false;
11321299
let newUnconfirmedTxs: TLdkUnconfirmedTransactions = [];
11331300
await Promise.all(
@@ -1169,8 +1336,9 @@ class LightningManager {
11691336

11701337
await this.updateUnconfirmedTxs(newUnconfirmedTxs);
11711338
if (needsToSync) {
1172-
await this.syncLdk();
1339+
await this.syncLdk({ force: true });
11731340
}
1341+
return ok('Unconfirmed transactions checked');
11741342
};
11751343

11761344
/**
@@ -1821,7 +1989,7 @@ class LightningManager {
18211989
): Promise<void> {
18221990
// Payment Received/Invoice Paid.
18231991
console.log(`onChannelManagerPaymentClaimed: ${JSON.stringify(res)}`);
1824-
this.syncLdk().then();
1992+
this.syncLdk({ force: true }).then();
18251993
}
18261994

18271995
/**

lib/src/utils/helpers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,14 @@ export const findOutputsFromRawTxs = (
296296

297297
return result;
298298
};
299+
300+
/**
301+
* Pauses execution of a function.
302+
* @param {number} ms The time to wait in milliseconds.
303+
* @returns {Promise<void>}
304+
*/
305+
export const sleep = (ms = 1000): Promise<void> => {
306+
return new Promise((resolve) => {
307+
setTimeout(resolve, ms);
308+
});
309+
};

0 commit comments

Comments
 (0)