@flashnet/sdk 0.4.0 → 0.4.1
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.
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/src/client/FlashnetClient.d.ts +137 -0
- package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/cjs/src/client/FlashnetClient.js +483 -0
- package/dist/cjs/src/client/FlashnetClient.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/src/client/FlashnetClient.d.ts +137 -0
- package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/esm/src/client/FlashnetClient.js +483 -0
- package/dist/esm/src/client/FlashnetClient.js.map +1 -1
- package/package.json +1 -1
|
@@ -1788,6 +1788,489 @@ 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 = 100, // 1% default
|
|
1869
|
+
maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 10000, } = options;
|
|
1870
|
+
try {
|
|
1871
|
+
// Step 1: Get a quote for the payment
|
|
1872
|
+
const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
|
|
1873
|
+
maxSlippageBps,
|
|
1874
|
+
integratorFeeRateBps,
|
|
1875
|
+
});
|
|
1876
|
+
// Step 2: Check balance
|
|
1877
|
+
await this.checkBalance({
|
|
1878
|
+
balancesToCheck: [
|
|
1879
|
+
{
|
|
1880
|
+
assetAddress: tokenAddress,
|
|
1881
|
+
amount: quote.tokenAmountRequired,
|
|
1882
|
+
},
|
|
1883
|
+
],
|
|
1884
|
+
errorPrefix: "Insufficient token balance for Lightning payment: ",
|
|
1885
|
+
});
|
|
1886
|
+
// Step 3: Get pool details
|
|
1887
|
+
const pool = await this.getPool(quote.poolId);
|
|
1888
|
+
// Step 4: Determine swap direction and execute
|
|
1889
|
+
const assetInAddress = quote.tokenIsAssetA
|
|
1890
|
+
? pool.assetAAddress
|
|
1891
|
+
: pool.assetBAddress;
|
|
1892
|
+
const assetOutAddress = quote.tokenIsAssetA
|
|
1893
|
+
? pool.assetBAddress
|
|
1894
|
+
: pool.assetAAddress;
|
|
1895
|
+
// Calculate min amount out with slippage protection
|
|
1896
|
+
const minBtcOut = this.calculateMinAmountOut(quote.btcAmountRequired, maxSlippageBps);
|
|
1897
|
+
// Execute the swap
|
|
1898
|
+
const swapResponse = await this.executeSwap({
|
|
1899
|
+
poolId: quote.poolId,
|
|
1900
|
+
assetInAddress,
|
|
1901
|
+
assetOutAddress,
|
|
1902
|
+
amountIn: quote.tokenAmountRequired,
|
|
1903
|
+
maxSlippageBps,
|
|
1904
|
+
minAmountOut: minBtcOut,
|
|
1905
|
+
integratorFeeRateBps,
|
|
1906
|
+
integratorPublicKey,
|
|
1907
|
+
});
|
|
1908
|
+
if (!swapResponse.accepted || !swapResponse.outboundTransferId) {
|
|
1909
|
+
return {
|
|
1910
|
+
success: false,
|
|
1911
|
+
poolId: quote.poolId,
|
|
1912
|
+
tokenAmountSpent: quote.tokenAmountRequired,
|
|
1913
|
+
btcAmountReceived: "0",
|
|
1914
|
+
swapTransferId: swapResponse.outboundTransferId || "",
|
|
1915
|
+
ammFeePaid: quote.estimatedAmmFee,
|
|
1916
|
+
error: swapResponse.error || "Swap was not accepted",
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
// Step 5: Wait for the transfer to complete
|
|
1920
|
+
const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
|
|
1921
|
+
if (!transferComplete) {
|
|
1922
|
+
return {
|
|
1923
|
+
success: false,
|
|
1924
|
+
poolId: quote.poolId,
|
|
1925
|
+
tokenAmountSpent: quote.tokenAmountRequired,
|
|
1926
|
+
btcAmountReceived: swapResponse.amountOut || "0",
|
|
1927
|
+
swapTransferId: swapResponse.outboundTransferId,
|
|
1928
|
+
ammFeePaid: quote.estimatedAmmFee,
|
|
1929
|
+
error: "Transfer did not complete within timeout",
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
// Step 6: Calculate Lightning fee limit
|
|
1933
|
+
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
1934
|
+
const effectiveMaxLightningFee = maxLightningFeeSats ??
|
|
1935
|
+
Math.max(5, Math.ceil(invoiceAmountSats * 0.0017)); // 17 bps or 5 sats minimum
|
|
1936
|
+
// Step 7: Pay the Lightning invoice
|
|
1937
|
+
const lightningPayment = await this._wallet.payLightningInvoice({
|
|
1938
|
+
invoice,
|
|
1939
|
+
maxFeeSats: effectiveMaxLightningFee,
|
|
1940
|
+
preferSpark,
|
|
1941
|
+
});
|
|
1942
|
+
return {
|
|
1943
|
+
success: true,
|
|
1944
|
+
poolId: quote.poolId,
|
|
1945
|
+
tokenAmountSpent: quote.tokenAmountRequired,
|
|
1946
|
+
btcAmountReceived: swapResponse.amountOut || quote.btcAmountRequired,
|
|
1947
|
+
swapTransferId: swapResponse.outboundTransferId,
|
|
1948
|
+
lightningPaymentId: lightningPayment.id,
|
|
1949
|
+
ammFeePaid: quote.estimatedAmmFee,
|
|
1950
|
+
lightningFeePaid: effectiveMaxLightningFee,
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
catch (error) {
|
|
1954
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1955
|
+
return {
|
|
1956
|
+
success: false,
|
|
1957
|
+
poolId: "",
|
|
1958
|
+
tokenAmountSpent: "0",
|
|
1959
|
+
btcAmountReceived: "0",
|
|
1960
|
+
swapTransferId: "",
|
|
1961
|
+
ammFeePaid: "0",
|
|
1962
|
+
error: errorMessage,
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Find the best pool for swapping a token to BTC
|
|
1968
|
+
* @private
|
|
1969
|
+
*/
|
|
1970
|
+
async findBestPoolForTokenToBtc(tokenAddress, btcAmountNeeded, integratorFeeRateBps) {
|
|
1971
|
+
const tokenHex = this.toHexTokenIdentifier(tokenAddress);
|
|
1972
|
+
const btcHex = index$1.BTC_ASSET_PUBKEY;
|
|
1973
|
+
// Find all pools that have this token paired with BTC
|
|
1974
|
+
const poolsWithTokenAsA = await this.listPools({
|
|
1975
|
+
assetAAddress: tokenHex,
|
|
1976
|
+
assetBAddress: btcHex,
|
|
1977
|
+
});
|
|
1978
|
+
const poolsWithTokenAsB = await this.listPools({
|
|
1979
|
+
assetAAddress: btcHex,
|
|
1980
|
+
assetBAddress: tokenHex,
|
|
1981
|
+
});
|
|
1982
|
+
const allPools = [
|
|
1983
|
+
...poolsWithTokenAsA.pools.map((p) => ({ ...p, tokenIsAssetA: true })),
|
|
1984
|
+
...poolsWithTokenAsB.pools.map((p) => ({ ...p, tokenIsAssetA: false })),
|
|
1985
|
+
];
|
|
1986
|
+
if (allPools.length === 0) {
|
|
1987
|
+
throw new Error(`No liquidity pool found for token ${tokenAddress} paired with BTC`);
|
|
1988
|
+
}
|
|
1989
|
+
// Find the best pool (lowest token cost for the required BTC)
|
|
1990
|
+
let bestPool = null;
|
|
1991
|
+
let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
|
|
1992
|
+
let bestSimulation = null;
|
|
1993
|
+
for (const pool of allPools) {
|
|
1994
|
+
try {
|
|
1995
|
+
// Get pool details for reserves
|
|
1996
|
+
const poolDetails = await this.getPool(pool.lpPublicKey);
|
|
1997
|
+
// Calculate the token amount needed using AMM math
|
|
1998
|
+
const calculation = this.calculateTokenAmountForBtcOutput(btcAmountNeeded, poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
|
|
1999
|
+
const tokenAmount = BigInt(calculation.amountIn);
|
|
2000
|
+
// Check if this is better than our current best
|
|
2001
|
+
if (tokenAmount < bestTokenAmount) {
|
|
2002
|
+
// Verify with simulation
|
|
2003
|
+
const simulation = await this.simulateSwap({
|
|
2004
|
+
poolId: pool.lpPublicKey,
|
|
2005
|
+
assetInAddress: pool.tokenIsAssetA
|
|
2006
|
+
? poolDetails.assetAAddress
|
|
2007
|
+
: poolDetails.assetBAddress,
|
|
2008
|
+
assetOutAddress: pool.tokenIsAssetA
|
|
2009
|
+
? poolDetails.assetBAddress
|
|
2010
|
+
: poolDetails.assetAAddress,
|
|
2011
|
+
amountIn: calculation.amountIn,
|
|
2012
|
+
integratorBps: integratorFeeRateBps,
|
|
2013
|
+
});
|
|
2014
|
+
// Verify the output is sufficient
|
|
2015
|
+
if (BigInt(simulation.amountOut) >= BigInt(btcAmountNeeded)) {
|
|
2016
|
+
bestPool = pool;
|
|
2017
|
+
bestTokenAmount = tokenAmount;
|
|
2018
|
+
bestSimulation = {
|
|
2019
|
+
amountIn: calculation.amountIn,
|
|
2020
|
+
fee: calculation.totalFee,
|
|
2021
|
+
executionPrice: simulation.executionPrice || "0",
|
|
2022
|
+
priceImpactPct: simulation.priceImpactPct || "0",
|
|
2023
|
+
warningMessage: simulation.warningMessage,
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
catch { }
|
|
2029
|
+
}
|
|
2030
|
+
if (!bestPool || !bestSimulation) {
|
|
2031
|
+
throw new Error(`No pool has sufficient liquidity for ${btcAmountNeeded} sats`);
|
|
2032
|
+
}
|
|
2033
|
+
const poolDetails = await this.getPool(bestPool.lpPublicKey);
|
|
2034
|
+
return {
|
|
2035
|
+
poolId: bestPool.lpPublicKey,
|
|
2036
|
+
tokenAmountRequired: bestSimulation.amountIn,
|
|
2037
|
+
estimatedAmmFee: bestSimulation.fee,
|
|
2038
|
+
executionPrice: bestSimulation.executionPrice,
|
|
2039
|
+
priceImpactPct: bestSimulation.priceImpactPct,
|
|
2040
|
+
tokenIsAssetA: bestPool.tokenIsAssetA,
|
|
2041
|
+
poolReserves: {
|
|
2042
|
+
assetAReserve: poolDetails.assetAReserve,
|
|
2043
|
+
assetBReserve: poolDetails.assetBReserve,
|
|
2044
|
+
},
|
|
2045
|
+
warningMessage: bestSimulation.warningMessage,
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Calculate the token amount needed to get a specific BTC output.
|
|
2050
|
+
* Implements the AMM fee-inclusive model.
|
|
2051
|
+
* @private
|
|
2052
|
+
*/
|
|
2053
|
+
calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
|
|
2054
|
+
const amountOut = BigInt(btcAmountOut);
|
|
2055
|
+
const resA = BigInt(reserveA);
|
|
2056
|
+
const resB = BigInt(reserveB);
|
|
2057
|
+
const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
|
|
2058
|
+
const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
|
|
2059
|
+
// Token is the input asset
|
|
2060
|
+
// BTC is the output asset
|
|
2061
|
+
if (tokenIsAssetA) {
|
|
2062
|
+
// Token is asset A, BTC is asset B
|
|
2063
|
+
// A → B swap: we want BTC out (asset B)
|
|
2064
|
+
// reserve_in = reserveA (token), reserve_out = reserveB (BTC)
|
|
2065
|
+
// Constant product formula for amount_in given amount_out:
|
|
2066
|
+
// amount_in_effective = (reserve_in * amount_out) / (reserve_out - amount_out)
|
|
2067
|
+
const reserveIn = resA;
|
|
2068
|
+
const reserveOut = resB;
|
|
2069
|
+
if (amountOut >= reserveOut) {
|
|
2070
|
+
throw new Error("Insufficient liquidity: requested BTC amount exceeds reserve");
|
|
2071
|
+
}
|
|
2072
|
+
// Calculate effective amount in (before fees)
|
|
2073
|
+
const amountInEffective = (reserveIn * amountOut) / (reserveOut - amountOut) + 1n; // +1 for rounding up
|
|
2074
|
+
// A→B swap: LP fee deducted from input A, integrator fee from output B
|
|
2075
|
+
// amount_in = amount_in_effective * (1 + lp_fee_rate)
|
|
2076
|
+
// Then integrator fee is deducted from output, so we need slightly more input
|
|
2077
|
+
const lpFeeRate = Number(lpFeeBps) / 10000;
|
|
2078
|
+
const integratorFeeRate = Number(integratorFeeBps || 0) / 10000;
|
|
2079
|
+
// Account for LP fee on input
|
|
2080
|
+
const amountInWithLpFee = BigInt(Math.ceil(Number(amountInEffective) * (1 + lpFeeRate)));
|
|
2081
|
+
// Account for integrator fee on output (need more input to get same output after fee)
|
|
2082
|
+
const amountIn = integratorFeeRate > 0
|
|
2083
|
+
? BigInt(Math.ceil(Number(amountInWithLpFee) * (1 + integratorFeeRate)))
|
|
2084
|
+
: amountInWithLpFee;
|
|
2085
|
+
const totalFee = amountIn - amountInEffective;
|
|
2086
|
+
return {
|
|
2087
|
+
amountIn: amountIn.toString(),
|
|
2088
|
+
totalFee: totalFee.toString(),
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
else {
|
|
2092
|
+
// Token is asset B, BTC is asset A
|
|
2093
|
+
// B → A swap: we want BTC out (asset A)
|
|
2094
|
+
// reserve_in = reserveB (token), reserve_out = reserveA (BTC)
|
|
2095
|
+
const reserveIn = resB;
|
|
2096
|
+
const reserveOut = resA;
|
|
2097
|
+
if (amountOut >= reserveOut) {
|
|
2098
|
+
throw new Error("Insufficient liquidity: requested BTC amount exceeds reserve");
|
|
2099
|
+
}
|
|
2100
|
+
// Calculate effective amount in (before fees)
|
|
2101
|
+
const amountInEffective = (reserveIn * amountOut) / (reserveOut - amountOut) + 1n; // +1 for rounding up
|
|
2102
|
+
// B→A swap: ALL fees (LP + integrator) deducted from input B
|
|
2103
|
+
// amount_in = amount_in_effective * (1 + total_fee_rate)
|
|
2104
|
+
const amountIn = BigInt(Math.ceil(Number(amountInEffective) * (1 + feeRate)));
|
|
2105
|
+
// Fee calculation: fee = amount_in * fee_rate / (1 + fee_rate)
|
|
2106
|
+
const totalFee = BigInt(Math.ceil((Number(amountIn) * feeRate) / (1 + feeRate)));
|
|
2107
|
+
return {
|
|
2108
|
+
amountIn: amountIn.toString(),
|
|
2109
|
+
totalFee: totalFee.toString(),
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Calculate minimum amount out with slippage protection
|
|
2115
|
+
* @private
|
|
2116
|
+
*/
|
|
2117
|
+
calculateMinAmountOut(expectedAmount, slippageBps) {
|
|
2118
|
+
const amount = BigInt(expectedAmount);
|
|
2119
|
+
const slippageFactor = BigInt(10000 - slippageBps);
|
|
2120
|
+
const minAmount = (amount * slippageFactor) / 10000n;
|
|
2121
|
+
return minAmount.toString();
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Wait for a transfer to be claimed using wallet events.
|
|
2125
|
+
* This is more efficient than polling as it uses the wallet's event stream.
|
|
2126
|
+
* @private
|
|
2127
|
+
*/
|
|
2128
|
+
async waitForTransferCompletion(transferId, timeoutMs) {
|
|
2129
|
+
return new Promise((resolve) => {
|
|
2130
|
+
const timeout = setTimeout(() => {
|
|
2131
|
+
// Remove listener on timeout
|
|
2132
|
+
try {
|
|
2133
|
+
this._wallet.removeListener?.("transfer:claimed", handler);
|
|
2134
|
+
}
|
|
2135
|
+
catch {
|
|
2136
|
+
// Ignore if removeListener doesn't exist
|
|
2137
|
+
}
|
|
2138
|
+
resolve(false);
|
|
2139
|
+
}, timeoutMs);
|
|
2140
|
+
const handler = (claimedTransferId, _balance) => {
|
|
2141
|
+
if (claimedTransferId === transferId) {
|
|
2142
|
+
clearTimeout(timeout);
|
|
2143
|
+
try {
|
|
2144
|
+
this._wallet.removeListener?.("transfer:claimed", handler);
|
|
2145
|
+
}
|
|
2146
|
+
catch {
|
|
2147
|
+
// Ignore if removeListener doesn't exist
|
|
2148
|
+
}
|
|
2149
|
+
resolve(true);
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
// Subscribe to transfer claimed events
|
|
2153
|
+
// The wallet's RPC stream will automatically claim incoming transfers
|
|
2154
|
+
try {
|
|
2155
|
+
this._wallet.on?.("transfer:claimed", handler);
|
|
2156
|
+
}
|
|
2157
|
+
catch {
|
|
2158
|
+
// If event subscription fails, fall back to polling
|
|
2159
|
+
clearTimeout(timeout);
|
|
2160
|
+
this.pollForTransferCompletion(transferId, timeoutMs).then(resolve);
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Fallback polling method for transfer completion
|
|
2166
|
+
* @private
|
|
2167
|
+
*/
|
|
2168
|
+
async pollForTransferCompletion(transferId, timeoutMs) {
|
|
2169
|
+
const startTime = Date.now();
|
|
2170
|
+
const pollIntervalMs = 500;
|
|
2171
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
2172
|
+
try {
|
|
2173
|
+
const transfer = await this._wallet.getTransfer(transferId);
|
|
2174
|
+
if (transfer) {
|
|
2175
|
+
if (transfer.status === "TRANSFER_STATUS_COMPLETED") {
|
|
2176
|
+
return true;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
catch {
|
|
2181
|
+
// Ignore errors and continue polling
|
|
2182
|
+
}
|
|
2183
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
2184
|
+
}
|
|
2185
|
+
return false;
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Get Lightning fee estimate for an invoice
|
|
2189
|
+
* @private
|
|
2190
|
+
*/
|
|
2191
|
+
async getLightningFeeEstimate(invoice) {
|
|
2192
|
+
try {
|
|
2193
|
+
const feeEstimate = await this._wallet.getLightningSendFeeEstimate({
|
|
2194
|
+
encodedInvoice: invoice,
|
|
2195
|
+
});
|
|
2196
|
+
// The fee estimate might be returned as a number or an object
|
|
2197
|
+
if (typeof feeEstimate === "number") {
|
|
2198
|
+
return feeEstimate;
|
|
2199
|
+
}
|
|
2200
|
+
if (feeEstimate?.fee || feeEstimate?.feeEstimate) {
|
|
2201
|
+
return Number(feeEstimate.fee || feeEstimate.feeEstimate);
|
|
2202
|
+
}
|
|
2203
|
+
// Fallback to invoice amount-based estimate
|
|
2204
|
+
const invoiceAmount = await this.decodeInvoiceAmount(invoice);
|
|
2205
|
+
return Math.max(5, Math.ceil(invoiceAmount * 0.0017)); // 17 bps or 5 sats minimum
|
|
2206
|
+
}
|
|
2207
|
+
catch {
|
|
2208
|
+
// Fallback to invoice amount-based estimate
|
|
2209
|
+
const invoiceAmount = await this.decodeInvoiceAmount(invoice);
|
|
2210
|
+
return Math.max(5, Math.ceil(invoiceAmount * 0.0017));
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Decode the amount from a Lightning invoice (in sats)
|
|
2215
|
+
* @private
|
|
2216
|
+
*/
|
|
2217
|
+
async decodeInvoiceAmount(invoice) {
|
|
2218
|
+
// Extract amount from BOLT11 invoice
|
|
2219
|
+
// Format: ln[network][amount][multiplier]...
|
|
2220
|
+
// Amount multipliers: m = milli (0.001), u = micro (0.000001), n = nano, p = pico
|
|
2221
|
+
const lowerInvoice = invoice.toLowerCase();
|
|
2222
|
+
// Find where the amount starts (after network prefix)
|
|
2223
|
+
let amountStart = 0;
|
|
2224
|
+
if (lowerInvoice.startsWith("lnbc")) {
|
|
2225
|
+
amountStart = 4;
|
|
2226
|
+
}
|
|
2227
|
+
else if (lowerInvoice.startsWith("lntb")) {
|
|
2228
|
+
amountStart = 4;
|
|
2229
|
+
}
|
|
2230
|
+
else if (lowerInvoice.startsWith("lnbcrt")) {
|
|
2231
|
+
amountStart = 6;
|
|
2232
|
+
}
|
|
2233
|
+
else if (lowerInvoice.startsWith("lntbs")) {
|
|
2234
|
+
amountStart = 5;
|
|
2235
|
+
}
|
|
2236
|
+
else {
|
|
2237
|
+
// Unknown format, try to find amount
|
|
2238
|
+
const match = lowerInvoice.match(/^ln[a-z]+/);
|
|
2239
|
+
if (match) {
|
|
2240
|
+
amountStart = match[0].length;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
// Extract amount and multiplier
|
|
2244
|
+
const afterPrefix = lowerInvoice.substring(amountStart);
|
|
2245
|
+
const amountMatch = afterPrefix.match(/^(\d+)([munp]?)/);
|
|
2246
|
+
if (!amountMatch || !amountMatch[1]) {
|
|
2247
|
+
return 0; // Zero-amount invoice
|
|
2248
|
+
}
|
|
2249
|
+
const amount = parseInt(amountMatch[1], 10);
|
|
2250
|
+
const multiplier = amountMatch[2] ?? "";
|
|
2251
|
+
// Convert to satoshis (1 BTC = 100,000,000 sats)
|
|
2252
|
+
// Invoice amounts are in BTC by default
|
|
2253
|
+
let btcAmount;
|
|
2254
|
+
switch (multiplier) {
|
|
2255
|
+
case "m": // milli-BTC (0.001 BTC)
|
|
2256
|
+
btcAmount = amount * 0.001;
|
|
2257
|
+
break;
|
|
2258
|
+
case "u": // micro-BTC (0.000001 BTC)
|
|
2259
|
+
btcAmount = amount * 0.000001;
|
|
2260
|
+
break;
|
|
2261
|
+
case "n": // nano-BTC (0.000000001 BTC)
|
|
2262
|
+
btcAmount = amount * 0.000000001;
|
|
2263
|
+
break;
|
|
2264
|
+
case "p": // pico-BTC (0.000000000001 BTC)
|
|
2265
|
+
btcAmount = amount * 0.000000000001;
|
|
2266
|
+
break;
|
|
2267
|
+
default: // BTC
|
|
2268
|
+
btcAmount = amount;
|
|
2269
|
+
break;
|
|
2270
|
+
}
|
|
2271
|
+
// Convert BTC to sats
|
|
2272
|
+
return Math.round(btcAmount * 100000000);
|
|
2273
|
+
}
|
|
1791
2274
|
/**
|
|
1792
2275
|
* Clean up wallet connections
|
|
1793
2276
|
*/
|