Integrating LSPS1 into ZEUS

Shubham Kumar
8 min readJul 7, 2024

--

Lightning Service Provider (LSP)

Note: This blog post is designed to be beginner-friendly, so if you’re new to the Lightning Network or LSPs, you’re in the right place! I will provide a thorough explanation, starting with an overview of the LSPS1 specifications and then detailing how we’ve implemented these in our ZEUS app.

What is a Lightning Service Provider (LSP)?

A Lightning Service Provider (LSP) is a crucial component of the Lightning Network ecosystem. The Lightning Network is a second layer on top of the Bitcoin blockchain that enables faster and cheaper transactions. However, to use Lightning Network, you need to open channels, manage liquidity, and handle various technical aspects, which can be quite complex for regular users.

This is where LSPs come in. LSPs simplify the process by offering services that handle all the technical details for you. Here’s a breakdown of what LSPs do:

  1. Channel Management and providing liquidity: LSPs play a crucial role in Lightning Network by managing payment channels between users. LSPs provide liquidity to users by opening channels with enough Bitcoin to enable smooth transactions. Without sufficient liquidity, transactions can fail or be delayed. LSPs ensure that there is always enough liquidity available.
  2. Routing Assistance: LSPs help in finding the best routes for your transactions to ensure they are completed quickly and at the lowest possible cost.
  3. Reliability and uptime: LSPs offer security features and ensure that the channels remain reliable. They monitor the network for any issues and take steps to resolve them, providing a more secure and stable experience for users.

In summary, LSPs make the Lightning Network accessible to everyone by taking care of the complex backend operations.

What is LSPS1 ?

LSPS1 is a new set of rules (API specification) for LSPs. These rules help different Lightning wallets and LSPs talk to each other in the same language. Before LSPS1, each LSP might have its own way of doing things, making it hard for wallets to work with different LSPs. With LSPS1, there’s a standard way for wallets to ask LSPs for channels. This makes everything easier and more consistent. You can read more about the LSPS1 specification here.

OLYMPUS by ZEUS: Our own Lightning Service Provider :

ZEUS introduces its Lightning Service Provider (LSP) known as OLYMPUS by ZEUS. OLYMPUS enables Lightning Network users to purchase and manage payment channels directly through the ZEUS wallet. Previously, users could access just-in-time channels from OLYMPUS, enhancing transaction efficiency. Learn more about the just-in-time channel feature here.

Now, with the implementation of LSPS1 specifications, users can purchase channels in advance.

Understanding LSPS1 Specifications and Channel Purchase Flow:

LSPS1 is built on the premise that clients trust the LSP to fulfill their commitments. Since channel purchases aren’t atomic, there’s a risk for clients that they may not receive what was promised if the LSP acts maliciously. Here’s a simplified outline of the order flow:

  • Clients begin by using lsps1.get_info to retrieve the LSP’s available options.
  • Clients proceed to create an order using lsps1.create_order.
  • Payment for the order can be made either on-chain or off-chain.
  • Once payment is confirmed, the LSP promptly opens the channel.
  • In case the channel opening fails, the LSP initiates a refund process for the client’s payment.

Implementing LSPS1 using REST calls

Now, let’s go ahead with explaining the REST method we used to implement LSPS1 in our app.

STEP 1 — `get_info` REST call:

The getInfoREST function fetches information from LSP using a REST call. This call retrieves the available options from the LSP, which are needed for creating an order for a channel request.

Here’s a detailed breakdown of the code:

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;
});
};
  1. Endpoint Definition:
const endpoint = `${this.getLSPS1Rest()}/api/v1/get_info`;

This line creates the URL for the REST call. For testnet, this would be https://testnet-lsps1.lnolymp.us/api/v1/get_info, and for mainnet, it would be https://lsps1.lnolymp.us/api/v1/get_info.

2. Handling the response:

.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;
}
})
  • If the response status is 200 (OK), the response data is parsed from JSON and stored.
  • It extracts the pubkey from the first URI in the uris array and assigns it to this.pubkey so we can use it in our create order channel requests later.
  • If an error occurs during parsing or extracting the pubkey, it is caught silently.
  • The loading state is set to false once the data is processed.

3. Error handling:

.catch(() => {
this.error = true;
this.error_msg = 'Error fetching get_info data';
this.loading = false;
});
  • If the fetch request fails, it sets the error state and an error message.

Sample Response:

When the get_info call is successful, the response data might look like this:

{
"min_channel_balance_sat": "100000",
"max_channel_balance_sat": "10000000",
"min_initial_lsp_balance_sat": "100000",
"max_initial_lsp_balance_sat": "10000000",
"min_initial_client_balance_sat": "0",
"max_initial_client_balance_sat": "0",
"max_channel_expiry_blocks": 26280,
"min_funding_confirms_within_blocks": 6,
"min_required_channel_confirmations": 6,
"supports_zero_channel_reserve": false,
"uris": [
"031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581@45.79.192.236:9735",
"031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581@r46dwvxcdri754hf6n3rwexmc53h5x4natg5g6hidnxfzejm5xrqn2id.onion:9735"
]
}

Here, the uris field provides the connection points for the client's node to connect to the LSP before proceeding to create an order for a channel request.

STEP 2 — create_order REST Call:

The createOrderREST function is responsible for sending a POST request to the LSP to create an order for a new payment channel. Here’s an explanation of how this function works:

public createOrderREST = (state: any) => {
const data = JSON.stringify({
lsp_balance_sat: state.lspBalanceSat.toString(),
client_balance_sat: state.clientBalanceSat.toString(),
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 = errorToUserFriendly(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;
});
};

Explanation:

Data Preparation:

  • The function prepares the data payload (data) required for the create_order request. It includes:
  • lsp_balance_sat: The amount of satoshis the LSP will contribute to the channel.
  • client_balance_sat: The amount of satoshis the client will contribute to the channel.
  • required_channel_confirmations: Number of confirmations required for the channel.
  • funding_confirms_within_blocks: Number of blocks within which funding must be confirmed.
  • channel_expiry_blocks: Number of blocks until the channel expires.
  • token: Field for a coupon code or authentication token.
  • refund_onchain_address: Address to which the client's refund will be sent if necessary.
  • announce_channel: Whether to announce the channel to the network.
  • public_key: Public key of the client's node.

Sending the Request:

  • The function constructs the endpoint URL for the create_order API.
  • It uses ReactNativeBlobUtil.fetch to send a POST request to the endpoint with the JSON data.

Handling the Response:

  • Upon receiving a response, it parses the JSON response (responseData).
  • If responseData.error exists, it sets the error state (this.error) and formats the error message (this.error_msg) for display to the user.
  • If successful, it stores the response (this.createOrderResponse).

Sample Response

When a successful create_order call is made, the response might look like this:

{
"announce_channel": false,
"channel": null,
"channel_expiry_blocks": 26280,
"funding_confirms_within_blocks": 6,
"created_at": "2024-07-15T18:16:26.129Z",
"lsp_balance_sat": "100000",
"client_balance_sat": "0",
"order_id": "299813af0b0ed5852158a4567b23635f",
"order_state": "CREATED",
"payment": {
"bolt11": {
"order_total_sat": "7875",
"fee_total_sat": "7875",
"invoice": "lnbc78750n1pnf2em6pp5cyznyndmwlv0lhxr4mpe5hrxuva2yzvtykdvqtsvd328946rml4sdyzgd5xzmnwv4kzqvpwxqcrzvpsxqcrqgr5dusryvpjx5knqvfdxy69gvpk8gcnvw3jxchrzvp4tgszsv3e8yurzvmpvccxyvr9vs6nsdfjxy6nscf5x5mrwc3jxvmrxdtx9ycqzzsxqrrsssp5hde8s8mnwq4jp4svrqlzfhcqrqce2e0gfghfaqsqayugxmglgesq9qxpqysgqu7ypplx35sjpszp3sehs6r3mlxvmwkskv373f3lgjg4tk3rf3s4yhy8thcvvdlaca6ry8hzv3qu0z35uszdpchjjuyunlgcjdcezrcgp4pq3ap",
"expires_at": "2024-07-15T19:16:26.129Z",
"state": "EXPECT_PAYMENT"
}
},
"token": ""
}
  • announce_channel: Indicates whether the channel will be announced to the network.
  • channel: Details of the created channel (if successful).
  • channel_expiry_blocks: Number of blocks until the channel expires.
  • funding_confirms_within_blocks: Number of blocks within which funding must be confirmed.
  • created_at: Timestamp when the order was created.
  • lsp_balance_sat: Amount of satoshis the LSP will contribute to the channel.
  • client_balance_sat: Amount of satoshis the client will contribute to the channel.
  • order_id: Unique identifier for the order which we will use to retrieve the order info in the get_order call later in the blog.
  • order_state: Current state of the order (CREATED in this case).
  • payment: Details of the payment required to initiate the channel opening process.
  • bolt11: Lightning Network BOLT 11 invoice details.
  • order_total_sat: Total amount in satoshis required for the payment.
  • fee_total_sat: Total fee in satoshis included in the payment.
  • invoice: Lightning invoice that the client needs to pay.
  • expires_at: Timestamp when the payment invoice expires.
  • state: Current state of the payment (EXPECT_PAYMENT in this case).
  • token: Field for a coupon code or authentication token.

STEP 3 — get_order REST Call

The getOrderREST function is designed to retrieve details about a specific order from the LSP using a GET request. It takes order_id as a param, which represents the unique identifier of the order.

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;
}
this.loading = false; // Ensure loading indicator is turned off
})
.catch((error) => {
console.error('Error sending custom message:', error);
this.error = true;
this.error_msg = errorToUserFriendly(error);
this.loading = false;
});
}

Explanation:

Endpoint Construction:

  • The function constructs the endpoint URL for the get_order API using the provided id which is the order id of the order request.

Sending the Request:

  • Uses ReactNativeBlobUtil.fetch to send a GET request to the endpoint.

Handling the Response:

  • Upon receiving a response, it parses the JSON response (responseData).
  • If responseData.error exists, sets the error state (this.error) and assigns the error message (responseData.message) to this.error_msg.
  • If successful, stores the response data (this.getOrderResponse).

Response: The response data structure is identical to the createOrderREST API's response.

LSPS1 Feature Demo in ZEUS

Here’s a video demo of ZEUS demonstrating the implementation of the LSPS1 feature, where we request a channel in advance. Check it out to see how it works!

In the LSPS1 view, we can see the service info about what the LSP is providing, including the initial LSP and client balance we can set.

  1. We set the LSP balance to around 3.2 million sats and 1 million sats from the client side.
  2. We create the order, review the order request, and pay the total order value.
  3. We return to the LSPS1 order view to check our order status.
  4. We then go to the channel listing view, where we can see our channel listed with 1 million sats outbound and 3.2 million sats inbound.

Web Portal for LSPS1 Functionality

In addition to the LSPS1 feature in our ZEUS app, I have developed a web portal as well, that offers the same functionality. This web portal allows users to interact with the LSPS1 features directly from their browser, making it even more accessible and convenient.

Check out the demo video below to see how you can use the web portal to request channels in advance and manage your lightning channels.

Conclusion

In this blog post, we’ve explored the LSPS1 specification and how we’ve implemented it in our ZEUS app using the REST method. We’ve also shared demo videos showcasing the new features in action, both in the ZEUS app and on our web portal.

Stay tuned for the upcoming features of ZEUS. Thank you for reading! If you have any questions or feedback, feel free to reach out to us. Don’t forget to follow me and ZEUS on X for the latest news and updates. Happy Lightning!

--

--