@flashnet/sdk 0.5.3 → 0.5.5

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.
@@ -9,6 +9,7 @@ import { getHexFromUint8Array } from '../utils/hex.js';
9
9
  import { generateConstantProductPoolInitializationIntentMessage, generatePoolInitializationIntentMessage, generatePoolConfirmInitialDepositIntentMessage, generatePoolSwapIntentMessage, generateRouteSwapIntentMessage, generateAddLiquidityIntentMessage, generateRemoveLiquidityIntentMessage, generateRegisterHostIntentMessage, generateWithdrawHostFeesIntentMessage, generateWithdrawIntegratorFeesIntentMessage, generateCreateEscrowIntentMessage, generateFundEscrowIntentMessage, generateClaimEscrowIntentMessage, generateClawbackIntentMessage, generateCreateConcentratedPoolIntentMessage, generateDecreaseLiquidityIntentMessage, generateCollectFeesIntentMessage, generateWithdrawBalanceIntentMessage, generateIncreaseLiquidityIntentMessage, generateRebalancePositionIntentMessage, generateDepositBalanceIntentMessage } from '../utils/intents.js';
10
10
  import { getSparkNetworkFromAddress, encodeSparkAddressNew } from '../utils/spark-address.js';
11
11
  import { encodeSparkHumanReadableTokenIdentifier, decodeSparkHumanReadableTokenIdentifier } from '../utils/tokenAddress.js';
12
+ import { safeBigInt } from '../utils/bigint.js';
12
13
  import { FlashnetError } from '../types/errors.js';
13
14
 
14
15
  /**
@@ -239,7 +240,8 @@ class FlashnetClient {
239
240
  const tokenIdentifierHex = getHexFromUint8Array(info.rawTokenIdentifier);
240
241
  const tokenAddress = encodeSparkHumanReadableTokenIdentifier(info.rawTokenIdentifier, this.sparkNetwork);
241
242
  tokenBalances.set(tokenPubkey, {
242
- balance: BigInt(tokenData.balance),
243
+ balance: FlashnetClient.safeBigInt(tokenData.ownedBalance),
244
+ availableToSendBalance: FlashnetClient.safeBigInt(tokenData.availableToSendBalance),
243
245
  tokenInfo: {
244
246
  tokenIdentifier: tokenIdentifierHex,
245
247
  tokenAddress,
@@ -252,7 +254,7 @@ class FlashnetClient {
252
254
  }
253
255
  }
254
256
  return {
255
- balance: BigInt(balance.balance),
257
+ balance: FlashnetClient.safeBigInt(balance.balance),
256
258
  tokenBalances,
257
259
  };
258
260
  }
@@ -260,17 +262,17 @@ class FlashnetClient {
260
262
  * Check if wallet has sufficient balance for an operation
261
263
  */
262
264
  async checkBalance(params) {
263
- const balance = await this.getBalance();
265
+ const balance = params.walletBalance ?? (await this.getBalance());
264
266
  // Check balance
265
267
  const requirements = {
266
268
  tokens: new Map(),
267
269
  };
268
270
  for (const balance of params.balancesToCheck) {
269
271
  if (balance.assetAddress === BTC_ASSET_PUBKEY) {
270
- requirements.btc = BigInt(balance.amount);
272
+ requirements.btc = FlashnetClient.safeBigInt(balance.amount);
271
273
  }
272
274
  else {
273
- requirements.tokens?.set(balance.assetAddress, BigInt(balance.amount));
275
+ requirements.tokens?.set(balance.assetAddress, FlashnetClient.safeBigInt(balance.amount));
274
276
  }
275
277
  }
276
278
  // Check BTC balance
@@ -288,7 +290,9 @@ class FlashnetClient {
288
290
  const hrKey = this.toHumanReadableTokenIdentifier(tokenPubkey);
289
291
  const effectiveTokenBalance = balance.tokenBalances.get(tokenPubkey) ??
290
292
  balance.tokenBalances.get(hrKey);
291
- const available = effectiveTokenBalance?.balance ?? 0n;
293
+ const available = params.useAvailableBalance
294
+ ? (effectiveTokenBalance?.availableToSendBalance ?? 0n)
295
+ : (effectiveTokenBalance?.balance ?? 0n);
292
296
  if (available < requiredAmount) {
293
297
  throw new Error([
294
298
  params.errorPrefix ?? "",
@@ -350,6 +354,7 @@ class FlashnetClient {
350
354
  },
351
355
  ],
352
356
  errorPrefix: "Insufficient balance for initial liquidity: ",
357
+ useAvailableBalance: params.useAvailableBalance,
353
358
  });
354
359
  }
355
360
  const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
@@ -409,6 +414,12 @@ class FlashnetClient {
409
414
  throw new Error(`${name} must be positive integer`);
410
415
  }
411
416
  }
417
+ /**
418
+ * Safely convert a value to BigInt. Delegates to the shared `safeBigInt` utility.
419
+ */
420
+ static safeBigInt(value, fallback = 0n) {
421
+ return safeBigInt(value, fallback);
422
+ }
412
423
  /**
413
424
  * Calculates virtual reserves for a bonding curve AMM.
414
425
  *
@@ -429,7 +440,7 @@ class FlashnetClient {
429
440
  }
430
441
  const supply = FlashnetClient.parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
431
442
  const targetB = FlashnetClient.parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
432
- const graduationThresholdPct = BigInt(params.graduationThresholdPct);
443
+ const graduationThresholdPct = FlashnetClient.safeBigInt(params.graduationThresholdPct);
433
444
  // Align bounds with Rust AMM (20%..95%), then check feasibility for g=1 (requires >50%).
434
445
  const MIN_PCT = 20n;
435
446
  const MAX_PCT = 95n;
@@ -479,6 +490,7 @@ class FlashnetClient {
479
490
  },
480
491
  ],
481
492
  errorPrefix: "Insufficient balance for pool creation: ",
493
+ useAvailableBalance: params.useAvailableBalance,
482
494
  });
483
495
  const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
484
496
  // Generate intent
@@ -599,10 +611,14 @@ class FlashnetClient {
599
611
  });
600
612
  // If using free balance (V3 pools only), skip the Spark transfer
601
613
  if (params.useFreeBalance) {
602
- return this.executeSwapIntent({
614
+ const swapResponse = await this.executeSwapIntent({
603
615
  ...params,
604
616
  // No transferId - triggers free balance mode
605
617
  });
618
+ return {
619
+ ...swapResponse,
620
+ inboundSparkTransferId: swapResponse.requestId,
621
+ };
606
622
  }
607
623
  // Transfer assets to pool using new address encoding
608
624
  const lpSparkAddress = encodeSparkAddressNew({
@@ -613,12 +629,13 @@ class FlashnetClient {
613
629
  receiverSparkAddress: lpSparkAddress,
614
630
  assetAddress: params.assetInAddress,
615
631
  amount: params.amountIn,
616
- }, "Insufficient balance for swap: ");
632
+ }, "Insufficient balance for swap: ", params.useAvailableBalance);
617
633
  // Execute with auto-clawback on failure
618
- return this.executeWithAutoClawback(() => this.executeSwapIntent({
634
+ const swapResponse = await this.executeWithAutoClawback(() => this.executeSwapIntent({
619
635
  ...params,
620
636
  transferId,
621
637
  }), [transferId], params.poolId);
638
+ return { ...swapResponse, inboundSparkTransferId: transferId };
622
639
  }
623
640
  /**
624
641
  * Execute a swap with a pre-created transfer or using free balance.
@@ -748,7 +765,7 @@ class FlashnetClient {
748
765
  receiverSparkAddress: lpSparkAddress,
749
766
  assetAddress: params.initialAssetAddress,
750
767
  amount: params.inputAmount,
751
- }, "Insufficient balance for route swap: ");
768
+ }, "Insufficient balance for route swap: ", params.useAvailableBalance);
752
769
  // Execute with auto-clawback on failure
753
770
  return this.executeWithAutoClawback(async () => {
754
771
  // Prepare hops for validation
@@ -871,7 +888,7 @@ class FlashnetClient {
871
888
  assetAddress: pool.assetBAddress,
872
889
  amount: params.assetBAmount,
873
890
  },
874
- ], "Insufficient balance for adding liquidity: ");
891
+ ], "Insufficient balance for adding liquidity: ", params.useAvailableBalance);
875
892
  // Execute with auto-clawback on failure
876
893
  return this.executeWithAutoClawback(async () => {
877
894
  // Generate add liquidity intent
@@ -1177,6 +1194,7 @@ class FlashnetClient {
1177
1194
  depositAddress: createResponse.depositAddress,
1178
1195
  assetId: params.assetId,
1179
1196
  assetAmount: params.assetAmount,
1197
+ useAvailableBalance: params.useAvailableBalance,
1180
1198
  });
1181
1199
  }
1182
1200
  /**
@@ -1194,6 +1212,7 @@ class FlashnetClient {
1194
1212
  { assetAddress: params.assetId, amount: params.assetAmount },
1195
1213
  ],
1196
1214
  errorPrefix: "Insufficient balance to fund escrow: ",
1215
+ useAvailableBalance: params.useAvailableBalance,
1197
1216
  });
1198
1217
  // 2. Perform transfer
1199
1218
  const escrowSparkAddress = encodeSparkAddressNew({
@@ -1708,19 +1727,20 @@ class FlashnetClient {
1708
1727
  /**
1709
1728
  * Performs asset transfer using generalized asset address for both BTC and tokens.
1710
1729
  */
1711
- async transferAsset(recipient, checkBalanceErrorPrefix) {
1712
- const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix);
1730
+ async transferAsset(recipient, checkBalanceErrorPrefix, useAvailableBalance) {
1731
+ const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix, useAvailableBalance);
1713
1732
  return transferIds[0];
1714
1733
  }
1715
1734
  /**
1716
1735
  * Performs asset transfers using generalized asset addresses for both BTC and tokens.
1717
1736
  * Supports optional generic to hardcode recipients length so output list can be typed with same length.
1718
1737
  */
1719
- async transferAssets(recipients, checkBalanceErrorPrefix) {
1738
+ async transferAssets(recipients, checkBalanceErrorPrefix, useAvailableBalance) {
1720
1739
  if (checkBalanceErrorPrefix) {
1721
1740
  await this.checkBalance({
1722
1741
  balancesToCheck: recipients,
1723
1742
  errorPrefix: checkBalanceErrorPrefix,
1743
+ useAvailableBalance,
1724
1744
  });
1725
1745
  }
1726
1746
  const transferIds = [];
@@ -1735,7 +1755,7 @@ class FlashnetClient {
1735
1755
  else {
1736
1756
  const transferId = await this._wallet.transferTokens({
1737
1757
  tokenIdentifier: this.toHumanReadableTokenIdentifier(recipient.assetAddress),
1738
- tokenAmount: BigInt(recipient.amount),
1758
+ tokenAmount: FlashnetClient.safeBigInt(recipient.amount),
1739
1759
  receiverSparkAddress: recipient.receiverSparkAddress,
1740
1760
  });
1741
1761
  transferIds.push(transferId);
@@ -1819,38 +1839,37 @@ class FlashnetClient {
1819
1839
  await this.ensureInitialized();
1820
1840
  // Decode the invoice to get the amount
1821
1841
  const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1842
+ // Zero-amount invoice: forward-direction quoting using caller-specified tokenAmount
1822
1843
  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
- });
1844
+ const tokenAmount = options?.tokenAmount;
1845
+ if (!tokenAmount || BigInt(tokenAmount) <= 0n) {
1846
+ throw new FlashnetError("Zero-amount invoice requires tokenAmount in options.", {
1847
+ response: {
1848
+ errorCode: "FSAG-1002",
1849
+ errorCategory: "Validation",
1850
+ message: "Zero-amount invoice requires tokenAmount in options.",
1851
+ requestId: "",
1852
+ timestamp: new Date().toISOString(),
1853
+ service: "sdk",
1854
+ severity: "Error",
1855
+ remediation: "Provide tokenAmount when using a zero-amount invoice.",
1856
+ },
1857
+ });
1858
+ }
1859
+ return this.getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options);
1835
1860
  }
1836
1861
  // Get Lightning fee estimate
1837
1862
  const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
1838
- // Total BTC needed = invoice amount + lightning fee
1839
- 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;
1863
+ // Total BTC needed = invoice amount + lightning fee (unmasked).
1864
+ // Bitmasking for V2 pools is handled inside findBestPoolForTokenToBtc.
1865
+ const baseBtcNeeded = FlashnetClient.safeBigInt(invoiceAmountSats) +
1866
+ FlashnetClient.safeBigInt(lightningFeeEstimate);
1848
1867
  // Check Flashnet minimum amounts early to provide clear error messages
1849
1868
  const minAmounts = await this.getEnabledMinAmountsMap();
1850
1869
  // Check BTC minimum (output from swap)
1851
1870
  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.`;
1871
+ if (btcMinAmount && baseBtcNeeded < btcMinAmount) {
1872
+ const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${baseBtcNeeded} sats.`;
1854
1873
  throw new FlashnetError(msg, {
1855
1874
  response: {
1856
1875
  errorCode: "FSAG-1003",
@@ -1864,13 +1883,14 @@ class FlashnetClient {
1864
1883
  },
1865
1884
  });
1866
1885
  }
1867
- // Find the best pool to swap token -> BTC
1868
- const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, totalBtcNeeded.toString(), options?.integratorFeeRateBps);
1886
+ // Find the best pool to swap token -> BTC.
1887
+ // Bitmasking is applied per-pool inside this function (V2 pools get masked, V3 pools don't).
1888
+ const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded.toString(), options?.integratorFeeRateBps);
1869
1889
  // Check token minimum (input to swap)
1870
1890
  const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
1871
1891
  const tokenMinAmount = minAmounts.get(tokenHex);
1872
1892
  if (tokenMinAmount &&
1873
- BigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1893
+ FlashnetClient.safeBigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1874
1894
  const msg = `Token amount too small. Minimum input is ${tokenMinAmount} units, but calculated amount is only ${poolQuote.tokenAmountRequired} units.`;
1875
1895
  throw new FlashnetError(msg, {
1876
1896
  response: {
@@ -1885,13 +1905,14 @@ class FlashnetClient {
1885
1905
  },
1886
1906
  });
1887
1907
  }
1888
- // Calculate the BTC variable fee adjustment (how much extra we're requesting)
1889
- const btcVariableFeeAdjustment = Number(totalBtcNeeded - baseBtcNeeded);
1908
+ // BTC variable fee adjustment: difference between what the pool targets and unmasked base.
1909
+ // For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
1910
+ const btcVariableFeeAdjustment = Number(FlashnetClient.safeBigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
1890
1911
  return {
1891
1912
  poolId: poolQuote.poolId,
1892
1913
  tokenAddress: this.toHexTokenIdentifier(tokenAddress),
1893
1914
  tokenAmountRequired: poolQuote.tokenAmountRequired,
1894
- btcAmountRequired: totalBtcNeeded.toString(),
1915
+ btcAmountRequired: poolQuote.btcAmountUsed,
1895
1916
  invoiceAmountSats: invoiceAmountSats,
1896
1917
  estimatedAmmFee: poolQuote.estimatedAmmFee,
1897
1918
  estimatedLightningFee: lightningFeeEstimate,
@@ -1901,6 +1922,138 @@ class FlashnetClient {
1901
1922
  tokenIsAssetA: poolQuote.tokenIsAssetA,
1902
1923
  poolReserves: poolQuote.poolReserves,
1903
1924
  warningMessage: poolQuote.warningMessage,
1925
+ curveType: poolQuote.curveType,
1926
+ isZeroAmountInvoice: false,
1927
+ };
1928
+ }
1929
+ /**
1930
+ * Generate a quote for a zero-amount invoice.
1931
+ * Forward-direction: simulate swapping tokenAmount and pick the pool with the best BTC output.
1932
+ * @private
1933
+ */
1934
+ async getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options) {
1935
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress);
1936
+ const btcHex = BTC_ASSET_PUBKEY;
1937
+ // Discover all token/BTC pools
1938
+ const [poolsWithTokenAsA, poolsWithTokenAsB] = await Promise.all([
1939
+ this.listPools({ assetAAddress: tokenHex, assetBAddress: btcHex }),
1940
+ this.listPools({ assetAAddress: btcHex, assetBAddress: tokenHex }),
1941
+ ]);
1942
+ const poolMap = new Map();
1943
+ for (const p of [...poolsWithTokenAsA.pools, ...poolsWithTokenAsB.pools]) {
1944
+ if (!poolMap.has(p.lpPublicKey)) {
1945
+ const tokenIsAssetA = p.assetAAddress?.toLowerCase() === tokenHex.toLowerCase();
1946
+ poolMap.set(p.lpPublicKey, { pool: p, tokenIsAssetA });
1947
+ }
1948
+ }
1949
+ const allPools = Array.from(poolMap.values());
1950
+ if (allPools.length === 0) {
1951
+ throw new FlashnetError(`No liquidity pool found for token ${tokenAddress} paired with BTC`, {
1952
+ response: {
1953
+ errorCode: "FSAG-4001",
1954
+ errorCategory: "Business",
1955
+ message: `No liquidity pool found for token ${tokenAddress} paired with BTC`,
1956
+ requestId: "",
1957
+ timestamp: new Date().toISOString(),
1958
+ service: "sdk",
1959
+ severity: "Error",
1960
+ },
1961
+ });
1962
+ }
1963
+ // Simulate each pool with tokenAmount as input, pick highest BTC output
1964
+ let bestResult = null;
1965
+ let bestBtcOut = 0n;
1966
+ for (const { pool, tokenIsAssetA } of allPools) {
1967
+ try {
1968
+ const poolDetails = await this.getPool(pool.lpPublicKey);
1969
+ const assetInAddress = tokenIsAssetA
1970
+ ? poolDetails.assetAAddress
1971
+ : poolDetails.assetBAddress;
1972
+ const assetOutAddress = tokenIsAssetA
1973
+ ? poolDetails.assetBAddress
1974
+ : poolDetails.assetAAddress;
1975
+ const simulation = await this.simulateSwap({
1976
+ poolId: pool.lpPublicKey,
1977
+ assetInAddress,
1978
+ assetOutAddress,
1979
+ amountIn: tokenAmount,
1980
+ integratorBps: options?.integratorFeeRateBps,
1981
+ });
1982
+ const btcOut = FlashnetClient.safeBigInt(simulation.amountOut);
1983
+ if (btcOut > bestBtcOut) {
1984
+ bestBtcOut = btcOut;
1985
+ bestResult = {
1986
+ poolId: pool.lpPublicKey,
1987
+ tokenIsAssetA,
1988
+ simulation,
1989
+ curveType: poolDetails.curveType,
1990
+ poolReserves: {
1991
+ assetAReserve: poolDetails.assetAReserve,
1992
+ assetBReserve: poolDetails.assetBReserve,
1993
+ },
1994
+ };
1995
+ }
1996
+ }
1997
+ catch {
1998
+ // Skip pools that fail simulation
1999
+ }
2000
+ }
2001
+ if (!bestResult || bestBtcOut <= 0n) {
2002
+ throw new FlashnetError("No pool can produce BTC output for the given token amount", {
2003
+ response: {
2004
+ errorCode: "FSAG-4201",
2005
+ errorCategory: "Business",
2006
+ message: "No pool can produce BTC output for the given token amount",
2007
+ requestId: "",
2008
+ timestamp: new Date().toISOString(),
2009
+ service: "sdk",
2010
+ severity: "Error",
2011
+ remediation: "Try a larger token amount.",
2012
+ },
2013
+ });
2014
+ }
2015
+ // Estimate lightning fee from the BTC output
2016
+ let lightningFeeEstimate;
2017
+ try {
2018
+ lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
2019
+ }
2020
+ catch {
2021
+ lightningFeeEstimate = Math.max(5, Math.ceil(Number(bestBtcOut) * 0.0017));
2022
+ }
2023
+ // Check minimum amounts
2024
+ const minAmounts = await this.getEnabledMinAmountsMap();
2025
+ const btcMinAmount = minAmounts.get(BTC_ASSET_PUBKEY.toLowerCase());
2026
+ if (btcMinAmount && bestBtcOut < btcMinAmount) {
2027
+ const msg = `BTC output too small. Minimum is ${btcMinAmount} sats, but swap would produce only ${bestBtcOut} sats.`;
2028
+ throw new FlashnetError(msg, {
2029
+ response: {
2030
+ errorCode: "FSAG-1003",
2031
+ errorCategory: "Validation",
2032
+ message: msg,
2033
+ requestId: "",
2034
+ timestamp: new Date().toISOString(),
2035
+ service: "sdk",
2036
+ severity: "Error",
2037
+ remediation: "Use a larger token amount.",
2038
+ },
2039
+ });
2040
+ }
2041
+ return {
2042
+ poolId: bestResult.poolId,
2043
+ tokenAddress: tokenHex,
2044
+ tokenAmountRequired: tokenAmount,
2045
+ btcAmountRequired: bestBtcOut.toString(),
2046
+ invoiceAmountSats: 0,
2047
+ estimatedAmmFee: bestResult.simulation.feePaidAssetIn || "0",
2048
+ estimatedLightningFee: lightningFeeEstimate,
2049
+ btcVariableFeeAdjustment: 0,
2050
+ executionPrice: bestResult.simulation.executionPrice || "0",
2051
+ priceImpactPct: bestResult.simulation.priceImpactPct || "0",
2052
+ tokenIsAssetA: bestResult.tokenIsAssetA,
2053
+ poolReserves: bestResult.poolReserves,
2054
+ warningMessage: bestResult.simulation.warningMessage,
2055
+ curveType: bestResult.curveType,
2056
+ isZeroAmountInvoice: true,
1904
2057
  };
1905
2058
  }
1906
2059
  /**
@@ -1912,14 +2065,15 @@ class FlashnetClient {
1912
2065
  */
1913
2066
  async payLightningWithToken(options) {
1914
2067
  await this.ensureInitialized();
1915
- const { invoice, tokenAddress, maxSlippageBps = 500, // 5% default
2068
+ const { invoice, tokenAddress, tokenAmount, maxSlippageBps = 500, // 5% default
1916
2069
  maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 30000, // 30s default
1917
- rollbackOnFailure = false, useExistingBtcBalance = false, } = options;
2070
+ rollbackOnFailure = false, useExistingBtcBalance = false, useAvailableBalance = false, } = options;
1918
2071
  try {
1919
2072
  // Step 1: Get a quote for the payment
1920
2073
  const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
1921
2074
  maxSlippageBps,
1922
2075
  integratorFeeRateBps,
2076
+ tokenAmount,
1923
2077
  });
1924
2078
  // Step 2: Check token balance (always required)
1925
2079
  await this.checkBalance({
@@ -1930,17 +2084,8 @@ class FlashnetClient {
1930
2084
  },
1931
2085
  ],
1932
2086
  errorPrefix: "Insufficient token balance for Lightning payment: ",
2087
+ useAvailableBalance,
1933
2088
  });
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
2089
  // Step 3: Get pool details
1945
2090
  const pool = await this.getPool(quote.poolId);
1946
2091
  // Step 4: Determine swap direction and execute
@@ -1962,6 +2107,7 @@ class FlashnetClient {
1962
2107
  minAmountOut: minBtcOut,
1963
2108
  integratorFeeRateBps,
1964
2109
  integratorPublicKey,
2110
+ useAvailableBalance,
1965
2111
  });
1966
2112
  if (!swapResponse.accepted || !swapResponse.outboundTransferId) {
1967
2113
  return {
@@ -1971,10 +2117,20 @@ class FlashnetClient {
1971
2117
  btcAmountReceived: "0",
1972
2118
  swapTransferId: swapResponse.outboundTransferId || "",
1973
2119
  ammFeePaid: quote.estimatedAmmFee,
2120
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
1974
2121
  error: swapResponse.error || "Swap was not accepted",
1975
2122
  };
1976
2123
  }
1977
- // Step 5: Wait for the transfer to complete (unless paying immediately with existing BTC)
2124
+ // Step 5: Wait for transfer (skip useExistingBtcBalance for zero-amount invoices)
2125
+ let canPayImmediately = false;
2126
+ if (!quote.isZeroAmountInvoice && useExistingBtcBalance) {
2127
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
2128
+ const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2129
+ const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
2130
+ const balance = await this.getBalance();
2131
+ canPayImmediately =
2132
+ balance.balance >= FlashnetClient.safeBigInt(btcNeededForPayment);
2133
+ }
1978
2134
  if (!canPayImmediately) {
1979
2135
  const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
1980
2136
  if (!transferComplete) {
@@ -1985,20 +2141,57 @@ class FlashnetClient {
1985
2141
  btcAmountReceived: swapResponse.amountOut || "0",
1986
2142
  swapTransferId: swapResponse.outboundTransferId,
1987
2143
  ammFeePaid: quote.estimatedAmmFee,
2144
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
1988
2145
  error: "Transfer did not complete within timeout",
1989
2146
  };
1990
2147
  }
1991
2148
  }
1992
- // Step 6: Calculate Lightning fee limit - use the quoted estimate, not a recalculation
2149
+ // Step 6: Calculate Lightning fee and payment amount
1993
2150
  const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
1994
- // Step 7: Pay the Lightning invoice
1995
2151
  const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
2152
+ // Step 7: Pay the Lightning invoice
1996
2153
  try {
1997
- const lightningPayment = await this._wallet.payLightningInvoice({
1998
- invoice,
1999
- maxFeeSats: effectiveMaxLightningFee,
2000
- preferSpark,
2001
- });
2154
+ let lightningPayment;
2155
+ let invoiceAmountPaid;
2156
+ if (quote.isZeroAmountInvoice) {
2157
+ // Zero-amount invoice: pay whatever BTC we received minus lightning fee
2158
+ const actualBtc = FlashnetClient.safeBigInt(btcReceived);
2159
+ const lnFee = FlashnetClient.safeBigInt(effectiveMaxLightningFee);
2160
+ const amountToPay = actualBtc - lnFee;
2161
+ if (amountToPay <= 0n) {
2162
+ return {
2163
+ success: false,
2164
+ poolId: quote.poolId,
2165
+ tokenAmountSpent: quote.tokenAmountRequired,
2166
+ btcAmountReceived: btcReceived,
2167
+ swapTransferId: swapResponse.outboundTransferId,
2168
+ ammFeePaid: quote.estimatedAmmFee,
2169
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2170
+ error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
2171
+ };
2172
+ }
2173
+ invoiceAmountPaid = Number(amountToPay);
2174
+ lightningPayment = await this._wallet.payLightningInvoice({
2175
+ invoice,
2176
+ amountSats: invoiceAmountPaid,
2177
+ maxFeeSats: effectiveMaxLightningFee,
2178
+ preferSpark,
2179
+ });
2180
+ }
2181
+ else {
2182
+ // Standard invoice: pay the specified amount
2183
+ lightningPayment = await this._wallet.payLightningInvoice({
2184
+ invoice,
2185
+ maxFeeSats: effectiveMaxLightningFee,
2186
+ preferSpark,
2187
+ });
2188
+ }
2189
+ // Extract the Spark transfer ID from the lightning payment result.
2190
+ // payLightningInvoice returns LightningSendRequest | WalletTransfer:
2191
+ // - LightningSendRequest has .transfer?.sparkId (the Sparkscan-visible transfer ID)
2192
+ // - WalletTransfer (Spark-to-Spark) has .id directly as the transfer ID
2193
+ // Note: lightningPayment.id (the SSP request ID) is already returned as lightningPaymentId
2194
+ const sparkLightningTransferId = lightningPayment.transfer?.sparkId;
2002
2195
  return {
2003
2196
  success: true,
2004
2197
  poolId: quote.poolId,
@@ -2008,6 +2201,9 @@ class FlashnetClient {
2008
2201
  lightningPaymentId: lightningPayment.id,
2009
2202
  ammFeePaid: quote.estimatedAmmFee,
2010
2203
  lightningFeePaid: effectiveMaxLightningFee,
2204
+ invoiceAmountPaid,
2205
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2206
+ sparkLightningTransferId,
2011
2207
  };
2012
2208
  }
2013
2209
  catch (lightningError) {
@@ -2027,6 +2223,7 @@ class FlashnetClient {
2027
2223
  btcAmountReceived: "0",
2028
2224
  swapTransferId: swapResponse.outboundTransferId,
2029
2225
  ammFeePaid: quote.estimatedAmmFee,
2226
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2030
2227
  error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
2031
2228
  };
2032
2229
  }
@@ -2042,6 +2239,7 @@ class FlashnetClient {
2042
2239
  btcAmountReceived: btcReceived,
2043
2240
  swapTransferId: swapResponse.outboundTransferId,
2044
2241
  ammFeePaid: quote.estimatedAmmFee,
2242
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2045
2243
  error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
2046
2244
  };
2047
2245
  }
@@ -2053,6 +2251,7 @@ class FlashnetClient {
2053
2251
  btcAmountReceived: btcReceived,
2054
2252
  swapTransferId: swapResponse.outboundTransferId,
2055
2253
  ammFeePaid: quote.estimatedAmmFee,
2254
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2056
2255
  error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
2057
2256
  };
2058
2257
  }
@@ -2113,7 +2312,7 @@ class FlashnetClient {
2113
2312
  * Find the best pool for swapping a token to BTC
2114
2313
  * @private
2115
2314
  */
2116
- async findBestPoolForTokenToBtc(tokenAddress, btcAmountNeeded, integratorFeeRateBps) {
2315
+ async findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded, integratorFeeRateBps) {
2117
2316
  const tokenHex = this.toHexTokenIdentifier(tokenAddress);
2118
2317
  const btcHex = BTC_ASSET_PUBKEY;
2119
2318
  // Find all pools that have this token paired with BTC
@@ -2157,8 +2356,8 @@ class FlashnetClient {
2157
2356
  const minAmounts = await this.getMinAmountsMap();
2158
2357
  const btcMinAmount = minAmounts.get(BTC_ASSET_PUBKEY.toLowerCase());
2159
2358
  // 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.`;
2359
+ if (btcMinAmount && BigInt(baseBtcNeeded) < btcMinAmount) {
2360
+ const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${baseBtcNeeded} sats.`;
2162
2361
  throw new FlashnetError(msg, {
2163
2362
  response: {
2164
2363
  errorCode: "FSAG-1003",
@@ -2172,60 +2371,99 @@ class FlashnetClient {
2172
2371
  },
2173
2372
  });
2174
2373
  }
2374
+ // Compute V2 masked BTC amount (round up to next multiple of 64 for bit masking)
2375
+ const baseBtc = BigInt(baseBtcNeeded);
2376
+ const BTC_VARIABLE_FEE_BITS = 6n;
2377
+ const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
2378
+ const maskedBtc = ((baseBtc + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
2379
+ BTC_VARIABLE_FEE_MASK;
2175
2380
  // Find the best pool (lowest token cost for the required BTC)
2176
2381
  let bestPool = null;
2177
2382
  let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
2383
+ let bestBtcTarget = 0n;
2384
+ let bestCurveType = "";
2178
2385
  let bestSimulation = null;
2179
2386
  // Track errors for each pool to provide better diagnostics
2180
2387
  const poolErrors = [];
2181
2388
  for (const pool of allPools) {
2182
2389
  try {
2183
- // Get pool details for reserves
2390
+ // Get pool details for reserves and curve type
2184
2391
  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) {
2392
+ const isV3 = poolDetails.curveType === "V3_CONCENTRATED";
2393
+ // V3 pools use exact BTC amount, V2 pools use masked amount
2394
+ const btcTarget = isV3 ? baseBtc : maskedBtc;
2395
+ const assetInAddress = pool.tokenIsAssetA
2396
+ ? poolDetails.assetAAddress
2397
+ : poolDetails.assetBAddress;
2398
+ const assetOutAddress = pool.tokenIsAssetA
2399
+ ? poolDetails.assetBAddress
2400
+ : poolDetails.assetAAddress;
2401
+ let tokenAmount;
2402
+ let fee;
2403
+ let executionPrice;
2404
+ let priceImpactPct;
2405
+ let warningMessage;
2406
+ if (isV3) {
2407
+ // V3: binary search with simulateSwap
2408
+ const v3Result = await this.findV3TokenAmountForBtcOutput({
2409
+ poolId: pool.lpPublicKey,
2410
+ assetInAddress,
2411
+ assetOutAddress,
2412
+ desiredBtcOut: btcTarget,
2413
+ currentPriceAInB: poolDetails.currentPriceAInB,
2414
+ tokenIsAssetA: pool.tokenIsAssetA,
2415
+ integratorBps: integratorFeeRateBps,
2416
+ });
2417
+ tokenAmount = FlashnetClient.safeBigInt(v3Result.amountIn);
2418
+ fee = v3Result.totalFee;
2419
+ executionPrice = v3Result.simulation.executionPrice || "0";
2420
+ priceImpactPct = v3Result.simulation.priceImpactPct || "0";
2421
+ warningMessage = v3Result.simulation.warningMessage;
2422
+ }
2423
+ else {
2424
+ // V2: constant product math + simulation verification
2425
+ const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
2426
+ tokenAmount = FlashnetClient.safeBigInt(calculation.amountIn);
2190
2427
  // Verify with simulation
2191
2428
  const simulation = await this.simulateSwap({
2192
2429
  poolId: pool.lpPublicKey,
2193
- assetInAddress: pool.tokenIsAssetA
2194
- ? poolDetails.assetAAddress
2195
- : poolDetails.assetBAddress,
2196
- assetOutAddress: pool.tokenIsAssetA
2197
- ? poolDetails.assetBAddress
2198
- : poolDetails.assetAAddress,
2430
+ assetInAddress,
2431
+ assetOutAddress,
2199
2432
  amountIn: calculation.amountIn,
2200
2433
  integratorBps: integratorFeeRateBps,
2201
2434
  });
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
2435
+ if (FlashnetClient.safeBigInt(simulation.amountOut) < btcTarget) {
2216
2436
  const btcReserve = pool.tokenIsAssetA
2217
2437
  ? poolDetails.assetBReserve
2218
2438
  : poolDetails.assetAReserve;
2219
2439
  poolErrors.push({
2220
2440
  poolId: pool.lpPublicKey,
2221
- error: `Simulation output (${simulation.amountOut} sats) < required (${btcAmountNeeded} sats)`,
2441
+ error: `Simulation output (${simulation.amountOut} sats) < required (${btcTarget} sats)`,
2222
2442
  btcReserve,
2223
2443
  });
2444
+ continue;
2224
2445
  }
2446
+ fee = calculation.totalFee;
2447
+ executionPrice = simulation.executionPrice || "0";
2448
+ priceImpactPct = simulation.priceImpactPct || "0";
2449
+ warningMessage = simulation.warningMessage;
2450
+ }
2451
+ // Check if this pool offers a better rate
2452
+ if (tokenAmount < bestTokenAmount) {
2453
+ bestPool = pool;
2454
+ bestTokenAmount = tokenAmount;
2455
+ bestBtcTarget = btcTarget;
2456
+ bestCurveType = poolDetails.curveType;
2457
+ bestSimulation = {
2458
+ amountIn: tokenAmount.toString(),
2459
+ fee,
2460
+ executionPrice,
2461
+ priceImpactPct,
2462
+ warningMessage,
2463
+ };
2225
2464
  }
2226
2465
  }
2227
2466
  catch (e) {
2228
- // Capture pool errors for diagnostics
2229
2467
  const errorMessage = e instanceof Error ? e.message : String(e);
2230
2468
  poolErrors.push({
2231
2469
  poolId: pool.lpPublicKey,
@@ -2234,7 +2472,7 @@ class FlashnetClient {
2234
2472
  }
2235
2473
  }
2236
2474
  if (!bestPool || !bestSimulation) {
2237
- let errorMessage = `No pool has sufficient liquidity for ${btcAmountNeeded} sats`;
2475
+ let errorMessage = `No pool has sufficient liquidity for ${baseBtcNeeded} sats`;
2238
2476
  if (poolErrors.length > 0) {
2239
2477
  const details = poolErrors
2240
2478
  .map((pe) => {
@@ -2272,6 +2510,8 @@ class FlashnetClient {
2272
2510
  assetBReserve: poolDetails.assetBReserve,
2273
2511
  },
2274
2512
  warningMessage: bestSimulation.warningMessage,
2513
+ btcAmountUsed: bestBtcTarget.toString(),
2514
+ curveType: bestCurveType,
2275
2515
  };
2276
2516
  }
2277
2517
  /**
@@ -2280,9 +2520,9 @@ class FlashnetClient {
2280
2520
  * @private
2281
2521
  */
2282
2522
  calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
2283
- const amountOut = BigInt(btcAmountOut);
2284
- const resA = BigInt(reserveA);
2285
- const resB = BigInt(reserveB);
2523
+ const amountOut = FlashnetClient.safeBigInt(btcAmountOut);
2524
+ const resA = FlashnetClient.safeBigInt(reserveA);
2525
+ const resB = FlashnetClient.safeBigInt(reserveB);
2286
2526
  const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
2287
2527
  const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
2288
2528
  // Token is the input asset
@@ -2339,12 +2579,133 @@ class FlashnetClient {
2339
2579
  };
2340
2580
  }
2341
2581
  }
2582
+ /**
2583
+ * Find the token amount needed to get a specific BTC output from a V3 concentrated liquidity pool.
2584
+ * Uses binary search with simulateSwap since V3 tick-based math can't be inverted locally.
2585
+ * @private
2586
+ */
2587
+ async findV3TokenAmountForBtcOutput(params) {
2588
+ const { poolId, assetInAddress, assetOutAddress, desiredBtcOut, currentPriceAInB, tokenIsAssetA, integratorBps, } = params;
2589
+ // Step 1: Compute initial estimate from pool price
2590
+ let estimate;
2591
+ if (currentPriceAInB && currentPriceAInB !== "0") {
2592
+ const price = Number(currentPriceAInB);
2593
+ if (tokenIsAssetA) {
2594
+ // priceAInB = how much B (BTC) per 1 A (token), so tokenNeeded = btcOut / price
2595
+ estimate = BigInt(Math.ceil(Number(desiredBtcOut) / price));
2596
+ }
2597
+ else {
2598
+ // priceAInB = how much B (token) per 1 A (BTC), so tokenNeeded = btcOut * price
2599
+ estimate = BigInt(Math.ceil(Number(desiredBtcOut) * price));
2600
+ }
2601
+ // Ensure non-zero
2602
+ if (estimate <= 0n) {
2603
+ estimate = desiredBtcOut * 2n;
2604
+ }
2605
+ }
2606
+ else {
2607
+ estimate = desiredBtcOut * 2n;
2608
+ }
2609
+ // Step 2: Find upper bound by simulating with estimate + 10% buffer
2610
+ let upperBound = (estimate * 110n) / 100n;
2611
+ if (upperBound <= 0n) {
2612
+ upperBound = 1n;
2613
+ }
2614
+ let upperSim = null;
2615
+ for (let attempt = 0; attempt < 3; attempt++) {
2616
+ const sim = await this.simulateSwap({
2617
+ poolId,
2618
+ assetInAddress,
2619
+ assetOutAddress,
2620
+ amountIn: upperBound.toString(),
2621
+ integratorBps,
2622
+ });
2623
+ if (FlashnetClient.safeBigInt(sim.amountOut) >= desiredBtcOut) {
2624
+ upperSim = sim;
2625
+ break;
2626
+ }
2627
+ // Double the upper bound
2628
+ upperBound = upperBound * 2n;
2629
+ }
2630
+ if (!upperSim) {
2631
+ throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
2632
+ }
2633
+ // Step 3: Refine estimate via linear interpolation
2634
+ const upperOut = FlashnetClient.safeBigInt(upperSim.amountOut);
2635
+ // Scale proportionally: if upperBound produced upperOut, we need roughly
2636
+ // (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
2637
+ let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
2638
+ if (refined <= 0n) {
2639
+ refined = 1n;
2640
+ }
2641
+ let bestAmountIn = upperBound;
2642
+ let bestSim = upperSim;
2643
+ // Check if the refined estimate is tighter
2644
+ if (refined < upperBound) {
2645
+ const refinedSim = await this.simulateSwap({
2646
+ poolId,
2647
+ assetInAddress,
2648
+ assetOutAddress,
2649
+ amountIn: refined.toString(),
2650
+ integratorBps,
2651
+ });
2652
+ if (FlashnetClient.safeBigInt(refinedSim.amountOut) >= desiredBtcOut) {
2653
+ bestAmountIn = refined;
2654
+ bestSim = refinedSim;
2655
+ }
2656
+ else {
2657
+ // Refined estimate was slightly too low. Keep upperBound as best,
2658
+ // and let binary search narrow between refined (too low) and upperBound (sufficient).
2659
+ bestAmountIn = upperBound;
2660
+ bestSim = upperSim;
2661
+ }
2662
+ }
2663
+ // Step 4: Binary search to converge on minimum amountIn
2664
+ // Use a tight range: the interpolation is close, so search between 99.5% and 100% of best
2665
+ let lo = bestAmountIn === upperBound
2666
+ ? refined < upperBound
2667
+ ? refined
2668
+ : (bestAmountIn * 99n) / 100n
2669
+ : (bestAmountIn * 999n) / 1000n;
2670
+ if (lo <= 0n) {
2671
+ lo = 1n;
2672
+ }
2673
+ let hi = bestAmountIn;
2674
+ for (let i = 0; i < 6; i++) {
2675
+ if (hi - lo <= 1n) {
2676
+ break;
2677
+ }
2678
+ const mid = (lo + hi) / 2n;
2679
+ const midSim = await this.simulateSwap({
2680
+ poolId,
2681
+ assetInAddress,
2682
+ assetOutAddress,
2683
+ amountIn: mid.toString(),
2684
+ integratorBps,
2685
+ });
2686
+ if (FlashnetClient.safeBigInt(midSim.amountOut) >= desiredBtcOut) {
2687
+ hi = mid;
2688
+ bestAmountIn = mid;
2689
+ bestSim = midSim;
2690
+ }
2691
+ else {
2692
+ lo = mid;
2693
+ }
2694
+ }
2695
+ // Compute fee from the best simulation
2696
+ const totalFee = bestSim.feePaidAssetIn || "0";
2697
+ return {
2698
+ amountIn: bestAmountIn.toString(),
2699
+ totalFee,
2700
+ simulation: bestSim,
2701
+ };
2702
+ }
2342
2703
  /**
2343
2704
  * Calculate minimum amount out with slippage protection
2344
2705
  * @private
2345
2706
  */
2346
2707
  calculateMinAmountOut(expectedAmount, slippageBps) {
2347
- const amount = BigInt(expectedAmount);
2708
+ const amount = FlashnetClient.safeBigInt(expectedAmount);
2348
2709
  const slippageFactor = BigInt(10000 - slippageBps);
2349
2710
  const minAmount = (amount * slippageFactor) / 10000n;
2350
2711
  return minAmount.toString();
@@ -2563,8 +2924,11 @@ class FlashnetClient {
2563
2924
  const map = new Map();
2564
2925
  for (const item of config) {
2565
2926
  if (item.enabled) {
2927
+ if (item.min_amount == null) {
2928
+ continue;
2929
+ }
2566
2930
  const key = item.asset_identifier.toLowerCase();
2567
- const value = BigInt(String(item.min_amount));
2931
+ const value = FlashnetClient.safeBigInt(item.min_amount);
2568
2932
  map.set(key, value);
2569
2933
  }
2570
2934
  }
@@ -2586,8 +2950,8 @@ class FlashnetClient {
2586
2950
  const outHex = this.getHexAddress(params.assetOutAddress);
2587
2951
  const minIn = minMap.get(inHex);
2588
2952
  const minOut = minMap.get(outHex);
2589
- const amountIn = BigInt(String(params.amountIn));
2590
- const minAmountOut = BigInt(String(params.minAmountOut));
2953
+ const amountIn = FlashnetClient.safeBigInt(params.amountIn);
2954
+ const minAmountOut = FlashnetClient.safeBigInt(params.minAmountOut);
2591
2955
  if (minIn && minOut) {
2592
2956
  if (amountIn < minIn) {
2593
2957
  throw new Error(`Minimum amount not met for input asset. Required \
@@ -2621,13 +2985,13 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2621
2985
  const aMin = minMap.get(aHex);
2622
2986
  const bMin = minMap.get(bHex);
2623
2987
  if (aMin) {
2624
- const aAmt = BigInt(String(params.assetAAmount));
2988
+ const aAmt = FlashnetClient.safeBigInt(params.assetAAmount);
2625
2989
  if (aAmt < aMin) {
2626
2990
  throw new Error(`Minimum amount not met for Asset A. Required ${aMin.toString()}, provided ${aAmt.toString()}`);
2627
2991
  }
2628
2992
  }
2629
2993
  if (bMin) {
2630
- const bAmt = BigInt(String(params.assetBAmount));
2994
+ const bAmt = FlashnetClient.safeBigInt(params.assetBAmount);
2631
2995
  if (bAmt < bMin) {
2632
2996
  throw new Error(`Minimum amount not met for Asset B. Required ${bMin.toString()}, provided ${bAmt.toString()}`);
2633
2997
  }
@@ -2649,14 +3013,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2649
3013
  const aMin = minMap.get(aHex);
2650
3014
  const bMin = minMap.get(bHex);
2651
3015
  if (aMin) {
2652
- const predictedAOut = BigInt(String(simulation.assetAAmount));
3016
+ const predictedAOut = FlashnetClient.safeBigInt(simulation.assetAAmount);
2653
3017
  const relaxedA = aMin / 2n; // apply 50% relaxation for outputs
2654
3018
  if (predictedAOut < relaxedA) {
2655
3019
  throw new Error(`Minimum amount not met for Asset A on withdrawal. Required at least ${relaxedA.toString()} (50% relaxed), predicted ${predictedAOut.toString()}`);
2656
3020
  }
2657
3021
  }
2658
3022
  if (bMin) {
2659
- const predictedBOut = BigInt(String(simulation.assetBAmount));
3023
+ const predictedBOut = FlashnetClient.safeBigInt(simulation.assetBAmount);
2660
3024
  const relaxedB = bMin / 2n;
2661
3025
  if (predictedBOut < relaxedB) {
2662
3026
  throw new Error(`Minimum amount not met for Asset B on withdrawal. Required at least ${relaxedB.toString()} (50% relaxed), predicted ${predictedBOut.toString()}`);
@@ -2769,20 +3133,20 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2769
3133
  const transferIds = [];
2770
3134
  // Transfer assets if not using free balance
2771
3135
  if (!params.useFreeBalance) {
2772
- if (BigInt(params.amountADesired) > 0n) {
3136
+ if (FlashnetClient.safeBigInt(params.amountADesired) > 0n) {
2773
3137
  assetATransferId = await this.transferAsset({
2774
3138
  receiverSparkAddress: lpSparkAddress,
2775
3139
  assetAddress: pool.assetAAddress,
2776
3140
  amount: params.amountADesired,
2777
- }, "Insufficient balance for adding V3 liquidity (Asset A): ");
3141
+ }, "Insufficient balance for adding V3 liquidity (Asset A): ", params.useAvailableBalance);
2778
3142
  transferIds.push(assetATransferId);
2779
3143
  }
2780
- if (BigInt(params.amountBDesired) > 0n) {
3144
+ if (FlashnetClient.safeBigInt(params.amountBDesired) > 0n) {
2781
3145
  assetBTransferId = await this.transferAsset({
2782
3146
  receiverSparkAddress: lpSparkAddress,
2783
3147
  assetAddress: pool.assetBAddress,
2784
3148
  amount: params.amountBDesired,
2785
- }, "Insufficient balance for adding V3 liquidity (Asset B): ");
3149
+ }, "Insufficient balance for adding V3 liquidity (Asset B): ", params.useAvailableBalance);
2786
3150
  transferIds.push(assetBTransferId);
2787
3151
  }
2788
3152
  }
@@ -2975,14 +3339,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2975
3339
  receiverSparkAddress: lpSparkAddress,
2976
3340
  assetAddress: pool.assetAAddress,
2977
3341
  amount: params.additionalAmountA,
2978
- }, "Insufficient balance for rebalance (Asset A): ");
3342
+ }, "Insufficient balance for rebalance (Asset A): ", params.useAvailableBalance);
2979
3343
  }
2980
3344
  if (params.additionalAmountB && BigInt(params.additionalAmountB) > 0n) {
2981
3345
  assetBTransferId = await this.transferAsset({
2982
3346
  receiverSparkAddress: lpSparkAddress,
2983
3347
  assetAddress: pool.assetBAddress,
2984
3348
  amount: params.additionalAmountB,
2985
- }, "Insufficient balance for rebalance (Asset B): ");
3349
+ }, "Insufficient balance for rebalance (Asset B): ", params.useAvailableBalance);
2986
3350
  }
2987
3351
  // Collect transfer IDs for potential clawback
2988
3352
  const transferIds = [];
@@ -3174,20 +3538,20 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3174
3538
  let assetBTransferId = "";
3175
3539
  const transferIds = [];
3176
3540
  // Transfer assets to pool
3177
- if (BigInt(params.amountA) > 0n) {
3541
+ if (FlashnetClient.safeBigInt(params.amountA) > 0n) {
3178
3542
  assetATransferId = await this.transferAsset({
3179
3543
  receiverSparkAddress: lpSparkAddress,
3180
3544
  assetAddress: pool.assetAAddress,
3181
3545
  amount: params.amountA,
3182
- }, "Insufficient balance for depositing to V3 pool (Asset A): ");
3546
+ }, "Insufficient balance for depositing to V3 pool (Asset A): ", params.useAvailableBalance);
3183
3547
  transferIds.push(assetATransferId);
3184
3548
  }
3185
- if (BigInt(params.amountB) > 0n) {
3549
+ if (FlashnetClient.safeBigInt(params.amountB) > 0n) {
3186
3550
  assetBTransferId = await this.transferAsset({
3187
3551
  receiverSparkAddress: lpSparkAddress,
3188
3552
  assetAddress: pool.assetBAddress,
3189
3553
  amount: params.amountB,
3190
- }, "Insufficient balance for depositing to V3 pool (Asset B): ");
3554
+ }, "Insufficient balance for depositing to V3 pool (Asset B): ", params.useAvailableBalance);
3191
3555
  transferIds.push(assetBTransferId);
3192
3556
  }
3193
3557
  const executeDeposit = async () => {