@flashnet/sdk 0.5.3 → 0.5.4
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/src/client/FlashnetClient.d.ts +27 -1
- package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/cjs/src/client/FlashnetClient.js +402 -79
- package/dist/cjs/src/client/FlashnetClient.js.map +1 -1
- package/dist/esm/src/client/FlashnetClient.d.ts +27 -1
- package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/esm/src/client/FlashnetClient.js +402 -79
- package/dist/esm/src/client/FlashnetClient.js.map +1 -1
- package/package.json +3 -3
|
@@ -1819,38 +1819,36 @@ class FlashnetClient {
|
|
|
1819
1819
|
await this.ensureInitialized();
|
|
1820
1820
|
// Decode the invoice to get the amount
|
|
1821
1821
|
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
1822
|
+
// Zero-amount invoice: forward-direction quoting using caller-specified tokenAmount
|
|
1822
1823
|
if (!invoiceAmountSats || invoiceAmountSats <= 0) {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1824
|
+
const tokenAmount = options?.tokenAmount;
|
|
1825
|
+
if (!tokenAmount || BigInt(tokenAmount) <= 0n) {
|
|
1826
|
+
throw new FlashnetError("Zero-amount invoice requires tokenAmount in options.", {
|
|
1827
|
+
response: {
|
|
1828
|
+
errorCode: "FSAG-1002",
|
|
1829
|
+
errorCategory: "Validation",
|
|
1830
|
+
message: "Zero-amount invoice requires tokenAmount in options.",
|
|
1831
|
+
requestId: "",
|
|
1832
|
+
timestamp: new Date().toISOString(),
|
|
1833
|
+
service: "sdk",
|
|
1834
|
+
severity: "Error",
|
|
1835
|
+
remediation: "Provide tokenAmount when using a zero-amount invoice.",
|
|
1836
|
+
},
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
return this.getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options);
|
|
1835
1840
|
}
|
|
1836
1841
|
// Get Lightning fee estimate
|
|
1837
1842
|
const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
|
|
1838
|
-
// Total BTC needed = invoice amount + lightning fee
|
|
1843
|
+
// Total BTC needed = invoice amount + lightning fee (unmasked).
|
|
1844
|
+
// Bitmasking for V2 pools is handled inside findBestPoolForTokenToBtc.
|
|
1839
1845
|
const baseBtcNeeded = BigInt(invoiceAmountSats) + BigInt(lightningFeeEstimate);
|
|
1840
|
-
// Round up to next multiple of 64 (2^6) to account for BTC variable fee bit masking.
|
|
1841
|
-
// The AMM zeroes the lowest 6 bits of BTC output, so we need to request enough
|
|
1842
|
-
// that after masking we still have the required amount.
|
|
1843
|
-
// Example: 5014 -> masked to 4992, but if we request 5056, masked stays 5056
|
|
1844
|
-
const BTC_VARIABLE_FEE_BITS = 6n;
|
|
1845
|
-
const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
|
|
1846
|
-
const totalBtcNeeded = ((baseBtcNeeded + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
|
|
1847
|
-
BTC_VARIABLE_FEE_MASK;
|
|
1848
1846
|
// Check Flashnet minimum amounts early to provide clear error messages
|
|
1849
1847
|
const minAmounts = await this.getEnabledMinAmountsMap();
|
|
1850
1848
|
// Check BTC minimum (output from swap)
|
|
1851
1849
|
const btcMinAmount = minAmounts.get(BTC_ASSET_PUBKEY.toLowerCase());
|
|
1852
|
-
if (btcMinAmount &&
|
|
1853
|
-
const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${
|
|
1850
|
+
if (btcMinAmount && baseBtcNeeded < btcMinAmount) {
|
|
1851
|
+
const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${baseBtcNeeded} sats.`;
|
|
1854
1852
|
throw new FlashnetError(msg, {
|
|
1855
1853
|
response: {
|
|
1856
1854
|
errorCode: "FSAG-1003",
|
|
@@ -1864,8 +1862,9 @@ class FlashnetClient {
|
|
|
1864
1862
|
},
|
|
1865
1863
|
});
|
|
1866
1864
|
}
|
|
1867
|
-
// Find the best pool to swap token -> BTC
|
|
1868
|
-
|
|
1865
|
+
// Find the best pool to swap token -> BTC.
|
|
1866
|
+
// Bitmasking is applied per-pool inside this function (V2 pools get masked, V3 pools don't).
|
|
1867
|
+
const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded.toString(), options?.integratorFeeRateBps);
|
|
1869
1868
|
// Check token minimum (input to swap)
|
|
1870
1869
|
const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
|
|
1871
1870
|
const tokenMinAmount = minAmounts.get(tokenHex);
|
|
@@ -1885,13 +1884,14 @@ class FlashnetClient {
|
|
|
1885
1884
|
},
|
|
1886
1885
|
});
|
|
1887
1886
|
}
|
|
1888
|
-
//
|
|
1889
|
-
|
|
1887
|
+
// BTC variable fee adjustment: difference between what the pool targets and unmasked base.
|
|
1888
|
+
// For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
|
|
1889
|
+
const btcVariableFeeAdjustment = Number(BigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
|
|
1890
1890
|
return {
|
|
1891
1891
|
poolId: poolQuote.poolId,
|
|
1892
1892
|
tokenAddress: this.toHexTokenIdentifier(tokenAddress),
|
|
1893
1893
|
tokenAmountRequired: poolQuote.tokenAmountRequired,
|
|
1894
|
-
btcAmountRequired:
|
|
1894
|
+
btcAmountRequired: poolQuote.btcAmountUsed,
|
|
1895
1895
|
invoiceAmountSats: invoiceAmountSats,
|
|
1896
1896
|
estimatedAmmFee: poolQuote.estimatedAmmFee,
|
|
1897
1897
|
estimatedLightningFee: lightningFeeEstimate,
|
|
@@ -1901,6 +1901,138 @@ class FlashnetClient {
|
|
|
1901
1901
|
tokenIsAssetA: poolQuote.tokenIsAssetA,
|
|
1902
1902
|
poolReserves: poolQuote.poolReserves,
|
|
1903
1903
|
warningMessage: poolQuote.warningMessage,
|
|
1904
|
+
curveType: poolQuote.curveType,
|
|
1905
|
+
isZeroAmountInvoice: false,
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Generate a quote for a zero-amount invoice.
|
|
1910
|
+
* Forward-direction: simulate swapping tokenAmount and pick the pool with the best BTC output.
|
|
1911
|
+
* @private
|
|
1912
|
+
*/
|
|
1913
|
+
async getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options) {
|
|
1914
|
+
const tokenHex = this.toHexTokenIdentifier(tokenAddress);
|
|
1915
|
+
const btcHex = BTC_ASSET_PUBKEY;
|
|
1916
|
+
// Discover all token/BTC pools
|
|
1917
|
+
const [poolsWithTokenAsA, poolsWithTokenAsB] = await Promise.all([
|
|
1918
|
+
this.listPools({ assetAAddress: tokenHex, assetBAddress: btcHex }),
|
|
1919
|
+
this.listPools({ assetAAddress: btcHex, assetBAddress: tokenHex }),
|
|
1920
|
+
]);
|
|
1921
|
+
const poolMap = new Map();
|
|
1922
|
+
for (const p of [...poolsWithTokenAsA.pools, ...poolsWithTokenAsB.pools]) {
|
|
1923
|
+
if (!poolMap.has(p.lpPublicKey)) {
|
|
1924
|
+
const tokenIsAssetA = p.assetAAddress?.toLowerCase() === tokenHex.toLowerCase();
|
|
1925
|
+
poolMap.set(p.lpPublicKey, { pool: p, tokenIsAssetA });
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const allPools = Array.from(poolMap.values());
|
|
1929
|
+
if (allPools.length === 0) {
|
|
1930
|
+
throw new FlashnetError(`No liquidity pool found for token ${tokenAddress} paired with BTC`, {
|
|
1931
|
+
response: {
|
|
1932
|
+
errorCode: "FSAG-4001",
|
|
1933
|
+
errorCategory: "Business",
|
|
1934
|
+
message: `No liquidity pool found for token ${tokenAddress} paired with BTC`,
|
|
1935
|
+
requestId: "",
|
|
1936
|
+
timestamp: new Date().toISOString(),
|
|
1937
|
+
service: "sdk",
|
|
1938
|
+
severity: "Error",
|
|
1939
|
+
},
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
// Simulate each pool with tokenAmount as input, pick highest BTC output
|
|
1943
|
+
let bestResult = null;
|
|
1944
|
+
let bestBtcOut = 0n;
|
|
1945
|
+
for (const { pool, tokenIsAssetA } of allPools) {
|
|
1946
|
+
try {
|
|
1947
|
+
const poolDetails = await this.getPool(pool.lpPublicKey);
|
|
1948
|
+
const assetInAddress = tokenIsAssetA
|
|
1949
|
+
? poolDetails.assetAAddress
|
|
1950
|
+
: poolDetails.assetBAddress;
|
|
1951
|
+
const assetOutAddress = tokenIsAssetA
|
|
1952
|
+
? poolDetails.assetBAddress
|
|
1953
|
+
: poolDetails.assetAAddress;
|
|
1954
|
+
const simulation = await this.simulateSwap({
|
|
1955
|
+
poolId: pool.lpPublicKey,
|
|
1956
|
+
assetInAddress,
|
|
1957
|
+
assetOutAddress,
|
|
1958
|
+
amountIn: tokenAmount,
|
|
1959
|
+
integratorBps: options?.integratorFeeRateBps,
|
|
1960
|
+
});
|
|
1961
|
+
const btcOut = BigInt(simulation.amountOut);
|
|
1962
|
+
if (btcOut > bestBtcOut) {
|
|
1963
|
+
bestBtcOut = btcOut;
|
|
1964
|
+
bestResult = {
|
|
1965
|
+
poolId: pool.lpPublicKey,
|
|
1966
|
+
tokenIsAssetA,
|
|
1967
|
+
simulation,
|
|
1968
|
+
curveType: poolDetails.curveType,
|
|
1969
|
+
poolReserves: {
|
|
1970
|
+
assetAReserve: poolDetails.assetAReserve,
|
|
1971
|
+
assetBReserve: poolDetails.assetBReserve,
|
|
1972
|
+
},
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
catch {
|
|
1977
|
+
// Skip pools that fail simulation
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
if (!bestResult || bestBtcOut <= 0n) {
|
|
1981
|
+
throw new FlashnetError("No pool can produce BTC output for the given token amount", {
|
|
1982
|
+
response: {
|
|
1983
|
+
errorCode: "FSAG-4201",
|
|
1984
|
+
errorCategory: "Business",
|
|
1985
|
+
message: "No pool can produce BTC output for the given token amount",
|
|
1986
|
+
requestId: "",
|
|
1987
|
+
timestamp: new Date().toISOString(),
|
|
1988
|
+
service: "sdk",
|
|
1989
|
+
severity: "Error",
|
|
1990
|
+
remediation: "Try a larger token amount.",
|
|
1991
|
+
},
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
// Estimate lightning fee from the BTC output
|
|
1995
|
+
let lightningFeeEstimate;
|
|
1996
|
+
try {
|
|
1997
|
+
lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
|
|
1998
|
+
}
|
|
1999
|
+
catch {
|
|
2000
|
+
lightningFeeEstimate = Math.max(5, Math.ceil(Number(bestBtcOut) * 0.0017));
|
|
2001
|
+
}
|
|
2002
|
+
// Check minimum amounts
|
|
2003
|
+
const minAmounts = await this.getEnabledMinAmountsMap();
|
|
2004
|
+
const btcMinAmount = minAmounts.get(BTC_ASSET_PUBKEY.toLowerCase());
|
|
2005
|
+
if (btcMinAmount && bestBtcOut < btcMinAmount) {
|
|
2006
|
+
const msg = `BTC output too small. Minimum is ${btcMinAmount} sats, but swap would produce only ${bestBtcOut} sats.`;
|
|
2007
|
+
throw new FlashnetError(msg, {
|
|
2008
|
+
response: {
|
|
2009
|
+
errorCode: "FSAG-1003",
|
|
2010
|
+
errorCategory: "Validation",
|
|
2011
|
+
message: msg,
|
|
2012
|
+
requestId: "",
|
|
2013
|
+
timestamp: new Date().toISOString(),
|
|
2014
|
+
service: "sdk",
|
|
2015
|
+
severity: "Error",
|
|
2016
|
+
remediation: "Use a larger token amount.",
|
|
2017
|
+
},
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
return {
|
|
2021
|
+
poolId: bestResult.poolId,
|
|
2022
|
+
tokenAddress: tokenHex,
|
|
2023
|
+
tokenAmountRequired: tokenAmount,
|
|
2024
|
+
btcAmountRequired: bestBtcOut.toString(),
|
|
2025
|
+
invoiceAmountSats: 0,
|
|
2026
|
+
estimatedAmmFee: bestResult.simulation.feePaidAssetIn || "0",
|
|
2027
|
+
estimatedLightningFee: lightningFeeEstimate,
|
|
2028
|
+
btcVariableFeeAdjustment: 0,
|
|
2029
|
+
executionPrice: bestResult.simulation.executionPrice || "0",
|
|
2030
|
+
priceImpactPct: bestResult.simulation.priceImpactPct || "0",
|
|
2031
|
+
tokenIsAssetA: bestResult.tokenIsAssetA,
|
|
2032
|
+
poolReserves: bestResult.poolReserves,
|
|
2033
|
+
warningMessage: bestResult.simulation.warningMessage,
|
|
2034
|
+
curveType: bestResult.curveType,
|
|
2035
|
+
isZeroAmountInvoice: true,
|
|
1904
2036
|
};
|
|
1905
2037
|
}
|
|
1906
2038
|
/**
|
|
@@ -1912,7 +2044,7 @@ class FlashnetClient {
|
|
|
1912
2044
|
*/
|
|
1913
2045
|
async payLightningWithToken(options) {
|
|
1914
2046
|
await this.ensureInitialized();
|
|
1915
|
-
const { invoice, tokenAddress, maxSlippageBps = 500, // 5% default
|
|
2047
|
+
const { invoice, tokenAddress, tokenAmount, maxSlippageBps = 500, // 5% default
|
|
1916
2048
|
maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 30000, // 30s default
|
|
1917
2049
|
rollbackOnFailure = false, useExistingBtcBalance = false, } = options;
|
|
1918
2050
|
try {
|
|
@@ -1920,6 +2052,7 @@ class FlashnetClient {
|
|
|
1920
2052
|
const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
|
|
1921
2053
|
maxSlippageBps,
|
|
1922
2054
|
integratorFeeRateBps,
|
|
2055
|
+
tokenAmount,
|
|
1923
2056
|
});
|
|
1924
2057
|
// Step 2: Check token balance (always required)
|
|
1925
2058
|
await this.checkBalance({
|
|
@@ -1931,16 +2064,6 @@ class FlashnetClient {
|
|
|
1931
2064
|
],
|
|
1932
2065
|
errorPrefix: "Insufficient token balance for Lightning payment: ",
|
|
1933
2066
|
});
|
|
1934
|
-
// Determine if we can pay immediately using existing BTC balance
|
|
1935
|
-
let canPayImmediately = false;
|
|
1936
|
-
if (useExistingBtcBalance) {
|
|
1937
|
-
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
1938
|
-
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
1939
|
-
const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
|
|
1940
|
-
// Check if we have enough BTC (don't throw if not, just fall back to waiting)
|
|
1941
|
-
const balance = await this.getBalance();
|
|
1942
|
-
canPayImmediately = balance.balance >= BigInt(btcNeededForPayment);
|
|
1943
|
-
}
|
|
1944
2067
|
// Step 3: Get pool details
|
|
1945
2068
|
const pool = await this.getPool(quote.poolId);
|
|
1946
2069
|
// Step 4: Determine swap direction and execute
|
|
@@ -1974,7 +2097,15 @@ class FlashnetClient {
|
|
|
1974
2097
|
error: swapResponse.error || "Swap was not accepted",
|
|
1975
2098
|
};
|
|
1976
2099
|
}
|
|
1977
|
-
// Step 5: Wait for
|
|
2100
|
+
// Step 5: Wait for transfer (skip useExistingBtcBalance for zero-amount invoices)
|
|
2101
|
+
let canPayImmediately = false;
|
|
2102
|
+
if (!quote.isZeroAmountInvoice && useExistingBtcBalance) {
|
|
2103
|
+
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
2104
|
+
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
2105
|
+
const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
|
|
2106
|
+
const balance = await this.getBalance();
|
|
2107
|
+
canPayImmediately = balance.balance >= BigInt(btcNeededForPayment);
|
|
2108
|
+
}
|
|
1978
2109
|
if (!canPayImmediately) {
|
|
1979
2110
|
const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
|
|
1980
2111
|
if (!transferComplete) {
|
|
@@ -1989,16 +2120,45 @@ class FlashnetClient {
|
|
|
1989
2120
|
};
|
|
1990
2121
|
}
|
|
1991
2122
|
}
|
|
1992
|
-
// Step 6: Calculate Lightning fee
|
|
2123
|
+
// Step 6: Calculate Lightning fee and payment amount
|
|
1993
2124
|
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
1994
|
-
// Step 7: Pay the Lightning invoice
|
|
1995
2125
|
const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
|
|
2126
|
+
// Step 7: Pay the Lightning invoice
|
|
1996
2127
|
try {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2128
|
+
let lightningPayment;
|
|
2129
|
+
let invoiceAmountPaid;
|
|
2130
|
+
if (quote.isZeroAmountInvoice) {
|
|
2131
|
+
// Zero-amount invoice: pay whatever BTC we received minus lightning fee
|
|
2132
|
+
const actualBtc = BigInt(btcReceived);
|
|
2133
|
+
const lnFee = BigInt(effectiveMaxLightningFee);
|
|
2134
|
+
const amountToPay = actualBtc - lnFee;
|
|
2135
|
+
if (amountToPay <= 0n) {
|
|
2136
|
+
return {
|
|
2137
|
+
success: false,
|
|
2138
|
+
poolId: quote.poolId,
|
|
2139
|
+
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2140
|
+
btcAmountReceived: btcReceived,
|
|
2141
|
+
swapTransferId: swapResponse.outboundTransferId,
|
|
2142
|
+
ammFeePaid: quote.estimatedAmmFee,
|
|
2143
|
+
error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
invoiceAmountPaid = Number(amountToPay);
|
|
2147
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2148
|
+
invoice,
|
|
2149
|
+
amountSats: invoiceAmountPaid,
|
|
2150
|
+
maxFeeSats: effectiveMaxLightningFee,
|
|
2151
|
+
preferSpark,
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
// Standard invoice: pay the specified amount
|
|
2156
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2157
|
+
invoice,
|
|
2158
|
+
maxFeeSats: effectiveMaxLightningFee,
|
|
2159
|
+
preferSpark,
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2002
2162
|
return {
|
|
2003
2163
|
success: true,
|
|
2004
2164
|
poolId: quote.poolId,
|
|
@@ -2008,6 +2168,7 @@ class FlashnetClient {
|
|
|
2008
2168
|
lightningPaymentId: lightningPayment.id,
|
|
2009
2169
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2010
2170
|
lightningFeePaid: effectiveMaxLightningFee,
|
|
2171
|
+
invoiceAmountPaid,
|
|
2011
2172
|
};
|
|
2012
2173
|
}
|
|
2013
2174
|
catch (lightningError) {
|
|
@@ -2113,7 +2274,7 @@ class FlashnetClient {
|
|
|
2113
2274
|
* Find the best pool for swapping a token to BTC
|
|
2114
2275
|
* @private
|
|
2115
2276
|
*/
|
|
2116
|
-
async findBestPoolForTokenToBtc(tokenAddress,
|
|
2277
|
+
async findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded, integratorFeeRateBps) {
|
|
2117
2278
|
const tokenHex = this.toHexTokenIdentifier(tokenAddress);
|
|
2118
2279
|
const btcHex = BTC_ASSET_PUBKEY;
|
|
2119
2280
|
// Find all pools that have this token paired with BTC
|
|
@@ -2157,8 +2318,8 @@ class FlashnetClient {
|
|
|
2157
2318
|
const minAmounts = await this.getMinAmountsMap();
|
|
2158
2319
|
const btcMinAmount = minAmounts.get(BTC_ASSET_PUBKEY.toLowerCase());
|
|
2159
2320
|
// Check if the BTC amount needed is below the minimum
|
|
2160
|
-
if (btcMinAmount && BigInt(
|
|
2161
|
-
const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${
|
|
2321
|
+
if (btcMinAmount && BigInt(baseBtcNeeded) < btcMinAmount) {
|
|
2322
|
+
const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${baseBtcNeeded} sats.`;
|
|
2162
2323
|
throw new FlashnetError(msg, {
|
|
2163
2324
|
response: {
|
|
2164
2325
|
errorCode: "FSAG-1003",
|
|
@@ -2172,60 +2333,99 @@ class FlashnetClient {
|
|
|
2172
2333
|
},
|
|
2173
2334
|
});
|
|
2174
2335
|
}
|
|
2336
|
+
// Compute V2 masked BTC amount (round up to next multiple of 64 for bit masking)
|
|
2337
|
+
const baseBtc = BigInt(baseBtcNeeded);
|
|
2338
|
+
const BTC_VARIABLE_FEE_BITS = 6n;
|
|
2339
|
+
const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
|
|
2340
|
+
const maskedBtc = ((baseBtc + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
|
|
2341
|
+
BTC_VARIABLE_FEE_MASK;
|
|
2175
2342
|
// Find the best pool (lowest token cost for the required BTC)
|
|
2176
2343
|
let bestPool = null;
|
|
2177
2344
|
let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
|
|
2345
|
+
let bestBtcTarget = 0n;
|
|
2346
|
+
let bestCurveType = "";
|
|
2178
2347
|
let bestSimulation = null;
|
|
2179
2348
|
// Track errors for each pool to provide better diagnostics
|
|
2180
2349
|
const poolErrors = [];
|
|
2181
2350
|
for (const pool of allPools) {
|
|
2182
2351
|
try {
|
|
2183
|
-
// Get pool details for reserves
|
|
2352
|
+
// Get pool details for reserves and curve type
|
|
2184
2353
|
const poolDetails = await this.getPool(pool.lpPublicKey);
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2354
|
+
const isV3 = poolDetails.curveType === "V3_CONCENTRATED";
|
|
2355
|
+
// V3 pools use exact BTC amount, V2 pools use masked amount
|
|
2356
|
+
const btcTarget = isV3 ? baseBtc : maskedBtc;
|
|
2357
|
+
const assetInAddress = pool.tokenIsAssetA
|
|
2358
|
+
? poolDetails.assetAAddress
|
|
2359
|
+
: poolDetails.assetBAddress;
|
|
2360
|
+
const assetOutAddress = pool.tokenIsAssetA
|
|
2361
|
+
? poolDetails.assetBAddress
|
|
2362
|
+
: poolDetails.assetAAddress;
|
|
2363
|
+
let tokenAmount;
|
|
2364
|
+
let fee;
|
|
2365
|
+
let executionPrice;
|
|
2366
|
+
let priceImpactPct;
|
|
2367
|
+
let warningMessage;
|
|
2368
|
+
if (isV3) {
|
|
2369
|
+
// V3: binary search with simulateSwap
|
|
2370
|
+
const v3Result = await this.findV3TokenAmountForBtcOutput({
|
|
2371
|
+
poolId: pool.lpPublicKey,
|
|
2372
|
+
assetInAddress,
|
|
2373
|
+
assetOutAddress,
|
|
2374
|
+
desiredBtcOut: btcTarget,
|
|
2375
|
+
currentPriceAInB: poolDetails.currentPriceAInB,
|
|
2376
|
+
tokenIsAssetA: pool.tokenIsAssetA,
|
|
2377
|
+
integratorBps: integratorFeeRateBps,
|
|
2378
|
+
});
|
|
2379
|
+
tokenAmount = BigInt(v3Result.amountIn);
|
|
2380
|
+
fee = v3Result.totalFee;
|
|
2381
|
+
executionPrice = v3Result.simulation.executionPrice || "0";
|
|
2382
|
+
priceImpactPct = v3Result.simulation.priceImpactPct || "0";
|
|
2383
|
+
warningMessage = v3Result.simulation.warningMessage;
|
|
2384
|
+
}
|
|
2385
|
+
else {
|
|
2386
|
+
// V2: constant product math + simulation verification
|
|
2387
|
+
const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
|
|
2388
|
+
tokenAmount = BigInt(calculation.amountIn);
|
|
2190
2389
|
// Verify with simulation
|
|
2191
2390
|
const simulation = await this.simulateSwap({
|
|
2192
2391
|
poolId: pool.lpPublicKey,
|
|
2193
|
-
assetInAddress
|
|
2194
|
-
|
|
2195
|
-
: poolDetails.assetBAddress,
|
|
2196
|
-
assetOutAddress: pool.tokenIsAssetA
|
|
2197
|
-
? poolDetails.assetBAddress
|
|
2198
|
-
: poolDetails.assetAAddress,
|
|
2392
|
+
assetInAddress,
|
|
2393
|
+
assetOutAddress,
|
|
2199
2394
|
amountIn: calculation.amountIn,
|
|
2200
2395
|
integratorBps: integratorFeeRateBps,
|
|
2201
2396
|
});
|
|
2202
|
-
|
|
2203
|
-
if (BigInt(simulation.amountOut) >= BigInt(btcAmountNeeded)) {
|
|
2204
|
-
bestPool = pool;
|
|
2205
|
-
bestTokenAmount = tokenAmount;
|
|
2206
|
-
bestSimulation = {
|
|
2207
|
-
amountIn: calculation.amountIn,
|
|
2208
|
-
fee: calculation.totalFee,
|
|
2209
|
-
executionPrice: simulation.executionPrice || "0",
|
|
2210
|
-
priceImpactPct: simulation.priceImpactPct || "0",
|
|
2211
|
-
warningMessage: simulation.warningMessage,
|
|
2212
|
-
};
|
|
2213
|
-
}
|
|
2214
|
-
else {
|
|
2215
|
-
// Simulation output was insufficient
|
|
2397
|
+
if (BigInt(simulation.amountOut) < btcTarget) {
|
|
2216
2398
|
const btcReserve = pool.tokenIsAssetA
|
|
2217
2399
|
? poolDetails.assetBReserve
|
|
2218
2400
|
: poolDetails.assetAReserve;
|
|
2219
2401
|
poolErrors.push({
|
|
2220
2402
|
poolId: pool.lpPublicKey,
|
|
2221
|
-
error: `Simulation output (${simulation.amountOut} sats) < required (${
|
|
2403
|
+
error: `Simulation output (${simulation.amountOut} sats) < required (${btcTarget} sats)`,
|
|
2222
2404
|
btcReserve,
|
|
2223
2405
|
});
|
|
2406
|
+
continue;
|
|
2224
2407
|
}
|
|
2408
|
+
fee = calculation.totalFee;
|
|
2409
|
+
executionPrice = simulation.executionPrice || "0";
|
|
2410
|
+
priceImpactPct = simulation.priceImpactPct || "0";
|
|
2411
|
+
warningMessage = simulation.warningMessage;
|
|
2412
|
+
}
|
|
2413
|
+
// Check if this pool offers a better rate
|
|
2414
|
+
if (tokenAmount < bestTokenAmount) {
|
|
2415
|
+
bestPool = pool;
|
|
2416
|
+
bestTokenAmount = tokenAmount;
|
|
2417
|
+
bestBtcTarget = btcTarget;
|
|
2418
|
+
bestCurveType = poolDetails.curveType;
|
|
2419
|
+
bestSimulation = {
|
|
2420
|
+
amountIn: tokenAmount.toString(),
|
|
2421
|
+
fee,
|
|
2422
|
+
executionPrice,
|
|
2423
|
+
priceImpactPct,
|
|
2424
|
+
warningMessage,
|
|
2425
|
+
};
|
|
2225
2426
|
}
|
|
2226
2427
|
}
|
|
2227
2428
|
catch (e) {
|
|
2228
|
-
// Capture pool errors for diagnostics
|
|
2229
2429
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2230
2430
|
poolErrors.push({
|
|
2231
2431
|
poolId: pool.lpPublicKey,
|
|
@@ -2234,7 +2434,7 @@ class FlashnetClient {
|
|
|
2234
2434
|
}
|
|
2235
2435
|
}
|
|
2236
2436
|
if (!bestPool || !bestSimulation) {
|
|
2237
|
-
let errorMessage = `No pool has sufficient liquidity for ${
|
|
2437
|
+
let errorMessage = `No pool has sufficient liquidity for ${baseBtcNeeded} sats`;
|
|
2238
2438
|
if (poolErrors.length > 0) {
|
|
2239
2439
|
const details = poolErrors
|
|
2240
2440
|
.map((pe) => {
|
|
@@ -2272,6 +2472,8 @@ class FlashnetClient {
|
|
|
2272
2472
|
assetBReserve: poolDetails.assetBReserve,
|
|
2273
2473
|
},
|
|
2274
2474
|
warningMessage: bestSimulation.warningMessage,
|
|
2475
|
+
btcAmountUsed: bestBtcTarget.toString(),
|
|
2476
|
+
curveType: bestCurveType,
|
|
2275
2477
|
};
|
|
2276
2478
|
}
|
|
2277
2479
|
/**
|
|
@@ -2339,6 +2541,127 @@ class FlashnetClient {
|
|
|
2339
2541
|
};
|
|
2340
2542
|
}
|
|
2341
2543
|
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Find the token amount needed to get a specific BTC output from a V3 concentrated liquidity pool.
|
|
2546
|
+
* Uses binary search with simulateSwap since V3 tick-based math can't be inverted locally.
|
|
2547
|
+
* @private
|
|
2548
|
+
*/
|
|
2549
|
+
async findV3TokenAmountForBtcOutput(params) {
|
|
2550
|
+
const { poolId, assetInAddress, assetOutAddress, desiredBtcOut, currentPriceAInB, tokenIsAssetA, integratorBps, } = params;
|
|
2551
|
+
// Step 1: Compute initial estimate from pool price
|
|
2552
|
+
let estimate;
|
|
2553
|
+
if (currentPriceAInB && currentPriceAInB !== "0") {
|
|
2554
|
+
const price = Number(currentPriceAInB);
|
|
2555
|
+
if (tokenIsAssetA) {
|
|
2556
|
+
// priceAInB = how much B (BTC) per 1 A (token), so tokenNeeded = btcOut / price
|
|
2557
|
+
estimate = BigInt(Math.ceil(Number(desiredBtcOut) / price));
|
|
2558
|
+
}
|
|
2559
|
+
else {
|
|
2560
|
+
// priceAInB = how much B (token) per 1 A (BTC), so tokenNeeded = btcOut * price
|
|
2561
|
+
estimate = BigInt(Math.ceil(Number(desiredBtcOut) * price));
|
|
2562
|
+
}
|
|
2563
|
+
// Ensure non-zero
|
|
2564
|
+
if (estimate <= 0n) {
|
|
2565
|
+
estimate = desiredBtcOut * 2n;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
else {
|
|
2569
|
+
estimate = desiredBtcOut * 2n;
|
|
2570
|
+
}
|
|
2571
|
+
// Step 2: Find upper bound by simulating with estimate + 10% buffer
|
|
2572
|
+
let upperBound = (estimate * 110n) / 100n;
|
|
2573
|
+
if (upperBound <= 0n) {
|
|
2574
|
+
upperBound = 1n;
|
|
2575
|
+
}
|
|
2576
|
+
let upperSim = null;
|
|
2577
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2578
|
+
const sim = await this.simulateSwap({
|
|
2579
|
+
poolId,
|
|
2580
|
+
assetInAddress,
|
|
2581
|
+
assetOutAddress,
|
|
2582
|
+
amountIn: upperBound.toString(),
|
|
2583
|
+
integratorBps,
|
|
2584
|
+
});
|
|
2585
|
+
if (BigInt(sim.amountOut) >= desiredBtcOut) {
|
|
2586
|
+
upperSim = sim;
|
|
2587
|
+
break;
|
|
2588
|
+
}
|
|
2589
|
+
// Double the upper bound
|
|
2590
|
+
upperBound = upperBound * 2n;
|
|
2591
|
+
}
|
|
2592
|
+
if (!upperSim) {
|
|
2593
|
+
throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
|
|
2594
|
+
}
|
|
2595
|
+
// Step 3: Refine estimate via linear interpolation
|
|
2596
|
+
const upperOut = BigInt(upperSim.amountOut);
|
|
2597
|
+
// Scale proportionally: if upperBound produced upperOut, we need roughly
|
|
2598
|
+
// (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
|
|
2599
|
+
let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
|
|
2600
|
+
if (refined <= 0n) {
|
|
2601
|
+
refined = 1n;
|
|
2602
|
+
}
|
|
2603
|
+
let bestAmountIn = upperBound;
|
|
2604
|
+
let bestSim = upperSim;
|
|
2605
|
+
// Check if the refined estimate is tighter
|
|
2606
|
+
if (refined < upperBound) {
|
|
2607
|
+
const refinedSim = await this.simulateSwap({
|
|
2608
|
+
poolId,
|
|
2609
|
+
assetInAddress,
|
|
2610
|
+
assetOutAddress,
|
|
2611
|
+
amountIn: refined.toString(),
|
|
2612
|
+
integratorBps,
|
|
2613
|
+
});
|
|
2614
|
+
if (BigInt(refinedSim.amountOut) >= desiredBtcOut) {
|
|
2615
|
+
bestAmountIn = refined;
|
|
2616
|
+
bestSim = refinedSim;
|
|
2617
|
+
}
|
|
2618
|
+
else {
|
|
2619
|
+
// Refined estimate was slightly too low. Keep upperBound as best,
|
|
2620
|
+
// and let binary search narrow between refined (too low) and upperBound (sufficient).
|
|
2621
|
+
bestAmountIn = upperBound;
|
|
2622
|
+
bestSim = upperSim;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
// Step 4: Binary search to converge on minimum amountIn
|
|
2626
|
+
// Use a tight range: the interpolation is close, so search between 99.5% and 100% of best
|
|
2627
|
+
let lo = bestAmountIn === upperBound
|
|
2628
|
+
? refined < upperBound
|
|
2629
|
+
? refined
|
|
2630
|
+
: (bestAmountIn * 99n) / 100n
|
|
2631
|
+
: (bestAmountIn * 999n) / 1000n;
|
|
2632
|
+
if (lo <= 0n) {
|
|
2633
|
+
lo = 1n;
|
|
2634
|
+
}
|
|
2635
|
+
let hi = bestAmountIn;
|
|
2636
|
+
for (let i = 0; i < 6; i++) {
|
|
2637
|
+
if (hi - lo <= 1n) {
|
|
2638
|
+
break;
|
|
2639
|
+
}
|
|
2640
|
+
const mid = (lo + hi) / 2n;
|
|
2641
|
+
const midSim = await this.simulateSwap({
|
|
2642
|
+
poolId,
|
|
2643
|
+
assetInAddress,
|
|
2644
|
+
assetOutAddress,
|
|
2645
|
+
amountIn: mid.toString(),
|
|
2646
|
+
integratorBps,
|
|
2647
|
+
});
|
|
2648
|
+
if (BigInt(midSim.amountOut) >= desiredBtcOut) {
|
|
2649
|
+
hi = mid;
|
|
2650
|
+
bestAmountIn = mid;
|
|
2651
|
+
bestSim = midSim;
|
|
2652
|
+
}
|
|
2653
|
+
else {
|
|
2654
|
+
lo = mid;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
// Compute fee from the best simulation
|
|
2658
|
+
const totalFee = bestSim.feePaidAssetIn || "0";
|
|
2659
|
+
return {
|
|
2660
|
+
amountIn: bestAmountIn.toString(),
|
|
2661
|
+
totalFee,
|
|
2662
|
+
simulation: bestSim,
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2342
2665
|
/**
|
|
2343
2666
|
* Calculate minimum amount out with slippage protection
|
|
2344
2667
|
* @private
|