Repository: ZeusLN/zeus
Pull request #2931: Backends: allow override of default timeout #2887
Author: login: ajaysehwal
Labels: name: Core Lightning
description: Issues specific to Core Lightning
name: LND
description: Issues specific to LND nodes
name: Networking
name: LND REST
name: CLNRest
Created: 2025-03-26T10:35:40Z | Merged: 2025-04-21T14:17:48Z

# Description

Relates to issue: **ZEUS-0000**
Backends: allow override of default timeout #2887


_Please enter a description and screenshots, if appropriate, of the work covered in this PR_
Changes Made:
Updated CLNREST and LND  in backends to include an optional timeout parameter in the request for allow override of default timeout

This pull request is categorized as a:

- [ ] New feature
- [x] Bug fix
- [x] Code refactor
- [ ] Configuration change
- [ ] Locales update
- [ ] Quality assurance
- [ ] Other

## Checklist
- [x] I’ve run `yarn run tsc` and made sure my code compiles correctly
- [x] I’ve run `yarn run lint` and made sure my code didn’t contain any problematic patterns
- [x] I’ve run `yarn run prettier` and made sure my code is formatted correctly
- [x] I’ve run `yarn run test` and made sure all of the tests pass

## Testing

If you modified or added a utility file, did you add new unit tests?

- [ ] No, I’m a fool
- [ ] Yes
- [x] N/A

I have tested this PR on the following platforms (please specify OS version and phone model/VM):

- [ ] Android
- [x] iOS

I have tested this PR with the following types of nodes (please specify node version and API version where appropriate):

- [ ] Embedded LND
- [x] LND (REST)
- [x] LND (Lightning Node Connect)
- [x] Core Lightning (CLNRest)
- [ ] Nostr Wallet Connect
- [ ] LndHub

### Locales
- [ ] I’ve added new locale text that requires translations
- [ ] I’m aware that new translations should be made on the ZEUS [Transfix page](https://app.transifex.com/ZeusLN/zeus/) and not directly to this repo

### Third Party Dependencies and Packages

- [ ] Contributors will need to run `yarn` after this PR is merged in
- [ ] 3rd party dependencies have been modified:
    * verify that `package.json` and `yarn.lock` have been properly updated
    * verify that dependencies are installed for both iOS and Android platforms

### Other:

- [ ] Changes were made that require an update to the README
- [ ] Changes were made that require an update to onboarding



Diff:
diff --git a/backends/CLNRest.ts b/backends/CLNRest.ts
index 9367ca2694..5c18d50d7e 100644
--- a/backends/CLNRest.ts
+++ b/backends/CLNRest.ts
@@ -18,6 +18,7 @@ import { doTorRequest, RequestMethod } from '../utils/TorUtils';
 const calls = new Map<string, Promise<any>>();
 
 export default class CLNRest {
+    private defaultTimeout: number = 30000;
     getHeaders = (rune: string): any => {
         return {
             Rune: rune
@@ -49,7 +50,8 @@ export default class CLNRest {
         method: any,
         data?: any,
         certVerification?: boolean,
-        useTor?: boolean
+        useTor?: boolean,
+        timeout?: number
     ) => {
         // use body data as an identifier too, we don't want to cancel when we
         // are making multiples calls to get all the node names, for example
@@ -74,7 +76,10 @@ export default class CLNRest {
             );
         } else {
             const timeoutPromise = new Promise((_, reject) => {
-                setTimeout(() => reject(new Error('Request timeout')), 30000);
+                setTimeout(
+                    () => reject(new Error('Request timeout')),
+                    timeout || this.defaultTimeout
+                );
             });
 
             const fetchPromise = ReactNativeBlobUtil.config({
@@ -122,7 +127,6 @@ export default class CLNRest {
                 timeoutPromise
             ]).catch((error) => {
                 calls.delete(id);
-                console.log('Request timed out for:', url);
                 throw error;
             });
 
@@ -132,7 +136,13 @@ export default class CLNRest {
         return await calls.get(id);
     };
 
-    request = (route: string, method: string, data?: any, params?: any) => {
+    request = (
+        route: string,
+        method: string,
+        data?: any,
+        params?: any,
+        timeout?: number
+    ) => {
         const { host, port, rune, certVerification, enableTor } = settingsStore;
 
         if (params) {
@@ -152,7 +162,8 @@ export default class CLNRest {
             method,
             data,
             certVerification,
-            enableTor
+            enableTor,
+            timeout
         );
     };
 
@@ -176,8 +187,8 @@ export default class CLNRest {
         return `${baseUrl}${route}`;
     };
 
-    postRequest = (route: string, data?: any) =>
-        this.request(route, 'post', data);
+    postRequest = (route: string, data?: any, timeout?: number) =>
+        this.request(route, 'post', data, null, timeout);
 
     getNode = (data: any) =>
         this.postRequest('/v1/listnodes', { id: data.id }).then((res) => {
@@ -310,19 +321,27 @@ export default class CLNRest {
         });
 
     payLightningInvoice = (data: any) =>
-        this.postRequest('/v1/pay', {
-            bolt11: data.payment_request,
-            amount_msat: Number(data.amt && data.amt * 1000),
-            maxfeepercent: data.max_fee_percent,
-            retry_for: data.timeout_seconds
-        });
+        this.postRequest(
+            '/v1/pay',
+            {
+                bolt11: data.payment_request,
+                amount_msat: Number(data.amt && data.amt * 1000),
+                maxfeepercent: data.max_fee_percent,
+                retry_for: data.timeout_seconds
+            },
+            data.timeout_seconds * 1000
+        );
     sendKeysend = (data: any) => {
-        return this.postRequest('/v1/keysend', {
-            destination: data.pubkey,
-            amount_msat: Number(data.amt && data.amt * 1000),
-            maxfeepercent: data.max_fee_percent,
-            retry_for: data.timeout_seconds
-        });
+        return this.postRequest(
+            '/v1/keysend',
+            {
+                destination: data.pubkey,
+                amount_msat: Number(data.amt && data.amt * 1000),
+                maxfeepercent: data.max_fee_percent,
+                retry_for: data.timeout_seconds
+            },
+            data.timeout_seconds * 1000
+        );
     };
     closeChannel = (urlParams?: Array<string>) => {
         const request = {
diff --git a/backends/LND.ts b/backends/LND.ts
index 8b5f9db98d..f3c661e258 100644
--- a/backends/LND.ts
+++ b/backends/LND.ts
@@ -19,7 +19,7 @@ const calls = new Map<string, Promise<any>>();
 
 export default class LND {
     torSocksPort?: number = undefined;
-
+    private defaultTimeout: number = 30000;
     clearCachedCalls = () => calls.clear();
 
     restReq = async (
@@ -28,7 +28,8 @@ export default class LND {
         method: any,
         data?: any,
         certVerification?: boolean,
-        useTor?: boolean
+        useTor?: boolean,
+        timeout?: number
     ) => {
         // use body data as an identifier too, we don't want to cancel when we
         // are making multiples calls to get all the node names, for example
@@ -53,7 +54,10 @@ export default class LND {
             );
         } else {
             const timeoutPromise = new Promise((_, reject) => {
-                setTimeout(() => reject(new Error('Request timeout')), 30000);
+                setTimeout(
+                    () => reject(new Error('Request timeout')),
+                    timeout || this.defaultTimeout
+                );
             });
 
             const fetchPromise = ReactNativeBlobUtil.config({
@@ -196,7 +200,13 @@ export default class LND {
         return `${baseUrl}${route}`;
     };
 
-    request = (route: string, method: string, data?: any, params?: any) => {
+    request = (
+        route: string,
+        method: string,
+        data?: any,
+        params?: any,
+        timeout?: number
+    ) => {
         const {
             host,
             lndhubUrl,
@@ -223,14 +233,15 @@ export default class LND {
             method,
             data,
             certVerification,
-            enableTor
+            enableTor,
+            timeout
         );
     };
 
     getRequest = (route: string, data?: any) =>
         this.request(route, 'get', null, data);
-    postRequest = (route: string, data?: any) =>
-        this.request(route, 'post', data);
+    postRequest = (route: string, data?: any, timeout?: number) =>
+        this.request(route, 'post', data, null, timeout);
     deleteRequest = (route: string) => this.request(route, 'delete', null);
 
     getTransactions = (data: any) =>
@@ -485,10 +496,14 @@ export default class LND {
         };
 
         const call = () =>
-            this.postRequest('/v2/router/send', {
-                ...data,
-                allow_self_payment: true
-            });
+            this.postRequest(
+                '/v2/router/send',
+                {
+                    ...data,
+                    allow_self_payment: true
+                },
+                data.timeout_seconds * 1000
+            );
 
         const result: any = await Promise.race([
             forcedTimeout((data.timeout_seconds + 1) * 1000, {


Reviews:
user: login: kaloudis
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T18:31:37Z
user: login: ajaysehwal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T03:54:02Z
user: login: kaloudis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:21:20Z
user: login: ajaysehwal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T03:07:18Z
user: login: kaloudis
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T03:26:13Z
user: login: ajaysehwal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T05:14:33Z
user: login: shubhamkmr04
body: @ajaysehwal please fix the commit structure. we have 3 commits with almost similar message. you can probably keep just 2 commits, one for each backend.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T05:47:43Z
user: login: shubhamkmr04
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T07:10:37Z
user: login: shubhamkmr04
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T07:13:23Z
user: login: shubhamkmr04
body: The changes works fine for `payLightningInvoice` in both backends.  
But for `sendKeysend`, it's not wired up from the frontend I guess
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T07:28:12Z
user: login: ajaysehwal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T08:05:31Z
user: login: ajaysehwal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T08:08:50Z
user: login: shubhamkmr04
body: tACK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:40:43Z
user: login: shubhamkmr04
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T04:50:38Z
user: login: ajaysehwal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T04:52:22Z
user: login: shubhamkmr04
body: tACK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T06:52:56Z
user: login: kaloudis
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T14:17:43Z

Review comments:
diff_hunk: @@ -176,8 +187,8 @@ export default class CLNRest {
         return `${baseUrl}${route}`;
     };
 
-    postRequest = (route: string, data?: any) =>
-        this.request(route, 'post', data);
+    postRequest = (route: string, data?: any, timeout?: number) =>
+        this.request(route, 'post', data, undefined, timeout);
path: backends/CLNRest.ts
user: login: kaloudis
body: should we perhaps destructure the params on the request func?
created_at: 2025-03-26T18:30:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,8 +187,8 @@ export default class CLNRest {
         return `${baseUrl}${route}`;
     };
 
-    postRequest = (route: string, data?: any) =>
-        this.request(route, 'post', data);
+    postRequest = (route: string, data?: any, timeout?: number) =>
+        this.request(route, 'post', data, undefined, timeout);
path: backends/CLNRest.ts
user: login: ajaysehwal
body: yes , we can do 
created_at: 2025-03-27T03:54:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,8 +187,8 @@ export default class CLNRest {
         return `${baseUrl}${route}`;
     };
 
-    postRequest = (route: string, data?: any) =>
-        this.request(route, 'post', data);
+    postRequest = (route: string, data?: any, timeout?: number) =>
+        this.request(route, 'post', data, undefined, timeout);
path: backends/CLNRest.ts
user: login: kaloudis
body: Instead of destructuring everything, perhaps we just have the four param be an options object where you can specify tor and timeout. Would be a bit cleaner and require less overall change to the PR
created_at: 2025-03-27T17:21:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,8 +187,8 @@ export default class CLNRest {
         return `${baseUrl}${route}`;
     };
 
-    postRequest = (route: string, data?: any) =>
-        this.request(route, 'post', data);
+    postRequest = (route: string, data?: any, timeout?: number) =>
+        this.request(route, 'post', data, undefined, timeout);
path: backends/CLNRest.ts
user: login: ajaysehwal
body: thank you for your suggestion! I agree that using an options object with tor and timeout would make the code cleaner and require fewer changes to the PR. I’ll update it accordingly to keep things minimal and tidy.
created_at: 2025-03-28T03:07:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -132,7 +138,19 @@ export default class CLNRest {
         return await calls.get(id);
     };
 
-    request = (route: string, method: string, data?: any, params?: any) => {
+    request = ({
+        route,
+        method,
+        data,
+        params,
+        timeout
+    }: {
+        route: string;
+        method: string;
+        data?: any;
+        params?: any;
+        timeout?: number;
+    }) => {
path: backends/CLNRest.ts
user: login: kaloudis
body: let's undo all the destructuring on the request methods. Too many unnecessary changes
created_at: 2025-03-28T03:26:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -132,7 +138,19 @@ export default class CLNRest {
         return await calls.get(id);
     };
 
-    request = (route: string, method: string, data?: any, params?: any) => {
+    request = ({
+        route,
+        method,
+        data,
+        params,
+        timeout
+    }: {
+        route: string;
+        method: string;
+        data?: any;
+        params?: any;
+        timeout?: number;
+    }) => {
path: backends/CLNRest.ts
user: login: ajaysehwal
body: sure

created_at: 2025-03-28T05:14:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,8 +90,8 @@ export default class CLNRest {
                     calls.delete(id);
                     if (response.info().status < 300) {
                         // handle ws responses
-                        if (response.data.includes('\n')) {
-                            const split = response.data.split('\n');
+                        if (response.data.includes('\\n')) {
path: backends/CLNRest.ts
user: login: shubhamkmr04
body: why is this change necessary?
created_at: 2025-04-14T07:10:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,8 +187,8 @@ export default class CLNRest {
         return `${baseUrl}${route}`;
     };
 
-    postRequest = (route: string, data?: any) =>
-        this.request(route, 'post', data);
+    postRequest = (route: string, data?: any, timeout?: number) =>
+        this.request(route, 'post', data, null, timeout);
path: backends/CLNRest.ts
user: login: shubhamkmr04
body: why the `null` here
created_at: 2025-04-14T07:13:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,8 +187,8 @@ export default class CLNRest {
         return `${baseUrl}${route}`;
     };
 
-    postRequest = (route: string, data?: any) =>
-        this.request(route, 'post', data);
+    postRequest = (route: string, data?: any, timeout?: number) =>
+        this.request(route, 'post', data, null, timeout);
path: backends/CLNRest.ts
user: login: ajaysehwal
body: @shubhamkmr04 Currently, I'm using null for params. I initially destructured postRequest, but it felt messy due to the extensive backend changes. So, I opted for null, though undefined could work too. If you have a better approach to handle this issue more effectively, please share your suggestions!
created_at: 2025-04-14T08:05:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,8 +90,8 @@ export default class CLNRest {
                     calls.delete(id);
                     if (response.info().status < 300) {
                         // handle ws responses
-                        if (response.data.includes('\n')) {
-                            const split = response.data.split('\n');
+                        if (response.data.includes('\\n')) {
path: backends/CLNRest.ts
user: login: ajaysehwal
body: Oh, I am really sorry for this unnecessary changes, I will fix it now 
created_at: 2025-04-14T08:08:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,8 +68,8 @@ export default class LND {
                     calls.delete(id);
                     if (response.info().status < 300) {
                         // handle ws responses
-                        if (response.data.includes('\n')) {
-                            const split = response.data.split('\n');
+                        if (response.data.includes('\\n')) {
path: backends/LND.ts
user: login: shubhamkmr04
body: let's undo these changes here also
created_at: 2025-04-15T04:50:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,8 +68,8 @@ export default class LND {
                     calls.delete(id);
                     if (response.info().status < 300) {
                         // handle ws responses
-                        if (response.data.includes('\n')) {
-                            const split = response.data.split('\n');
+                        if (response.data.includes('\\n')) {
path: backends/LND.ts
user: login: ajaysehwal
body: sure 

created_at: 2025-04-15T04:52:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: kaloudis
created_at: 2025-03-26T18:31:28Z
body: This is on the right track but we'd also want this on LND REST and other applicable backends.

Did you really test this with a CLN node?
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-03-27T03:23:16Z
body: Hi @kaloudis, first of all, thanks for your feedback. I am checking out this issue again deeply and fixing all other applicable backends
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-03-27T15:16:33Z
body: Hi @kaloudis, could you please review my pull request again? I've fully tested the refactored code in the test environment. Let me know if I need to update anything else or if you have any suggestions for improvement. Thanks!
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-13T05:19:31Z
body: Hi @kaloudis , Is there anything else I need to update in this PR
author_association: CONTRIBUTOR
user: login: kaloudis
created_at: 2025-04-13T21:21:29Z
body: We need to do some testing.

In the meantime link the related issue and fill out the PR description, please.
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-14T06:00:38Z
body: > @ajaysehwal please fix the commit structure. we have 3 commits with almost similar message. you can probably keep just 2 commits, one for each backend.

Ok I will do it now 


author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-14T06:12:57Z
body: @shubhamkmr04, can you please review again my PR, or do I need to change something else 
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-14T08:18:44Z
body: > The changes works fine for `payLightningInvoice` in both backends. But for `sendKeysend`, it's not wired up from the frontend I guess

@shubhamkmr04  So What are your thoughts? Should I remove the changes for sendKeysend or explore another approach? but I tested the changes on both devices, and they work fine.
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-15T04:25:31Z
body: > tACK

Hi @shubhamkmr04 , Could you please explain what you meant by 'tACK'?  I'd love to understand better!
author_association: CONTRIBUTOR
user: login: shubhamkmr04
created_at: 2025-04-15T04:54:00Z
body: > > tACK
> 
> Hi @shubhamkmr04 , Could you please explain what you meant by 'tACK'? I'd love to understand better!

That means I have acknowledged the changes and tested it .
author_association: CONTRIBUTOR
user: login: shubhamkmr04
created_at: 2025-04-15T04:56:04Z
body: @ajaysehwal we don't need this extra 2nd commit 
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-15T04:57:49Z
body: > > > tACK
> > 
> > 
> > Hi @shubhamkmr04, Could you please explain what you meant by 'tACK'? I'd love to understand better!
> 
> That means I have acknowledged the changes and tested it.

Oh, I got it, and thank you
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-15T04:58:26Z
body: > we don't need this extra 2nd commit

Ok then i will change structure of PR again

author_association: CONTRIBUTOR
user: login: shubhamkmr04
created_at: 2025-04-15T05:01:53Z
body: > > we don't need this extra 2nd commit
> 
> Ok then i will change structure of PR again

you need to reset 2 last commits and force push with only one commit. let me know if you need help with that
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-15T05:04:51Z
body: > > > we don't need this extra 2nd commit
> > 
> > 
> > Ok then i will change structure of PR again
> 
> you need to reset 2 last commits and force push with only one commit. let me know if you need help with that

can you please review again , or i need to change something else 
author_association: CONTRIBUTOR
user: login: shubhamkmr04
created_at: 2025-04-15T05:07:40Z
body: > can you please review again , or i need to change something else

looks good now
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-16T11:44:34Z
body: Hi @kaloudis, could you please update the status of this PR? It still indicates that changes are requested. Thank you!
author_association: CONTRIBUTOR
user: login: ajaysehwal
created_at: 2025-04-19T15:51:10Z
body: Hi @kaloudis, for this PR, I believe @shubhamkmr04 has already reviewed it. Could you please check the conversation again?
author_association: CONTRIBUTOR