@flashnet/sdk 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1788,6 +1788,588 @@ class FlashnetClient {
1788
1788
  throw new Error(errorMessage);
1789
1789
  }
1790
1790
  }
1791
+ // ===== Lightning Payment with Token =====
1792
+ /**
1793
+ * Get a quote for paying a Lightning invoice with a token.
1794
+ * This calculates the optimal pool and token amount needed.
1795
+ *
1796
+ * @param invoice - BOLT11-encoded Lightning invoice
1797
+ * @param tokenAddress - Token identifier to use for payment
1798
+ * @param options - Optional configuration (slippage, integrator fees, etc.)
1799
+ * @returns Quote with pricing details
1800
+ * @throws Error if invoice amount or token amount is below Flashnet minimums
1801
+ */
1802
+ async getPayLightningWithTokenQuote(invoice, tokenAddress, options) {
1803
+ await this.ensureInitialized();
1804
+ // Decode the invoice to get the amount
1805
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1806
+ if (!invoiceAmountSats || invoiceAmountSats <= 0) {
1807
+ throw new Error("Unable to decode invoice amount. Zero-amount invoices are not supported for token payments.");
1808
+ }
1809
+ // Get Lightning fee estimate
1810
+ const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
1811
+ // Total BTC needed = invoice amount + lightning fee
1812
+ const baseBtcNeeded = BigInt(invoiceAmountSats) + BigInt(lightningFeeEstimate);
1813
+ // Round up to next multiple of 64 (2^6) to account for BTC variable fee bit masking.
1814
+ // The AMM zeroes the lowest 6 bits of BTC output, so we need to request enough
1815
+ // that after masking we still have the required amount.
1816
+ // Example: 5014 -> masked to 4992, but if we request 5056, masked stays 5056
1817
+ const BTC_VARIABLE_FEE_BITS = 6n;
1818
+ const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
1819
+ const totalBtcNeeded = ((baseBtcNeeded + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
1820
+ BTC_VARIABLE_FEE_MASK;
1821
+ // Check Flashnet minimum amounts early to provide clear error messages
1822
+ const minAmounts = await this.getEnabledMinAmountsMap();
1823
+ // Check BTC minimum (output from swap)
1824
+ const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
1825
+ if (btcMinAmount && totalBtcNeeded < btcMinAmount) {
1826
+ throw new Error(`Invoice amount too small. Flashnet minimum BTC output is ${btcMinAmount} sats, ` +
1827
+ `but invoice + lightning fee totals only ${totalBtcNeeded} sats. ` +
1828
+ `Please use an invoice of at least ${btcMinAmount} sats.`);
1829
+ }
1830
+ // Find the best pool to swap token -> BTC
1831
+ const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, totalBtcNeeded.toString(), options?.integratorFeeRateBps);
1832
+ // Check token minimum (input to swap)
1833
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
1834
+ const tokenMinAmount = minAmounts.get(tokenHex);
1835
+ if (tokenMinAmount &&
1836
+ BigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1837
+ throw new Error(`Token amount too small. Flashnet minimum input is ${tokenMinAmount} units, ` +
1838
+ `but calculated amount is only ${poolQuote.tokenAmountRequired} units. ` +
1839
+ `Please use a larger invoice amount.`);
1840
+ }
1841
+ // Calculate the BTC variable fee adjustment (how much extra we're requesting)
1842
+ const btcVariableFeeAdjustment = Number(totalBtcNeeded - baseBtcNeeded);
1843
+ return {
1844
+ poolId: poolQuote.poolId,
1845
+ tokenAddress: this.toHexTokenIdentifier(tokenAddress),
1846
+ tokenAmountRequired: poolQuote.tokenAmountRequired,
1847
+ btcAmountRequired: totalBtcNeeded.toString(),
1848
+ invoiceAmountSats: invoiceAmountSats,
1849
+ estimatedAmmFee: poolQuote.estimatedAmmFee,
1850
+ estimatedLightningFee: lightningFeeEstimate,
1851
+ btcVariableFeeAdjustment,
1852
+ executionPrice: poolQuote.executionPrice,
1853
+ priceImpactPct: poolQuote.priceImpactPct,
1854
+ tokenIsAssetA: poolQuote.tokenIsAssetA,
1855
+ poolReserves: poolQuote.poolReserves,
1856
+ warningMessage: poolQuote.warningMessage,
1857
+ };
1858
+ }
1859
+ /**
1860
+ * Pay a Lightning invoice using a token.
1861
+ * This swaps the token to BTC on Flashnet and uses the BTC to pay the invoice.
1862
+ *
1863
+ * @param options - Payment options including invoice and token address
1864
+ * @returns Payment result with transaction details
1865
+ */
1866
+ async payLightningWithToken(options) {
1867
+ await this.ensureInitialized();
1868
+ const { invoice, tokenAddress, maxSlippageBps = 500, // 5% default
1869
+ maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 30000, // 30s default
1870
+ rollbackOnFailure = false, useExistingBtcBalance = false, } = options;
1871
+ try {
1872
+ // Step 1: Get a quote for the payment
1873
+ const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
1874
+ maxSlippageBps,
1875
+ integratorFeeRateBps,
1876
+ });
1877
+ // Step 2: Check token balance (always required)
1878
+ await this.checkBalance({
1879
+ balancesToCheck: [
1880
+ {
1881
+ assetAddress: tokenAddress,
1882
+ amount: quote.tokenAmountRequired,
1883
+ },
1884
+ ],
1885
+ errorPrefix: "Insufficient token balance for Lightning payment: ",
1886
+ });
1887
+ // Determine if we can pay immediately using existing BTC balance
1888
+ let canPayImmediately = false;
1889
+ if (useExistingBtcBalance) {
1890
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1891
+ const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
1892
+ const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
1893
+ // Check if we have enough BTC (don't throw if not, just fall back to waiting)
1894
+ const balance = await this.getBalance();
1895
+ canPayImmediately = balance.balance >= BigInt(btcNeededForPayment);
1896
+ }
1897
+ // Step 3: Get pool details
1898
+ const pool = await this.getPool(quote.poolId);
1899
+ // Step 4: Determine swap direction and execute
1900
+ const assetInAddress = quote.tokenIsAssetA
1901
+ ? pool.assetAAddress
1902
+ : pool.assetBAddress;
1903
+ const assetOutAddress = quote.tokenIsAssetA
1904
+ ? pool.assetBAddress
1905
+ : pool.assetAAddress;
1906
+ // Calculate min amount out with slippage protection
1907
+ const minBtcOut = this.calculateMinAmountOut(quote.btcAmountRequired, maxSlippageBps);
1908
+ // Execute the swap
1909
+ const swapResponse = await this.executeSwap({
1910
+ poolId: quote.poolId,
1911
+ assetInAddress,
1912
+ assetOutAddress,
1913
+ amountIn: quote.tokenAmountRequired,
1914
+ maxSlippageBps,
1915
+ minAmountOut: minBtcOut,
1916
+ integratorFeeRateBps,
1917
+ integratorPublicKey,
1918
+ });
1919
+ if (!swapResponse.accepted || !swapResponse.outboundTransferId) {
1920
+ return {
1921
+ success: false,
1922
+ poolId: quote.poolId,
1923
+ tokenAmountSpent: quote.tokenAmountRequired,
1924
+ btcAmountReceived: "0",
1925
+ swapTransferId: swapResponse.outboundTransferId || "",
1926
+ ammFeePaid: quote.estimatedAmmFee,
1927
+ error: swapResponse.error || "Swap was not accepted",
1928
+ };
1929
+ }
1930
+ // Step 5: Wait for the transfer to complete (unless paying immediately with existing BTC)
1931
+ if (!canPayImmediately) {
1932
+ const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
1933
+ if (!transferComplete) {
1934
+ return {
1935
+ success: false,
1936
+ poolId: quote.poolId,
1937
+ tokenAmountSpent: quote.tokenAmountRequired,
1938
+ btcAmountReceived: swapResponse.amountOut || "0",
1939
+ swapTransferId: swapResponse.outboundTransferId,
1940
+ ammFeePaid: quote.estimatedAmmFee,
1941
+ error: "Transfer did not complete within timeout",
1942
+ };
1943
+ }
1944
+ }
1945
+ // Step 6: Calculate Lightning fee limit - use the quoted estimate, not a recalculation
1946
+ const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
1947
+ // Step 7: Pay the Lightning invoice
1948
+ const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
1949
+ try {
1950
+ const lightningPayment = await this._wallet.payLightningInvoice({
1951
+ invoice,
1952
+ maxFeeSats: effectiveMaxLightningFee,
1953
+ preferSpark,
1954
+ });
1955
+ return {
1956
+ success: true,
1957
+ poolId: quote.poolId,
1958
+ tokenAmountSpent: quote.tokenAmountRequired,
1959
+ btcAmountReceived: btcReceived,
1960
+ swapTransferId: swapResponse.outboundTransferId,
1961
+ lightningPaymentId: lightningPayment.id,
1962
+ ammFeePaid: quote.estimatedAmmFee,
1963
+ lightningFeePaid: effectiveMaxLightningFee,
1964
+ };
1965
+ }
1966
+ catch (lightningError) {
1967
+ // Lightning payment failed after swap succeeded
1968
+ const lightningErrorMessage = lightningError instanceof Error
1969
+ ? lightningError.message
1970
+ : String(lightningError);
1971
+ // Attempt rollback if requested
1972
+ if (rollbackOnFailure) {
1973
+ try {
1974
+ const rollbackResult = await this.rollbackSwap(quote.poolId, btcReceived, tokenAddress, maxSlippageBps);
1975
+ if (rollbackResult.success) {
1976
+ return {
1977
+ success: false,
1978
+ poolId: quote.poolId,
1979
+ tokenAmountSpent: "0", // Rolled back
1980
+ btcAmountReceived: "0",
1981
+ swapTransferId: swapResponse.outboundTransferId,
1982
+ ammFeePaid: quote.estimatedAmmFee,
1983
+ error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
1984
+ };
1985
+ }
1986
+ }
1987
+ catch (rollbackError) {
1988
+ const rollbackErrorMessage = rollbackError instanceof Error
1989
+ ? rollbackError.message
1990
+ : String(rollbackError);
1991
+ return {
1992
+ success: false,
1993
+ poolId: quote.poolId,
1994
+ tokenAmountSpent: quote.tokenAmountRequired,
1995
+ btcAmountReceived: btcReceived,
1996
+ swapTransferId: swapResponse.outboundTransferId,
1997
+ ammFeePaid: quote.estimatedAmmFee,
1998
+ error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
1999
+ };
2000
+ }
2001
+ }
2002
+ return {
2003
+ success: false,
2004
+ poolId: quote.poolId,
2005
+ tokenAmountSpent: quote.tokenAmountRequired,
2006
+ btcAmountReceived: btcReceived,
2007
+ swapTransferId: swapResponse.outboundTransferId,
2008
+ ammFeePaid: quote.estimatedAmmFee,
2009
+ error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
2010
+ };
2011
+ }
2012
+ }
2013
+ catch (error) {
2014
+ const errorMessage = error instanceof Error ? error.message : String(error);
2015
+ return {
2016
+ success: false,
2017
+ poolId: "",
2018
+ tokenAmountSpent: "0",
2019
+ btcAmountReceived: "0",
2020
+ swapTransferId: "",
2021
+ ammFeePaid: "0",
2022
+ error: errorMessage,
2023
+ };
2024
+ }
2025
+ }
2026
+ /**
2027
+ * Attempt to rollback a swap by swapping BTC back to the original token
2028
+ * @private
2029
+ */
2030
+ async rollbackSwap(poolId, btcAmount, tokenAddress, maxSlippageBps) {
2031
+ const pool = await this.getPool(poolId);
2032
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress);
2033
+ // Determine swap direction (BTC -> Token)
2034
+ const tokenIsAssetA = pool.assetAAddress === tokenHex;
2035
+ const assetInAddress = tokenIsAssetA
2036
+ ? pool.assetBAddress
2037
+ : pool.assetAAddress; // BTC
2038
+ const assetOutAddress = tokenIsAssetA
2039
+ ? pool.assetAAddress
2040
+ : pool.assetBAddress; // Token
2041
+ // Calculate expected token output and min amount with slippage
2042
+ // For rollback, we accept more slippage since we're recovering from failure
2043
+ const minAmountOut = "0"; // Accept any amount to ensure rollback succeeds
2044
+ // Execute reverse swap
2045
+ const swapResponse = await this.executeSwap({
2046
+ poolId,
2047
+ assetInAddress,
2048
+ assetOutAddress,
2049
+ amountIn: btcAmount,
2050
+ maxSlippageBps: maxSlippageBps * 2, // Double slippage for rollback
2051
+ minAmountOut,
2052
+ });
2053
+ if (!swapResponse.accepted) {
2054
+ throw new Error(swapResponse.error || "Rollback swap not accepted");
2055
+ }
2056
+ // Wait for the rollback transfer
2057
+ if (swapResponse.outboundTransferId) {
2058
+ await this.waitForTransferCompletion(swapResponse.outboundTransferId, 30000);
2059
+ }
2060
+ return {
2061
+ success: true,
2062
+ tokenAmount: swapResponse.amountOut,
2063
+ };
2064
+ }
2065
+ /**
2066
+ * Find the best pool for swapping a token to BTC
2067
+ * @private
2068
+ */
2069
+ async findBestPoolForTokenToBtc(tokenAddress, btcAmountNeeded, integratorFeeRateBps) {
2070
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress);
2071
+ const btcHex = index$1.BTC_ASSET_PUBKEY;
2072
+ // Find all pools that have this token paired with BTC
2073
+ const poolsWithTokenAsA = await this.listPools({
2074
+ assetAAddress: tokenHex,
2075
+ assetBAddress: btcHex,
2076
+ });
2077
+ const poolsWithTokenAsB = await this.listPools({
2078
+ assetAAddress: btcHex,
2079
+ assetBAddress: tokenHex,
2080
+ });
2081
+ const allPools = [
2082
+ ...poolsWithTokenAsA.pools.map((p) => ({ ...p, tokenIsAssetA: true })),
2083
+ ...poolsWithTokenAsB.pools.map((p) => ({ ...p, tokenIsAssetA: false })),
2084
+ ];
2085
+ if (allPools.length === 0) {
2086
+ throw new Error(`No liquidity pool found for token ${tokenAddress} paired with BTC`);
2087
+ }
2088
+ // Find the best pool (lowest token cost for the required BTC)
2089
+ let bestPool = null;
2090
+ let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
2091
+ let bestSimulation = null;
2092
+ for (const pool of allPools) {
2093
+ try {
2094
+ // Get pool details for reserves
2095
+ const poolDetails = await this.getPool(pool.lpPublicKey);
2096
+ // Calculate the token amount needed using AMM math
2097
+ const calculation = this.calculateTokenAmountForBtcOutput(btcAmountNeeded, poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
2098
+ const tokenAmount = BigInt(calculation.amountIn);
2099
+ // Check if this is better than our current best
2100
+ if (tokenAmount < bestTokenAmount) {
2101
+ // Verify with simulation
2102
+ const simulation = await this.simulateSwap({
2103
+ poolId: pool.lpPublicKey,
2104
+ assetInAddress: pool.tokenIsAssetA
2105
+ ? poolDetails.assetAAddress
2106
+ : poolDetails.assetBAddress,
2107
+ assetOutAddress: pool.tokenIsAssetA
2108
+ ? poolDetails.assetBAddress
2109
+ : poolDetails.assetAAddress,
2110
+ amountIn: calculation.amountIn,
2111
+ integratorBps: integratorFeeRateBps,
2112
+ });
2113
+ // Verify the output is sufficient
2114
+ if (BigInt(simulation.amountOut) >= BigInt(btcAmountNeeded)) {
2115
+ bestPool = pool;
2116
+ bestTokenAmount = tokenAmount;
2117
+ bestSimulation = {
2118
+ amountIn: calculation.amountIn,
2119
+ fee: calculation.totalFee,
2120
+ executionPrice: simulation.executionPrice || "0",
2121
+ priceImpactPct: simulation.priceImpactPct || "0",
2122
+ warningMessage: simulation.warningMessage,
2123
+ };
2124
+ }
2125
+ }
2126
+ }
2127
+ catch { }
2128
+ }
2129
+ if (!bestPool || !bestSimulation) {
2130
+ throw new Error(`No pool has sufficient liquidity for ${btcAmountNeeded} sats`);
2131
+ }
2132
+ const poolDetails = await this.getPool(bestPool.lpPublicKey);
2133
+ return {
2134
+ poolId: bestPool.lpPublicKey,
2135
+ tokenAmountRequired: bestSimulation.amountIn,
2136
+ estimatedAmmFee: bestSimulation.fee,
2137
+ executionPrice: bestSimulation.executionPrice,
2138
+ priceImpactPct: bestSimulation.priceImpactPct,
2139
+ tokenIsAssetA: bestPool.tokenIsAssetA,
2140
+ poolReserves: {
2141
+ assetAReserve: poolDetails.assetAReserve,
2142
+ assetBReserve: poolDetails.assetBReserve,
2143
+ },
2144
+ warningMessage: bestSimulation.warningMessage,
2145
+ };
2146
+ }
2147
+ /**
2148
+ * Calculate the token amount needed to get a specific BTC output.
2149
+ * Implements the AMM fee-inclusive model.
2150
+ * @private
2151
+ */
2152
+ calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
2153
+ const amountOut = BigInt(btcAmountOut);
2154
+ const resA = BigInt(reserveA);
2155
+ const resB = BigInt(reserveB);
2156
+ const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
2157
+ const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
2158
+ // Token is the input asset
2159
+ // BTC is the output asset
2160
+ if (tokenIsAssetA) {
2161
+ // Token is asset A, BTC is asset B
2162
+ // A → B swap: we want BTC out (asset B)
2163
+ // reserve_in = reserveA (token), reserve_out = reserveB (BTC)
2164
+ // Constant product formula for amount_in given amount_out:
2165
+ // amount_in_effective = (reserve_in * amount_out) / (reserve_out - amount_out)
2166
+ const reserveIn = resA;
2167
+ const reserveOut = resB;
2168
+ if (amountOut >= reserveOut) {
2169
+ throw new Error("Insufficient liquidity: requested BTC amount exceeds reserve");
2170
+ }
2171
+ // Calculate effective amount in (before fees)
2172
+ const amountInEffective = (reserveIn * amountOut) / (reserveOut - amountOut) + 1n; // +1 for rounding up
2173
+ // A→B swap: LP fee deducted from input A, integrator fee from output B
2174
+ // amount_in = amount_in_effective * (1 + lp_fee_rate)
2175
+ // Then integrator fee is deducted from output, so we need slightly more input
2176
+ const lpFeeRate = Number(lpFeeBps) / 10000;
2177
+ const integratorFeeRate = Number(integratorFeeBps || 0) / 10000;
2178
+ // Account for LP fee on input
2179
+ const amountInWithLpFee = BigInt(Math.ceil(Number(amountInEffective) * (1 + lpFeeRate)));
2180
+ // Account for integrator fee on output (need more input to get same output after fee)
2181
+ const amountIn = integratorFeeRate > 0
2182
+ ? BigInt(Math.ceil(Number(amountInWithLpFee) * (1 + integratorFeeRate)))
2183
+ : amountInWithLpFee;
2184
+ const totalFee = amountIn - amountInEffective;
2185
+ return {
2186
+ amountIn: amountIn.toString(),
2187
+ totalFee: totalFee.toString(),
2188
+ };
2189
+ }
2190
+ else {
2191
+ // Token is asset B, BTC is asset A
2192
+ // B → A swap: we want BTC out (asset A)
2193
+ // reserve_in = reserveB (token), reserve_out = reserveA (BTC)
2194
+ const reserveIn = resB;
2195
+ const reserveOut = resA;
2196
+ if (amountOut >= reserveOut) {
2197
+ throw new Error("Insufficient liquidity: requested BTC amount exceeds reserve");
2198
+ }
2199
+ // Calculate effective amount in (before fees)
2200
+ const amountInEffective = (reserveIn * amountOut) / (reserveOut - amountOut) + 1n; // +1 for rounding up
2201
+ // B→A swap: ALL fees (LP + integrator) deducted from input B
2202
+ // amount_in = amount_in_effective * (1 + total_fee_rate)
2203
+ const amountIn = BigInt(Math.ceil(Number(amountInEffective) * (1 + feeRate)));
2204
+ // Fee calculation: fee = amount_in * fee_rate / (1 + fee_rate)
2205
+ const totalFee = BigInt(Math.ceil((Number(amountIn) * feeRate) / (1 + feeRate)));
2206
+ return {
2207
+ amountIn: amountIn.toString(),
2208
+ totalFee: totalFee.toString(),
2209
+ };
2210
+ }
2211
+ }
2212
+ /**
2213
+ * Calculate minimum amount out with slippage protection
2214
+ * @private
2215
+ */
2216
+ calculateMinAmountOut(expectedAmount, slippageBps) {
2217
+ const amount = BigInt(expectedAmount);
2218
+ const slippageFactor = BigInt(10000 - slippageBps);
2219
+ const minAmount = (amount * slippageFactor) / 10000n;
2220
+ return minAmount.toString();
2221
+ }
2222
+ /**
2223
+ * Wait for a transfer to be claimed using wallet events.
2224
+ * This is more efficient than polling as it uses the wallet's event stream.
2225
+ * @private
2226
+ */
2227
+ async waitForTransferCompletion(transferId, timeoutMs) {
2228
+ return new Promise((resolve) => {
2229
+ const timeout = setTimeout(() => {
2230
+ // Remove listener on timeout
2231
+ try {
2232
+ this._wallet.removeListener?.("transfer:claimed", handler);
2233
+ }
2234
+ catch {
2235
+ // Ignore if removeListener doesn't exist
2236
+ }
2237
+ resolve(false);
2238
+ }, timeoutMs);
2239
+ const handler = (claimedTransferId, _balance) => {
2240
+ if (claimedTransferId === transferId) {
2241
+ clearTimeout(timeout);
2242
+ try {
2243
+ this._wallet.removeListener?.("transfer:claimed", handler);
2244
+ }
2245
+ catch {
2246
+ // Ignore if removeListener doesn't exist
2247
+ }
2248
+ resolve(true);
2249
+ }
2250
+ };
2251
+ // Subscribe to transfer claimed events
2252
+ // The wallet's RPC stream will automatically claim incoming transfers
2253
+ try {
2254
+ this._wallet.on?.("transfer:claimed", handler);
2255
+ }
2256
+ catch {
2257
+ // If event subscription fails, fall back to polling
2258
+ clearTimeout(timeout);
2259
+ this.pollForTransferCompletion(transferId, timeoutMs).then(resolve);
2260
+ }
2261
+ });
2262
+ }
2263
+ /**
2264
+ * Fallback polling method for transfer completion
2265
+ * @private
2266
+ */
2267
+ async pollForTransferCompletion(transferId, timeoutMs) {
2268
+ const startTime = Date.now();
2269
+ const pollIntervalMs = 500;
2270
+ while (Date.now() - startTime < timeoutMs) {
2271
+ try {
2272
+ const transfer = await this._wallet.getTransfer(transferId);
2273
+ if (transfer) {
2274
+ if (transfer.status === "TRANSFER_STATUS_COMPLETED") {
2275
+ return true;
2276
+ }
2277
+ }
2278
+ }
2279
+ catch {
2280
+ // Ignore errors and continue polling
2281
+ }
2282
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
2283
+ }
2284
+ return false;
2285
+ }
2286
+ /**
2287
+ * Get Lightning fee estimate for an invoice
2288
+ * @private
2289
+ */
2290
+ async getLightningFeeEstimate(invoice) {
2291
+ try {
2292
+ const feeEstimate = await this._wallet.getLightningSendFeeEstimate({
2293
+ encodedInvoice: invoice,
2294
+ });
2295
+ // The fee estimate might be returned as a number or an object
2296
+ if (typeof feeEstimate === "number") {
2297
+ return feeEstimate;
2298
+ }
2299
+ if (feeEstimate?.fee || feeEstimate?.feeEstimate) {
2300
+ return Number(feeEstimate.fee || feeEstimate.feeEstimate);
2301
+ }
2302
+ // Fallback to invoice amount-based estimate
2303
+ const invoiceAmount = await this.decodeInvoiceAmount(invoice);
2304
+ return Math.max(5, Math.ceil(invoiceAmount * 0.0017)); // 17 bps or 5 sats minimum
2305
+ }
2306
+ catch {
2307
+ // Fallback to invoice amount-based estimate
2308
+ const invoiceAmount = await this.decodeInvoiceAmount(invoice);
2309
+ return Math.max(5, Math.ceil(invoiceAmount * 0.0017));
2310
+ }
2311
+ }
2312
+ /**
2313
+ * Decode the amount from a Lightning invoice (in sats)
2314
+ * @private
2315
+ */
2316
+ async decodeInvoiceAmount(invoice) {
2317
+ // Extract amount from BOLT11 invoice
2318
+ // Format: ln[network][amount][multiplier]...
2319
+ // Amount multipliers: m = milli (0.001), u = micro (0.000001), n = nano, p = pico
2320
+ const lowerInvoice = invoice.toLowerCase();
2321
+ // Find where the amount starts (after network prefix)
2322
+ let amountStart = 0;
2323
+ if (lowerInvoice.startsWith("lnbc")) {
2324
+ amountStart = 4;
2325
+ }
2326
+ else if (lowerInvoice.startsWith("lntb")) {
2327
+ amountStart = 4;
2328
+ }
2329
+ else if (lowerInvoice.startsWith("lnbcrt")) {
2330
+ amountStart = 6;
2331
+ }
2332
+ else if (lowerInvoice.startsWith("lntbs")) {
2333
+ amountStart = 5;
2334
+ }
2335
+ else {
2336
+ // Unknown format, try to find amount
2337
+ const match = lowerInvoice.match(/^ln[a-z]+/);
2338
+ if (match) {
2339
+ amountStart = match[0].length;
2340
+ }
2341
+ }
2342
+ // Extract amount and multiplier
2343
+ const afterPrefix = lowerInvoice.substring(amountStart);
2344
+ const amountMatch = afterPrefix.match(/^(\d+)([munp]?)/);
2345
+ if (!amountMatch || !amountMatch[1]) {
2346
+ return 0; // Zero-amount invoice
2347
+ }
2348
+ const amount = parseInt(amountMatch[1], 10);
2349
+ const multiplier = amountMatch[2] ?? "";
2350
+ // Convert to satoshis (1 BTC = 100,000,000 sats)
2351
+ // Invoice amounts are in BTC by default
2352
+ let btcAmount;
2353
+ switch (multiplier) {
2354
+ case "m": // milli-BTC (0.001 BTC)
2355
+ btcAmount = amount * 0.001;
2356
+ break;
2357
+ case "u": // micro-BTC (0.000001 BTC)
2358
+ btcAmount = amount * 0.000001;
2359
+ break;
2360
+ case "n": // nano-BTC (0.000000001 BTC)
2361
+ btcAmount = amount * 0.000000001;
2362
+ break;
2363
+ case "p": // pico-BTC (0.000000000001 BTC)
2364
+ btcAmount = amount * 0.000000000001;
2365
+ break;
2366
+ default: // BTC
2367
+ btcAmount = amount;
2368
+ break;
2369
+ }
2370
+ // Convert BTC to sats
2371
+ return Math.round(btcAmount * 100000000);
2372
+ }
1791
2373
  /**
1792
2374
  * Clean up wallet connections
1793
2375
  */