@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.
@@ -11,6 +11,7 @@ var hex = require('../utils/hex.js');
11
11
  var intents = require('../utils/intents.js');
12
12
  var sparkAddress = require('../utils/spark-address.js');
13
13
  var tokenAddress = require('../utils/tokenAddress.js');
14
+ var bigint = require('../utils/bigint.js');
14
15
  var errors = require('../types/errors.js');
15
16
 
16
17
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -245,7 +246,8 @@ class FlashnetClient {
245
246
  const tokenIdentifierHex = hex.getHexFromUint8Array(info.rawTokenIdentifier);
246
247
  const tokenAddress$1 = tokenAddress.encodeSparkHumanReadableTokenIdentifier(info.rawTokenIdentifier, this.sparkNetwork);
247
248
  tokenBalances.set(tokenPubkey, {
248
- balance: BigInt(tokenData.balance),
249
+ balance: FlashnetClient.safeBigInt(tokenData.ownedBalance),
250
+ availableToSendBalance: FlashnetClient.safeBigInt(tokenData.availableToSendBalance),
249
251
  tokenInfo: {
250
252
  tokenIdentifier: tokenIdentifierHex,
251
253
  tokenAddress: tokenAddress$1,
@@ -258,7 +260,7 @@ class FlashnetClient {
258
260
  }
259
261
  }
260
262
  return {
261
- balance: BigInt(balance.balance),
263
+ balance: FlashnetClient.safeBigInt(balance.balance),
262
264
  tokenBalances,
263
265
  };
264
266
  }
@@ -266,17 +268,17 @@ class FlashnetClient {
266
268
  * Check if wallet has sufficient balance for an operation
267
269
  */
268
270
  async checkBalance(params) {
269
- const balance = await this.getBalance();
271
+ const balance = params.walletBalance ?? (await this.getBalance());
270
272
  // Check balance
271
273
  const requirements = {
272
274
  tokens: new Map(),
273
275
  };
274
276
  for (const balance of params.balancesToCheck) {
275
277
  if (balance.assetAddress === index$1.BTC_ASSET_PUBKEY) {
276
- requirements.btc = BigInt(balance.amount);
278
+ requirements.btc = FlashnetClient.safeBigInt(balance.amount);
277
279
  }
278
280
  else {
279
- requirements.tokens?.set(balance.assetAddress, BigInt(balance.amount));
281
+ requirements.tokens?.set(balance.assetAddress, FlashnetClient.safeBigInt(balance.amount));
280
282
  }
281
283
  }
282
284
  // Check BTC balance
@@ -294,7 +296,9 @@ class FlashnetClient {
294
296
  const hrKey = this.toHumanReadableTokenIdentifier(tokenPubkey);
295
297
  const effectiveTokenBalance = balance.tokenBalances.get(tokenPubkey) ??
296
298
  balance.tokenBalances.get(hrKey);
297
- const available = effectiveTokenBalance?.balance ?? 0n;
299
+ const available = params.useAvailableBalance
300
+ ? (effectiveTokenBalance?.availableToSendBalance ?? 0n)
301
+ : (effectiveTokenBalance?.balance ?? 0n);
298
302
  if (available < requiredAmount) {
299
303
  throw new Error([
300
304
  params.errorPrefix ?? "",
@@ -356,6 +360,7 @@ class FlashnetClient {
356
360
  },
357
361
  ],
358
362
  errorPrefix: "Insufficient balance for initial liquidity: ",
363
+ useAvailableBalance: params.useAvailableBalance,
359
364
  });
360
365
  }
361
366
  const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
@@ -415,6 +420,12 @@ class FlashnetClient {
415
420
  throw new Error(`${name} must be positive integer`);
416
421
  }
417
422
  }
423
+ /**
424
+ * Safely convert a value to BigInt. Delegates to the shared `safeBigInt` utility.
425
+ */
426
+ static safeBigInt(value, fallback = 0n) {
427
+ return bigint.safeBigInt(value, fallback);
428
+ }
418
429
  /**
419
430
  * Calculates virtual reserves for a bonding curve AMM.
420
431
  *
@@ -435,7 +446,7 @@ class FlashnetClient {
435
446
  }
436
447
  const supply = FlashnetClient.parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
437
448
  const targetB = FlashnetClient.parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
438
- const graduationThresholdPct = BigInt(params.graduationThresholdPct);
449
+ const graduationThresholdPct = FlashnetClient.safeBigInt(params.graduationThresholdPct);
439
450
  // Align bounds with Rust AMM (20%..95%), then check feasibility for g=1 (requires >50%).
440
451
  const MIN_PCT = 20n;
441
452
  const MAX_PCT = 95n;
@@ -485,6 +496,7 @@ class FlashnetClient {
485
496
  },
486
497
  ],
487
498
  errorPrefix: "Insufficient balance for pool creation: ",
499
+ useAvailableBalance: params.useAvailableBalance,
488
500
  });
489
501
  const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
490
502
  // Generate intent
@@ -605,10 +617,14 @@ class FlashnetClient {
605
617
  });
606
618
  // If using free balance (V3 pools only), skip the Spark transfer
607
619
  if (params.useFreeBalance) {
608
- return this.executeSwapIntent({
620
+ const swapResponse = await this.executeSwapIntent({
609
621
  ...params,
610
622
  // No transferId - triggers free balance mode
611
623
  });
624
+ return {
625
+ ...swapResponse,
626
+ inboundSparkTransferId: swapResponse.requestId,
627
+ };
612
628
  }
613
629
  // Transfer assets to pool using new address encoding
614
630
  const lpSparkAddress = sparkAddress.encodeSparkAddressNew({
@@ -619,12 +635,13 @@ class FlashnetClient {
619
635
  receiverSparkAddress: lpSparkAddress,
620
636
  assetAddress: params.assetInAddress,
621
637
  amount: params.amountIn,
622
- }, "Insufficient balance for swap: ");
638
+ }, "Insufficient balance for swap: ", params.useAvailableBalance);
623
639
  // Execute with auto-clawback on failure
624
- return this.executeWithAutoClawback(() => this.executeSwapIntent({
640
+ const swapResponse = await this.executeWithAutoClawback(() => this.executeSwapIntent({
625
641
  ...params,
626
642
  transferId,
627
643
  }), [transferId], params.poolId);
644
+ return { ...swapResponse, inboundSparkTransferId: transferId };
628
645
  }
629
646
  /**
630
647
  * Execute a swap with a pre-created transfer or using free balance.
@@ -754,7 +771,7 @@ class FlashnetClient {
754
771
  receiverSparkAddress: lpSparkAddress,
755
772
  assetAddress: params.initialAssetAddress,
756
773
  amount: params.inputAmount,
757
- }, "Insufficient balance for route swap: ");
774
+ }, "Insufficient balance for route swap: ", params.useAvailableBalance);
758
775
  // Execute with auto-clawback on failure
759
776
  return this.executeWithAutoClawback(async () => {
760
777
  // Prepare hops for validation
@@ -877,7 +894,7 @@ class FlashnetClient {
877
894
  assetAddress: pool.assetBAddress,
878
895
  amount: params.assetBAmount,
879
896
  },
880
- ], "Insufficient balance for adding liquidity: ");
897
+ ], "Insufficient balance for adding liquidity: ", params.useAvailableBalance);
881
898
  // Execute with auto-clawback on failure
882
899
  return this.executeWithAutoClawback(async () => {
883
900
  // Generate add liquidity intent
@@ -1183,6 +1200,7 @@ class FlashnetClient {
1183
1200
  depositAddress: createResponse.depositAddress,
1184
1201
  assetId: params.assetId,
1185
1202
  assetAmount: params.assetAmount,
1203
+ useAvailableBalance: params.useAvailableBalance,
1186
1204
  });
1187
1205
  }
1188
1206
  /**
@@ -1200,6 +1218,7 @@ class FlashnetClient {
1200
1218
  { assetAddress: params.assetId, amount: params.assetAmount },
1201
1219
  ],
1202
1220
  errorPrefix: "Insufficient balance to fund escrow: ",
1221
+ useAvailableBalance: params.useAvailableBalance,
1203
1222
  });
1204
1223
  // 2. Perform transfer
1205
1224
  const escrowSparkAddress = sparkAddress.encodeSparkAddressNew({
@@ -1714,19 +1733,20 @@ class FlashnetClient {
1714
1733
  /**
1715
1734
  * Performs asset transfer using generalized asset address for both BTC and tokens.
1716
1735
  */
1717
- async transferAsset(recipient, checkBalanceErrorPrefix) {
1718
- const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix);
1736
+ async transferAsset(recipient, checkBalanceErrorPrefix, useAvailableBalance) {
1737
+ const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix, useAvailableBalance);
1719
1738
  return transferIds[0];
1720
1739
  }
1721
1740
  /**
1722
1741
  * Performs asset transfers using generalized asset addresses for both BTC and tokens.
1723
1742
  * Supports optional generic to hardcode recipients length so output list can be typed with same length.
1724
1743
  */
1725
- async transferAssets(recipients, checkBalanceErrorPrefix) {
1744
+ async transferAssets(recipients, checkBalanceErrorPrefix, useAvailableBalance) {
1726
1745
  if (checkBalanceErrorPrefix) {
1727
1746
  await this.checkBalance({
1728
1747
  balancesToCheck: recipients,
1729
1748
  errorPrefix: checkBalanceErrorPrefix,
1749
+ useAvailableBalance,
1730
1750
  });
1731
1751
  }
1732
1752
  const transferIds = [];
@@ -1741,7 +1761,7 @@ class FlashnetClient {
1741
1761
  else {
1742
1762
  const transferId = await this._wallet.transferTokens({
1743
1763
  tokenIdentifier: this.toHumanReadableTokenIdentifier(recipient.assetAddress),
1744
- tokenAmount: BigInt(recipient.amount),
1764
+ tokenAmount: FlashnetClient.safeBigInt(recipient.amount),
1745
1765
  receiverSparkAddress: recipient.receiverSparkAddress,
1746
1766
  });
1747
1767
  transferIds.push(transferId);
@@ -1825,38 +1845,37 @@ class FlashnetClient {
1825
1845
  await this.ensureInitialized();
1826
1846
  // Decode the invoice to get the amount
1827
1847
  const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
1848
+ // Zero-amount invoice: forward-direction quoting using caller-specified tokenAmount
1828
1849
  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
- });
1850
+ const tokenAmount = options?.tokenAmount;
1851
+ if (!tokenAmount || BigInt(tokenAmount) <= 0n) {
1852
+ throw new errors.FlashnetError("Zero-amount invoice requires tokenAmount in options.", {
1853
+ response: {
1854
+ errorCode: "FSAG-1002",
1855
+ errorCategory: "Validation",
1856
+ message: "Zero-amount invoice requires tokenAmount in options.",
1857
+ requestId: "",
1858
+ timestamp: new Date().toISOString(),
1859
+ service: "sdk",
1860
+ severity: "Error",
1861
+ remediation: "Provide tokenAmount when using a zero-amount invoice.",
1862
+ },
1863
+ });
1864
+ }
1865
+ return this.getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options);
1841
1866
  }
1842
1867
  // Get Lightning fee estimate
1843
1868
  const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
1844
- // Total BTC needed = invoice amount + lightning fee
1845
- 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;
1869
+ // Total BTC needed = invoice amount + lightning fee (unmasked).
1870
+ // Bitmasking for V2 pools is handled inside findBestPoolForTokenToBtc.
1871
+ const baseBtcNeeded = FlashnetClient.safeBigInt(invoiceAmountSats) +
1872
+ FlashnetClient.safeBigInt(lightningFeeEstimate);
1854
1873
  // Check Flashnet minimum amounts early to provide clear error messages
1855
1874
  const minAmounts = await this.getEnabledMinAmountsMap();
1856
1875
  // Check BTC minimum (output from swap)
1857
1876
  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.`;
1877
+ if (btcMinAmount && baseBtcNeeded < btcMinAmount) {
1878
+ const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${baseBtcNeeded} sats.`;
1860
1879
  throw new errors.FlashnetError(msg, {
1861
1880
  response: {
1862
1881
  errorCode: "FSAG-1003",
@@ -1870,13 +1889,14 @@ class FlashnetClient {
1870
1889
  },
1871
1890
  });
1872
1891
  }
1873
- // Find the best pool to swap token -> BTC
1874
- const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, totalBtcNeeded.toString(), options?.integratorFeeRateBps);
1892
+ // Find the best pool to swap token -> BTC.
1893
+ // Bitmasking is applied per-pool inside this function (V2 pools get masked, V3 pools don't).
1894
+ const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded.toString(), options?.integratorFeeRateBps);
1875
1895
  // Check token minimum (input to swap)
1876
1896
  const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
1877
1897
  const tokenMinAmount = minAmounts.get(tokenHex);
1878
1898
  if (tokenMinAmount &&
1879
- BigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1899
+ FlashnetClient.safeBigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1880
1900
  const msg = `Token amount too small. Minimum input is ${tokenMinAmount} units, but calculated amount is only ${poolQuote.tokenAmountRequired} units.`;
1881
1901
  throw new errors.FlashnetError(msg, {
1882
1902
  response: {
@@ -1891,13 +1911,14 @@ class FlashnetClient {
1891
1911
  },
1892
1912
  });
1893
1913
  }
1894
- // Calculate the BTC variable fee adjustment (how much extra we're requesting)
1895
- const btcVariableFeeAdjustment = Number(totalBtcNeeded - baseBtcNeeded);
1914
+ // BTC variable fee adjustment: difference between what the pool targets and unmasked base.
1915
+ // For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
1916
+ const btcVariableFeeAdjustment = Number(FlashnetClient.safeBigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
1896
1917
  return {
1897
1918
  poolId: poolQuote.poolId,
1898
1919
  tokenAddress: this.toHexTokenIdentifier(tokenAddress),
1899
1920
  tokenAmountRequired: poolQuote.tokenAmountRequired,
1900
- btcAmountRequired: totalBtcNeeded.toString(),
1921
+ btcAmountRequired: poolQuote.btcAmountUsed,
1901
1922
  invoiceAmountSats: invoiceAmountSats,
1902
1923
  estimatedAmmFee: poolQuote.estimatedAmmFee,
1903
1924
  estimatedLightningFee: lightningFeeEstimate,
@@ -1907,6 +1928,138 @@ class FlashnetClient {
1907
1928
  tokenIsAssetA: poolQuote.tokenIsAssetA,
1908
1929
  poolReserves: poolQuote.poolReserves,
1909
1930
  warningMessage: poolQuote.warningMessage,
1931
+ curveType: poolQuote.curveType,
1932
+ isZeroAmountInvoice: false,
1933
+ };
1934
+ }
1935
+ /**
1936
+ * Generate a quote for a zero-amount invoice.
1937
+ * Forward-direction: simulate swapping tokenAmount and pick the pool with the best BTC output.
1938
+ * @private
1939
+ */
1940
+ async getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options) {
1941
+ const tokenHex = this.toHexTokenIdentifier(tokenAddress);
1942
+ const btcHex = index$1.BTC_ASSET_PUBKEY;
1943
+ // Discover all token/BTC pools
1944
+ const [poolsWithTokenAsA, poolsWithTokenAsB] = await Promise.all([
1945
+ this.listPools({ assetAAddress: tokenHex, assetBAddress: btcHex }),
1946
+ this.listPools({ assetAAddress: btcHex, assetBAddress: tokenHex }),
1947
+ ]);
1948
+ const poolMap = new Map();
1949
+ for (const p of [...poolsWithTokenAsA.pools, ...poolsWithTokenAsB.pools]) {
1950
+ if (!poolMap.has(p.lpPublicKey)) {
1951
+ const tokenIsAssetA = p.assetAAddress?.toLowerCase() === tokenHex.toLowerCase();
1952
+ poolMap.set(p.lpPublicKey, { pool: p, tokenIsAssetA });
1953
+ }
1954
+ }
1955
+ const allPools = Array.from(poolMap.values());
1956
+ if (allPools.length === 0) {
1957
+ throw new errors.FlashnetError(`No liquidity pool found for token ${tokenAddress} paired with BTC`, {
1958
+ response: {
1959
+ errorCode: "FSAG-4001",
1960
+ errorCategory: "Business",
1961
+ message: `No liquidity pool found for token ${tokenAddress} paired with BTC`,
1962
+ requestId: "",
1963
+ timestamp: new Date().toISOString(),
1964
+ service: "sdk",
1965
+ severity: "Error",
1966
+ },
1967
+ });
1968
+ }
1969
+ // Simulate each pool with tokenAmount as input, pick highest BTC output
1970
+ let bestResult = null;
1971
+ let bestBtcOut = 0n;
1972
+ for (const { pool, tokenIsAssetA } of allPools) {
1973
+ try {
1974
+ const poolDetails = await this.getPool(pool.lpPublicKey);
1975
+ const assetInAddress = tokenIsAssetA
1976
+ ? poolDetails.assetAAddress
1977
+ : poolDetails.assetBAddress;
1978
+ const assetOutAddress = tokenIsAssetA
1979
+ ? poolDetails.assetBAddress
1980
+ : poolDetails.assetAAddress;
1981
+ const simulation = await this.simulateSwap({
1982
+ poolId: pool.lpPublicKey,
1983
+ assetInAddress,
1984
+ assetOutAddress,
1985
+ amountIn: tokenAmount,
1986
+ integratorBps: options?.integratorFeeRateBps,
1987
+ });
1988
+ const btcOut = FlashnetClient.safeBigInt(simulation.amountOut);
1989
+ if (btcOut > bestBtcOut) {
1990
+ bestBtcOut = btcOut;
1991
+ bestResult = {
1992
+ poolId: pool.lpPublicKey,
1993
+ tokenIsAssetA,
1994
+ simulation,
1995
+ curveType: poolDetails.curveType,
1996
+ poolReserves: {
1997
+ assetAReserve: poolDetails.assetAReserve,
1998
+ assetBReserve: poolDetails.assetBReserve,
1999
+ },
2000
+ };
2001
+ }
2002
+ }
2003
+ catch {
2004
+ // Skip pools that fail simulation
2005
+ }
2006
+ }
2007
+ if (!bestResult || bestBtcOut <= 0n) {
2008
+ throw new errors.FlashnetError("No pool can produce BTC output for the given token amount", {
2009
+ response: {
2010
+ errorCode: "FSAG-4201",
2011
+ errorCategory: "Business",
2012
+ message: "No pool can produce BTC output for the given token amount",
2013
+ requestId: "",
2014
+ timestamp: new Date().toISOString(),
2015
+ service: "sdk",
2016
+ severity: "Error",
2017
+ remediation: "Try a larger token amount.",
2018
+ },
2019
+ });
2020
+ }
2021
+ // Estimate lightning fee from the BTC output
2022
+ let lightningFeeEstimate;
2023
+ try {
2024
+ lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
2025
+ }
2026
+ catch {
2027
+ lightningFeeEstimate = Math.max(5, Math.ceil(Number(bestBtcOut) * 0.0017));
2028
+ }
2029
+ // Check minimum amounts
2030
+ const minAmounts = await this.getEnabledMinAmountsMap();
2031
+ const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
2032
+ if (btcMinAmount && bestBtcOut < btcMinAmount) {
2033
+ const msg = `BTC output too small. Minimum is ${btcMinAmount} sats, but swap would produce only ${bestBtcOut} sats.`;
2034
+ throw new errors.FlashnetError(msg, {
2035
+ response: {
2036
+ errorCode: "FSAG-1003",
2037
+ errorCategory: "Validation",
2038
+ message: msg,
2039
+ requestId: "",
2040
+ timestamp: new Date().toISOString(),
2041
+ service: "sdk",
2042
+ severity: "Error",
2043
+ remediation: "Use a larger token amount.",
2044
+ },
2045
+ });
2046
+ }
2047
+ return {
2048
+ poolId: bestResult.poolId,
2049
+ tokenAddress: tokenHex,
2050
+ tokenAmountRequired: tokenAmount,
2051
+ btcAmountRequired: bestBtcOut.toString(),
2052
+ invoiceAmountSats: 0,
2053
+ estimatedAmmFee: bestResult.simulation.feePaidAssetIn || "0",
2054
+ estimatedLightningFee: lightningFeeEstimate,
2055
+ btcVariableFeeAdjustment: 0,
2056
+ executionPrice: bestResult.simulation.executionPrice || "0",
2057
+ priceImpactPct: bestResult.simulation.priceImpactPct || "0",
2058
+ tokenIsAssetA: bestResult.tokenIsAssetA,
2059
+ poolReserves: bestResult.poolReserves,
2060
+ warningMessage: bestResult.simulation.warningMessage,
2061
+ curveType: bestResult.curveType,
2062
+ isZeroAmountInvoice: true,
1910
2063
  };
1911
2064
  }
1912
2065
  /**
@@ -1918,14 +2071,15 @@ class FlashnetClient {
1918
2071
  */
1919
2072
  async payLightningWithToken(options) {
1920
2073
  await this.ensureInitialized();
1921
- const { invoice, tokenAddress, maxSlippageBps = 500, // 5% default
2074
+ const { invoice, tokenAddress, tokenAmount, maxSlippageBps = 500, // 5% default
1922
2075
  maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 30000, // 30s default
1923
- rollbackOnFailure = false, useExistingBtcBalance = false, } = options;
2076
+ rollbackOnFailure = false, useExistingBtcBalance = false, useAvailableBalance = false, } = options;
1924
2077
  try {
1925
2078
  // Step 1: Get a quote for the payment
1926
2079
  const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
1927
2080
  maxSlippageBps,
1928
2081
  integratorFeeRateBps,
2082
+ tokenAmount,
1929
2083
  });
1930
2084
  // Step 2: Check token balance (always required)
1931
2085
  await this.checkBalance({
@@ -1936,17 +2090,8 @@ class FlashnetClient {
1936
2090
  },
1937
2091
  ],
1938
2092
  errorPrefix: "Insufficient token balance for Lightning payment: ",
2093
+ useAvailableBalance,
1939
2094
  });
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
2095
  // Step 3: Get pool details
1951
2096
  const pool = await this.getPool(quote.poolId);
1952
2097
  // Step 4: Determine swap direction and execute
@@ -1968,6 +2113,7 @@ class FlashnetClient {
1968
2113
  minAmountOut: minBtcOut,
1969
2114
  integratorFeeRateBps,
1970
2115
  integratorPublicKey,
2116
+ useAvailableBalance,
1971
2117
  });
1972
2118
  if (!swapResponse.accepted || !swapResponse.outboundTransferId) {
1973
2119
  return {
@@ -1977,10 +2123,20 @@ class FlashnetClient {
1977
2123
  btcAmountReceived: "0",
1978
2124
  swapTransferId: swapResponse.outboundTransferId || "",
1979
2125
  ammFeePaid: quote.estimatedAmmFee,
2126
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
1980
2127
  error: swapResponse.error || "Swap was not accepted",
1981
2128
  };
1982
2129
  }
1983
- // Step 5: Wait for the transfer to complete (unless paying immediately with existing BTC)
2130
+ // Step 5: Wait for transfer (skip useExistingBtcBalance for zero-amount invoices)
2131
+ let canPayImmediately = false;
2132
+ if (!quote.isZeroAmountInvoice && useExistingBtcBalance) {
2133
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
2134
+ const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2135
+ const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
2136
+ const balance = await this.getBalance();
2137
+ canPayImmediately =
2138
+ balance.balance >= FlashnetClient.safeBigInt(btcNeededForPayment);
2139
+ }
1984
2140
  if (!canPayImmediately) {
1985
2141
  const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
1986
2142
  if (!transferComplete) {
@@ -1991,20 +2147,57 @@ class FlashnetClient {
1991
2147
  btcAmountReceived: swapResponse.amountOut || "0",
1992
2148
  swapTransferId: swapResponse.outboundTransferId,
1993
2149
  ammFeePaid: quote.estimatedAmmFee,
2150
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
1994
2151
  error: "Transfer did not complete within timeout",
1995
2152
  };
1996
2153
  }
1997
2154
  }
1998
- // Step 6: Calculate Lightning fee limit - use the quoted estimate, not a recalculation
2155
+ // Step 6: Calculate Lightning fee and payment amount
1999
2156
  const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2000
- // Step 7: Pay the Lightning invoice
2001
2157
  const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
2158
+ // Step 7: Pay the Lightning invoice
2002
2159
  try {
2003
- const lightningPayment = await this._wallet.payLightningInvoice({
2004
- invoice,
2005
- maxFeeSats: effectiveMaxLightningFee,
2006
- preferSpark,
2007
- });
2160
+ let lightningPayment;
2161
+ let invoiceAmountPaid;
2162
+ if (quote.isZeroAmountInvoice) {
2163
+ // Zero-amount invoice: pay whatever BTC we received minus lightning fee
2164
+ const actualBtc = FlashnetClient.safeBigInt(btcReceived);
2165
+ const lnFee = FlashnetClient.safeBigInt(effectiveMaxLightningFee);
2166
+ const amountToPay = actualBtc - lnFee;
2167
+ if (amountToPay <= 0n) {
2168
+ return {
2169
+ success: false,
2170
+ poolId: quote.poolId,
2171
+ tokenAmountSpent: quote.tokenAmountRequired,
2172
+ btcAmountReceived: btcReceived,
2173
+ swapTransferId: swapResponse.outboundTransferId,
2174
+ ammFeePaid: quote.estimatedAmmFee,
2175
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2176
+ error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
2177
+ };
2178
+ }
2179
+ invoiceAmountPaid = Number(amountToPay);
2180
+ lightningPayment = await this._wallet.payLightningInvoice({
2181
+ invoice,
2182
+ amountSats: invoiceAmountPaid,
2183
+ maxFeeSats: effectiveMaxLightningFee,
2184
+ preferSpark,
2185
+ });
2186
+ }
2187
+ else {
2188
+ // Standard invoice: pay the specified amount
2189
+ lightningPayment = await this._wallet.payLightningInvoice({
2190
+ invoice,
2191
+ maxFeeSats: effectiveMaxLightningFee,
2192
+ preferSpark,
2193
+ });
2194
+ }
2195
+ // Extract the Spark transfer ID from the lightning payment result.
2196
+ // payLightningInvoice returns LightningSendRequest | WalletTransfer:
2197
+ // - LightningSendRequest has .transfer?.sparkId (the Sparkscan-visible transfer ID)
2198
+ // - WalletTransfer (Spark-to-Spark) has .id directly as the transfer ID
2199
+ // Note: lightningPayment.id (the SSP request ID) is already returned as lightningPaymentId
2200
+ const sparkLightningTransferId = lightningPayment.transfer?.sparkId;
2008
2201
  return {
2009
2202
  success: true,
2010
2203
  poolId: quote.poolId,
@@ -2014,6 +2207,9 @@ class FlashnetClient {
2014
2207
  lightningPaymentId: lightningPayment.id,
2015
2208
  ammFeePaid: quote.estimatedAmmFee,
2016
2209
  lightningFeePaid: effectiveMaxLightningFee,
2210
+ invoiceAmountPaid,
2211
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2212
+ sparkLightningTransferId,
2017
2213
  };
2018
2214
  }
2019
2215
  catch (lightningError) {
@@ -2033,6 +2229,7 @@ class FlashnetClient {
2033
2229
  btcAmountReceived: "0",
2034
2230
  swapTransferId: swapResponse.outboundTransferId,
2035
2231
  ammFeePaid: quote.estimatedAmmFee,
2232
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2036
2233
  error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
2037
2234
  };
2038
2235
  }
@@ -2048,6 +2245,7 @@ class FlashnetClient {
2048
2245
  btcAmountReceived: btcReceived,
2049
2246
  swapTransferId: swapResponse.outboundTransferId,
2050
2247
  ammFeePaid: quote.estimatedAmmFee,
2248
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2051
2249
  error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
2052
2250
  };
2053
2251
  }
@@ -2059,6 +2257,7 @@ class FlashnetClient {
2059
2257
  btcAmountReceived: btcReceived,
2060
2258
  swapTransferId: swapResponse.outboundTransferId,
2061
2259
  ammFeePaid: quote.estimatedAmmFee,
2260
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2062
2261
  error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
2063
2262
  };
2064
2263
  }
@@ -2119,7 +2318,7 @@ class FlashnetClient {
2119
2318
  * Find the best pool for swapping a token to BTC
2120
2319
  * @private
2121
2320
  */
2122
- async findBestPoolForTokenToBtc(tokenAddress, btcAmountNeeded, integratorFeeRateBps) {
2321
+ async findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded, integratorFeeRateBps) {
2123
2322
  const tokenHex = this.toHexTokenIdentifier(tokenAddress);
2124
2323
  const btcHex = index$1.BTC_ASSET_PUBKEY;
2125
2324
  // Find all pools that have this token paired with BTC
@@ -2163,8 +2362,8 @@ class FlashnetClient {
2163
2362
  const minAmounts = await this.getMinAmountsMap();
2164
2363
  const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
2165
2364
  // 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.`;
2365
+ if (btcMinAmount && BigInt(baseBtcNeeded) < btcMinAmount) {
2366
+ const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${baseBtcNeeded} sats.`;
2168
2367
  throw new errors.FlashnetError(msg, {
2169
2368
  response: {
2170
2369
  errorCode: "FSAG-1003",
@@ -2178,60 +2377,99 @@ class FlashnetClient {
2178
2377
  },
2179
2378
  });
2180
2379
  }
2380
+ // Compute V2 masked BTC amount (round up to next multiple of 64 for bit masking)
2381
+ const baseBtc = BigInt(baseBtcNeeded);
2382
+ const BTC_VARIABLE_FEE_BITS = 6n;
2383
+ const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
2384
+ const maskedBtc = ((baseBtc + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
2385
+ BTC_VARIABLE_FEE_MASK;
2181
2386
  // Find the best pool (lowest token cost for the required BTC)
2182
2387
  let bestPool = null;
2183
2388
  let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
2389
+ let bestBtcTarget = 0n;
2390
+ let bestCurveType = "";
2184
2391
  let bestSimulation = null;
2185
2392
  // Track errors for each pool to provide better diagnostics
2186
2393
  const poolErrors = [];
2187
2394
  for (const pool of allPools) {
2188
2395
  try {
2189
- // Get pool details for reserves
2396
+ // Get pool details for reserves and curve type
2190
2397
  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) {
2398
+ const isV3 = poolDetails.curveType === "V3_CONCENTRATED";
2399
+ // V3 pools use exact BTC amount, V2 pools use masked amount
2400
+ const btcTarget = isV3 ? baseBtc : maskedBtc;
2401
+ const assetInAddress = pool.tokenIsAssetA
2402
+ ? poolDetails.assetAAddress
2403
+ : poolDetails.assetBAddress;
2404
+ const assetOutAddress = pool.tokenIsAssetA
2405
+ ? poolDetails.assetBAddress
2406
+ : poolDetails.assetAAddress;
2407
+ let tokenAmount;
2408
+ let fee;
2409
+ let executionPrice;
2410
+ let priceImpactPct;
2411
+ let warningMessage;
2412
+ if (isV3) {
2413
+ // V3: binary search with simulateSwap
2414
+ const v3Result = await this.findV3TokenAmountForBtcOutput({
2415
+ poolId: pool.lpPublicKey,
2416
+ assetInAddress,
2417
+ assetOutAddress,
2418
+ desiredBtcOut: btcTarget,
2419
+ currentPriceAInB: poolDetails.currentPriceAInB,
2420
+ tokenIsAssetA: pool.tokenIsAssetA,
2421
+ integratorBps: integratorFeeRateBps,
2422
+ });
2423
+ tokenAmount = FlashnetClient.safeBigInt(v3Result.amountIn);
2424
+ fee = v3Result.totalFee;
2425
+ executionPrice = v3Result.simulation.executionPrice || "0";
2426
+ priceImpactPct = v3Result.simulation.priceImpactPct || "0";
2427
+ warningMessage = v3Result.simulation.warningMessage;
2428
+ }
2429
+ else {
2430
+ // V2: constant product math + simulation verification
2431
+ const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
2432
+ tokenAmount = FlashnetClient.safeBigInt(calculation.amountIn);
2196
2433
  // Verify with simulation
2197
2434
  const simulation = await this.simulateSwap({
2198
2435
  poolId: pool.lpPublicKey,
2199
- assetInAddress: pool.tokenIsAssetA
2200
- ? poolDetails.assetAAddress
2201
- : poolDetails.assetBAddress,
2202
- assetOutAddress: pool.tokenIsAssetA
2203
- ? poolDetails.assetBAddress
2204
- : poolDetails.assetAAddress,
2436
+ assetInAddress,
2437
+ assetOutAddress,
2205
2438
  amountIn: calculation.amountIn,
2206
2439
  integratorBps: integratorFeeRateBps,
2207
2440
  });
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
2441
+ if (FlashnetClient.safeBigInt(simulation.amountOut) < btcTarget) {
2222
2442
  const btcReserve = pool.tokenIsAssetA
2223
2443
  ? poolDetails.assetBReserve
2224
2444
  : poolDetails.assetAReserve;
2225
2445
  poolErrors.push({
2226
2446
  poolId: pool.lpPublicKey,
2227
- error: `Simulation output (${simulation.amountOut} sats) < required (${btcAmountNeeded} sats)`,
2447
+ error: `Simulation output (${simulation.amountOut} sats) < required (${btcTarget} sats)`,
2228
2448
  btcReserve,
2229
2449
  });
2450
+ continue;
2230
2451
  }
2452
+ fee = calculation.totalFee;
2453
+ executionPrice = simulation.executionPrice || "0";
2454
+ priceImpactPct = simulation.priceImpactPct || "0";
2455
+ warningMessage = simulation.warningMessage;
2456
+ }
2457
+ // Check if this pool offers a better rate
2458
+ if (tokenAmount < bestTokenAmount) {
2459
+ bestPool = pool;
2460
+ bestTokenAmount = tokenAmount;
2461
+ bestBtcTarget = btcTarget;
2462
+ bestCurveType = poolDetails.curveType;
2463
+ bestSimulation = {
2464
+ amountIn: tokenAmount.toString(),
2465
+ fee,
2466
+ executionPrice,
2467
+ priceImpactPct,
2468
+ warningMessage,
2469
+ };
2231
2470
  }
2232
2471
  }
2233
2472
  catch (e) {
2234
- // Capture pool errors for diagnostics
2235
2473
  const errorMessage = e instanceof Error ? e.message : String(e);
2236
2474
  poolErrors.push({
2237
2475
  poolId: pool.lpPublicKey,
@@ -2240,7 +2478,7 @@ class FlashnetClient {
2240
2478
  }
2241
2479
  }
2242
2480
  if (!bestPool || !bestSimulation) {
2243
- let errorMessage = `No pool has sufficient liquidity for ${btcAmountNeeded} sats`;
2481
+ let errorMessage = `No pool has sufficient liquidity for ${baseBtcNeeded} sats`;
2244
2482
  if (poolErrors.length > 0) {
2245
2483
  const details = poolErrors
2246
2484
  .map((pe) => {
@@ -2278,6 +2516,8 @@ class FlashnetClient {
2278
2516
  assetBReserve: poolDetails.assetBReserve,
2279
2517
  },
2280
2518
  warningMessage: bestSimulation.warningMessage,
2519
+ btcAmountUsed: bestBtcTarget.toString(),
2520
+ curveType: bestCurveType,
2281
2521
  };
2282
2522
  }
2283
2523
  /**
@@ -2286,9 +2526,9 @@ class FlashnetClient {
2286
2526
  * @private
2287
2527
  */
2288
2528
  calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
2289
- const amountOut = BigInt(btcAmountOut);
2290
- const resA = BigInt(reserveA);
2291
- const resB = BigInt(reserveB);
2529
+ const amountOut = FlashnetClient.safeBigInt(btcAmountOut);
2530
+ const resA = FlashnetClient.safeBigInt(reserveA);
2531
+ const resB = FlashnetClient.safeBigInt(reserveB);
2292
2532
  const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
2293
2533
  const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
2294
2534
  // Token is the input asset
@@ -2345,12 +2585,133 @@ class FlashnetClient {
2345
2585
  };
2346
2586
  }
2347
2587
  }
2588
+ /**
2589
+ * Find the token amount needed to get a specific BTC output from a V3 concentrated liquidity pool.
2590
+ * Uses binary search with simulateSwap since V3 tick-based math can't be inverted locally.
2591
+ * @private
2592
+ */
2593
+ async findV3TokenAmountForBtcOutput(params) {
2594
+ const { poolId, assetInAddress, assetOutAddress, desiredBtcOut, currentPriceAInB, tokenIsAssetA, integratorBps, } = params;
2595
+ // Step 1: Compute initial estimate from pool price
2596
+ let estimate;
2597
+ if (currentPriceAInB && currentPriceAInB !== "0") {
2598
+ const price = Number(currentPriceAInB);
2599
+ if (tokenIsAssetA) {
2600
+ // priceAInB = how much B (BTC) per 1 A (token), so tokenNeeded = btcOut / price
2601
+ estimate = BigInt(Math.ceil(Number(desiredBtcOut) / price));
2602
+ }
2603
+ else {
2604
+ // priceAInB = how much B (token) per 1 A (BTC), so tokenNeeded = btcOut * price
2605
+ estimate = BigInt(Math.ceil(Number(desiredBtcOut) * price));
2606
+ }
2607
+ // Ensure non-zero
2608
+ if (estimate <= 0n) {
2609
+ estimate = desiredBtcOut * 2n;
2610
+ }
2611
+ }
2612
+ else {
2613
+ estimate = desiredBtcOut * 2n;
2614
+ }
2615
+ // Step 2: Find upper bound by simulating with estimate + 10% buffer
2616
+ let upperBound = (estimate * 110n) / 100n;
2617
+ if (upperBound <= 0n) {
2618
+ upperBound = 1n;
2619
+ }
2620
+ let upperSim = null;
2621
+ for (let attempt = 0; attempt < 3; attempt++) {
2622
+ const sim = await this.simulateSwap({
2623
+ poolId,
2624
+ assetInAddress,
2625
+ assetOutAddress,
2626
+ amountIn: upperBound.toString(),
2627
+ integratorBps,
2628
+ });
2629
+ if (FlashnetClient.safeBigInt(sim.amountOut) >= desiredBtcOut) {
2630
+ upperSim = sim;
2631
+ break;
2632
+ }
2633
+ // Double the upper bound
2634
+ upperBound = upperBound * 2n;
2635
+ }
2636
+ if (!upperSim) {
2637
+ throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
2638
+ }
2639
+ // Step 3: Refine estimate via linear interpolation
2640
+ const upperOut = FlashnetClient.safeBigInt(upperSim.amountOut);
2641
+ // Scale proportionally: if upperBound produced upperOut, we need roughly
2642
+ // (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
2643
+ let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
2644
+ if (refined <= 0n) {
2645
+ refined = 1n;
2646
+ }
2647
+ let bestAmountIn = upperBound;
2648
+ let bestSim = upperSim;
2649
+ // Check if the refined estimate is tighter
2650
+ if (refined < upperBound) {
2651
+ const refinedSim = await this.simulateSwap({
2652
+ poolId,
2653
+ assetInAddress,
2654
+ assetOutAddress,
2655
+ amountIn: refined.toString(),
2656
+ integratorBps,
2657
+ });
2658
+ if (FlashnetClient.safeBigInt(refinedSim.amountOut) >= desiredBtcOut) {
2659
+ bestAmountIn = refined;
2660
+ bestSim = refinedSim;
2661
+ }
2662
+ else {
2663
+ // Refined estimate was slightly too low. Keep upperBound as best,
2664
+ // and let binary search narrow between refined (too low) and upperBound (sufficient).
2665
+ bestAmountIn = upperBound;
2666
+ bestSim = upperSim;
2667
+ }
2668
+ }
2669
+ // Step 4: Binary search to converge on minimum amountIn
2670
+ // Use a tight range: the interpolation is close, so search between 99.5% and 100% of best
2671
+ let lo = bestAmountIn === upperBound
2672
+ ? refined < upperBound
2673
+ ? refined
2674
+ : (bestAmountIn * 99n) / 100n
2675
+ : (bestAmountIn * 999n) / 1000n;
2676
+ if (lo <= 0n) {
2677
+ lo = 1n;
2678
+ }
2679
+ let hi = bestAmountIn;
2680
+ for (let i = 0; i < 6; i++) {
2681
+ if (hi - lo <= 1n) {
2682
+ break;
2683
+ }
2684
+ const mid = (lo + hi) / 2n;
2685
+ const midSim = await this.simulateSwap({
2686
+ poolId,
2687
+ assetInAddress,
2688
+ assetOutAddress,
2689
+ amountIn: mid.toString(),
2690
+ integratorBps,
2691
+ });
2692
+ if (FlashnetClient.safeBigInt(midSim.amountOut) >= desiredBtcOut) {
2693
+ hi = mid;
2694
+ bestAmountIn = mid;
2695
+ bestSim = midSim;
2696
+ }
2697
+ else {
2698
+ lo = mid;
2699
+ }
2700
+ }
2701
+ // Compute fee from the best simulation
2702
+ const totalFee = bestSim.feePaidAssetIn || "0";
2703
+ return {
2704
+ amountIn: bestAmountIn.toString(),
2705
+ totalFee,
2706
+ simulation: bestSim,
2707
+ };
2708
+ }
2348
2709
  /**
2349
2710
  * Calculate minimum amount out with slippage protection
2350
2711
  * @private
2351
2712
  */
2352
2713
  calculateMinAmountOut(expectedAmount, slippageBps) {
2353
- const amount = BigInt(expectedAmount);
2714
+ const amount = FlashnetClient.safeBigInt(expectedAmount);
2354
2715
  const slippageFactor = BigInt(10000 - slippageBps);
2355
2716
  const minAmount = (amount * slippageFactor) / 10000n;
2356
2717
  return minAmount.toString();
@@ -2569,8 +2930,11 @@ class FlashnetClient {
2569
2930
  const map = new Map();
2570
2931
  for (const item of config) {
2571
2932
  if (item.enabled) {
2933
+ if (item.min_amount == null) {
2934
+ continue;
2935
+ }
2572
2936
  const key = item.asset_identifier.toLowerCase();
2573
- const value = BigInt(String(item.min_amount));
2937
+ const value = FlashnetClient.safeBigInt(item.min_amount);
2574
2938
  map.set(key, value);
2575
2939
  }
2576
2940
  }
@@ -2592,8 +2956,8 @@ class FlashnetClient {
2592
2956
  const outHex = this.getHexAddress(params.assetOutAddress);
2593
2957
  const minIn = minMap.get(inHex);
2594
2958
  const minOut = minMap.get(outHex);
2595
- const amountIn = BigInt(String(params.amountIn));
2596
- const minAmountOut = BigInt(String(params.minAmountOut));
2959
+ const amountIn = FlashnetClient.safeBigInt(params.amountIn);
2960
+ const minAmountOut = FlashnetClient.safeBigInt(params.minAmountOut);
2597
2961
  if (minIn && minOut) {
2598
2962
  if (amountIn < minIn) {
2599
2963
  throw new Error(`Minimum amount not met for input asset. Required \
@@ -2627,13 +2991,13 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2627
2991
  const aMin = minMap.get(aHex);
2628
2992
  const bMin = minMap.get(bHex);
2629
2993
  if (aMin) {
2630
- const aAmt = BigInt(String(params.assetAAmount));
2994
+ const aAmt = FlashnetClient.safeBigInt(params.assetAAmount);
2631
2995
  if (aAmt < aMin) {
2632
2996
  throw new Error(`Minimum amount not met for Asset A. Required ${aMin.toString()}, provided ${aAmt.toString()}`);
2633
2997
  }
2634
2998
  }
2635
2999
  if (bMin) {
2636
- const bAmt = BigInt(String(params.assetBAmount));
3000
+ const bAmt = FlashnetClient.safeBigInt(params.assetBAmount);
2637
3001
  if (bAmt < bMin) {
2638
3002
  throw new Error(`Minimum amount not met for Asset B. Required ${bMin.toString()}, provided ${bAmt.toString()}`);
2639
3003
  }
@@ -2655,14 +3019,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2655
3019
  const aMin = minMap.get(aHex);
2656
3020
  const bMin = minMap.get(bHex);
2657
3021
  if (aMin) {
2658
- const predictedAOut = BigInt(String(simulation.assetAAmount));
3022
+ const predictedAOut = FlashnetClient.safeBigInt(simulation.assetAAmount);
2659
3023
  const relaxedA = aMin / 2n; // apply 50% relaxation for outputs
2660
3024
  if (predictedAOut < relaxedA) {
2661
3025
  throw new Error(`Minimum amount not met for Asset A on withdrawal. Required at least ${relaxedA.toString()} (50% relaxed), predicted ${predictedAOut.toString()}`);
2662
3026
  }
2663
3027
  }
2664
3028
  if (bMin) {
2665
- const predictedBOut = BigInt(String(simulation.assetBAmount));
3029
+ const predictedBOut = FlashnetClient.safeBigInt(simulation.assetBAmount);
2666
3030
  const relaxedB = bMin / 2n;
2667
3031
  if (predictedBOut < relaxedB) {
2668
3032
  throw new Error(`Minimum amount not met for Asset B on withdrawal. Required at least ${relaxedB.toString()} (50% relaxed), predicted ${predictedBOut.toString()}`);
@@ -2775,20 +3139,20 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2775
3139
  const transferIds = [];
2776
3140
  // Transfer assets if not using free balance
2777
3141
  if (!params.useFreeBalance) {
2778
- if (BigInt(params.amountADesired) > 0n) {
3142
+ if (FlashnetClient.safeBigInt(params.amountADesired) > 0n) {
2779
3143
  assetATransferId = await this.transferAsset({
2780
3144
  receiverSparkAddress: lpSparkAddress,
2781
3145
  assetAddress: pool.assetAAddress,
2782
3146
  amount: params.amountADesired,
2783
- }, "Insufficient balance for adding V3 liquidity (Asset A): ");
3147
+ }, "Insufficient balance for adding V3 liquidity (Asset A): ", params.useAvailableBalance);
2784
3148
  transferIds.push(assetATransferId);
2785
3149
  }
2786
- if (BigInt(params.amountBDesired) > 0n) {
3150
+ if (FlashnetClient.safeBigInt(params.amountBDesired) > 0n) {
2787
3151
  assetBTransferId = await this.transferAsset({
2788
3152
  receiverSparkAddress: lpSparkAddress,
2789
3153
  assetAddress: pool.assetBAddress,
2790
3154
  amount: params.amountBDesired,
2791
- }, "Insufficient balance for adding V3 liquidity (Asset B): ");
3155
+ }, "Insufficient balance for adding V3 liquidity (Asset B): ", params.useAvailableBalance);
2792
3156
  transferIds.push(assetBTransferId);
2793
3157
  }
2794
3158
  }
@@ -2981,14 +3345,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2981
3345
  receiverSparkAddress: lpSparkAddress,
2982
3346
  assetAddress: pool.assetAAddress,
2983
3347
  amount: params.additionalAmountA,
2984
- }, "Insufficient balance for rebalance (Asset A): ");
3348
+ }, "Insufficient balance for rebalance (Asset A): ", params.useAvailableBalance);
2985
3349
  }
2986
3350
  if (params.additionalAmountB && BigInt(params.additionalAmountB) > 0n) {
2987
3351
  assetBTransferId = await this.transferAsset({
2988
3352
  receiverSparkAddress: lpSparkAddress,
2989
3353
  assetAddress: pool.assetBAddress,
2990
3354
  amount: params.additionalAmountB,
2991
- }, "Insufficient balance for rebalance (Asset B): ");
3355
+ }, "Insufficient balance for rebalance (Asset B): ", params.useAvailableBalance);
2992
3356
  }
2993
3357
  // Collect transfer IDs for potential clawback
2994
3358
  const transferIds = [];
@@ -3180,20 +3544,20 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3180
3544
  let assetBTransferId = "";
3181
3545
  const transferIds = [];
3182
3546
  // Transfer assets to pool
3183
- if (BigInt(params.amountA) > 0n) {
3547
+ if (FlashnetClient.safeBigInt(params.amountA) > 0n) {
3184
3548
  assetATransferId = await this.transferAsset({
3185
3549
  receiverSparkAddress: lpSparkAddress,
3186
3550
  assetAddress: pool.assetAAddress,
3187
3551
  amount: params.amountA,
3188
- }, "Insufficient balance for depositing to V3 pool (Asset A): ");
3552
+ }, "Insufficient balance for depositing to V3 pool (Asset A): ", params.useAvailableBalance);
3189
3553
  transferIds.push(assetATransferId);
3190
3554
  }
3191
- if (BigInt(params.amountB) > 0n) {
3555
+ if (FlashnetClient.safeBigInt(params.amountB) > 0n) {
3192
3556
  assetBTransferId = await this.transferAsset({
3193
3557
  receiverSparkAddress: lpSparkAddress,
3194
3558
  assetAddress: pool.assetBAddress,
3195
3559
  amount: params.amountB,
3196
- }, "Insufficient balance for depositing to V3 pool (Asset B): ");
3560
+ }, "Insufficient balance for depositing to V3 pool (Asset B): ", params.useAvailableBalance);
3197
3561
  transferIds.push(assetBTransferId);
3198
3562
  }
3199
3563
  const executeDeposit = async () => {