@flashnet/sdk 0.5.2 → 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.
@@ -1825,38 +1825,36 @@ class FlashnetClient {
1825
1825
  await this.ensureInitialized();
1826
1826
  // Decode the invoice to get the amount
1827
1827
  const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1828
+ // Zero-amount invoice: forward-direction quoting using caller-specified tokenAmount
1828
1829
  if (!invoiceAmountSats || invoiceAmountSats <= 0) {
1829
- throw new errors.FlashnetError("Unable to decode invoice amount. Zero-amount invoices are not supported for token payments.", {
1830
- response: {
1831
- errorCode: "FSAG-1002",
1832
- errorCategory: "Validation",
1833
- message: "Unable to decode invoice amount. Zero-amount invoices are not supported for token payments.",
1834
- requestId: "",
1835
- timestamp: new Date().toISOString(),
1836
- service: "sdk",
1837
- severity: "Error",
1838
- remediation: "Provide a valid BOLT11 invoice with a non-zero amount.",
1839
- },
1840
- });
1830
+ const tokenAmount = options?.tokenAmount;
1831
+ if (!tokenAmount || BigInt(tokenAmount) <= 0n) {
1832
+ throw new errors.FlashnetError("Zero-amount invoice requires tokenAmount in options.", {
1833
+ response: {
1834
+ errorCode: "FSAG-1002",
1835
+ errorCategory: "Validation",
1836
+ message: "Zero-amount invoice requires tokenAmount in options.",
1837
+ requestId: "",
1838
+ timestamp: new Date().toISOString(),
1839
+ service: "sdk",
1840
+ severity: "Error",
1841
+ remediation: "Provide tokenAmount when using a zero-amount invoice.",
1842
+ },
1843
+ });
1844
+ }
1845
+ return this.getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options);
1841
1846
  }
1842
1847
  // Get Lightning fee estimate
1843
1848
  const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
1844
- // Total BTC needed = invoice amount + lightning fee
1849
+ // Total BTC needed = invoice amount + lightning fee (unmasked).
1850
+ // Bitmasking for V2 pools is handled inside findBestPoolForTokenToBtc.
1845
1851
  const baseBtcNeeded = BigInt(invoiceAmountSats) + BigInt(lightningFeeEstimate);
1846
- // Round up to next multiple of 64 (2^6) to account for BTC variable fee bit masking.
1847
- // The AMM zeroes the lowest 6 bits of BTC output, so we need to request enough
1848
- // that after masking we still have the required amount.
1849
- // Example: 5014 -> masked to 4992, but if we request 5056, masked stays 5056
1850
- const BTC_VARIABLE_FEE_BITS = 6n;
1851
- const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
1852
- const totalBtcNeeded = ((baseBtcNeeded + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
1853
- BTC_VARIABLE_FEE_MASK;
1854
1852
  // Check Flashnet minimum amounts early to provide clear error messages
1855
1853
  const minAmounts = await this.getEnabledMinAmountsMap();
1856
1854
  // Check BTC minimum (output from swap)
1857
1855
  const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
1858
- if (btcMinAmount && totalBtcNeeded < btcMinAmount) {
1859
- const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${totalBtcNeeded} sats.`;
1856
+ if (btcMinAmount && baseBtcNeeded < btcMinAmount) {
1857
+ const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${baseBtcNeeded} sats.`;
1860
1858
  throw new errors.FlashnetError(msg, {
1861
1859
  response: {
1862
1860
  errorCode: "FSAG-1003",
@@ -1870,8 +1868,9 @@ class FlashnetClient {
1870
1868
  },
1871
1869
  });
1872
1870
  }
1873
- // Find the best pool to swap token -> BTC
1874
- const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, totalBtcNeeded.toString(), options?.integratorFeeRateBps);
1871
+ // Find the best pool to swap token -> BTC.
1872
+ // Bitmasking is applied per-pool inside this function (V2 pools get masked, V3 pools don't).
1873
+ const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded.toString(), options?.integratorFeeRateBps);
1875
1874
  // Check token minimum (input to swap)
1876
1875
  const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
1877
1876
  const tokenMinAmount = minAmounts.get(tokenHex);
@@ -1891,13 +1890,14 @@ class FlashnetClient {
1891
1890
  },
1892
1891
  });
1893
1892
  }
1894
- // Calculate the BTC variable fee adjustment (how much extra we're requesting)
1895
- const btcVariableFeeAdjustment = Number(totalBtcNeeded - baseBtcNeeded);
1893
+ // BTC variable fee adjustment: difference between what the pool targets and unmasked base.
1894
+ // For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
1895
+ const btcVariableFeeAdjustment = Number(BigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
1896
1896
  return {
1897
1897
  poolId: poolQuote.poolId,
1898
1898
  tokenAddress: this.toHexTokenIdentifier(tokenAddress),
1899
1899
  tokenAmountRequired: poolQuote.tokenAmountRequired,
1900
- btcAmountRequired: totalBtcNeeded.toString(),
1900
+ btcAmountRequired: poolQuote.btcAmountUsed,
1901
1901
  invoiceAmountSats: invoiceAmountSats,
1902
1902
  estimatedAmmFee: poolQuote.estimatedAmmFee,
1903
1903
  estimatedLightningFee: lightningFeeEstimate,
@@ -1907,6 +1907,138 @@ class FlashnetClient {
1907
1907
  tokenIsAssetA: poolQuote.tokenIsAssetA,
1908
1908
  poolReserves: poolQuote.poolReserves,
1909
1909
  warningMessage: poolQuote.warningMessage,
1910
+ curveType: poolQuote.curveType,
1911
+ isZeroAmountInvoice: false,
1912
+ };
1913
+ }
1914
+ /**
1915
+ * Generate a quote for a zero-amount invoice.
1916
+ * Forward-direction: simulate swapping tokenAmount and pick the pool with the best BTC output.
1917
+ * @private
1918
+ */
1919
+ async getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options) {
1920
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress);
1921
+ const btcHex = index$1.BTC_ASSET_PUBKEY;
1922
+ // Discover all token/BTC pools
1923
+ const [poolsWithTokenAsA, poolsWithTokenAsB] = await Promise.all([
1924
+ this.listPools({ assetAAddress: tokenHex, assetBAddress: btcHex }),
1925
+ this.listPools({ assetAAddress: btcHex, assetBAddress: tokenHex }),
1926
+ ]);
1927
+ const poolMap = new Map();
1928
+ for (const p of [...poolsWithTokenAsA.pools, ...poolsWithTokenAsB.pools]) {
1929
+ if (!poolMap.has(p.lpPublicKey)) {
1930
+ const tokenIsAssetA = p.assetAAddress?.toLowerCase() === tokenHex.toLowerCase();
1931
+ poolMap.set(p.lpPublicKey, { pool: p, tokenIsAssetA });
1932
+ }
1933
+ }
1934
+ const allPools = Array.from(poolMap.values());
1935
+ if (allPools.length === 0) {
1936
+ throw new errors.FlashnetError(`No liquidity pool found for token ${tokenAddress} paired with BTC`, {
1937
+ response: {
1938
+ errorCode: "FSAG-4001",
1939
+ errorCategory: "Business",
1940
+ message: `No liquidity pool found for token ${tokenAddress} paired with BTC`,
1941
+ requestId: "",
1942
+ timestamp: new Date().toISOString(),
1943
+ service: "sdk",
1944
+ severity: "Error",
1945
+ },
1946
+ });
1947
+ }
1948
+ // Simulate each pool with tokenAmount as input, pick highest BTC output
1949
+ let bestResult = null;
1950
+ let bestBtcOut = 0n;
1951
+ for (const { pool, tokenIsAssetA } of allPools) {
1952
+ try {
1953
+ const poolDetails = await this.getPool(pool.lpPublicKey);
1954
+ const assetInAddress = tokenIsAssetA
1955
+ ? poolDetails.assetAAddress
1956
+ : poolDetails.assetBAddress;
1957
+ const assetOutAddress = tokenIsAssetA
1958
+ ? poolDetails.assetBAddress
1959
+ : poolDetails.assetAAddress;
1960
+ const simulation = await this.simulateSwap({
1961
+ poolId: pool.lpPublicKey,
1962
+ assetInAddress,
1963
+ assetOutAddress,
1964
+ amountIn: tokenAmount,
1965
+ integratorBps: options?.integratorFeeRateBps,
1966
+ });
1967
+ const btcOut = BigInt(simulation.amountOut);
1968
+ if (btcOut > bestBtcOut) {
1969
+ bestBtcOut = btcOut;
1970
+ bestResult = {
1971
+ poolId: pool.lpPublicKey,
1972
+ tokenIsAssetA,
1973
+ simulation,
1974
+ curveType: poolDetails.curveType,
1975
+ poolReserves: {
1976
+ assetAReserve: poolDetails.assetAReserve,
1977
+ assetBReserve: poolDetails.assetBReserve,
1978
+ },
1979
+ };
1980
+ }
1981
+ }
1982
+ catch {
1983
+ // Skip pools that fail simulation
1984
+ }
1985
+ }
1986
+ if (!bestResult || bestBtcOut <= 0n) {
1987
+ throw new errors.FlashnetError("No pool can produce BTC output for the given token amount", {
1988
+ response: {
1989
+ errorCode: "FSAG-4201",
1990
+ errorCategory: "Business",
1991
+ message: "No pool can produce BTC output for the given token amount",
1992
+ requestId: "",
1993
+ timestamp: new Date().toISOString(),
1994
+ service: "sdk",
1995
+ severity: "Error",
1996
+ remediation: "Try a larger token amount.",
1997
+ },
1998
+ });
1999
+ }
2000
+ // Estimate lightning fee from the BTC output
2001
+ let lightningFeeEstimate;
2002
+ try {
2003
+ lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
2004
+ }
2005
+ catch {
2006
+ lightningFeeEstimate = Math.max(5, Math.ceil(Number(bestBtcOut) * 0.0017));
2007
+ }
2008
+ // Check minimum amounts
2009
+ const minAmounts = await this.getEnabledMinAmountsMap();
2010
+ const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
2011
+ if (btcMinAmount && bestBtcOut < btcMinAmount) {
2012
+ const msg = `BTC output too small. Minimum is ${btcMinAmount} sats, but swap would produce only ${bestBtcOut} sats.`;
2013
+ throw new errors.FlashnetError(msg, {
2014
+ response: {
2015
+ errorCode: "FSAG-1003",
2016
+ errorCategory: "Validation",
2017
+ message: msg,
2018
+ requestId: "",
2019
+ timestamp: new Date().toISOString(),
2020
+ service: "sdk",
2021
+ severity: "Error",
2022
+ remediation: "Use a larger token amount.",
2023
+ },
2024
+ });
2025
+ }
2026
+ return {
2027
+ poolId: bestResult.poolId,
2028
+ tokenAddress: tokenHex,
2029
+ tokenAmountRequired: tokenAmount,
2030
+ btcAmountRequired: bestBtcOut.toString(),
2031
+ invoiceAmountSats: 0,
2032
+ estimatedAmmFee: bestResult.simulation.feePaidAssetIn || "0",
2033
+ estimatedLightningFee: lightningFeeEstimate,
2034
+ btcVariableFeeAdjustment: 0,
2035
+ executionPrice: bestResult.simulation.executionPrice || "0",
2036
+ priceImpactPct: bestResult.simulation.priceImpactPct || "0",
2037
+ tokenIsAssetA: bestResult.tokenIsAssetA,
2038
+ poolReserves: bestResult.poolReserves,
2039
+ warningMessage: bestResult.simulation.warningMessage,
2040
+ curveType: bestResult.curveType,
2041
+ isZeroAmountInvoice: true,
1910
2042
  };
1911
2043
  }
1912
2044
  /**
@@ -1918,7 +2050,7 @@ class FlashnetClient {
1918
2050
  */
1919
2051
  async payLightningWithToken(options) {
1920
2052
  await this.ensureInitialized();
1921
- const { invoice, tokenAddress, maxSlippageBps = 500, // 5% default
2053
+ const { invoice, tokenAddress, tokenAmount, maxSlippageBps = 500, // 5% default
1922
2054
  maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 30000, // 30s default
1923
2055
  rollbackOnFailure = false, useExistingBtcBalance = false, } = options;
1924
2056
  try {
@@ -1926,6 +2058,7 @@ class FlashnetClient {
1926
2058
  const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
1927
2059
  maxSlippageBps,
1928
2060
  integratorFeeRateBps,
2061
+ tokenAmount,
1929
2062
  });
1930
2063
  // Step 2: Check token balance (always required)
1931
2064
  await this.checkBalance({
@@ -1937,16 +2070,6 @@ class FlashnetClient {
1937
2070
  ],
1938
2071
  errorPrefix: "Insufficient token balance for Lightning payment: ",
1939
2072
  });
1940
- // Determine if we can pay immediately using existing BTC balance
1941
- let canPayImmediately = false;
1942
- if (useExistingBtcBalance) {
1943
- const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1944
- const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
1945
- const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
1946
- // Check if we have enough BTC (don't throw if not, just fall back to waiting)
1947
- const balance = await this.getBalance();
1948
- canPayImmediately = balance.balance >= BigInt(btcNeededForPayment);
1949
- }
1950
2073
  // Step 3: Get pool details
1951
2074
  const pool = await this.getPool(quote.poolId);
1952
2075
  // Step 4: Determine swap direction and execute
@@ -1980,7 +2103,15 @@ class FlashnetClient {
1980
2103
  error: swapResponse.error || "Swap was not accepted",
1981
2104
  };
1982
2105
  }
1983
- // Step 5: Wait for the transfer to complete (unless paying immediately with existing BTC)
2106
+ // Step 5: Wait for transfer (skip useExistingBtcBalance for zero-amount invoices)
2107
+ let canPayImmediately = false;
2108
+ if (!quote.isZeroAmountInvoice && useExistingBtcBalance) {
2109
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
2110
+ const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2111
+ const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
2112
+ const balance = await this.getBalance();
2113
+ canPayImmediately = balance.balance >= BigInt(btcNeededForPayment);
2114
+ }
1984
2115
  if (!canPayImmediately) {
1985
2116
  const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
1986
2117
  if (!transferComplete) {
@@ -1995,16 +2126,45 @@ class FlashnetClient {
1995
2126
  };
1996
2127
  }
1997
2128
  }
1998
- // Step 6: Calculate Lightning fee limit - use the quoted estimate, not a recalculation
2129
+ // Step 6: Calculate Lightning fee and payment amount
1999
2130
  const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2000
- // Step 7: Pay the Lightning invoice
2001
2131
  const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
2132
+ // Step 7: Pay the Lightning invoice
2002
2133
  try {
2003
- const lightningPayment = await this._wallet.payLightningInvoice({
2004
- invoice,
2005
- maxFeeSats: effectiveMaxLightningFee,
2006
- preferSpark,
2007
- });
2134
+ let lightningPayment;
2135
+ let invoiceAmountPaid;
2136
+ if (quote.isZeroAmountInvoice) {
2137
+ // Zero-amount invoice: pay whatever BTC we received minus lightning fee
2138
+ const actualBtc = BigInt(btcReceived);
2139
+ const lnFee = BigInt(effectiveMaxLightningFee);
2140
+ const amountToPay = actualBtc - lnFee;
2141
+ if (amountToPay <= 0n) {
2142
+ return {
2143
+ success: false,
2144
+ poolId: quote.poolId,
2145
+ tokenAmountSpent: quote.tokenAmountRequired,
2146
+ btcAmountReceived: btcReceived,
2147
+ swapTransferId: swapResponse.outboundTransferId,
2148
+ ammFeePaid: quote.estimatedAmmFee,
2149
+ error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
2150
+ };
2151
+ }
2152
+ invoiceAmountPaid = Number(amountToPay);
2153
+ lightningPayment = await this._wallet.payLightningInvoice({
2154
+ invoice,
2155
+ amountSats: invoiceAmountPaid,
2156
+ maxFeeSats: effectiveMaxLightningFee,
2157
+ preferSpark,
2158
+ });
2159
+ }
2160
+ else {
2161
+ // Standard invoice: pay the specified amount
2162
+ lightningPayment = await this._wallet.payLightningInvoice({
2163
+ invoice,
2164
+ maxFeeSats: effectiveMaxLightningFee,
2165
+ preferSpark,
2166
+ });
2167
+ }
2008
2168
  return {
2009
2169
  success: true,
2010
2170
  poolId: quote.poolId,
@@ -2014,6 +2174,7 @@ class FlashnetClient {
2014
2174
  lightningPaymentId: lightningPayment.id,
2015
2175
  ammFeePaid: quote.estimatedAmmFee,
2016
2176
  lightningFeePaid: effectiveMaxLightningFee,
2177
+ invoiceAmountPaid,
2017
2178
  };
2018
2179
  }
2019
2180
  catch (lightningError) {
@@ -2119,7 +2280,7 @@ class FlashnetClient {
2119
2280
  * Find the best pool for swapping a token to BTC
2120
2281
  * @private
2121
2282
  */
2122
- async findBestPoolForTokenToBtc(tokenAddress, btcAmountNeeded, integratorFeeRateBps) {
2283
+ async findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded, integratorFeeRateBps) {
2123
2284
  const tokenHex = this.toHexTokenIdentifier(tokenAddress);
2124
2285
  const btcHex = index$1.BTC_ASSET_PUBKEY;
2125
2286
  // Find all pools that have this token paired with BTC
@@ -2163,8 +2324,8 @@ class FlashnetClient {
2163
2324
  const minAmounts = await this.getMinAmountsMap();
2164
2325
  const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
2165
2326
  // Check if the BTC amount needed is below the minimum
2166
- if (btcMinAmount && BigInt(btcAmountNeeded) < btcMinAmount) {
2167
- const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${btcAmountNeeded} sats.`;
2327
+ if (btcMinAmount && BigInt(baseBtcNeeded) < btcMinAmount) {
2328
+ const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${baseBtcNeeded} sats.`;
2168
2329
  throw new errors.FlashnetError(msg, {
2169
2330
  response: {
2170
2331
  errorCode: "FSAG-1003",
@@ -2178,60 +2339,99 @@ class FlashnetClient {
2178
2339
  },
2179
2340
  });
2180
2341
  }
2342
+ // Compute V2 masked BTC amount (round up to next multiple of 64 for bit masking)
2343
+ const baseBtc = BigInt(baseBtcNeeded);
2344
+ const BTC_VARIABLE_FEE_BITS = 6n;
2345
+ const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
2346
+ const maskedBtc = ((baseBtc + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
2347
+ BTC_VARIABLE_FEE_MASK;
2181
2348
  // Find the best pool (lowest token cost for the required BTC)
2182
2349
  let bestPool = null;
2183
2350
  let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
2351
+ let bestBtcTarget = 0n;
2352
+ let bestCurveType = "";
2184
2353
  let bestSimulation = null;
2185
2354
  // Track errors for each pool to provide better diagnostics
2186
2355
  const poolErrors = [];
2187
2356
  for (const pool of allPools) {
2188
2357
  try {
2189
- // Get pool details for reserves
2358
+ // Get pool details for reserves and curve type
2190
2359
  const poolDetails = await this.getPool(pool.lpPublicKey);
2191
- // Calculate the token amount needed using AMM math
2192
- const calculation = this.calculateTokenAmountForBtcOutput(btcAmountNeeded, poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
2193
- const tokenAmount = BigInt(calculation.amountIn);
2194
- // Check if this is better than our current best
2195
- if (tokenAmount < bestTokenAmount) {
2360
+ const isV3 = poolDetails.curveType === "V3_CONCENTRATED";
2361
+ // V3 pools use exact BTC amount, V2 pools use masked amount
2362
+ const btcTarget = isV3 ? baseBtc : maskedBtc;
2363
+ const assetInAddress = pool.tokenIsAssetA
2364
+ ? poolDetails.assetAAddress
2365
+ : poolDetails.assetBAddress;
2366
+ const assetOutAddress = pool.tokenIsAssetA
2367
+ ? poolDetails.assetBAddress
2368
+ : poolDetails.assetAAddress;
2369
+ let tokenAmount;
2370
+ let fee;
2371
+ let executionPrice;
2372
+ let priceImpactPct;
2373
+ let warningMessage;
2374
+ if (isV3) {
2375
+ // V3: binary search with simulateSwap
2376
+ const v3Result = await this.findV3TokenAmountForBtcOutput({
2377
+ poolId: pool.lpPublicKey,
2378
+ assetInAddress,
2379
+ assetOutAddress,
2380
+ desiredBtcOut: btcTarget,
2381
+ currentPriceAInB: poolDetails.currentPriceAInB,
2382
+ tokenIsAssetA: pool.tokenIsAssetA,
2383
+ integratorBps: integratorFeeRateBps,
2384
+ });
2385
+ tokenAmount = BigInt(v3Result.amountIn);
2386
+ fee = v3Result.totalFee;
2387
+ executionPrice = v3Result.simulation.executionPrice || "0";
2388
+ priceImpactPct = v3Result.simulation.priceImpactPct || "0";
2389
+ warningMessage = v3Result.simulation.warningMessage;
2390
+ }
2391
+ else {
2392
+ // V2: constant product math + simulation verification
2393
+ const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
2394
+ tokenAmount = BigInt(calculation.amountIn);
2196
2395
  // Verify with simulation
2197
2396
  const simulation = await this.simulateSwap({
2198
2397
  poolId: pool.lpPublicKey,
2199
- assetInAddress: pool.tokenIsAssetA
2200
- ? poolDetails.assetAAddress
2201
- : poolDetails.assetBAddress,
2202
- assetOutAddress: pool.tokenIsAssetA
2203
- ? poolDetails.assetBAddress
2204
- : poolDetails.assetAAddress,
2398
+ assetInAddress,
2399
+ assetOutAddress,
2205
2400
  amountIn: calculation.amountIn,
2206
2401
  integratorBps: integratorFeeRateBps,
2207
2402
  });
2208
- // Verify the output is sufficient
2209
- if (BigInt(simulation.amountOut) >= BigInt(btcAmountNeeded)) {
2210
- bestPool = pool;
2211
- bestTokenAmount = tokenAmount;
2212
- bestSimulation = {
2213
- amountIn: calculation.amountIn,
2214
- fee: calculation.totalFee,
2215
- executionPrice: simulation.executionPrice || "0",
2216
- priceImpactPct: simulation.priceImpactPct || "0",
2217
- warningMessage: simulation.warningMessage,
2218
- };
2219
- }
2220
- else {
2221
- // Simulation output was insufficient
2403
+ if (BigInt(simulation.amountOut) < btcTarget) {
2222
2404
  const btcReserve = pool.tokenIsAssetA
2223
2405
  ? poolDetails.assetBReserve
2224
2406
  : poolDetails.assetAReserve;
2225
2407
  poolErrors.push({
2226
2408
  poolId: pool.lpPublicKey,
2227
- error: `Simulation output (${simulation.amountOut} sats) < required (${btcAmountNeeded} sats)`,
2409
+ error: `Simulation output (${simulation.amountOut} sats) < required (${btcTarget} sats)`,
2228
2410
  btcReserve,
2229
2411
  });
2412
+ continue;
2230
2413
  }
2414
+ fee = calculation.totalFee;
2415
+ executionPrice = simulation.executionPrice || "0";
2416
+ priceImpactPct = simulation.priceImpactPct || "0";
2417
+ warningMessage = simulation.warningMessage;
2418
+ }
2419
+ // Check if this pool offers a better rate
2420
+ if (tokenAmount < bestTokenAmount) {
2421
+ bestPool = pool;
2422
+ bestTokenAmount = tokenAmount;
2423
+ bestBtcTarget = btcTarget;
2424
+ bestCurveType = poolDetails.curveType;
2425
+ bestSimulation = {
2426
+ amountIn: tokenAmount.toString(),
2427
+ fee,
2428
+ executionPrice,
2429
+ priceImpactPct,
2430
+ warningMessage,
2431
+ };
2231
2432
  }
2232
2433
  }
2233
2434
  catch (e) {
2234
- // Capture pool errors for diagnostics
2235
2435
  const errorMessage = e instanceof Error ? e.message : String(e);
2236
2436
  poolErrors.push({
2237
2437
  poolId: pool.lpPublicKey,
@@ -2240,7 +2440,7 @@ class FlashnetClient {
2240
2440
  }
2241
2441
  }
2242
2442
  if (!bestPool || !bestSimulation) {
2243
- let errorMessage = `No pool has sufficient liquidity for ${btcAmountNeeded} sats`;
2443
+ let errorMessage = `No pool has sufficient liquidity for ${baseBtcNeeded} sats`;
2244
2444
  if (poolErrors.length > 0) {
2245
2445
  const details = poolErrors
2246
2446
  .map((pe) => {
@@ -2278,6 +2478,8 @@ class FlashnetClient {
2278
2478
  assetBReserve: poolDetails.assetBReserve,
2279
2479
  },
2280
2480
  warningMessage: bestSimulation.warningMessage,
2481
+ btcAmountUsed: bestBtcTarget.toString(),
2482
+ curveType: bestCurveType,
2281
2483
  };
2282
2484
  }
2283
2485
  /**
@@ -2345,6 +2547,127 @@ class FlashnetClient {
2345
2547
  };
2346
2548
  }
2347
2549
  }
2550
+ /**
2551
+ * Find the token amount needed to get a specific BTC output from a V3 concentrated liquidity pool.
2552
+ * Uses binary search with simulateSwap since V3 tick-based math can't be inverted locally.
2553
+ * @private
2554
+ */
2555
+ async findV3TokenAmountForBtcOutput(params) {
2556
+ const { poolId, assetInAddress, assetOutAddress, desiredBtcOut, currentPriceAInB, tokenIsAssetA, integratorBps, } = params;
2557
+ // Step 1: Compute initial estimate from pool price
2558
+ let estimate;
2559
+ if (currentPriceAInB && currentPriceAInB !== "0") {
2560
+ const price = Number(currentPriceAInB);
2561
+ if (tokenIsAssetA) {
2562
+ // priceAInB = how much B (BTC) per 1 A (token), so tokenNeeded = btcOut / price
2563
+ estimate = BigInt(Math.ceil(Number(desiredBtcOut) / price));
2564
+ }
2565
+ else {
2566
+ // priceAInB = how much B (token) per 1 A (BTC), so tokenNeeded = btcOut * price
2567
+ estimate = BigInt(Math.ceil(Number(desiredBtcOut) * price));
2568
+ }
2569
+ // Ensure non-zero
2570
+ if (estimate <= 0n) {
2571
+ estimate = desiredBtcOut * 2n;
2572
+ }
2573
+ }
2574
+ else {
2575
+ estimate = desiredBtcOut * 2n;
2576
+ }
2577
+ // Step 2: Find upper bound by simulating with estimate + 10% buffer
2578
+ let upperBound = (estimate * 110n) / 100n;
2579
+ if (upperBound <= 0n) {
2580
+ upperBound = 1n;
2581
+ }
2582
+ let upperSim = null;
2583
+ for (let attempt = 0; attempt < 3; attempt++) {
2584
+ const sim = await this.simulateSwap({
2585
+ poolId,
2586
+ assetInAddress,
2587
+ assetOutAddress,
2588
+ amountIn: upperBound.toString(),
2589
+ integratorBps,
2590
+ });
2591
+ if (BigInt(sim.amountOut) >= desiredBtcOut) {
2592
+ upperSim = sim;
2593
+ break;
2594
+ }
2595
+ // Double the upper bound
2596
+ upperBound = upperBound * 2n;
2597
+ }
2598
+ if (!upperSim) {
2599
+ throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
2600
+ }
2601
+ // Step 3: Refine estimate via linear interpolation
2602
+ const upperOut = BigInt(upperSim.amountOut);
2603
+ // Scale proportionally: if upperBound produced upperOut, we need roughly
2604
+ // (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
2605
+ let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
2606
+ if (refined <= 0n) {
2607
+ refined = 1n;
2608
+ }
2609
+ let bestAmountIn = upperBound;
2610
+ let bestSim = upperSim;
2611
+ // Check if the refined estimate is tighter
2612
+ if (refined < upperBound) {
2613
+ const refinedSim = await this.simulateSwap({
2614
+ poolId,
2615
+ assetInAddress,
2616
+ assetOutAddress,
2617
+ amountIn: refined.toString(),
2618
+ integratorBps,
2619
+ });
2620
+ if (BigInt(refinedSim.amountOut) >= desiredBtcOut) {
2621
+ bestAmountIn = refined;
2622
+ bestSim = refinedSim;
2623
+ }
2624
+ else {
2625
+ // Refined estimate was slightly too low. Keep upperBound as best,
2626
+ // and let binary search narrow between refined (too low) and upperBound (sufficient).
2627
+ bestAmountIn = upperBound;
2628
+ bestSim = upperSim;
2629
+ }
2630
+ }
2631
+ // Step 4: Binary search to converge on minimum amountIn
2632
+ // Use a tight range: the interpolation is close, so search between 99.5% and 100% of best
2633
+ let lo = bestAmountIn === upperBound
2634
+ ? refined < upperBound
2635
+ ? refined
2636
+ : (bestAmountIn * 99n) / 100n
2637
+ : (bestAmountIn * 999n) / 1000n;
2638
+ if (lo <= 0n) {
2639
+ lo = 1n;
2640
+ }
2641
+ let hi = bestAmountIn;
2642
+ for (let i = 0; i < 6; i++) {
2643
+ if (hi - lo <= 1n) {
2644
+ break;
2645
+ }
2646
+ const mid = (lo + hi) / 2n;
2647
+ const midSim = await this.simulateSwap({
2648
+ poolId,
2649
+ assetInAddress,
2650
+ assetOutAddress,
2651
+ amountIn: mid.toString(),
2652
+ integratorBps,
2653
+ });
2654
+ if (BigInt(midSim.amountOut) >= desiredBtcOut) {
2655
+ hi = mid;
2656
+ bestAmountIn = mid;
2657
+ bestSim = midSim;
2658
+ }
2659
+ else {
2660
+ lo = mid;
2661
+ }
2662
+ }
2663
+ // Compute fee from the best simulation
2664
+ const totalFee = bestSim.feePaidAssetIn || "0";
2665
+ return {
2666
+ amountIn: bestAmountIn.toString(),
2667
+ totalFee,
2668
+ simulation: bestSim,
2669
+ };
2670
+ }
2348
2671
  /**
2349
2672
  * Calculate minimum amount out with slippage protection
2350
2673
  * @private
@@ -3159,49 +3482,92 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3159
3482
  * Deposits assets to your free balance in a V3 concentrated liquidity pool.
3160
3483
  *
3161
3484
  * Free balance can be used for adding liquidity to positions without requiring
3162
- * additional Spark transfers. The deposit requires Spark transfer IDs for the
3163
- * assets being deposited.
3485
+ * additional Spark transfers. The SDK handles the Spark transfers internally.
3164
3486
  *
3165
3487
  * @param params - Deposit parameters
3166
3488
  * @param params.poolId - The pool identifier (LP identity public key)
3167
3489
  * @param params.amountA - Amount of asset A to deposit (use "0" to skip)
3168
3490
  * @param params.amountB - Amount of asset B to deposit (use "0" to skip)
3169
- * @param params.assetASparkTransferId - Spark transfer ID for asset A (use "" to skip)
3170
- * @param params.assetBSparkTransferId - Spark transfer ID for asset B (use "" to skip)
3171
3491
  * @returns Promise resolving to deposit response with updated balances
3172
3492
  * @throws Error if the deposit is rejected
3173
3493
  */
3174
3494
  async depositConcentratedBalance(params) {
3175
3495
  await this.ensureInitialized();
3176
- // Generate intent
3177
- const nonce = index$2.generateNonce();
3178
- const intentMessage = intents.generateDepositBalanceIntentMessage({
3179
- userPublicKey: this.publicKey,
3180
- lpIdentityPublicKey: params.poolId,
3181
- assetASparkTransferId: params.assetASparkTransferId,
3182
- assetBSparkTransferId: params.assetBSparkTransferId,
3183
- amountA: params.amountA,
3184
- amountB: params.amountB,
3185
- nonce,
3496
+ // Get pool details to know asset addresses
3497
+ const pool = await this.getPool(params.poolId);
3498
+ const lpSparkAddress = sparkAddress.encodeSparkAddressNew({
3499
+ identityPublicKey: params.poolId,
3500
+ network: this.sparkNetwork,
3186
3501
  });
3187
- // Sign intent
3188
- const messageHash = sha256__default.default(intentMessage);
3189
- const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
3190
- const request = {
3191
- poolId: params.poolId,
3192
- amountA: params.amountA,
3193
- amountB: params.amountB,
3194
- assetASparkTransferId: params.assetASparkTransferId,
3195
- assetBSparkTransferId: params.assetBSparkTransferId,
3196
- nonce,
3197
- signature: hex.getHexFromUint8Array(signature),
3502
+ let assetATransferId = "";
3503
+ let assetBTransferId = "";
3504
+ const transferIds = [];
3505
+ // Transfer assets to pool
3506
+ if (BigInt(params.amountA) > 0n) {
3507
+ assetATransferId = await this.transferAsset({
3508
+ receiverSparkAddress: lpSparkAddress,
3509
+ assetAddress: pool.assetAAddress,
3510
+ amount: params.amountA,
3511
+ }, "Insufficient balance for depositing to V3 pool (Asset A): ");
3512
+ transferIds.push(assetATransferId);
3513
+ }
3514
+ if (BigInt(params.amountB) > 0n) {
3515
+ assetBTransferId = await this.transferAsset({
3516
+ receiverSparkAddress: lpSparkAddress,
3517
+ assetAddress: pool.assetBAddress,
3518
+ amount: params.amountB,
3519
+ }, "Insufficient balance for depositing to V3 pool (Asset B): ");
3520
+ transferIds.push(assetBTransferId);
3521
+ }
3522
+ const executeDeposit = async () => {
3523
+ // Generate intent
3524
+ const nonce = index$2.generateNonce();
3525
+ const intentMessage = intents.generateDepositBalanceIntentMessage({
3526
+ userPublicKey: this.publicKey,
3527
+ lpIdentityPublicKey: params.poolId,
3528
+ assetASparkTransferId: assetATransferId,
3529
+ assetBSparkTransferId: assetBTransferId,
3530
+ amountA: params.amountA,
3531
+ amountB: params.amountB,
3532
+ nonce,
3533
+ });
3534
+ // Sign intent
3535
+ const messageHash = sha256__default.default(intentMessage);
3536
+ const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
3537
+ const request = {
3538
+ poolId: params.poolId,
3539
+ amountA: params.amountA,
3540
+ amountB: params.amountB,
3541
+ assetASparkTransferId: assetATransferId,
3542
+ assetBSparkTransferId: assetBTransferId,
3543
+ nonce,
3544
+ signature: hex.getHexFromUint8Array(signature),
3545
+ };
3546
+ const response = await this.typedApi.depositConcentratedBalance(request);
3547
+ if (!response.accepted) {
3548
+ const errorMessage = response.error || "Deposit balance rejected by the AMM";
3549
+ throw new errors.FlashnetError(errorMessage, {
3550
+ response: {
3551
+ errorCode: "UNKNOWN",
3552
+ errorCategory: "System",
3553
+ message: errorMessage,
3554
+ requestId: "",
3555
+ timestamp: new Date().toISOString(),
3556
+ service: "amm-gateway",
3557
+ severity: "Error",
3558
+ },
3559
+ httpStatus: 400,
3560
+ transferIds,
3561
+ lpIdentityPublicKey: params.poolId,
3562
+ });
3563
+ }
3564
+ return response;
3198
3565
  };
3199
- const response = await this.typedApi.depositConcentratedBalance(request);
3200
- if (!response.accepted) {
3201
- const errorMessage = response.error || "Deposit balance rejected by the AMM";
3202
- throw new Error(errorMessage);
3566
+ // Execute with auto-clawback if we made transfers
3567
+ if (transferIds.length > 0) {
3568
+ return this.executeWithAutoClawback(executeDeposit, transferIds, params.poolId);
3203
3569
  }
3204
- return response;
3570
+ return executeDeposit();
3205
3571
  }
3206
3572
  }
3207
3573