diff --git a/App.tsx b/App.tsx
index 3b8815acc..1a33d4e0e 100644
--- a/App.tsx
+++ b/App.tsx
@@ -138,6 +138,12 @@ import LspExplanationWrappedInvoices from './views/Explanations/LspExplanationWr
import LspExplanationOverview from './views/Explanations/LspExplanationOverview';
import RestoreChannelBackups from './views/Settings/EmbeddedNode/RestoreChannelBackups';
+// LSPS1
+import LSPS1 from './views/Settings/LSPS1/index';
+import LSPS1Settings from './views/Settings/LSPS1/Settings';
+import OrdersPane from './views/Settings/LSPS1/OrdersPane';
+import Orders from './views/Settings/LSPS1/Order';
+
import RawTxHex from './views/RawTxHex';
import CustodialWalletWarning from './views/Settings/CustodialWalletWarning';
@@ -785,6 +791,24 @@ export default class App extends React.PureComponent {
name="TxHex"
component={TxHex}
/>
+
+
+
+
>
diff --git a/assets/images/SVG/OlympusAnimated.svg b/assets/images/SVG/OlympusAnimated.svg
new file mode 100644
index 000000000..b9d675f1c
--- /dev/null
+++ b/assets/images/SVG/OlympusAnimated.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/assets/images/SVG/order-list.svg b/assets/images/SVG/order-list.svg
new file mode 100644
index 000000000..f33f118ab
--- /dev/null
+++ b/assets/images/SVG/order-list.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/backends/CLightningREST.ts b/backends/CLightningREST.ts
index c0591031a..13cef2a81 100644
--- a/backends/CLightningREST.ts
+++ b/backends/CLightningREST.ts
@@ -275,4 +275,6 @@ export default class CLightningREST extends LND {
supportsOnchainBatching = () => false;
supportsChannelBatching = () => false;
isLNDBased = () => false;
+ supportsLSPS1customMessage = () => false;
+ supportsLSPS1rest = () => true;
}
diff --git a/backends/Eclair.ts b/backends/Eclair.ts
index 313fb39b2..01f26ae46 100644
--- a/backends/Eclair.ts
+++ b/backends/Eclair.ts
@@ -507,6 +507,8 @@ export default class Eclair {
supportsOnchainBatching = () => false;
supportsChannelBatching = () => false;
isLNDBased = () => false;
+ supportsLSPS1customMessage = () => false;
+ supportsLSPS1rest = () => true;
}
const mapInvoice =
diff --git a/backends/EmbeddedLND.ts b/backends/EmbeddedLND.ts
index a01702ebc..9ee0e7f55 100644
--- a/backends/EmbeddedLND.ts
+++ b/backends/EmbeddedLND.ts
@@ -23,7 +23,9 @@ const {
getNetworkInfo,
queryRoutes,
lookupInvoice,
- fundingStateStep
+ fundingStateStep,
+ sendCustomMessage,
+ subscribeCustomMessages
} = lndMobile.index;
const {
channelBalance,
@@ -68,6 +70,9 @@ export default class EmbeddedLND extends LND {
data.spend_unconfirmed,
data.send_all
);
+ sendCustomMessage = async (data: any) =>
+ await sendCustomMessage(data.peer, data.type, data.data);
+ subscribeCustomMessages = async () => await subscribeCustomMessages();
getMyNodeInfo = async () => await getInfo();
getNetworkInfo = async () => await getNetworkInfo();
getInvoices = async () => await listInvoices();
@@ -290,4 +295,6 @@ export default class EmbeddedLND extends LND {
supportsOnchainBatching = () => true;
supportsChannelBatching = () => true;
isLNDBased = () => true;
+ supportsLSPS1customMessage = () => true;
+ supportsLSPS1rest = () => false;
}
diff --git a/backends/LND.ts b/backends/LND.ts
index 63d84fa0e..326f49fca 100644
--- a/backends/LND.ts
+++ b/backends/LND.ts
@@ -251,6 +251,57 @@ export default class LND {
spend_unconfirmed: data.spend_unconfirmed,
send_all: data.send_all
});
+ sendCustomMessage = (data: any) =>
+ this.postRequest('/v1/custommessage', {
+ peer: Base64Utils.hexToBase64(data.peer),
+ type: data.type,
+ data: Base64Utils.hexToBase64(data.data)
+ });
+ subscribeCustomMessages = (onResponse: any, onError: any) => {
+ const route = '/v1/custommessage/subscribe';
+ const method = 'GET';
+
+ const { host, lndhubUrl, port, macaroonHex, accessToken } =
+ stores.settingsStore;
+
+ const auth = macaroonHex || accessToken;
+ const headers: any = this.getHeaders(auth, true);
+ const methodRoute = `${route}?method=${method}`;
+ const url = this.getURL(host || lndhubUrl, port, methodRoute, true);
+
+ const ws: any = new WebSocket(url, null, {
+ headers
+ });
+
+ ws.addEventListener('open', () => {
+ // connection opened
+ console.log('subscribeCustomMessages ws open');
+ ws.send(JSON.stringify({}));
+ });
+
+ ws.addEventListener('message', (e: any) => {
+ // a message was received
+ const data = JSON.parse(e.data);
+ console.log('subscribeCustomMessagews message', data);
+ if (data.error) {
+ onError(data.error);
+ } else {
+ onResponse(data);
+ }
+ });
+
+ ws.addEventListener('error', (e: any) => {
+ // an error occurred
+ console.log('subscribeCustomMessages ws err', e);
+ const certWarning = localeString('backends.LND.wsReq.warning');
+ onError(e.message ? `${certWarning} (${e.message})` : certWarning);
+ });
+
+ ws.addEventListener('close', () => {
+ // ws closed
+ console.log('subscribeCustomMessages ws close');
+ });
+ };
getMyNodeInfo = () => this.getRequest('/v1/getinfo');
getInvoices = (data: any) =>
this.getRequest(
@@ -617,4 +668,6 @@ export default class LND {
supportsOnchainBatching = () => true;
supportsChannelBatching = () => true;
isLNDBased = () => true;
+ supportsLSPS1customMessage = () => true;
+ supportsLSPS1rest = () => false;
}
diff --git a/backends/LightningNodeConnect.ts b/backends/LightningNodeConnect.ts
index e2d16ba14..a39724571 100644
--- a/backends/LightningNodeConnect.ts
+++ b/backends/LightningNodeConnect.ts
@@ -123,6 +123,16 @@ export default class LightningNodeConnect {
send_all: data.send_all
})
.then((data: lnrpc.SendCoinsResponse) => snakeize(data));
+ sendCustomMessage = async (data: any) =>
+ await this.lnc.lnd.lightning
+ .sendCustomMessage({
+ peer: Base64Utils.hexToBase64(data.peer),
+ type: data.type,
+ data: Base64Utils.hexToBase64(data.data)
+ })
+ .then((data: lnrpc.SendCustomMessageResponse) => snakeize(data));
+ subscribeCustomMessages = () =>
+ this.lnc.lnd.lightning.subscribeCustomMessages({});
getMyNodeInfo = async () =>
await this.lnc.lnd.lightning
.getInfo({})
@@ -477,4 +487,6 @@ export default class LightningNodeConnect {
supportsOnchainBatching = () => true;
supportsChannelBatching = () => true;
isLNDBased = () => true;
+ supportsLSPS1customMessage = () => true;
+ supportsLSPS1rest = () => false;
}
diff --git a/backends/LndHub.ts b/backends/LndHub.ts
index 7f4212818..c5fccd704 100644
--- a/backends/LndHub.ts
+++ b/backends/LndHub.ts
@@ -155,4 +155,6 @@ export default class LndHub extends LND {
supportsOnchainBatching = () => false;
supportsChannelBatching = () => true;
isLNDBased = () => false;
+ supportsLSPS1customMessage = () => false;
+ supportsLSPS1rest = () => false;
}
diff --git a/backends/Spark.ts b/backends/Spark.ts
index 226034e78..b9b02189c 100644
--- a/backends/Spark.ts
+++ b/backends/Spark.ts
@@ -381,4 +381,6 @@ export default class Spark {
supportsOnchainBatching = () => false;
supportsChannelBatching = () => true;
isLNDBased = () => false;
+ supportsLSPS1customMessage = () => false;
+ supportsLSPS1rest = () => true;
}
diff --git a/components/LSPS1OrderResponse.tsx b/components/LSPS1OrderResponse.tsx
new file mode 100644
index 000000000..96f92fa1e
--- /dev/null
+++ b/components/LSPS1OrderResponse.tsx
@@ -0,0 +1,327 @@
+import * as React from 'react';
+import { inject, observer } from 'mobx-react';
+import { ScrollView, View } from 'react-native';
+import moment from 'moment';
+
+import Screen from './Screen';
+import KeyValue from './KeyValue';
+import Amount from './Amount';
+import Button from './Button';
+
+import { localeString } from '../utils/LocaleUtils';
+import { themeColor } from '../utils/ThemeUtils';
+import UrlUtils from '../utils/UrlUtils';
+
+import InvoicesStore from '../stores/InvoicesStore';
+import NodeInfoStore from '../stores/NodeInfoStore';
+import FiatStore from '../stores/FiatStore';
+
+interface LSPS1OrderResponseProps {
+ navigation: any;
+ orderResponse: any;
+ InvoicesStore: InvoicesStore;
+ NodeInfoStore: NodeInfoStore;
+ FiatStore: FiatStore;
+ orderView: boolean;
+}
+
+@inject('InvoicesStore', 'NodeInfoStore', 'FiatStore')
+@observer
+export default class LSPS1OrderResponse extends React.Component<
+ LSPS1OrderResponseProps,
+ null
+> {
+ render() {
+ const {
+ orderResponse,
+ InvoicesStore,
+ NodeInfoStore,
+ FiatStore,
+ orderView,
+ navigation
+ } = this.props;
+ const { testnet } = NodeInfoStore;
+ const payment = orderResponse?.payment;
+ const channel = orderResponse?.channel;
+ return (
+
+
+
+ {orderResponse?.lsp_balance_sat && (
+
+ }
+ />
+ )}
+ {orderResponse?.client_balance_sat && (
+
+ }
+ />
+ )}
+ {orderResponse?.lsp_balance_sat &&
+ orderResponse?.client_balance_sat && (
+
+ }
+ />
+ )}
+ {orderResponse?.announce_channel && (
+
+ )}
+ {orderResponse?.channel_expiry_blocks && (
+
+ )}
+
+ {orderResponse?.funding_confirms_within_blocks && (
+
+ )}
+ {orderResponse?.created_at && (
+
+ )}
+ {orderResponse?.expires_at && (
+
+ )}
+
+ {orderResponse?.order_id && (
+
+ )}
+ {orderResponse?.order_state && (
+
+ )}
+
+ {payment?.fee_total_sat && (
+
+ }
+ />
+ )}
+ {(payment?.lightning_invoice ||
+ payment?.bolt11_invoice) && (
+
+ )}
+ {payment?.state && (
+
+ )}
+ {payment?.min_fee_for_0conf && (
+
+ )}
+ {payment?.min_onchain_payment_confirmations && (
+
+ )}
+ {payment?.onchain_address && (
+
+ )}
+ {payment?.onchain_payment && (
+
+ )}
+ {payment?.order_total_sat && (
+
+ }
+ />
+ )}
+ {channel && (
+ <>
+
+
+
+
+ UrlUtils.goToBlockExplorerTXID(
+ channel?.funding_outpoint,
+ testnet
+ )
+ }
+ />
+ >
+ )}
+ {orderResponse?.order_state === 'CREATED' && orderView && (
+
+
+
+ );
+ }
+}
diff --git a/ios/LndMobile/Lnd.swift b/ios/LndMobile/Lnd.swift
index 3337c890b..747a369ab 100644
--- a/ios/LndMobile/Lnd.swift
+++ b/ios/LndMobile/Lnd.swift
@@ -75,6 +75,7 @@ open class Lnd {
"AddInvoice": { bytes, cb in LndmobileAddInvoice(bytes, cb) },
"InvoicesCancelInvoice": { bytes, cb in LndmobileInvoicesCancelInvoice(bytes, cb) },
"ConnectPeer": { bytes, cb in LndmobileConnectPeer(bytes, cb) },
+ "SendCustomMessage": { bytes, cb in LndmobileSendCustomMessage(bytes, cb) },
"DecodePayReq": { bytes, cb in LndmobileDecodePayReq(bytes, cb) },
"DescribeGraph": { bytes, cb in LndmobileDescribeGraph(bytes, cb) },
"GetInfo": { bytes, cb in LndmobileGetInfo(bytes, cb) },
@@ -144,6 +145,8 @@ open class Lnd {
"SubscribeState": { req, cb in return LndmobileSubscribeState(req, cb) },
"RouterTrackPaymentV2": { req, cb in return LndmobileRouterTrackPaymentV2(req, cb) },
"OpenChannel": { bytes, cb in LndmobileOpenChannel(bytes, cb) },
+ "SubscribeCustomMessages": { bytes, cb in LndmobileSubscribeCustomMessages(bytes, cb) },
+
// channel
//
"CloseChannel": { req, cb in return LndmobileCloseChannel(req, cb)},
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 2e97a3c03..907ae7d66 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -923,6 +923,10 @@ PODS:
- React-Core
- react-native-safe-area-context (4.10.1):
- React-Core
+ - react-native-slider (4.5.2):
+ - glog
+ - RCT-Folly (= 2022.05.16.00)
+ - React-Core
- react-native-tor (0.1.8):
- React
- react-native-udp (4.1.7):
@@ -1190,6 +1194,7 @@ DEPENDENCIES:
- react-native-randombytes (from `../node_modules/react-native-randombytes`)
- react-native-restart (from `../node_modules/react-native-restart`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
+ - "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-tor (from `../node_modules/react-native-tor`)
- react-native-udp (from `../node_modules/react-native-udp`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
@@ -1330,6 +1335,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-restart"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
+ react-native-slider:
+ :path: "../node_modules/@react-native-community/slider"
react-native-tor:
:path: "../node_modules/react-native-tor"
react-native-udp:
@@ -1417,7 +1424,7 @@ SPEC CHECKSUMS:
FBLazyVector: 9f533d5a4c75ca77c8ed774aced1a91a0701781e
FBReactNativeSpec: 40b791f4a1df779e7e4aa12c000319f4f216d40a
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
- glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
+ glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 39589e9c297d024e90fe68f6830ff86c4e01498a
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
lottie-ios: 8f97d3271e155c2d688875c29cd3c74908aef5f8
@@ -1458,6 +1465,7 @@ SPEC CHECKSUMS:
react-native-randombytes: 3638d24759d67c68f6ccba60c52a7a8a8faa6a23
react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162
react-native-safe-area-context: dcab599c527c2d7de2d76507a523d20a0b83823d
+ react-native-slider: 7a39874fc1fcdfee48e448fa72cce0a8f2c7c5d6
react-native-tor: 3b14e9160b2eb7fa3f310921b2dee71a5171e5b7
react-native-udp: df79c3cb72c4e71240cd3ce4687bfb8a137140d5
React-nativeconfig: 754233aac2a769578f828093b672b399355582e6
@@ -1502,4 +1510,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d507d08b1b80c9101ab17aa86eca80f453619ee1
-COCOAPODS: 1.15.2
+COCOAPODS: 1.12.1
diff --git a/lndmobile/LndMobileInjection.ts b/lndmobile/LndMobileInjection.ts
index 3d4664710..932eb4f7d 100644
--- a/lndmobile/LndMobileInjection.ts
+++ b/lndmobile/LndMobileInjection.ts
@@ -38,7 +38,10 @@ import {
listInvoices,
subscribeChannelGraph,
sendKeysendPaymentV2,
- fundingStateStep
+ fundingStateStep,
+ sendCustomMessage,
+ subscribeCustomMessages,
+ decodeCustomMessage
} from './index';
import {
channelBalance,
@@ -245,6 +248,13 @@ export interface ILndMobileInjections {
psbt_verify,
psbt_finalize
}: any) => Promise;
+ sendCustomMessage: (
+ peer: Uint8Array | null,
+ type: number | null,
+ data: Uint8Array | null
+ ) => Promise;
+ subscribeCustomMessages: () => Promise;
+ decodeCustomMessage: (data: string) => lnrpc.CustomMessage;
};
channel: {
channelBalance: () => Promise;
@@ -472,7 +482,10 @@ export default {
listInvoices,
subscribeChannelGraph,
sendKeysendPaymentV2,
- fundingStateStep
+ fundingStateStep,
+ sendCustomMessage,
+ subscribeCustomMessages,
+ decodeCustomMessage
},
channel: {
channelBalance,
diff --git a/lndmobile/index.ts b/lndmobile/index.ts
index 6dad43629..d42137018 100644
--- a/lndmobile/index.ts
+++ b/lndmobile/index.ts
@@ -165,6 +165,52 @@ export const connectPeer = async (
});
};
+/**
+ * @throws
+ */
+export const sendCustomMessage = async (
+ peer: string,
+ type: number,
+ data: string
+): Promise => {
+ return await sendCommand<
+ lnrpc.ISendCustomMessageRequest,
+ lnrpc.SendCustomMessageRequest,
+ lnrpc.SendCustomMessageResponse
+ >({
+ request: lnrpc.SendCustomMessageRequest,
+ response: lnrpc.SendCustomMessageResponse,
+ method: 'SendCustomMessage',
+ options: {
+ peer: Base64Utils.hexToBase64(peer),
+ type,
+ data: Base64Utils.hexToBase64(data)
+ }
+ });
+};
+
+/**
+ * @throws
+ */
+export const subscribeCustomMessages = async (): Promise => {
+ const response = await sendStreamCommand<
+ lnrpc.ISubscribeCustomMessagesRequest,
+ lnrpc.SubscribeCustomMessagesRequest
+ >({
+ request: lnrpc.SubscribeCustomMessagesRequest,
+ method: 'SubscribeCustomMessages',
+ options: {}
+ });
+ return response;
+};
+
+export const decodeCustomMessage = (data: string): lnrpc.CustomMessage => {
+ return decodeStreamResult({
+ response: lnrpc.CustomMessage,
+ base64Result: data
+ });
+};
+
/**
* @throws
*/
diff --git a/locales/en.json b/locales/en.json
index 7018ded00..aea4be692 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -88,6 +88,7 @@
"general.valid": "Valid",
"general.invalid": "Invalid",
"general.createdAt": "Created at",
+ "general.expiresAt": "Expires at",
"general.id": "ID",
"general.hash": "Hash",
"general.kind": "Kind",
@@ -104,6 +105,7 @@
"general.destination": "Destination",
"general.externalAccount": "External account",
"general.version": "Version",
+ "general.state": "State",
"general.mode": "Mode",
"general.automatic": "Automatic",
"general.custom": "Custom",
@@ -285,6 +287,7 @@
"views.Wallet.Channels.online": "Online",
"views.Wallet.Channels.offline": "Offline",
"views.Wallet.Channels.filters": "Filters",
+ "views.Wallet.Channels.purchaseInbound": "Purchase Inbound",
"views.OpenChannel.announceChannel": "Announce channel",
"views.OpenChannel.scidAlias": "Attempt to use SCID alias",
"views.OpenChannel.simpleTaprootChannel": "Simple Taproot Channel",
@@ -713,6 +716,7 @@
"views.Settings.Invoices.title": "Invoices settings",
"views.Settings.Invoices.showCustomPreimageField": "Show custom preimage field",
"views.Settings.Channels.title": "Channels settings",
+ "views.Settings.Channels.lsps1ShowPurchaseButton": "Show channel purchase button",
"views.Settings.Privacy.blockExplorer": "Default Block explorer",
"views.Settings.Privacy.BlockExplorer.custom": "Custom",
"views.Settings.Privacy.customBlockExplorer": "Custom Block explorer",
@@ -864,6 +868,8 @@
"views.Settings.LSP.zeroConfChans": "Zero-conf channels",
"views.Settings.LSP.learn0confConfig": "Learn how to configure 0-conf channels and Alias SCIDs",
"views.Settings.LSP.enableCertificateVerification": "Enable certificate verification",
+ "views.Settings.LSP.flow2": "Flow 2.0 API and spec",
+ "views.Settings.LSP.createWrappedInvoice": "Create a wrapped invoice",
"views.Settings.AddContact.name": "Name",
"views.Settings.AddContact.description": "Description (max 120)",
"views.Settings.AddContact.lnAddress": "LN address",
@@ -934,6 +940,39 @@
"views.Sync.currentBlockHeight": "Current block height",
"views.Sync.tip": "Tip",
"views.Sync.numBlocksUntilSynced": "Number of blocks until synced",
+ "views.LSPS1.pubkeyAndHostNotFound": "Node pubkey and host are not set",
+ "views.LSPS1.timeoutError": "Did not receive response from server",
+ "views.LSPS1.channelExpiryBlocks": "Channel Expiry Blocks",
+ "views.LSPS1.maxChannelExpiryBlocks": "Max channel Expiry Blocks",
+ "views.LSPS1.lspBalance": "LSP Balance",
+ "views.LSPS1.clientBalance": "Client balance",
+ "views.LSPS1.totalBalance": "Total balance",
+ "views.LSPS1.totalChannelSize": "Total channel size",
+ "views.LSPS1.confirmWithinBlocks": "Confirm within blocks",
+ "views.LSPS1.orderId": "Order ID",
+ "views.LSPS1.orderState": "Order state",
+ "views.LSPS1.miniFeeFor0Conf": "Min fee for 0 conf",
+ "views.LSPS1.minOnchainPaymentConfirmations": "Min Onchain Payment Confirmations",
+ "views.LSPS1.onchainPayment": "Onchain payment",
+ "views.LSPS1.totalOrderValue": "Total order value",
+ "views.LSPS1.initialLSPBalance": "Initial LSP Balance",
+ "views.LSPS1.initialClientBalance": "Initial Client Balance",
+ "views.LSPS1.minChannelConfirmations": "Min channel confirmations",
+ "views.LSPS1.minOnchainPaymentSize": "Min onchain payment size",
+ "views.LSPS1.supportZeroChannelReserve": "Support zero channel reserve",
+ "views.LSPS1.requiredChannelConfirmations": "Required channel confirmations",
+ "views.LSPS1.token": "Token",
+ "views.LSPS1.refundOnchainAddress": "Refund onchain address",
+ "views.LSPS1.getQuote": "Get quote",
+ "views.LSPS1.makePayment": "Make payment",
+ "views.LSPS1.goToSettings": "Go to settings",
+ "views.LSPS1.fundedAt": "Funded At",
+ "views.LSPS1.fundingOutpoint": "Funding outpoint",
+ "views.LSPS1.purchaseInbound": "Purchase inbound channel",
+ "views.LSPS1.lsps1Spec": "LSPS1 API and spec",
+ "views.LSPS1.lsps1Orders": "LSPS1 Orders",
+ "views.LSPS1.noOrdersError": "No orders are saved yet!",
+ "views.LSPS1.showingPreviousState": "showing previous state!",
"components.UTXOPicker.modal.title": "Select UTXOs to use",
"components.UTXOPicker.modal.description": "Select the UTXOs to be used in this operation. You may want to only use specific UTXOs to preserve your privacy.",
"components.UTXOPicker.modal.set": "Set UTXOs",
@@ -1060,6 +1099,9 @@
"views.Settings.CustodialWalletWarning.graph3": "ZEUS has the ability to create a self-custodial wallet in the app. This wallet provides you with a 24-word seed phrase that gives you full control of your funds.",
"views.Settings.CustodialWalletWarning.graph4": "To get started with your own self-custodial wallet, press the button below, and hit the 'Create mainnet wallet' button on the next screen.",
"views.Settings.CustodialWalletWarning.create": "Create self-custodial wallet",
+ "view.Settings.LSPServicesList.title": "LSP Services",
+ "view.Settings.LSPServicesList.flow2": "Just-in-time channels",
+ "view.Settings.LSPServicesList.lsps1": "Request channels in advance",
"views.LspExplanation.text1": "Zeus is a self-custodial lightning wallet. In order to send or receive a lightning payment, you must open a lightning payment channel, which has a setup fee.",
"views.LspExplanation.text2": "Once the channel is set up, you'll only have to pay normal network fees until your channel exhausts its capacity.",
"views.LspExplanation.buttonText": "Learn more about liquidity",
diff --git a/package.json b/package.json
index d49469b2f..b5bf4b953 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"@react-native-async-storage/async-storage": "1.22.0",
"@react-native-clipboard/clipboard": "1.13.2",
"@react-native-community/netinfo": "11.3.0",
+ "@react-native-community/slider": "4.5.2",
"@react-native-masked-view/masked-view": "0.3.1",
"@react-native-picker/picker": "2.6.1",
"@react-navigation/bottom-tabs": "7.0.0-alpha.22",
diff --git a/stores/LSPStore.ts b/stores/LSPStore.ts
index 9594df885..489a66f6a 100644
--- a/stores/LSPStore.ts
+++ b/stores/LSPStore.ts
@@ -1,26 +1,39 @@
import { action, observable } from 'mobx';
import ReactNativeBlobUtil from 'react-native-blob-util';
+import { v4 as uuidv4 } from 'uuid';
import SettingsStore from './SettingsStore';
import ChannelsStore from './ChannelsStore';
import NodeInfoStore from './NodeInfoStore';
import lndMobile from '../lndmobile/LndMobileInjection';
-const { channel } = lndMobile;
+const { index, channel } = lndMobile;
import BackendUtils from '../utils/BackendUtils';
import Base64Utils from '../utils/Base64Utils';
import { LndMobileEventEmitter } from '../utils/LndMobileUtils';
import { localeString } from '../utils/LocaleUtils';
+import { errorToUserFriendly } from '../utils/ErrorUtils';
export default class LSPStore {
@observable public info: any = {};
@observable public zeroConfFee: number | undefined;
@observable public feeId: string | undefined;
+ @observable public pubkey: string;
+ @observable public getInfoId: string;
+ @observable public createOrderId: string;
+ @observable public getOrderId: string;
+ @observable public loading: boolean = true;
@observable public error: boolean = false;
@observable public error_msg: string = '';
@observable public showLspSettings: boolean = false;
@observable public channelAcceptor: any;
+ @observable public customMessagesSubscriber: any;
+ @observable public getInfoData: any = {};
+ @observable public createOrderResponse: any = {};
+ @observable public getOrderResponse: any = {};
+
+ @observable public resolvedCustomMessage: boolean;
settingsStore: SettingsStore;
channelsStore: ChannelsStore;
@@ -44,6 +57,7 @@ export default class LSPStore {
this.error_msg = '';
this.showLspSettings = false;
this.channelAcceptor = undefined;
+ this.customMessagesSubscriber = undefined;
};
@action
@@ -51,11 +65,37 @@ export default class LSPStore {
this.zeroConfFee = undefined;
};
+ @action
+ public resetLSPS1Data = () => {
+ this.createOrderResponse = {};
+ this.getInfoData = {};
+ this.loading = true;
+ this.error = false;
+ this.error_msg = '';
+ };
+
getLSPHost = () =>
this.nodeInfoStore!.nodeInfo.isTestNet
? this.settingsStore.settings.lspTestnet
: this.settingsStore.settings.lspMainnet;
+ getLSPS1Pubkey = () =>
+ this.nodeInfoStore!.nodeInfo.isTestNet
+ ? this.settingsStore.settings.lsps1PubkeyTestnet
+ : this.settingsStore.settings.lsps1PubkeyMainnet;
+
+ getLSPS1Host = () =>
+ this.nodeInfoStore!.nodeInfo.isTestNet
+ ? this.settingsStore.settings.lsps1HostTestnet
+ : this.settingsStore.settings.lsps1HostMainnet;
+
+ getLSPS1Rest = () =>
+ this.nodeInfoStore!.nodeInfo.isTestNet
+ ? this.settingsStore.settings.lsps1RestTestnet
+ : this.settingsStore.settings.lsps1RestMainnet;
+
+ encodeMesage = (n: any) => Buffer.from(JSON.stringify(n)).toString('hex');
+
@action
public getLSPInfo = () => {
return new Promise((resolve, reject) => {
@@ -179,7 +219,7 @@ export default class LSPStore {
await channel.channelAcceptorResponse(
channelAcceptRequest.pending_chan_id,
!channelAcceptRequest.wants_zero_conf || isZeroConfAllowed,
- isZeroConfAllowed
+ isZeroConfAllowed && channelAcceptRequest.wants_zero_conf
);
} catch (error: any) {
console.error('handleChannelAcceptorEvent error:', error.message);
@@ -270,4 +310,252 @@ export default class LSPStore {
});
});
};
+
+ @action
+ public sendCustomMessage = ({
+ peer,
+ type,
+ data
+ }: {
+ peer: string;
+ type: number | null;
+ data: string;
+ }) => {
+ return new Promise((resolve, reject) => {
+ if (!peer || !type || !data) {
+ reject('Invalid parameters for custom message.');
+ return;
+ }
+
+ BackendUtils.sendCustomMessage({ peer, type, data })
+ .then((response: any) => {
+ resolve(response);
+ })
+ .catch((error: any) => {
+ this.error = true;
+ this.error_msg = errorToUserFriendly(error);
+ reject(error);
+ });
+ });
+ };
+
+ @action
+ public handleCustomMessages = (decoded: any) => {
+ const peer = Base64Utils.base64ToHex(decoded.peer);
+ const data = JSON.parse(Base64Utils.base64ToUtf8(decoded.data));
+
+ console.log('peer', peer);
+ console.log('data', data);
+
+ if (data.id === this.getInfoId) {
+ this.getInfoData = data;
+ this.loading = false;
+ } else if (data.id === this.createOrderId) {
+ if (data.error) {
+ this.error = true;
+ this.loading = false;
+ this.error_msg = data?.error?.data?.message;
+ } else {
+ this.createOrderResponse = data;
+ this.loading = false;
+ }
+ } else if (data.id === this.getOrderId) {
+ if (data.error) {
+ this.error = true;
+ this.loading = false;
+ this.error_msg = data?.error?.message;
+ } else {
+ this.getOrderResponse = data;
+ }
+ }
+ };
+
+ @action
+ public subscribeCustomMessages = async () => {
+ if (this.customMessagesSubscriber) return;
+ this.resolvedCustomMessage = false;
+ let timer = 7000;
+ const timeoutId = setTimeout(() => {
+ if (!this.resolvedCustomMessage) {
+ this.error = true;
+ this.error_msg = localeString('views.LSPS1.timeoutError');
+ this.loading = false;
+ }
+ }, timer);
+
+ if (this.settingsStore.implementation === 'embedded-lnd') {
+ this.customMessagesSubscriber = LndMobileEventEmitter.addListener(
+ 'SubscribeCustomMessages',
+ async (event: any) => {
+ try {
+ const decoded = index.decodeCustomMessage(event.data);
+ this.handleCustomMessages(decoded);
+ this.resolvedCustomMessage = true;
+ clearTimeout(timeoutId);
+ } catch (error: any) {
+ console.error(
+ 'sub custom messages error: ' + error.message
+ );
+ }
+ }
+ );
+
+ await index.subscribeCustomMessages();
+ } else {
+ BackendUtils.subscribeCustomMessages(
+ (response: any) => {
+ const decoded = response.result;
+ this.handleCustomMessages(decoded);
+ this.resolvedCustomMessage = true;
+ clearTimeout(timeoutId);
+ },
+ (error: any) => {
+ console.error(
+ 'sub custom messages error: ' + error.message
+ );
+ }
+ );
+ }
+ };
+
+ @action
+ public getInfoREST = () => {
+ const endpoint = `${this.getLSPS1Rest()}/api/v1/get_info`;
+
+ console.log('Fetching data from:', endpoint);
+
+ return ReactNativeBlobUtil.fetch('GET', endpoint)
+ .then((response) => {
+ if (response.info().status === 200) {
+ const responseData = JSON.parse(response.data);
+ this.getInfoData = responseData;
+ try {
+ const uri = responseData.uris[0];
+ const pubkey = uri.split('@')[0];
+ this.pubkey = pubkey;
+ } catch (e) {}
+ this.loading = false;
+ } else {
+ this.error = true;
+ this.error_msg = 'Error fetching get_info data';
+ this.loading = false;
+ }
+ })
+ .catch(() => {
+ this.error = true;
+ this.error_msg = 'Error fetching get_info data';
+ this.loading = false;
+ });
+ };
+
+ @action
+ public createOrderREST = (state: any) => {
+ const data = JSON.stringify({
+ lsp_balance_sat: state.lspBalanceSat,
+ client_balance_sat: state.clientBalanceSat,
+ required_channel_confirmations: parseInt(
+ state.requiredChannelConfirmations
+ ),
+ funding_confirms_within_blocks: parseInt(
+ state.confirmsWithinBlocks
+ ),
+ channel_expiry_blocks: parseInt(state.channelExpiryBlocks),
+ token: state.token,
+ refund_onchain_address: state.refundOnchainAddress,
+ announce_channel: state.announceChannel,
+ public_key: this.nodeInfoStore.nodeInfo.nodeId
+ });
+ this.loading = true;
+ this.error = false;
+ this.error_msg = '';
+ const endpoint = `${this.getLSPS1Rest()}/api/v1/create_order`;
+ console.log('Sending data to:', endpoint);
+
+ return ReactNativeBlobUtil.fetch(
+ 'POST',
+ endpoint,
+ {
+ 'Content-Type': 'application/json'
+ },
+ data
+ )
+ .then((response) => {
+ const responseData = JSON.parse(response.data);
+ if (responseData.error) {
+ this.error = true;
+ this.error_msg = responseData.message;
+ this.loading = false;
+ } else {
+ this.createOrderResponse = responseData;
+ this.loading = false;
+ console.log('Response received:', responseData);
+ }
+ })
+ .catch((error) => {
+ console.error(
+ 'Error sending (create_order) custom message:',
+ error
+ );
+ this.error = true;
+ this.error_msg = errorToUserFriendly(error);
+ this.loading = false;
+ });
+ };
+
+ @action
+ public getOrderREST(id: string, RESTHost: string) {
+ this.loading = true;
+ const endpoint = `${RESTHost}/api/v1/get_order?order_id=${id}`;
+
+ console.log('Sending data to:', endpoint);
+
+ return ReactNativeBlobUtil.fetch('GET', endpoint, {
+ 'Content-Type': 'application/json'
+ })
+ .then((response) => {
+ const responseData = JSON.parse(response.data);
+ console.log('Response received:', responseData);
+ if (responseData.error) {
+ this.error = true;
+ this.error_msg = responseData.message;
+ } else {
+ this.getOrderResponse = responseData;
+ }
+ })
+ .catch((error) => {
+ console.error('Error sending custom message:', error);
+ this.error = true;
+ this.error_msg = errorToUserFriendly(error);
+ this.loading = false;
+ });
+ }
+
+ @action
+ public getOrderCustomMessage(orderId: string, peer: string) {
+ console.log('Requesting LSPS1...');
+ this.loading = true;
+ const type = 37913;
+ const id = uuidv4();
+ this.getOrderId = id;
+ const data = this.encodeMesage({
+ jsonrpc: '2.0',
+ method: 'lsps1.get_order',
+ params: {
+ order_id: orderId
+ },
+ id: this.getOrderId
+ });
+
+ this.sendCustomMessage({
+ peer,
+ type,
+ data
+ })
+ .then((response) => {
+ console.log('Custom message sent:', response);
+ })
+ .catch((error) => {
+ console.error('Error sending custom message:', error);
+ });
+ }
}
diff --git a/stores/SettingsStore.ts b/stores/SettingsStore.ts
index 5625f1fd3..5ad3043b8 100644
--- a/stores/SettingsStore.ts
+++ b/stores/SettingsStore.ts
@@ -145,6 +145,15 @@ export interface Settings {
lspTestnet: string;
lspAccessKey: string;
requestSimpleTaproot: boolean;
+ //LSPS1
+ lsps1RestMainnet: string;
+ lsps1RestTestnet: string;
+ lsps1PubkeyMainnet: string;
+ lsps1PubkeyTestnet: string;
+ lsps1HostMainnet: string;
+ lsps1HostTestnet: string;
+ lsps1ShowPurchaseButton: boolean;
+
// Lightning Address
lightningAddress: LightningAddressSettings;
selectNodeOnStartup: boolean;
@@ -899,6 +908,17 @@ export const LNDHUB_AUTH_MODES = [
export const DEFAULT_LSP_MAINNET = 'https://0conf.lnolymp.us';
export const DEFAULT_LSP_TESTNET = 'https://testnet-0conf.lnolymp.us';
+// LSPS1 REST
+export const DEFAULT_LSPS1_REST_MAINNET = 'https://lsps1.lnolymp.us';
+export const DEFAULT_LSPS1_REST_TESTNET = 'https://testnet-lsps1.lnolymp.us';
+
+export const DEFAULT_LSPS1_PUBKEY_MAINNET =
+ '031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581';
+export const DEFAULT_LSPS1_PUBKEY_TESTNET =
+ '03e84a109cd70e57864274932fc87c5e6434c59ebb8e6e7d28532219ba38f7f6df';
+export const DEFAULT_LSPS1_HOST_MAINNET = '45.79.192.236:9735';
+export const DEFAULT_LSPS1_HOST_TESTNET = '139.144.22.237:9735';
+
export const DEFAULT_NOSTR_RELAYS = [
'wss://nostr.mutinywallet.com',
'wss://relay.damus.io',
@@ -1039,6 +1059,14 @@ export default class SettingsStore {
lspTestnet: DEFAULT_LSP_TESTNET,
lspAccessKey: '',
requestSimpleTaproot: false,
+ //lsps1
+ lsps1RestMainnet: DEFAULT_LSPS1_REST_MAINNET,
+ lsps1RestTestnet: DEFAULT_LSPS1_REST_TESTNET,
+ lsps1PubkeyMainnet: DEFAULT_LSPS1_PUBKEY_MAINNET,
+ lsps1PubkeyTestnet: DEFAULT_LSPS1_PUBKEY_TESTNET,
+ lsps1HostMainnet: DEFAULT_LSPS1_HOST_MAINNET,
+ lsps1HostTestnet: DEFAULT_LSPS1_HOST_TESTNET,
+ lsps1ShowPurchaseButton: true,
// Lightning Address
lightningAddress: {
enabled: false,
@@ -1343,6 +1371,42 @@ export default class SettingsStore {
await EncryptedStorage.setItem(MOD_KEY3, 'true');
}
+ const MOD_KEY4 = 'lsps1-hosts';
+ const mod4 = await EncryptedStorage.getItem(MOD_KEY4);
+ if (!mod4) {
+ if (!this.settings?.lsps1HostMainnet) {
+ this.settings.lsps1HostMainnet =
+ DEFAULT_LSPS1_HOST_MAINNET;
+ }
+ if (!this.settings?.lsps1HostTestnet) {
+ this.settings.lsps1HostTestnet =
+ DEFAULT_LSPS1_HOST_TESTNET;
+ }
+ if (!this.settings?.lsps1PubkeyMainnet) {
+ this.settings.lsps1PubkeyMainnet =
+ DEFAULT_LSPS1_PUBKEY_MAINNET;
+ }
+ if (!this.settings?.lsps1PubkeyTestnet) {
+ this.settings.lsps1PubkeyTestnet =
+ DEFAULT_LSPS1_PUBKEY_TESTNET;
+ }
+ if (!this.settings?.lsps1RestMainnet) {
+ this.settings.lsps1RestMainnet =
+ DEFAULT_LSPS1_REST_MAINNET;
+ }
+ if (!this.settings?.lsps1RestTestnet) {
+ this.settings.lsps1RestTestnet =
+ DEFAULT_LSPS1_REST_TESTNET;
+ }
+
+ if (!this.settings?.lsps1ShowPurchaseButton) {
+ this.settings.lsps1ShowPurchaseButton = true;
+ }
+
+ this.setSettings(JSON.stringify(this.settings));
+ await EncryptedStorage.setItem(MOD_KEY4, 'true');
+ }
+
// migrate old POS squareEnabled setting to posEnabled
if (newSettings?.pos?.squareEnabled) {
newSettings.pos.posEnabled = PosEnabled.Square;
diff --git a/utils/BackendUtils.ts b/utils/BackendUtils.ts
index 67873c1e3..c7c0c566d 100644
--- a/utils/BackendUtils.ts
+++ b/utils/BackendUtils.ts
@@ -69,6 +69,10 @@ class BackendUtils {
getLightningBalance = (...args: any[]) =>
this.call('getLightningBalance', args);
sendCoins = (...args: any[]) => this.call('sendCoins', args);
+ sendCustomMessage = (...args: any[]) =>
+ this.call('sendCustomMessage', args);
+ subscribeCustomMessages = (...args: any[]) =>
+ this.call('subscribeCustomMessages', args);
getMyNodeInfo = (...args: any[]) => this.call('getMyNodeInfo', args);
getNetworkInfo = (...args: any[]) => this.call('getNetworkInfo', args);
getInvoices = (...args: any[]) => this.call('getInvoices', args);
@@ -115,6 +119,10 @@ class BackendUtils {
this.call('subscribeTransactions', args);
initChanAcceptor = (...args: any[]) => this.call('initChanAcceptor', args);
+ //cln
+ supportsLSPS1customMessage = () => this.call('supportsLSPS1customMessage');
+ supportsLSPS1rest = () => this.call('supportsLSPS1rest');
+
// lndhub
login = (...args: any[]) => this.call('login', args);
diff --git a/views/Channels/ChannelsPane.tsx b/views/Channels/ChannelsPane.tsx
index f29a23db4..5d3a5ffc1 100644
--- a/views/Channels/ChannelsPane.tsx
+++ b/views/Channels/ChannelsPane.tsx
@@ -1,5 +1,13 @@
-import * as React from 'react';
-import { FlatList, View, TouchableHighlight } from 'react-native';
+import React, { useEffect, useRef, useState } from 'react';
+import {
+ Animated,
+ FlatList,
+ View,
+ StyleSheet,
+ Text,
+ TouchableHighlight,
+ TouchableOpacity
+} from 'react-native';
import { inject, observer } from 'mobx-react';
import { duration } from 'moment';
import { StackNavigationProp } from '@react-navigation/stack';
@@ -36,6 +44,45 @@ interface ChannelsProps {
SettingsStore?: SettingsStore;
}
+const ColorChangingButton = ({ onPress }) => {
+ const [forward, setForward] = useState(true);
+ const animation = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ // Toggle animation direction
+ setForward((prev) => !prev);
+ }, 5000); // Change color gradient every 6 seconds
+
+ return () => clearInterval(interval); // Cleanup interval on component unmount
+ }, []);
+
+ useEffect(() => {
+ // Animate from 0 to 1 or from 1 to 0 based on 'forward' value
+ Animated.timing(animation, {
+ toValue: forward ? 1 : 0,
+ duration: 4500,
+ useNativeDriver: true
+ }).start();
+ }, [forward]);
+
+ const backgroundColor: any = animation.interpolate({
+ inputRange: [0, 1],
+ outputRange: ['rgb(180, 26, 20)', 'rgb(255, 169, 0)'] // Red to Gold gradient
+ });
+
+ return (
+
+
+ {localeString('views.Wallet.Channels.purchaseInbound')}
+
+
+ );
+};
+
@inject('ChannelsStore', 'SettingsStore')
@observer
export default class ChannelsPane extends React.PureComponent {
@@ -140,8 +187,9 @@ export default class ChannelsPane extends React.PureComponent {
channelsType
} = ChannelsStore!;
- const lurkerMode: boolean =
- SettingsStore!.settings?.privacy?.lurkerMode || false;
+ const { settings } = SettingsStore!;
+
+ const lurkerMode: boolean = settings?.privacy?.lurkerMode || false;
let headerString;
let channelsData: Channel[];
@@ -184,6 +232,15 @@ export default class ChannelsPane extends React.PureComponent {
totalOffline={totalOffline}
lurkerMode={lurkerMode}
/>
+ {settings?.lsps1ShowPurchaseButton &&
+ (BackendUtils.supportsLSPS1customMessage() ||
+ BackendUtils.supportsLSPS1rest()) && (
+ {
+ navigation.navigate('LSPS1');
+ }}
+ />
+ )}
{showSearch && }
{loading ? (
@@ -205,3 +262,17 @@ export default class ChannelsPane extends React.PureComponent {
);
}
}
+
+const styles = StyleSheet.create({
+ button: {
+ padding: 10,
+ borderRadius: 5,
+ margin: 10
+ },
+ buttonText: {
+ fontFamily: 'PPNeueMontreal-Book',
+ color: 'white',
+ fontWeight: 'bold',
+ textAlign: 'center'
+ }
+});
diff --git a/views/Settings/ChannelsSettings.tsx b/views/Settings/ChannelsSettings.tsx
index 40b0b8075..c11b50af1 100644
--- a/views/Settings/ChannelsSettings.tsx
+++ b/views/Settings/ChannelsSettings.tsx
@@ -24,6 +24,7 @@ interface ChannelsSettingsState {
privateChannel: boolean;
scidAlias: boolean;
simpleTaprootChannel: boolean;
+ lsps1ShowPurchaseButton: boolean;
}
@inject('SettingsStore')
@@ -36,7 +37,8 @@ export default class ChannelsSettings extends React.Component<
min_confs: 1,
privateChannel: true,
scidAlias: true,
- simpleTaprootChannel: false
+ simpleTaprootChannel: false,
+ lsps1ShowPurchaseButton: true
};
async UNSAFE_componentWillMount() {
@@ -57,7 +59,11 @@ export default class ChannelsSettings extends React.Component<
simpleTaprootChannel:
settings?.channels?.simpleTaprootChannel !== null
? settings.channels.simpleTaprootChannel
- : false
+ : false,
+ lsps1ShowPurchaseButton:
+ settings?.lsps1ShowPurchaseButton !== null
+ ? settings.lsps1ShowPurchaseButton
+ : true
});
}
@@ -72,8 +78,13 @@ export default class ChannelsSettings extends React.Component<
render() {
const { navigation, SettingsStore } = this.props;
- const { min_confs, privateChannel, scidAlias, simpleTaprootChannel } =
- this.state;
+ const {
+ min_confs,
+ privateChannel,
+ scidAlias,
+ simpleTaprootChannel,
+ lsps1ShowPurchaseButton
+ } = this.state;
const { updateSettings }: any = SettingsStore;
return (
@@ -221,6 +232,36 @@ export default class ChannelsSettings extends React.Component<
/>
>
)}
+
+ {(BackendUtils.supportsLSPS1customMessage() ||
+ BackendUtils.supportsLSPS1rest()) && (
+ <>
+
+ {localeString(
+ 'views.Settings.Channels.lsps1ShowPurchaseButton'
+ )}
+
+ {
+ this.setState({
+ lsps1ShowPurchaseButton:
+ !lsps1ShowPurchaseButton
+ });
+
+ await updateSettings({
+ lsps1ShowPurchaseButton:
+ !lsps1ShowPurchaseButton
+ });
+ }}
+ />
+ >
+ )}
);
diff --git a/views/Settings/LSP.tsx b/views/Settings/LSP.tsx
index d2d436156..413625aca 100644
--- a/views/Settings/LSP.tsx
+++ b/views/Settings/LSP.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
-import { StyleSheet, Text, View } from 'react-native';
-import { ListItem } from 'react-native-elements';
+import { FlatList, StyleSheet, Text, View } from 'react-native';
+import { Icon, ListItem } from 'react-native-elements';
import { inject, observer } from 'mobx-react';
import { StackNavigationProp } from '@react-navigation/stack';
@@ -79,18 +79,20 @@ export default class LSP extends React.Component {
return (
+
-
{lspNotConfigured ? (
<>
@@ -345,6 +347,75 @@ export default class LSP extends React.Component {
>
)}
+ {!lspNotConfigured && (
+
+ (
+ {
+ if (item.nav)
+ navigation.navigate(item.nav);
+ if (item.url)
+ UrlUtils.goToUrl(item.url);
+ }}
+ >
+
+
+ {item.label}
+
+
+
+
+ )}
+ keyExtractor={(item, index) =>
+ `${item.label}-${index}`
+ }
+ ItemSeparatorComponent={
+
+ }
+ />
+
+ )}
);
}
diff --git a/views/Settings/LSPS1/Order.tsx b/views/Settings/LSPS1/Order.tsx
new file mode 100644
index 000000000..346d3ad6c
--- /dev/null
+++ b/views/Settings/LSPS1/Order.tsx
@@ -0,0 +1,231 @@
+import React from 'react';
+import { View, ScrollView } from 'react-native';
+import EncryptedStorage from 'react-native-encrypted-storage';
+import { inject, observer } from 'mobx-react';
+import { Route } from '@react-navigation/native';
+import { StackNavigationProp } from '@react-navigation/stack';
+
+import Screen from '../../../components/Screen';
+import Header from '../../../components/Header';
+import { WarningMessage } from '../../../components/SuccessErrorMessage';
+import LoadingIndicator from '../../../components/LoadingIndicator';
+
+import { themeColor } from '../../../utils/ThemeUtils';
+import BackendUtils from '../../../utils/BackendUtils';
+import { localeString } from '../../../utils/LocaleUtils';
+
+import LSPStore from '../../../stores/LSPStore';
+import SettingsStore from '../../../stores/SettingsStore';
+import InvoicesStore from '../../../stores/InvoicesStore';
+import NodeInfoStore from '../../../stores/NodeInfoStore';
+import LSPS1OrderResponse from '../../../components/LSPS1OrderResponse';
+
+interface OrderProps {
+ navigation: StackNavigationProp;
+ route: Route<'LSPS1Order', { orderId: string; orderShouldUpdate: boolean }>;
+ LSPStore: LSPStore;
+ SettingsStore: SettingsStore;
+ InvoicesStore: InvoicesStore;
+ NodeInfoStore: NodeInfoStore;
+}
+
+interface OrdersState {
+ order: any;
+ fetchOldOrder: boolean;
+}
+
+@inject('LSPStore', 'SettingsStore', 'InvoicesStore', 'NodeInfoStore')
+@observer
+export default class Orders extends React.Component {
+ constructor(props: OrderProps) {
+ super(props);
+ this.state = {
+ order: null,
+ fetchOldOrder: false
+ };
+ }
+
+ async componentDidMount() {
+ const { LSPStore, route } = this.props;
+ let temporaryOrder: any;
+ const id = route.params?.orderId;
+ const orderShouldUpdate = route.params?.orderShouldUpdate;
+
+ console.log('Looking for order in storage...');
+ EncryptedStorage.getItem('orderResponses')
+ .then((responseArrayString) => {
+ if (responseArrayString) {
+ const responseArray = JSON.parse(responseArrayString);
+ const order = responseArray.find((response) => {
+ const decodedResponse = JSON.parse(response);
+ const result =
+ decodedResponse?.order?.result ||
+ decodedResponse?.order;
+ return result?.order_id === id;
+ });
+ if (order) {
+ const parsedOrder = JSON.parse(order);
+ temporaryOrder = parsedOrder;
+ console.log('Order found in storage->', temporaryOrder);
+
+ BackendUtils.supportsLSPS1rest()
+ ? LSPStore.getOrderREST(
+ id,
+ temporaryOrder?.endpoint
+ )
+ : LSPStore.getOrderCustomMessage(
+ id,
+ temporaryOrder?.peer
+ );
+
+ setTimeout(() => {
+ if (LSPStore.error && LSPStore.error_msg !== '') {
+ this.setState({
+ order: temporaryOrder?.order,
+ fetchOldOrder: true
+ });
+ LSPStore.loading = false;
+ console.log('Old Order state fetched!');
+ } else if (
+ Object.keys(LSPStore.getOrderResponse)
+ .length !== 0
+ ) {
+ const getOrderData = LSPStore.getOrderResponse;
+ this.setState({
+ order: getOrderData,
+ fetchOldOrder: false
+ });
+ console.log(
+ 'Latest Order state fetched!',
+ this.state.order
+ );
+ LSPStore.loading = false;
+ const result =
+ getOrderData?.result || getOrderData;
+ if (
+ (result?.order_state === 'COMPLETED' ||
+ result?.order_state === 'FAILED') &&
+ !orderShouldUpdate
+ ) {
+ this.updateOrderInStorage(getOrderData);
+ }
+ }
+ }, 3000);
+ } else {
+ console.log('Order not found in encrypted storage.');
+ }
+ } else {
+ console.log(
+ 'No saved responses found in encrypted storage.'
+ );
+ }
+ })
+ .catch((error) => {
+ console.error(
+ 'Error retrieving saved responses from encrypted storage:',
+ error
+ );
+ });
+ }
+
+ updateOrderInStorage(order) {
+ console.log('Updating order in encrypted storage...');
+ EncryptedStorage.getItem('orderResponses')
+ .then((responseArrayString) => {
+ if (responseArrayString) {
+ let responseArray = JSON.parse(responseArrayString);
+ // Find the index of the order to be updated
+ const index = responseArray.findIndex((response) => {
+ const decodedResponse = JSON.parse(response);
+ const result =
+ decodedResponse?.order?.result ||
+ decodedResponse?.order;
+ const currentOrderResult = order?.result || order;
+ return result.order_id === currentOrderResult.order_id;
+ });
+ if (index !== -1) {
+ // Get the old order data
+ const oldOrder = JSON.parse(responseArray[index]);
+
+ // Replace the order property with the new order
+ oldOrder.order = order;
+
+ // Update the order in the array
+ responseArray[index] = JSON.stringify(oldOrder);
+
+ // Save the updated order array back to encrypted storage
+ EncryptedStorage.setItem(
+ 'orderResponses',
+ JSON.stringify(responseArray)
+ ).then(() => {
+ console.log('Order updated in encrypted storage!');
+ });
+ } else {
+ console.log('Order not found in encrypted storage.');
+ }
+ } else {
+ console.log(
+ 'No saved responses found in encrypted storage.'
+ );
+ }
+ })
+ .catch((error) => {
+ console.error(
+ 'Error retrieving saved responses from encrypted storage:',
+ error
+ );
+ });
+ }
+
+ render() {
+ const { navigation, LSPStore } = this.props;
+ const { order, fetchOldOrder } = this.state;
+ const result = order?.result || order;
+
+ return (
+
+ {
+ LSPStore.getOrderResponse = {};
+ LSPStore.error = false;
+ LSPStore.error_msg = '';
+ this.setState({ fetchOldOrder: false });
+ }}
+ navigation={navigation}
+ />
+ {LSPStore.loading ? (
+
+ ) : (
+
+ {fetchOldOrder && (
+
+
+
+ )}
+ {order && Object.keys(order).length > 0 && (
+
+ )}
+
+ )}
+
+ );
+ }
+}
diff --git a/views/Settings/LSPS1/OrdersPane.tsx b/views/Settings/LSPS1/OrdersPane.tsx
new file mode 100644
index 000000000..373fbc5e6
--- /dev/null
+++ b/views/Settings/LSPS1/OrdersPane.tsx
@@ -0,0 +1,287 @@
+import * as React from 'react';
+import EncryptedStorage from 'react-native-encrypted-storage';
+import moment from 'moment';
+import { inject, observer } from 'mobx-react';
+import { StackNavigationProp } from '@react-navigation/stack';
+import { View, FlatList, TouchableOpacity, Text } from 'react-native';
+
+import Header from '../../../components/Header';
+import Screen from '../../../components/Screen';
+import Amount from '../../../components/Amount';
+import LoadingIndicator from '../../../components/LoadingIndicator';
+
+import { themeColor } from '../../../utils/ThemeUtils';
+import { localeString } from '../../../utils/LocaleUtils';
+import BackendUtils from '../../../utils/BackendUtils';
+
+import LSPStore from '../../../stores/LSPStore';
+import NodeInfoStore from '../../../stores/NodeInfoStore';
+
+import { WarningMessage } from '../../../components/SuccessErrorMessage';
+
+interface OrdersPaneProps {
+ navigation: StackNavigationProp;
+ LSPStore: LSPStore;
+ NodeInfoStore: NodeInfoStore;
+}
+
+interface OrdersPaneState {
+ orders: any[];
+ isLoading: boolean;
+}
+
+@inject('LSPStore', 'NodeInfoStore')
+@observer
+export default class OrdersPane extends React.Component<
+ OrdersPaneProps,
+ OrdersPaneState
+> {
+ constructor(props: OrdersPaneProps) {
+ super(props);
+ this.state = {
+ orders: [],
+ isLoading: true
+ };
+ }
+
+ async componentDidMount() {
+ const { navigation, LSPStore } = this.props;
+ navigation.addListener('focus', async () => {
+ try {
+ // Retrieve saved responses from encrypted storage
+ const responseArrayString = await EncryptedStorage.getItem(
+ 'orderResponses'
+ );
+ if (responseArrayString) {
+ const responseArray = JSON.parse(responseArrayString);
+
+ if (responseArray.length === 0) {
+ console.log('No orders found!');
+ this.setState({ isLoading: false });
+ LSPStore.error = true;
+ LSPStore.error_msg = localeString(
+ 'views.LSPS1.noOrdersError'
+ );
+ return;
+ }
+
+ const decodedResponses = responseArray.map((response) =>
+ JSON.parse(response)
+ );
+
+ let selectedOrders;
+ if (BackendUtils.supportsLSPS1customMessage()) {
+ selectedOrders = decodedResponses.filter(
+ (response) =>
+ response?.uri &&
+ response.clientPubkey ===
+ this.props.NodeInfoStore.nodeInfo.nodeId
+ );
+ } else if (BackendUtils.supportsLSPS1rest()) {
+ selectedOrders = decodedResponses.filter(
+ (response) =>
+ response?.endpoint &&
+ response.clientPubkey ===
+ this.props.NodeInfoStore.nodeInfo.nodeId
+ );
+ }
+
+ const orders = selectedOrders.map((response) => {
+ const order =
+ response?.order?.result || response?.order;
+ return {
+ orderId: order?.order_id,
+ state: order?.order_state,
+ createdAt: order?.created_at,
+ fundedAt: order?.channel?.funded_at,
+ lspBalanceSat: order?.lsp_balance_sat
+ };
+ });
+
+ const reversedOrders = orders.reverse();
+
+ this.setState({
+ orders: reversedOrders,
+ isLoading: false
+ });
+ LSPStore.error = false;
+ LSPStore.error_msg = '';
+ } else {
+ this.setState({ isLoading: false });
+ LSPStore.error = true;
+ LSPStore.error_msg = localeString(
+ 'views.LSPS1.noOrdersError'
+ );
+ }
+ } catch (error) {
+ this.setState({ isLoading: false });
+ LSPStore.error = true;
+ LSPStore.error_msg = `An error occurred while retrieving orders: ${error}`;
+ }
+ });
+ }
+
+ renderSeparator = () => (
+
+ );
+
+ renderItem = ({ item }: { item: any }) => {
+ let stateColor;
+ switch (item.state) {
+ case 'CREATED':
+ stateColor = 'orange';
+ break;
+ case 'FAILED':
+ stateColor = 'red';
+ break;
+ case 'COMPLETED':
+ stateColor = 'green';
+ break;
+ default:
+ stateColor = themeColor('text');
+ break;
+ }
+
+ return (
+
+ this.props.navigation.navigate('LSPS1Order', {
+ orderId: item.orderId,
+ orderShouldUpdate:
+ item?.state === 'FAILED' ||
+ item?.state === 'COMPLETED'
+ })
+ }
+ style={{
+ padding: 15
+ }}
+ >
+
+
+ {localeString('views.LSPS1.lspBalance')}
+
+
+
+
+
+ {localeString('general.state')}
+
+
+ {item.state}
+
+
+
+
+ {localeString('general.createdAt')}
+
+
+ {moment(item.createdAt).format(
+ 'MMM Do YYYY, h:mm:ss a'
+ )}
+
+
+ {item.fundedAt && (
+
+
+ {localeString('views.LSPS1.fundedAt')}
+
+
+ {moment(item.fundedAt).format(
+ 'MMM Do YYYY, h:mm:ss a'
+ )}
+
+
+ )}
+
+ );
+ };
+
+ render() {
+ const { navigation, LSPStore } = this.props;
+ const { orders, isLoading } = this.state;
+
+ return (
+
+ {isLoading ? (
+
+
+
+ ) : LSPStore.error && LSPStore.error_msg ? (
+ <>
+ {
+ LSPStore.error = false;
+ LSPStore.error_msg = '';
+ }}
+ navigation={navigation}
+ />
+
+ >
+ ) : (
+ <>
+
+ item?.orderId?.toString()}
+ ItemSeparatorComponent={this.renderSeparator}
+ />
+ >
+ )}
+
+ );
+ }
+}
diff --git a/views/Settings/LSPS1/Settings.tsx b/views/Settings/LSPS1/Settings.tsx
new file mode 100644
index 000000000..bc78bf767
--- /dev/null
+++ b/views/Settings/LSPS1/Settings.tsx
@@ -0,0 +1,336 @@
+import * as React from 'react';
+import { inject, observer } from 'mobx-react';
+import { FlatList, View, Text } from 'react-native';
+import { Icon, ListItem } from 'react-native-elements';
+import { StackNavigationProp } from '@react-navigation/stack';
+
+import Button from '../../../components/Button';
+import Header from '../../../components/Header';
+import Screen from '../../../components/Screen';
+import Switch from '../../../components/Switch';
+import TextInput from '../../../components/TextInput';
+
+import BackendUtils from '../../../utils/BackendUtils';
+import { localeString } from '../../../utils/LocaleUtils';
+import { themeColor } from '../../../utils/ThemeUtils';
+import UrlUtils from '../../../utils/UrlUtils';
+
+import LSPStore from '../../../stores/LSPStore';
+import NodeInfoStore from '../../../stores/NodeInfoStore';
+import SettingsStore, {
+ DEFAULT_LSPS1_PUBKEY_MAINNET,
+ DEFAULT_LSPS1_PUBKEY_TESTNET,
+ DEFAULT_LSPS1_HOST_MAINNET,
+ DEFAULT_LSPS1_HOST_TESTNET,
+ DEFAULT_LSPS1_REST_MAINNET,
+ DEFAULT_LSPS1_REST_TESTNET
+} from '../../../stores/SettingsStore';
+
+import OlympusAnimated from '../../../assets/images/SVG/OlympusAnimated.svg';
+
+interface LSPS1SettingsProps {
+ navigation: StackNavigationProp;
+ LSPStore: LSPStore;
+ NodeInfoStore: NodeInfoStore;
+ SettingsStore: SettingsStore;
+}
+
+interface LSPS1SettingsState {
+ pubkey: string;
+ host: string;
+ restHost: string;
+ lsps1ShowPurchaseButton: boolean;
+}
+
+@inject('LSPStore', 'NodeInfoStore', 'SettingsStore')
+@observer
+export default class LSPS1Settings extends React.Component<
+ LSPS1SettingsProps,
+ LSPS1SettingsState
+> {
+ constructor(props: LSPS1SettingsProps) {
+ super(props);
+ this.state = {
+ pubkey: '',
+ host: '',
+ restHost: '',
+ lsps1ShowPurchaseButton: true
+ };
+ }
+
+ async UNSAFE_componentWillMount() {
+ const { LSPStore, SettingsStore } = this.props;
+ const { getSettings } = SettingsStore;
+ const settings = await getSettings();
+
+ this.setState({
+ pubkey: LSPStore.getLSPS1Pubkey(),
+ host: LSPStore.getLSPS1Host(),
+ restHost: LSPStore.getLSPS1Rest(),
+ lsps1ShowPurchaseButton:
+ settings?.lsps1ShowPurchaseButton !== null
+ ? settings.lsps1ShowPurchaseButton
+ : true
+ });
+ }
+
+ handleReset = async () => {
+ const isTestNet = this.props.NodeInfoStore?.nodeInfo?.isTestNet;
+ this.setState({
+ pubkey: isTestNet
+ ? DEFAULT_LSPS1_PUBKEY_TESTNET
+ : DEFAULT_LSPS1_PUBKEY_MAINNET,
+ host: isTestNet
+ ? DEFAULT_LSPS1_HOST_TESTNET
+ : DEFAULT_LSPS1_HOST_MAINNET,
+ restHost: isTestNet
+ ? DEFAULT_LSPS1_REST_TESTNET
+ : DEFAULT_LSPS1_REST_MAINNET
+ });
+ await this.props.SettingsStore.updateSettings({
+ lsps1RestMainnet: DEFAULT_LSPS1_REST_MAINNET,
+ lsps1RestTestnet: DEFAULT_LSPS1_REST_TESTNET,
+ lsps1PubkeyMainnet: DEFAULT_LSPS1_PUBKEY_MAINNET,
+ lsps1PubkeyTestnet: DEFAULT_LSPS1_PUBKEY_TESTNET,
+ lsps1HostMainnet: DEFAULT_LSPS1_HOST_MAINNET,
+ lsps1HostTestnet: DEFAULT_LSPS1_HOST_TESTNET
+ });
+ };
+
+ render() {
+ const { pubkey, host, restHost, lsps1ShowPurchaseButton } = this.state;
+ const { navigation, SettingsStore, NodeInfoStore } = this.props;
+ const { updateSettings } = SettingsStore;
+ const { nodeInfo } = NodeInfoStore;
+
+ const isOlympusMainnetCustom =
+ !nodeInfo.isTestNet &&
+ pubkey === DEFAULT_LSPS1_PUBKEY_MAINNET &&
+ host === DEFAULT_LSPS1_HOST_MAINNET;
+ const isOlympusTestnetCustom =
+ nodeInfo?.isTestNet &&
+ pubkey === DEFAULT_LSPS1_PUBKEY_TESTNET &&
+ host === DEFAULT_LSPS1_HOST_TESTNET;
+
+ const isOlympusMainnetRest =
+ !nodeInfo.isTestNet && restHost === DEFAULT_LSPS1_REST_MAINNET;
+ const isOlympusTestnetRest =
+ nodeInfo.isTestNet && restHost === DEFAULT_LSPS1_REST_TESTNET;
+
+ const isOlympusCustomMessage =
+ BackendUtils.supportsLSPS1customMessage() &&
+ (isOlympusMainnetCustom || isOlympusTestnetCustom);
+ const isOlympusRest =
+ BackendUtils.supportsLSPS1rest() &&
+ (isOlympusMainnetRest || isOlympusTestnetRest);
+
+ const isOlympus = isOlympusCustomMessage || isOlympusRest;
+
+ return (
+
+
+
+ {BackendUtils.supportsLSPS1customMessage() && (
+ <>
+
+ {localeString('views.OpenChannel.nodePubkey')}
+
+ {
+ this.setState({ pubkey: text });
+ await updateSettings(
+ nodeInfo?.isTestNet
+ ? {
+ lsps1PubkeyTestnet: text
+ }
+ : {
+ lsps1PubkeyMainnet: text
+ }
+ );
+ }}
+ />
+
+
+ {localeString('views.OpenChannel.host')}
+
+ {
+ this.setState({ host: text });
+ await updateSettings(
+ nodeInfo?.isTestNet
+ ? {
+ lsps1HostTestnet: text
+ }
+ : {
+ lsps1HostMainnet: text
+ }
+ );
+ }}
+ />
+ >
+ )}
+ {BackendUtils.supportsLSPS1rest() && (
+ <>
+
+ {localeString('general.lsp')}
+
+ {
+ this.setState({ restHost: text });
+ await updateSettings(
+ nodeInfo?.isTestNet
+ ? {
+ lsps1RestTestnet: text
+ }
+ : {
+ lsps1RestMainnet: text
+ }
+ );
+ }}
+ />
+ >
+ )}
+
+ {!isOlympus && (
+
+
+
+ {isOlympus && (
+
+
+
+ )}
+
+ (
+ {
+ if (item.nav) navigation.navigate(item.nav);
+ if (item.url) UrlUtils.goToUrl(item.url);
+ }}
+ >
+
+
+ {item.label}
+
+
+
+
+ )}
+ keyExtractor={(item, index) => `${item.label}-${index}`}
+ ItemSeparatorComponent={
+
+ }
+ />
+
+
+ );
+ }
+}
diff --git a/views/Settings/LSPS1/index.tsx b/views/Settings/LSPS1/index.tsx
new file mode 100644
index 000000000..96c1594cb
--- /dev/null
+++ b/views/Settings/LSPS1/index.tsx
@@ -0,0 +1,1154 @@
+import * as React from 'react';
+import { inject, observer } from 'mobx-react';
+import {
+ NativeEventEmitter,
+ NativeModules,
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity
+} from 'react-native';
+import EncryptedStorage from 'react-native-encrypted-storage';
+import Slider from '@react-native-community/slider';
+import { StackNavigationProp } from '@react-navigation/stack';
+import { v4 as uuidv4 } from 'uuid';
+
+import CaretDown from '../../../assets/images/SVG/Caret Down.svg';
+import CaretRight from '../../../assets/images/SVG/Caret Right.svg';
+import OrderList from '../../../assets/images/SVG/order-list.svg';
+
+import Header from '../../../components/Header';
+import Screen from '../../../components/Screen';
+import TextInput from '../../../components/TextInput';
+import Button from '../../../components/Button';
+import KeyValue from '../../../components/KeyValue';
+import Switch from '../../../components/Switch';
+import { ErrorMessage } from '../../../components/SuccessErrorMessage';
+import { Row } from '../../../components/layout/Row';
+
+import BackendUtils from '../../../utils/BackendUtils';
+import { themeColor } from '../../../utils/ThemeUtils';
+import { localeString } from '../../../utils/LocaleUtils';
+
+import LSPStore from '../../../stores/LSPStore';
+import InvoicesStore from '../../../stores/InvoicesStore';
+import ChannelsStore from '../../../stores/ChannelsStore';
+import SettingsStore from '../../../stores/SettingsStore';
+import FiatStore from '../../../stores/FiatStore';
+import NodeInfoStore from '../../../stores/NodeInfoStore';
+import { Icon } from 'react-native-elements';
+import LoadingIndicator from '../../../components/LoadingIndicator';
+import LSPS1OrderResponse from '../../../components/LSPS1OrderResponse';
+
+interface LSPS1Props {
+ LSPStore: LSPStore;
+ InvoicesStore: InvoicesStore;
+ ChannelsStore: ChannelsStore;
+ SettingsStore: SettingsStore;
+ FiatStore: FiatStore;
+ NodeInfoStore: NodeInfoStore;
+ navigation: StackNavigationProp;
+}
+
+interface LSPS1State {
+ lspBalanceSat: any;
+ clientBalanceSat: any;
+ requiredChannelConfirmations: any;
+ confirmsWithinBlocks: any;
+ channelExpiryBlocks: any;
+ token: any;
+ refundOnchainAddress: any;
+ announceChannel: boolean;
+ showInfo: boolean;
+ advancedSettings: boolean;
+}
+
+@inject(
+ 'LSPStore',
+ 'ChannelsStore',
+ 'InvoicesStore',
+ 'SettingsStore',
+ 'FiatStore',
+ 'NodeInfoStore'
+)
+@observer
+export default class LSPS1 extends React.Component {
+ listener: any;
+ constructor(props: LSPS1Props) {
+ super(props);
+ this.state = {
+ lspBalanceSat: 0,
+ clientBalanceSat: '0',
+ requiredChannelConfirmations: '8',
+ confirmsWithinBlocks: '6',
+ channelExpiryBlocks: 0,
+ token: '',
+ refundOnchainAddress: '',
+ showInfo: false,
+ advancedSettings: false,
+ announceChannel: false
+ };
+ }
+
+ encodeMesage = (n: any) => Buffer.from(JSON.stringify(n)).toString('hex');
+
+ async componentDidMount() {
+ const { LSPStore } = this.props;
+ LSPStore.resetLSPS1Data();
+ if (BackendUtils.supportsLSPS1rest()) {
+ LSPStore.getInfoREST();
+ } else {
+ console.log('connecting');
+ await this.connectPeer();
+ console.log('connected');
+ await this.subscribeToCustomMessages();
+ this.sendCustomMessage_lsps1();
+ }
+ }
+
+ subscribeToCustomMessages() {
+ if (
+ this.props.SettingsStore.implementation === 'lightning-node-connect'
+ ) {
+ const { LncModule } = NativeModules;
+ const eventName = BackendUtils.subscribeCustomMessages();
+ const eventEmitter = new NativeEventEmitter(LncModule);
+ this.listener = eventEmitter.addListener(
+ eventName,
+ (event: any) => {
+ if (event.result) {
+ try {
+ const result = JSON.parse(event.result);
+ this.props.LSPStore.handleCustomMessages(result);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ }
+ );
+ return;
+ } else {
+ return new Promise((resolve, reject) => {
+ this.props.LSPStore.subscribeCustomMessages()
+ .then((response) => {
+ console.log('Subscribed to custom messages:', response);
+ resolve({});
+ })
+ .catch((error) => {
+ console.error(
+ 'Error subscribing to custom messages:',
+ error
+ );
+ reject();
+ });
+ });
+ }
+ }
+
+ sendCustomMessage_lsps1() {
+ const { LSPStore } = this.props;
+ LSPStore.loading = true;
+ LSPStore.error = false;
+ LSPStore.error_msg = '';
+ const node_pubkey_string: string = LSPStore.getLSPS1Pubkey();
+ const type = 37913;
+ const id = uuidv4();
+ LSPStore.getInfoId = id;
+ const data = this.encodeMesage({
+ jsonrpc: '2.0',
+ method: 'lsps1.get_info',
+ params: {},
+ id: LSPStore.getInfoId
+ });
+
+ LSPStore.sendCustomMessage({
+ peer: node_pubkey_string,
+ type,
+ data
+ })
+ .then((response) => {
+ console.log('Custom message sent:', response);
+ })
+ .catch((error) => {
+ console.error(
+ 'Error sending (get_info) custom message:',
+ error
+ );
+ });
+ }
+
+ lsps1_createorder = () => {
+ const { LSPStore } = this.props;
+ const node_pubkey_string: string = LSPStore.getLSPS1Pubkey();
+ const type = 37913;
+ const id = uuidv4();
+ LSPStore.createOrderId = id;
+ LSPStore.loading = true;
+ LSPStore.error = false;
+ LSPStore.error_msg = '';
+ const data = this.encodeMesage({
+ jsonrpc: '2.0',
+ method: 'lsps1.create_order',
+ params: {
+ lsp_balance_sat: this.state.lspBalanceSat,
+ client_balance_sat: this.state.clientBalanceSat.toString(),
+ required_channel_confirmations: parseInt(
+ this.state.requiredChannelConfirmations
+ ),
+ funding_confirms_within_blocks: parseInt(
+ this.state.confirmsWithinBlocks
+ ),
+ channel_expiry_blocks: this.state.channelExpiryBlocks,
+ token: this.state.token,
+ refund_onchain_address: this.state.refundOnchainAddress,
+ announce_channel: this.state.announceChannel
+ },
+ id: LSPStore.createOrderId
+ });
+
+ LSPStore.sendCustomMessage({
+ peer: node_pubkey_string,
+ type,
+ data
+ })
+ .then(() => {})
+ .catch((error) => {
+ console.error(
+ 'Error sending (create_order) custom message:',
+ error
+ );
+ });
+ };
+
+ connectPeer = async () => {
+ const { ChannelsStore, LSPStore } = this.props;
+ const node_pubkey_string: string = LSPStore.getLSPS1Pubkey();
+ const host: string = LSPStore.getLSPS1Host();
+ try {
+ return await ChannelsStore.connectPeer(
+ {
+ node_pubkey_string,
+ host,
+ local_funding_amount: ''
+ },
+ true,
+ true
+ );
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ render() {
+ const { navigation, LSPStore, InvoicesStore, FiatStore } = this.props;
+ const {
+ showInfo,
+ advancedSettings,
+ lspBalanceSat,
+ clientBalanceSat,
+ channelExpiryBlocks
+ } = this.state;
+ const { getInfoData, createOrderResponse } = LSPStore;
+ const options = getInfoData?.result?.options || getInfoData?.options;
+ const result = createOrderResponse?.result || createOrderResponse;
+ const payment = result?.payment;
+
+ const OrderlistBtn = () => (
+ {
+ navigation.navigate('OrdersPane');
+ }}
+ accessibilityLabel={localeString('general.add')}
+ >
+
+
+ );
+
+ const SettingsBtn = () => (
+
+ {
+ this.props.navigation.navigate('LSPS1Settings');
+ }}
+ color={themeColor('text')}
+ underlayColor="transparent"
+ size={33}
+ />
+
+ );
+
+ if (lspBalanceSat === 0 && options?.min_initial_lsp_balance_sat) {
+ this.setState({
+ lspBalanceSat: parseInt(options.min_initial_lsp_balance_sat)
+ });
+ }
+ if (
+ clientBalanceSat === 0 &&
+ options?.min_initial_client_balance_sat > 0
+ ) {
+ this.setState({
+ clientBalanceSat: parseInt(
+ options.min_initial_client_balance_sat
+ )
+ });
+ }
+
+ if (channelExpiryBlocks === 0 && options?.max_channel_expiry_blocks) {
+ this.setState({
+ channelExpiryBlocks: parseInt(options.max_channel_expiry_blocks)
+ });
+ }
+
+ return (
+
+
+
+ {!LSPStore.loading && !LSPStore.error && (
+
+ )}
+
+ }
+ onBack={() => LSPStore.resetLSPS1Data()}
+ />
+
+ {BackendUtils.supportsLSPS1customMessage() &&
+ !LSPStore.getLSPS1Pubkey() &&
+ !LSPStore.getLSPS1Host() && (
+
+ )}
+
+ {LSPStore?.error &&
+ LSPStore?.error_msg &&
+ LSPStore?.error_msg !==
+ localeString('views.LSPS1.timeoutError') && (
+
+ )}
+
+ {LSPStore.loading ? (
+
+ ) : (LSPStore?.error && LSPStore?.error_msg) ===
+ localeString('views.LSPS1.timeoutError') ? (
+
+ ) : (
+ <>
+
+ {createOrderResponse &&
+ Object.keys(createOrderResponse).length > 0 &&
+ result &&
+ payment && (
+
+ )}
+
+ {Object.keys(createOrderResponse).length == 0 && (
+
+
+ {`${localeString(
+ 'views.LSPS1.initialLSPBalance'
+ )} (${localeString('general.sats')})`}
+
+ {
+ const intValue = parseInt(
+ text.replace(/,/g, ''),
+ 10
+ );
+ if (isNaN(intValue)) return;
+ this.setState({
+ lspBalanceSat: intValue
+ });
+ }}
+ keyboardType="numeric"
+ />
+
+
+ {FiatStore.numberWithCommas(
+ options?.min_initial_lsp_balance_sat
+ )}
+
+
+ {FiatStore.numberWithCommas(
+ options?.max_initial_lsp_balance_sat
+ )}
+
+
+
+ this.setState({
+ lspBalanceSat: value
+ })
+ }
+ step={10000}
+ />
+
+ {Object.keys(getInfoData).length > 0 && (
+ {
+ this.setState({
+ showInfo: !showInfo
+ });
+ }}
+ >
+
+
+
+
+
+ {showInfo ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+ {showInfo &&
+ getInfoData &&
+ Object.keys(createOrderResponse)
+ .length == 0 &&
+ Object.keys(getInfoData).length > 0 &&
+ options && (
+ <>
+ {options?.max_channel_balance_sat &&
+ options?.min_channel_balance_sat && (
+
+ )}
+ {options?.max_initial_client_balance_sat !==
+ '0' &&
+ options?.min_initial_client_balance_sat !==
+ '0' && (
+
+ )}
+
+ {options?.max_initial_lsp_balance_sat &&
+ options?.min_initial_lsp_balance_sat && (
+
+ )}
+ {options?.max_channel_expiry_blocks && (
+
+ )}
+ {options?.min_channel_confirmations && (
+
+ )}
+ {options?.min_onchain_payment_confirmations && (
+
+ )}
+ {options?.min_onchain_payment_size_sat && (
+
+ )}
+ {options?.supports_zero_channel_reserve !==
+ null && (
+
+ )}
+ >
+ )}
+
+ {Object.keys(getInfoData).length > 0 && (
+ {
+ this.setState({
+ advancedSettings:
+ !advancedSettings
+ });
+ }}
+ >
+
+
+
+
+
+ {advancedSettings ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+ {advancedSettings && (
+ <>
+ {options?.max_initial_client_balance_sat !==
+ '0' && (
+ <>
+
+ {`${localeString(
+ 'views.LSPS1.initialClientBalance'
+ )} (${localeString(
+ 'general.sats'
+ )})`}
+
+ {
+ const intValue =
+ parseInt(text);
+ if (isNaN(intValue))
+ return;
+ this.setState({
+ clientBalanceSat:
+ intValue
+ });
+ }}
+ keyboardType="numeric"
+ />
+
+
+ {FiatStore.numberWithCommas(
+ options?.min_initial_client_balance_sat
+ )}
+
+
+ {FiatStore.numberWithCommas(
+ options?.max_initial_client_balance_sat
+ )}
+
+
+
+ this.setState({
+ clientBalanceSat:
+ value
+ })
+ }
+ step={10000}
+ />
+ >
+ )}
+
+ {localeString(
+ 'views.LSPS1.requiredChannelConfirmations'
+ )}
+
+
+ this.setState({
+ requiredChannelConfirmations:
+ text
+ })
+ }
+ style={styles.textInput}
+ keyboardType="numeric"
+ />
+
+
+ {localeString(
+ 'views.LSPS1.confirmWithinBlocks'
+ )}
+
+
+ this.setState({
+ confirmsWithinBlocks:
+ text
+ })
+ }
+ style={styles.textInput}
+ keyboardType="numeric"
+ />
+
+
+ {localeString(
+ 'views.LSPS1.channelExpiryBlocks'
+ )}
+
+ {
+ const intValue = parseInt(
+ text.replace(/,/g, ''),
+ 10
+ );
+ if (isNaN(intValue)) return;
+ this.setState({
+ channelExpiryBlocks:
+ intValue
+ });
+ }}
+ style={styles.textInput}
+ keyboardType="numeric"
+ />
+
+ this.setState({
+ channelExpiryBlocks:
+ value
+ })
+ }
+ step={10}
+ />
+
+
+ {localeString(
+ 'views.LSPS1.token'
+ )}
+
+
+ this.setState({
+ token: text
+ })
+ }
+ style={styles.textInput}
+ />
+
+ {options?.min_onchain_payment_confirmations && (
+ <>
+
+ {localeString(
+ 'views.LSPS1.refundOnchainAddress'
+ )}
+
+
+ this.setState({
+ refundOnchainAddress:
+ text
+ })
+ }
+ style={styles.textInput}
+ />
+ >
+ )}
+
+
+ {localeString(
+ 'views.OpenChannel.announceChannel'
+ )}
+
+ {
+ this.setState({
+ announceChannel:
+ !this.state
+ .announceChannel
+ });
+ }}
+ />
+
+ >
+ )}
+
+ )}
+
+
+
+ >
+ )}
+
+ {!LSPStore.loading &&
+ LSPStore.error &&
+ LSPStore.error_msg ===
+ localeString('views.LSPS1.timeoutError') && (
+
+
+ )}
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ textInput: {
+ marginBottom: 25
+ }
+});
diff --git a/views/Settings/LSPServicesList.tsx b/views/Settings/LSPServicesList.tsx
new file mode 100644
index 000000000..c26bae29e
--- /dev/null
+++ b/views/Settings/LSPServicesList.tsx
@@ -0,0 +1,86 @@
+import * as React from 'react';
+import { FlatList, View } from 'react-native';
+import { Icon, ListItem } from 'react-native-elements';
+
+import Header from '../../components/Header';
+import Screen from '../../components/Screen';
+
+import { localeString } from '../../utils/LocaleUtils';
+import { themeColor } from '../../utils/ThemeUtils';
+
+interface LSPServicesListProps {
+ navigation: any;
+}
+
+function LSPServicesList(props: LSPServicesListProps) {
+ const { navigation } = props;
+
+ const renderSeparator = () => (
+
+ );
+
+ const LSP_ITEMS = [
+ {
+ label: localeString('view.Settings.LSPServicesList.flow2'),
+ nav: 'LSPSettings'
+ },
+ {
+ label: localeString('view.Settings.LSPServicesList.lsps1'),
+ nav: 'LSPS1Settings'
+ }
+ ];
+
+ return (
+
+
+ (
+ {
+ navigation.navigate(item.nav);
+ }}
+ >
+
+
+ {item.label}
+
+
+
+
+ )}
+ keyExtractor={(item, index) => `${item.label}-${index}`}
+ ItemSeparatorComponent={renderSeparator}
+ />
+
+ );
+}
+
+export default LSPServicesList;
diff --git a/views/Settings/Settings.tsx b/views/Settings/Settings.tsx
index dcc707182..8eb40a4bd 100644
--- a/views/Settings/Settings.tsx
+++ b/views/Settings/Settings.tsx
@@ -274,9 +274,19 @@ export default class Settings extends React.Component<
>
- navigation.navigate('LSPSettings')
- }
+ onPress={() => {
+ const supportsLSPS1 =
+ BackendUtils.supportsLSPS1customMessage() ||
+ BackendUtils.supportsLSPS1rest();
+ if (
+ BackendUtils.supportsLSPs() &&
+ supportsLSPS1
+ ) {
+ navigation.navigate('LSPServicesList');
+ } else {
+ navigation.navigate('LSPSettings');
+ }
+ }}
>