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

Commit a9519b0

Browse files
authored
Merge pull request #139 from synonymdev/stale-backup-recovery
Option to force close channels on startup
2 parents 9652689 + bf3d70f commit a9519b0

11 files changed

Lines changed: 171 additions & 8 deletions

File tree

example/App.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ import lm, {
3030
TChannelUpdate,
3131
} from '@synonymdev/react-native-ldk';
3232
import { peers } from './utils/constants';
33-
import { createNewAccount, getAddress } from './utils/helpers';
33+
import {
34+
createNewAccount,
35+
getAddress,
36+
simulateStaleRestore,
37+
} from './utils/helpers';
3438
import RNFS from 'react-native-fs';
3539

3640
let logSubscription: EmitterSubscription | undefined;
@@ -615,6 +619,17 @@ const App = (): ReactElement => {
615619
);
616620
}}
617621
/>
622+
<Button
623+
title={'Simulate stale backup restore'}
624+
onPress={async (): Promise<void> => {
625+
try {
626+
await simulateStaleRestore((msg) => setMessage(msg));
627+
} catch (e) {
628+
setMessage(e.message);
629+
return;
630+
}
631+
}}
632+
/>
618633
<Button
619634
title={'Restart node'}
620635
onPress={async (): Promise<void> => {

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.98):
305+
- react-native-ldk (0.0.99):
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: 0b2b9c3e57c80b19d94ec306f49765b29530758e
596+
react-native-ldk: 175104646d32a4f52d82299c6981ad4bf91ad603
597597
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
598598
react-native-tcp-socket: c1b7297619616b4c9caae6889bcb0aba78086989
599599
React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595

example/ldk/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ export const syncLdk = async (): Promise<Result<string>> => {
9595
* 3. Adds/Connects saved peers from storage. (Note: Not needed as LDK handles this automatically once a peer has been added successfully. Only used to make example app easier to test.)
9696
* 4. Syncs LDK.
9797
*/
98-
export const setupLdk = async (): Promise<Result<string>> => {
98+
export const setupLdk = async (
99+
forceCloseAllChannels = false,
100+
): Promise<Result<string>> => {
99101
try {
100102
await ldk.stop();
101103
const account = await getAccount();
@@ -121,6 +123,9 @@ export const setupLdk = async (): Promise<Result<string>> => {
121123
getTransactionPosition,
122124
broadcastTransaction,
123125
network: ldkNetwork(selectedNetwork),
126+
forceCloseOnStartup: forceCloseAllChannels
127+
? { forceClose: true, broadcastLatestTx: false }
128+
: undefined,
124129
});
125130

126131
if (lmStart.isErr()) {
@@ -323,6 +328,7 @@ export const backupAccount = async (
323328
*/
324329
export const importAccount = async (
325330
backup: string | TAccountBackup,
331+
forceCloseAllChannels = false,
326332
): Promise<Result<TAccount>> => {
327333
const importResponse = await lm.importAccount({
328334
backup,
@@ -333,7 +339,7 @@ export const importAccount = async (
333339
}
334340
await setAccount(importResponse.value);
335341
await setItem(EAccount.currentAccountKey, importResponse.value.name);
336-
await setupLdk();
342+
await setupLdk(forceCloseAllChannels);
337343
await syncLdk();
338344
return ok(importResponse.value);
339345
};

example/utils/helpers.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import Keychain from 'react-native-keychain';
2-
import { TAccount, TAvailableNetworks } from '@synonymdev/react-native-ldk';
3-
import { getItem, setItem } from '../ldk';
2+
import {
3+
EEventTypes,
4+
TAccount,
5+
TAvailableNetworks,
6+
} from '@synonymdev/react-native-ldk';
7+
import { backupAccount, getItem, importAccount, setItem } from '../ldk';
48
import { EAccount } from './types';
59
import { err, ok, Result } from './result';
610
import { randomBytes } from 'react-native-randombytes';
@@ -10,6 +14,8 @@ import RNFS from 'react-native-fs';
1014
import * as bip32 from 'bip32';
1115
import * as bip39 from 'bip39';
1216
import { ENetworks } from '@synonymdev/react-native-ldk/dist/utils/types';
17+
import ldk from '@synonymdev/react-native-ldk/dist/ldk';
18+
import Clipboard from '@react-native-clipboard/clipboard';
1319

1420
/**
1521
* Use Keychain to save LDK name & seed.
@@ -205,3 +211,78 @@ export const ldkNetwork = (network: TAvailableNetworks): ENetworks => {
205211
return ENetworks.mainnet;
206212
}
207213
};
214+
215+
export const simulateStaleRestore = async (
216+
onUpdate: (string) => void,
217+
): Promise<void> => {
218+
const channels = await ldk.listChannels();
219+
if (channels.isErr()) {
220+
throw channels.error;
221+
}
222+
if (channels.value.filter((c) => c.is_usable).length === 0) {
223+
throw new Error('No usable channels. Open a channel first.');
224+
}
225+
226+
onUpdate('Backing up...');
227+
const backupResponse = await backupAccount();
228+
if (backupResponse.isErr()) {
229+
throw backupResponse.error;
230+
}
231+
232+
const timeoutSeconds = 30;
233+
const invoice = await ldk.createPaymentRequest({
234+
amountSats: 12,
235+
description: 'crash test',
236+
expiryDeltaSeconds: timeoutSeconds,
237+
});
238+
if (invoice.isErr()) {
239+
throw invoice.error;
240+
}
241+
242+
let paymentClaimed = false;
243+
let paymentSubscription = ldk.onEvent(
244+
EEventTypes.channel_manager_payment_claimed,
245+
() => (paymentClaimed = true),
246+
);
247+
248+
Clipboard.setString(invoice.value.to_str);
249+
250+
//Keep checking if we got the payment
251+
for (let i = 0; i < timeoutSeconds; i++) {
252+
onUpdate(
253+
`Please pay invoice in clipboard to continue (${timeoutSeconds - i})...`,
254+
);
255+
256+
if (paymentClaimed) {
257+
onUpdate('Payment claimed! Testing stale restore...');
258+
break;
259+
}
260+
261+
await new Promise((resolve) => setTimeout(resolve, 1000));
262+
}
263+
264+
paymentSubscription.remove();
265+
266+
if (!paymentClaimed) {
267+
throw new Error('No payment claimed. Timeout out.');
268+
}
269+
270+
onUpdate('Importing stale backup and force closing all channels...');
271+
272+
await new Promise((resolve) => setTimeout(resolve, 2500));
273+
274+
await ldk.stop();
275+
const forceCloseAllChannels = true; //To test the crash restore set to false
276+
const importResponse = await importAccount(
277+
backupResponse.value,
278+
forceCloseAllChannels,
279+
);
280+
if (importResponse.isErr()) {
281+
throw importResponse.error;
282+
}
283+
284+
await new Promise((resolve) => setTimeout(resolve, 2500));
285+
onUpdate(
286+
"If this didn't crash and you can see your claimable balance, you're good!",
287+
);
288+
};

lib/android/src/main/java/com/reactnativeldk/LdkModule.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,19 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
612612
handleResolve(promise, LdkCallbackResponses.close_channel_success)
613613
}
614614

615+
@ReactMethod
616+
fun forceCloseAllChannels(broadcastLatestTx: Boolean, promise: Promise) {
617+
channelManager ?: return handleReject(promise, LdkErrors.init_channel_manager)
618+
619+
if (broadcastLatestTx) {
620+
channelManager!!.force_close_all_channels_broadcasting_latest_txn()
621+
} else {
622+
channelManager!!.force_close_all_channels_without_broadcasting_txn()
623+
}
624+
625+
handleResolve(promise, LdkCallbackResponses.close_channel_success)
626+
}
627+
615628
@ReactMethod
616629
fun spendOutputs(descriptorsSerialized: ReadableArray, outputs: ReadableArray, changeDestinationScript: String, feeRate: Double, promise: Promise) {
617630
keysManager ?: return handleReject(promise, LdkErrors.init_keys_manager)

lib/ios/Ldk.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ @interface RCT_EXTERN_MODULE(Ldk, NSObject)
6969
force:(BOOL *)force
7070
resolve:(RCTPromiseResolveBlock)resolve
7171
reject:(RCTPromiseRejectBlock)reject)
72+
RCT_EXTERN_METHOD(forceCloseAllChannels:(BOOL *)broadcastLatestTx
73+
resolve:(RCTPromiseResolveBlock)resolve
74+
reject:(RCTPromiseRejectBlock)reject)
7275
RCT_EXTERN_METHOD(spendOutputs:(NSArray *)descriptorsSerialized
7376
outputs:(NSArray *)outputs
7477
changeDestinationScript:(NSString *)changeDestinationScript

lib/ios/Ldk.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,21 @@ class Ldk: NSObject {
654654
return handleResolve(resolve, .close_channel_success)
655655
}
656656

657+
@objc
658+
func forceCloseAllChannels(_ broadcastLatestTx: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
659+
guard let channelManager = channelManager else {
660+
return handleReject(reject, .init_channel_manager)
661+
}
662+
663+
if broadcastLatestTx {
664+
channelManager.forceCloseAllChannelsBroadcastingLatestTxn()
665+
} else {
666+
channelManager.forceCloseAllChannelsWithoutBroadcastingTxn()
667+
}
668+
669+
return handleResolve(resolve, .close_channel_success)
670+
}
671+
657672
@objc
658673
func spendOutputs(_ descriptorsSerialized: NSArray, outputs: NSArray, changeDestinationScript: NSString, feeRate: NSInteger, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
659674
guard let keysManager = keysManager else {

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.98",
4+
"version": "0.0.99",
55
"description": "React Native wrapper for LDK",
66
"main": "./dist/index.js",
77
"types": "./dist/index.d.ts",

lib/src/ldk.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,24 @@ class LDK {
436436
}
437437
}
438438

439+
/**
440+
* Force close all channels
441+
* @param broadcastLatestTx
442+
* @returns {Promise<Err<unknown> | Ok<Ok<string> | Err<string>>>}
443+
*/
444+
async forceCloseAllChannels(
445+
broadcastLatestTx: boolean,
446+
): Promise<Result<string>> {
447+
try {
448+
const res = await NativeLDK.forceCloseAllChannels(broadcastLatestTx);
449+
this.writeDebugToLog('forceCloseAllChannels');
450+
return ok(res);
451+
} catch (e) {
452+
this.writeErrorToLog('forceCloseAllChannels', e);
453+
return err(e);
454+
}
455+
}
456+
439457
/**
440458
* Use LDK key manager to spend spendable outputs
441459
* https://docs.rs/lightning/latest/lightning/chain/keysinterface/struct.KeysManager.html#method.spend_spendable_outputs

lib/src/lightning-manager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ class LightningManager {
245245
broadcastTransaction,
246246
network,
247247
rapidGossipSyncUrl = 'https://rapidsync.lightningdevkit.org/snapshot/',
248+
forceCloseOnStartup,
248249
userConfig = defaultUserConfig,
249250
}: TLdkStart): Promise<Result<string>> {
250251
if (!account) {
@@ -426,6 +427,11 @@ class LightningManager {
426427
// Set fee estimates
427428
await this.setFees();
428429

430+
//Force close all channels on startup. Likely to recover funds after restoring from a stale backup.
431+
if (forceCloseOnStartup && forceCloseOnStartup.forceClose) {
432+
await ldk.forceCloseAllChannels(forceCloseOnStartup.broadcastLatestTx);
433+
}
434+
429435
// Add cached peers
430436
await this.addPeers();
431437

0 commit comments

Comments
 (0)