Reconciliation, Error Handling, and Testing
Welcome to the third installment of our Lightspark Connect Developer Guide series. In this developer guide, we'll focus on maintaining a robust Lightning implementation: reconciliation, error handling, and testing.
Building on the foundation laid in our previous guides on authentication, funding, withdrawals, and payment processing, we'll explore how to ensure your Lightspark Connect integration remains accurate, resilient, and production-ready.
Reconciliation is the process of ensuring that your transactions on Lightspark and in your application are synchronized. Maintaining consistency between both systems is crucial to ensure they represent the same financial state. To verify consistency, you can query Lightspark's transactions for a specific time period.
It’s sometimes helpful to start by checking your balance on the Lightspark network. Remember, there are different types of balances available. Here's a refresher of what those balances mean:
-
owned_balance: This represents the total amount you currently own, including funds that may not be immediately available (e.g., in-flight outgoing payments, in-flight withdrawals, commit fees). It's a snapshot of what you officially own at this instant.
-
available_to_send_balance: This represents the amount you can send on the Lightning network right now. It excludes funds that are temporarily locked (e.g., channel reserves).
-
available_to_withdraw_balance: This represents the amount you can withdraw to the Bitcoin network right now. It's usually equal to the owned_balance but doesn't include in-flight operations.
You can query these balances like the following:
const nodeId = "your_node_id_here"; // Replace with your actual node ID
const query = getLightsparkNodeQuery(nodeId);
const response = await client.executeQuery(query);
if (response && response.balances) {
console.log(`Owned balance: ${response.balances.ownedBalance.originalValue} ${response.balances.ownedBalance.originalUnit}`);
console.log(`Available to send: ${response.balances.availableToSendBalance.originalValue} ${response.balances.availableToSendBalance.originalUnit}`);
console.log(`Available to withdraw: ${response.balances.availableToWithdrawBalance.originalValue} ${response.balances.availableToWithdrawBalance.originalUnit}`);
} else {
console.log("Unable to fetch balances");
}
This query retrieves the different balances on the node. Refer to the LightsparkNode documentation for a complete list of node properties.
To retrieve a list of transactions for reconciliation, you can use the
getTransactions function. This function allows you to filter transactions based on various criteria, including date range, transaction types, and Bitcoin network. Here's an example of how to query transactions within a specific date range:const account = await client.getCurrentAccount();
if (account) {
const startDate = new Date('2024-01-01T00:00:00Z').toISOString();
const endDate = new Date('2024-03-31T23:59:59Z').toISOString();
const transactions = await account.getTransactions(
client,
100, // Limit to 100 transactions
undefined, // No cursor for pagination
undefined, // No specific transaction types filter
startDate, // Start date of the range
endDate, // End date of the range
BitcoinNetwork.MAINNET, // Specify the Bitcoin network
);
console.log(`Total transactions: ${transactions.count}`);
transactions.entities.forEach(transaction => {
console.log(`Transaction ID: ${transaction.id}`);
console.log(`Amount: ${transaction.amount.originalValue} ${transaction.amount.originalUnit}`);
console.log(`Status: ${transaction.status}`);
console.log('---');
});
} else {
console.log("Unable to fetch account");
}
This query fetches the first 100 transactions for the account. You can adjust the parameters to filter by transaction type, status, or Bitcoin network. If you have more than 100 transactions, you can use the cursor to iterate through multiple pages.
Each transaction will include the direction, amount, and routing fees paid, if applicable. You can then iterate through the list of transactions and verify that both your systems and Lightspark have recorded the same set of transactions for the period.
Here's an example of the JSON that's returned by this request:
{
"entities": [
{
"id": "Transaction:01234567-89ab-cdef-0123-456789abcdef",
"created_at": "2024-03-15T10:30:00Z",
"updated_at": "2024-03-15T10:35:00Z",
"status": "SUCCESS",
"resolved_at": "2024-03-15T10:35:00Z",
"amount": {
"original_value": 100000,
"original_unit": "SATOSHI",
"preferred_currency_unit": "USD",
"preferred_currency_value_rounded": 3456,
"preferred_currency_value_approx": 3456.78
},
"transaction_hash": "3a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
"typename": "IncomingPayment"
},
{
"id": "Transaction:fedcba98-7654-3210-fedc-ba9876543210",
"created_at": "2024-03-14T15:45:00Z",
"updated_at": "2024-03-14T15:45:00Z",
"status": "PENDING",
"resolved_at": null,
"amount": {
"original_value": 50000,
"original_unit": "SATOSHI",
"preferred_currency_unit": "USD",
"preferred_currency_value_rounded": 1728,
"preferred_currency_value_approx": 1728.39
},
"transaction_hash": null,
"typename": "OutgoingPayment"
}
],
"count": 2
}
Robust error handling is essential for maintaining a great user experience and reliable Lightning Network implementation. Let's explore common error scenarios and best practices for handling them.
| Error | Description | Recommendation |
|---|---|---|
| NO_ROUTE | No route available at the current fee rate | This can typically be caused by three issues: 1. The max routing fee is set too low, increase the maximum fee to which ever is greater: 5 sats or 17 bps and retry the payment. 2. The receiving node is offline. For example, a self custody mobile node. 3. The receiving node is not connected to the network |
| INSUFFICIENT_BALANCE | Not enough local balance to send the payment | Fund your node with more bitcoin |
| INVOICE_ALREADY_PAID | The invoice has already been paid | Check if the payment was previously successful |
| INVOICE_EXPIRED | The invoice has expired | Request a new invoice from the recipient |
| INVOICE_CANCELLED | The invoice has been canceled | Request a new invoice from the recipient |
| RISK_SCREENING_FAILED | The recipient failed risk screening | Review your compliance settings and the recipient's information |
| INCORRECT_PAYMENT_DETAILS | Invalid payment details (e.g., invalid amount, or problem generating valid invoice) | Verify the payment details and try again |
| TIMEOUT | The payment timed out | Retry the payment with exponential backoff |
| ERROR | A non-recoverable error occurred | Check logs for details and contact support if persistent |
| Error | Description | Recommendation |
|---|---|---|
| INSUFFICIENT_FUNDS | Not enough funds to open a channel | Ensure you have at least 50,000 satoshis for testing on mainnet |
| Error | Description | Recommendation |
|---|---|---|
| INSUFFICIENT_BALANCE | Not enough balance to complete the withdrawal | Check your available balance and adjust the withdrawal amount |
You can implement retries to overcome temporary failures due to network instability or intermittent service disruptions.
Lightspark supports idempotency for API calls. This means you'll receive the same result if you make the same call with identical parameters. This feature is particularly useful when dealing with network failures during a request. If a network issue occurs mid-request, you can resend the existing request without the risk of creating duplicate transactions or operations. Our system will recognize the repeated request and respond with the result from the original call, preventing unintended side effects. You can set the
idempotency-key when calling any method that moves funds off your node such as payInvoiceHere's an example of how to pay an invoice with an
idempotency-key:const idempotencyKey = "unique_payment_id_123"
const paymentResult = await client.payInvoice({
nodeId: "your_node_id",
encodedInvoice: "lnbc...", // The invoice you're paying
amountMsats: 1000000, // Amount in millisatoshis
timeoutSecs: 60,
maximumFeeMsats: 1000,
idempotencyKey,
});
When implementing retries, you can tailor your approach based on the operation context:
-
User-Facing Operations (User in Session)
- Limit retries to 1-2 attempts
- Keep retry delays short (e.g. 1 second)
- Provide immediate feedback to the user
- If retries fail, clearly communicate the issue and suggest next steps
-
Server-to-Server Operations (User Not in Session)
- Implement exponential backoff for retries
- Set a reasonable maximum number of retry attempts (e.g. 5)
- Log each retry attempt for monitoring
Best Practices:
- Set appropriate timeout limits for each operation type
- Log all retry attempts and failures
- For critical operations, create fallback mechanisms or manual review processes
Lightspark integrates with Chainalysis to simplify sharing your transaction data with your compliance tools.
Chainalysis is a blockchain data platform that provides crypto compliance solutions. They help us screen transactions and provide tools for advanced due diligence and fraud prevention.
To enable Chainalysis integration:
- Obtain a Chainalysis API key.
- Provide this API key to Lightspark.
Once you've provided the API key, Lightspark will pass transaction data to your Chainalysis account. The following data will be passed
- when screening - receiver node’s pubkey
- when registering the transaction - node pubkey, payment hash, amount and timestamp
Thorough testing is crucial to ensure your Lightspark Connect implementation is robust and ready for production use. Here are key areas to focus on:
- Test funding your node with various amounts
- Verify withdrawal webhooks are received and processed correctly
- Ensure your internal accounting system accurately reflects all funding and withdrawal transactions
- If applicable, test high balance scenario
- Verify high_balance webhook is received
- Verify your system's response to webhook is as expected
- If applicable, test low balance scenario
- Verify low_balance webhook is received
- Verify your system's response to webhook is as expected
- Send small amount payment (e.g., 1000 sats)
- Verify your system's controls for initiating small payments
- Ensure outgoing payment webhook is received and user's account is debited
- Send large amount payment (e.g., 1,000,000 sats)
- Verify your system's controls for initiating large payments
- Ensure outgoing payment webhook is received and user's account is debited
- Receive small amount payment (e.g., 1000 sats)
- Verify your system's controls for receiving small payments
- Ensure incoming payment webhook is received and user's account is credited
- Receive large amount payment (e.g., 1,000,000 sats)
- Verify your system's controls for receiving large payments
- Ensure incoming payment webhook is received and user's account is credited
- Simulate 'no route' error and verify handling
- Simulate 'insufficient balance' error and verify handling
- Attempt to send more than available balance
- Send payment with invalid/expired invoice
In our next guide, we'll dive deeper into the process of going live with your Lightspark Connect implementation, including best practices for monitoring, scaling, and maintaining your production deployment.
By following the practices outlined in this guide, you'll be well-equipped to handle reconciliation, manage errors effectively, simplify compliance, and thoroughly test your Lightspark Connect implementation. Remember, the key to a successful Lightning Network integration is continuous monitoring, testing, and refinement of your systems.