@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.
@@ -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
- throw new FlashnetError("Unable to decode invoice amount. Zero-amount invoices are not supported for token payments.", {
1824
- response: {
1825
- errorCode: "FSAG-1002",
1826
- errorCategory: "Validation",
1827
- message: "Unable to decode invoice amount. Zero-amount invoices are not supported for token payments.",
1828
- requestId: "",
1829
- timestamp: new Date().toISOString(),
1830
- service: "sdk",
1831
- severity: "Error",
1832
- remediation: "Provide a valid BOLT11 invoice with a non-zero amount.",
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 && totalBtcNeeded < btcMinAmount) {
1853
- const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${totalBtcNeeded} sats.`;
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
- const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, totalBtcNeeded.toString(), options?.integratorFeeRateBps);
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
- // Calculate the BTC variable fee adjustment (how much extra we're requesting)
1889
- const btcVariableFeeAdjustment = Number(totalBtcNeeded - baseBtcNeeded);
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: totalBtcNeeded.toString(),
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 the transfer to complete (unless paying immediately with existing BTC)
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 limit - use the quoted estimate, not a recalculation
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
- const lightningPayment = await this._wallet.payLightningInvoice({
1998
- invoice,
1999
- maxFeeSats: effectiveMaxLightningFee,
2000
- preferSpark,
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, btcAmountNeeded, integratorFeeRateBps) {
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(btcAmountNeeded) < btcMinAmount) {
2161
- const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${btcAmountNeeded} sats.`;
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
- // Calculate the token amount needed using AMM math
2186
- const calculation = this.calculateTokenAmountForBtcOutput(btcAmountNeeded, poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
2187
- const tokenAmount = BigInt(calculation.amountIn);
2188
- // Check if this is better than our current best
2189
- if (tokenAmount < bestTokenAmount) {
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: pool.tokenIsAssetA
2194
- ? poolDetails.assetAAddress
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
- // Verify the output is sufficient
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 (${btcAmountNeeded} sats)`,
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 ${btcAmountNeeded} sats`;
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