@flashnet/sdk 0.5.5 → 0.5.7

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.
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var sha256 = require('fast-sha256');
4
+ var lightBolt11Decoder = require('light-bolt11-decoder');
4
5
  var client = require('../api/client.js');
5
6
  var typedEndpoints = require('../api/typed-endpoints.js');
6
7
  var index$1 = require('../config/index.js');
@@ -246,8 +247,10 @@ class FlashnetClient {
246
247
  const tokenIdentifierHex = hex.getHexFromUint8Array(info.rawTokenIdentifier);
247
248
  const tokenAddress$1 = tokenAddress.encodeSparkHumanReadableTokenIdentifier(info.rawTokenIdentifier, this.sparkNetwork);
248
249
  tokenBalances.set(tokenPubkey, {
249
- balance: FlashnetClient.safeBigInt(tokenData.ownedBalance),
250
- availableToSendBalance: FlashnetClient.safeBigInt(tokenData.availableToSendBalance),
250
+ balance: bigint.safeBigInt(tokenData.ownedBalance ?? tokenData.balance),
251
+ availableToSendBalance: bigint.safeBigInt(tokenData.availableToSendBalance ??
252
+ tokenData.ownedBalance ??
253
+ tokenData.balance),
251
254
  tokenInfo: {
252
255
  tokenIdentifier: tokenIdentifierHex,
253
256
  tokenAddress: tokenAddress$1,
@@ -260,7 +263,7 @@ class FlashnetClient {
260
263
  }
261
264
  }
262
265
  return {
263
- balance: FlashnetClient.safeBigInt(balance.balance),
266
+ balance: bigint.safeBigInt(balance.balance),
264
267
  tokenBalances,
265
268
  };
266
269
  }
@@ -275,10 +278,10 @@ class FlashnetClient {
275
278
  };
276
279
  for (const balance of params.balancesToCheck) {
277
280
  if (balance.assetAddress === index$1.BTC_ASSET_PUBKEY) {
278
- requirements.btc = FlashnetClient.safeBigInt(balance.amount);
281
+ requirements.btc = BigInt(balance.amount);
279
282
  }
280
283
  else {
281
- requirements.tokens?.set(balance.assetAddress, FlashnetClient.safeBigInt(balance.amount));
284
+ requirements.tokens?.set(balance.assetAddress, BigInt(balance.amount));
282
285
  }
283
286
  }
284
287
  // Check BTC balance
@@ -292,10 +295,12 @@ class FlashnetClient {
292
295
  // Check token balances
293
296
  if (requirements.tokens) {
294
297
  for (const [tokenPubkey, requiredAmount,] of requirements.tokens.entries()) {
295
- // If direct lookup fails (possible representation mismatch), try the human-readable form
298
+ // Support both hex and Bech32m token identifiers by trying all representations
296
299
  const hrKey = this.toHumanReadableTokenIdentifier(tokenPubkey);
300
+ const hexKey = this.toHexTokenIdentifier(tokenPubkey);
297
301
  const effectiveTokenBalance = balance.tokenBalances.get(tokenPubkey) ??
298
- balance.tokenBalances.get(hrKey);
302
+ balance.tokenBalances.get(hrKey) ??
303
+ balance.tokenBalances.get(hexKey);
299
304
  const available = params.useAvailableBalance
300
305
  ? (effectiveTokenBalance?.availableToSendBalance ?? 0n)
301
306
  : (effectiveTokenBalance?.balance ?? 0n);
@@ -420,12 +425,6 @@ class FlashnetClient {
420
425
  throw new Error(`${name} must be positive integer`);
421
426
  }
422
427
  }
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
- }
429
428
  /**
430
429
  * Calculates virtual reserves for a bonding curve AMM.
431
430
  *
@@ -446,7 +445,7 @@ class FlashnetClient {
446
445
  }
447
446
  const supply = FlashnetClient.parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
448
447
  const targetB = FlashnetClient.parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
449
- const graduationThresholdPct = FlashnetClient.safeBigInt(params.graduationThresholdPct);
448
+ const graduationThresholdPct = BigInt(params.graduationThresholdPct);
450
449
  // Align bounds with Rust AMM (20%..95%), then check feasibility for g=1 (requires >50%).
451
450
  const MIN_PCT = 20n;
452
451
  const MAX_PCT = 95n;
@@ -1761,7 +1760,7 @@ class FlashnetClient {
1761
1760
  else {
1762
1761
  const transferId = await this._wallet.transferTokens({
1763
1762
  tokenIdentifier: this.toHumanReadableTokenIdentifier(recipient.assetAddress),
1764
- tokenAmount: FlashnetClient.safeBigInt(recipient.amount),
1763
+ tokenAmount: BigInt(recipient.amount),
1765
1764
  receiverSparkAddress: recipient.receiverSparkAddress,
1766
1765
  });
1767
1766
  transferIds.push(transferId);
@@ -1868,8 +1867,7 @@ class FlashnetClient {
1868
1867
  const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
1869
1868
  // Total BTC needed = invoice amount + lightning fee (unmasked).
1870
1869
  // Bitmasking for V2 pools is handled inside findBestPoolForTokenToBtc.
1871
- const baseBtcNeeded = FlashnetClient.safeBigInt(invoiceAmountSats) +
1872
- FlashnetClient.safeBigInt(lightningFeeEstimate);
1870
+ const baseBtcNeeded = BigInt(invoiceAmountSats) + BigInt(lightningFeeEstimate);
1873
1871
  // Check Flashnet minimum amounts early to provide clear error messages
1874
1872
  const minAmounts = await this.getEnabledMinAmountsMap();
1875
1873
  // Check BTC minimum (output from swap)
@@ -1896,7 +1894,7 @@ class FlashnetClient {
1896
1894
  const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
1897
1895
  const tokenMinAmount = minAmounts.get(tokenHex);
1898
1896
  if (tokenMinAmount &&
1899
- FlashnetClient.safeBigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1897
+ bigint.safeBigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1900
1898
  const msg = `Token amount too small. Minimum input is ${tokenMinAmount} units, but calculated amount is only ${poolQuote.tokenAmountRequired} units.`;
1901
1899
  throw new errors.FlashnetError(msg, {
1902
1900
  response: {
@@ -1913,7 +1911,7 @@ class FlashnetClient {
1913
1911
  }
1914
1912
  // BTC variable fee adjustment: difference between what the pool targets and unmasked base.
1915
1913
  // For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
1916
- const btcVariableFeeAdjustment = Number(FlashnetClient.safeBigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
1914
+ const btcVariableFeeAdjustment = Number(bigint.safeBigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
1917
1915
  return {
1918
1916
  poolId: poolQuote.poolId,
1919
1917
  tokenAddress: this.toHexTokenIdentifier(tokenAddress),
@@ -1985,7 +1983,7 @@ class FlashnetClient {
1985
1983
  amountIn: tokenAmount,
1986
1984
  integratorBps: options?.integratorFeeRateBps,
1987
1985
  });
1988
- const btcOut = FlashnetClient.safeBigInt(simulation.amountOut);
1986
+ const btcOut = bigint.safeBigInt(simulation.amountOut);
1989
1987
  if (btcOut > bestBtcOut) {
1990
1988
  bestBtcOut = btcOut;
1991
1989
  bestResult = {
@@ -2101,8 +2099,16 @@ class FlashnetClient {
2101
2099
  const assetOutAddress = quote.tokenIsAssetA
2102
2100
  ? pool.assetBAddress
2103
2101
  : pool.assetAAddress;
2104
- // Calculate min amount out with slippage protection
2105
- const minBtcOut = this.calculateMinAmountOut(quote.btcAmountRequired, maxSlippageBps);
2102
+ const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2103
+ // Floor minAmountOut at invoiceAmount + fee so the swap never returns
2104
+ // less BTC than the lightning payment requires.
2105
+ const slippageMin = this.calculateMinAmountOut(quote.btcAmountRequired, maxSlippageBps);
2106
+ const baseBtcNeeded = !quote.isZeroAmountInvoice
2107
+ ? BigInt(quote.invoiceAmountSats) + BigInt(effectiveMaxLightningFee)
2108
+ : 0n;
2109
+ const minBtcOut = BigInt(slippageMin) >= baseBtcNeeded
2110
+ ? slippageMin
2111
+ : baseBtcNeeded.toString();
2106
2112
  // Execute the swap
2107
2113
  const swapResponse = await this.executeSwap({
2108
2114
  poolId: quote.poolId,
@@ -2127,139 +2133,158 @@ class FlashnetClient {
2127
2133
  error: swapResponse.error || "Swap was not accepted",
2128
2134
  };
2129
2135
  }
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
- }
2140
- if (!canPayImmediately) {
2141
- const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
2142
- if (!transferComplete) {
2143
- return {
2144
- success: false,
2145
- poolId: quote.poolId,
2146
- tokenAmountSpent: quote.tokenAmountRequired,
2147
- btcAmountReceived: swapResponse.amountOut || "0",
2148
- swapTransferId: swapResponse.outboundTransferId,
2149
- ammFeePaid: quote.estimatedAmmFee,
2150
- sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2151
- error: "Transfer did not complete within timeout",
2152
- };
2153
- }
2154
- }
2155
- // Step 6: Calculate Lightning fee and payment amount
2156
- const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2157
- const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
2158
- // Step 7: Pay the Lightning invoice
2136
+ // Step 5: Claim the swap output and refresh wallet state.
2137
+ // Suppress leaf optimization for the entire claim-to-pay window so
2138
+ // the SSP cannot swap away the leaves we need for lightning payment.
2139
+ const restoreOptimization = this.suppressOptimization();
2159
2140
  try {
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) {
2141
+ let canPayImmediately = false;
2142
+ if (!quote.isZeroAmountInvoice && useExistingBtcBalance) {
2143
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
2144
+ const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
2145
+ const balance = await this.getBalance();
2146
+ canPayImmediately =
2147
+ balance.balance >= bigint.safeBigInt(btcNeededForPayment);
2148
+ }
2149
+ if (!canPayImmediately) {
2150
+ const claimed = await this.instaClaimTransfer(swapResponse.outboundTransferId, transferTimeoutMs);
2151
+ if (!claimed) {
2168
2152
  return {
2169
2153
  success: false,
2170
2154
  poolId: quote.poolId,
2171
2155
  tokenAmountSpent: quote.tokenAmountRequired,
2172
- btcAmountReceived: btcReceived,
2156
+ btcAmountReceived: swapResponse.amountOut || "0",
2173
2157
  swapTransferId: swapResponse.outboundTransferId,
2174
2158
  ammFeePaid: quote.estimatedAmmFee,
2175
2159
  sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2176
- error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
2160
+ error: "Transfer did not complete within timeout",
2177
2161
  };
2178
2162
  }
2179
- invoiceAmountPaid = Number(amountToPay);
2180
- lightningPayment = await this._wallet.payLightningInvoice({
2181
- invoice,
2182
- amountSats: invoiceAmountPaid,
2183
- maxFeeSats: effectiveMaxLightningFee,
2184
- preferSpark,
2185
- });
2186
2163
  }
2187
- else {
2188
- // Standard invoice: pay the specified amount
2189
- lightningPayment = await this._wallet.payLightningInvoice({
2190
- invoice,
2191
- maxFeeSats: effectiveMaxLightningFee,
2192
- preferSpark,
2193
- });
2164
+ // Step 6: Calculate payment amount
2165
+ const requestedMaxLightningFee = effectiveMaxLightningFee;
2166
+ const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
2167
+ // Cap the lightning fee budget to what the wallet can actually cover.
2168
+ // The swap output may be slightly less than quoted due to rounding or
2169
+ // price movement between quote and execution. The Spark SDK requires
2170
+ // invoiceAmount + maxFeeSats <= balance, so we adjust maxFeeSats down
2171
+ // when the actual BTC received is less than expected.
2172
+ let cappedMaxLightningFee = requestedMaxLightningFee;
2173
+ if (!quote.isZeroAmountInvoice) {
2174
+ const actualBtc = bigint.safeBigInt(btcReceived);
2175
+ const invoiceAmount = bigint.safeBigInt(quote.invoiceAmountSats);
2176
+ const available = actualBtc - invoiceAmount;
2177
+ if (available > 0n && available < bigint.safeBigInt(cappedMaxLightningFee)) {
2178
+ cappedMaxLightningFee = Number(available);
2179
+ }
2194
2180
  }
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;
2201
- return {
2202
- success: true,
2203
- poolId: quote.poolId,
2204
- tokenAmountSpent: quote.tokenAmountRequired,
2205
- btcAmountReceived: btcReceived,
2206
- swapTransferId: swapResponse.outboundTransferId,
2207
- lightningPaymentId: lightningPayment.id,
2208
- ammFeePaid: quote.estimatedAmmFee,
2209
- lightningFeePaid: effectiveMaxLightningFee,
2210
- invoiceAmountPaid,
2211
- sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2212
- sparkLightningTransferId,
2213
- };
2214
- }
2215
- catch (lightningError) {
2216
- // Lightning payment failed after swap succeeded
2217
- const lightningErrorMessage = lightningError instanceof Error
2218
- ? lightningError.message
2219
- : String(lightningError);
2220
- // Attempt rollback if requested
2221
- if (rollbackOnFailure) {
2222
- try {
2223
- const rollbackResult = await this.rollbackSwap(quote.poolId, btcReceived, tokenAddress, maxSlippageBps);
2224
- if (rollbackResult.success) {
2181
+ // Step 7: Pay the Lightning invoice
2182
+ try {
2183
+ let lightningPayment;
2184
+ let invoiceAmountPaid;
2185
+ if (quote.isZeroAmountInvoice) {
2186
+ const actualBtc = bigint.safeBigInt(btcReceived);
2187
+ const lnFee = bigint.safeBigInt(cappedMaxLightningFee);
2188
+ const amountToPay = actualBtc - lnFee;
2189
+ if (amountToPay <= 0n) {
2225
2190
  return {
2226
2191
  success: false,
2227
2192
  poolId: quote.poolId,
2228
- tokenAmountSpent: "0", // Rolled back
2229
- btcAmountReceived: "0",
2193
+ tokenAmountSpent: quote.tokenAmountRequired,
2194
+ btcAmountReceived: btcReceived,
2230
2195
  swapTransferId: swapResponse.outboundTransferId,
2231
2196
  ammFeePaid: quote.estimatedAmmFee,
2232
2197
  sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2233
- error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
2198
+ error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${cappedMaxLightningFee} sats).`,
2234
2199
  };
2235
2200
  }
2201
+ invoiceAmountPaid = Number(amountToPay);
2202
+ lightningPayment = await this._wallet.payLightningInvoice({
2203
+ invoice,
2204
+ amountSats: invoiceAmountPaid,
2205
+ maxFeeSats: cappedMaxLightningFee,
2206
+ preferSpark,
2207
+ });
2236
2208
  }
2237
- catch (rollbackError) {
2238
- const rollbackErrorMessage = rollbackError instanceof Error
2239
- ? rollbackError.message
2240
- : String(rollbackError);
2241
- return {
2242
- success: false,
2243
- poolId: quote.poolId,
2244
- tokenAmountSpent: quote.tokenAmountRequired,
2245
- btcAmountReceived: btcReceived,
2246
- swapTransferId: swapResponse.outboundTransferId,
2247
- ammFeePaid: quote.estimatedAmmFee,
2248
- sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2249
- error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
2250
- };
2209
+ else {
2210
+ lightningPayment = await this._wallet.payLightningInvoice({
2211
+ invoice,
2212
+ maxFeeSats: cappedMaxLightningFee,
2213
+ preferSpark,
2214
+ });
2251
2215
  }
2216
+ // Extract the Spark transfer ID from the lightning payment result.
2217
+ // payLightningInvoice returns LightningSendRequest | WalletTransfer:
2218
+ // - LightningSendRequest has .transfer?.sparkId (the Sparkscan-visible transfer ID)
2219
+ // - WalletTransfer (Spark-to-Spark) has .id directly as the transfer ID
2220
+ // Note: lightningPayment.id (the SSP request ID) is already returned as lightningPaymentId
2221
+ const sparkLightningTransferId = lightningPayment.transfer?.sparkId;
2222
+ return {
2223
+ success: true,
2224
+ poolId: quote.poolId,
2225
+ tokenAmountSpent: quote.tokenAmountRequired,
2226
+ btcAmountReceived: btcReceived,
2227
+ swapTransferId: swapResponse.outboundTransferId,
2228
+ lightningPaymentId: lightningPayment.id,
2229
+ ammFeePaid: quote.estimatedAmmFee,
2230
+ lightningFeePaid: cappedMaxLightningFee,
2231
+ invoiceAmountPaid,
2232
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2233
+ sparkLightningTransferId,
2234
+ };
2252
2235
  }
2253
- return {
2254
- success: false,
2255
- poolId: quote.poolId,
2256
- tokenAmountSpent: quote.tokenAmountRequired,
2257
- btcAmountReceived: btcReceived,
2258
- swapTransferId: swapResponse.outboundTransferId,
2259
- ammFeePaid: quote.estimatedAmmFee,
2260
- sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2261
- error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
2262
- };
2236
+ catch (lightningError) {
2237
+ // Lightning payment failed after swap succeeded
2238
+ const lightningErrorMessage = lightningError instanceof Error
2239
+ ? lightningError.message
2240
+ : String(lightningError);
2241
+ // Attempt rollback if requested
2242
+ if (rollbackOnFailure) {
2243
+ try {
2244
+ const rollbackResult = await this.rollbackSwap(quote.poolId, btcReceived, tokenAddress, maxSlippageBps);
2245
+ if (rollbackResult.success) {
2246
+ return {
2247
+ success: false,
2248
+ poolId: quote.poolId,
2249
+ tokenAmountSpent: "0", // Rolled back
2250
+ btcAmountReceived: "0",
2251
+ swapTransferId: swapResponse.outboundTransferId,
2252
+ ammFeePaid: quote.estimatedAmmFee,
2253
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2254
+ error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
2255
+ };
2256
+ }
2257
+ }
2258
+ catch (rollbackError) {
2259
+ const rollbackErrorMessage = rollbackError instanceof Error
2260
+ ? rollbackError.message
2261
+ : String(rollbackError);
2262
+ return {
2263
+ success: false,
2264
+ poolId: quote.poolId,
2265
+ tokenAmountSpent: quote.tokenAmountRequired,
2266
+ btcAmountReceived: btcReceived,
2267
+ swapTransferId: swapResponse.outboundTransferId,
2268
+ ammFeePaid: quote.estimatedAmmFee,
2269
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2270
+ error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
2271
+ };
2272
+ }
2273
+ }
2274
+ return {
2275
+ success: false,
2276
+ poolId: quote.poolId,
2277
+ tokenAmountSpent: quote.tokenAmountRequired,
2278
+ btcAmountReceived: btcReceived,
2279
+ swapTransferId: swapResponse.outboundTransferId,
2280
+ ammFeePaid: quote.estimatedAmmFee,
2281
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2282
+ error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
2283
+ };
2284
+ }
2285
+ }
2286
+ finally {
2287
+ restoreOptimization();
2263
2288
  }
2264
2289
  }
2265
2290
  catch (error) {
@@ -2420,7 +2445,7 @@ class FlashnetClient {
2420
2445
  tokenIsAssetA: pool.tokenIsAssetA,
2421
2446
  integratorBps: integratorFeeRateBps,
2422
2447
  });
2423
- tokenAmount = FlashnetClient.safeBigInt(v3Result.amountIn);
2448
+ tokenAmount = bigint.safeBigInt(v3Result.amountIn);
2424
2449
  fee = v3Result.totalFee;
2425
2450
  executionPrice = v3Result.simulation.executionPrice || "0";
2426
2451
  priceImpactPct = v3Result.simulation.priceImpactPct || "0";
@@ -2429,7 +2454,7 @@ class FlashnetClient {
2429
2454
  else {
2430
2455
  // V2: constant product math + simulation verification
2431
2456
  const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
2432
- tokenAmount = FlashnetClient.safeBigInt(calculation.amountIn);
2457
+ tokenAmount = bigint.safeBigInt(calculation.amountIn);
2433
2458
  // Verify with simulation
2434
2459
  const simulation = await this.simulateSwap({
2435
2460
  poolId: pool.lpPublicKey,
@@ -2438,7 +2463,7 @@ class FlashnetClient {
2438
2463
  amountIn: calculation.amountIn,
2439
2464
  integratorBps: integratorFeeRateBps,
2440
2465
  });
2441
- if (FlashnetClient.safeBigInt(simulation.amountOut) < btcTarget) {
2466
+ if (bigint.safeBigInt(simulation.amountOut) < btcTarget) {
2442
2467
  const btcReserve = pool.tokenIsAssetA
2443
2468
  ? poolDetails.assetBReserve
2444
2469
  : poolDetails.assetAReserve;
@@ -2526,9 +2551,9 @@ class FlashnetClient {
2526
2551
  * @private
2527
2552
  */
2528
2553
  calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
2529
- const amountOut = FlashnetClient.safeBigInt(btcAmountOut);
2530
- const resA = FlashnetClient.safeBigInt(reserveA);
2531
- const resB = FlashnetClient.safeBigInt(reserveB);
2554
+ const amountOut = bigint.safeBigInt(btcAmountOut);
2555
+ const resA = bigint.safeBigInt(reserveA);
2556
+ const resB = bigint.safeBigInt(reserveB);
2532
2557
  const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
2533
2558
  const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
2534
2559
  // Token is the input asset
@@ -2626,7 +2651,7 @@ class FlashnetClient {
2626
2651
  amountIn: upperBound.toString(),
2627
2652
  integratorBps,
2628
2653
  });
2629
- if (FlashnetClient.safeBigInt(sim.amountOut) >= desiredBtcOut) {
2654
+ if (bigint.safeBigInt(sim.amountOut) >= desiredBtcOut) {
2630
2655
  upperSim = sim;
2631
2656
  break;
2632
2657
  }
@@ -2637,7 +2662,7 @@ class FlashnetClient {
2637
2662
  throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
2638
2663
  }
2639
2664
  // Step 3: Refine estimate via linear interpolation
2640
- const upperOut = FlashnetClient.safeBigInt(upperSim.amountOut);
2665
+ const upperOut = bigint.safeBigInt(upperSim.amountOut);
2641
2666
  // Scale proportionally: if upperBound produced upperOut, we need roughly
2642
2667
  // (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
2643
2668
  let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
@@ -2655,7 +2680,7 @@ class FlashnetClient {
2655
2680
  amountIn: refined.toString(),
2656
2681
  integratorBps,
2657
2682
  });
2658
- if (FlashnetClient.safeBigInt(refinedSim.amountOut) >= desiredBtcOut) {
2683
+ if (bigint.safeBigInt(refinedSim.amountOut) >= desiredBtcOut) {
2659
2684
  bestAmountIn = refined;
2660
2685
  bestSim = refinedSim;
2661
2686
  }
@@ -2689,7 +2714,7 @@ class FlashnetClient {
2689
2714
  amountIn: mid.toString(),
2690
2715
  integratorBps,
2691
2716
  });
2692
- if (FlashnetClient.safeBigInt(midSim.amountOut) >= desiredBtcOut) {
2717
+ if (bigint.safeBigInt(midSim.amountOut) >= desiredBtcOut) {
2693
2718
  hi = mid;
2694
2719
  bestAmountIn = mid;
2695
2720
  bestSim = midSim;
@@ -2711,7 +2736,7 @@ class FlashnetClient {
2711
2736
  * @private
2712
2737
  */
2713
2738
  calculateMinAmountOut(expectedAmount, slippageBps) {
2714
- const amount = FlashnetClient.safeBigInt(expectedAmount);
2739
+ const amount = BigInt(expectedAmount);
2715
2740
  const slippageFactor = BigInt(10000 - slippageBps);
2716
2741
  const minAmount = (amount * slippageFactor) / 10000n;
2717
2742
  return minAmount.toString();
@@ -2780,6 +2805,73 @@ class FlashnetClient {
2780
2805
  }
2781
2806
  return false;
2782
2807
  }
2808
+ /**
2809
+ * Suppress leaf optimization on the wallet. Sets the internal
2810
+ * optimizationInProgress flag so optimizeLeaves() returns immediately.
2811
+ * Returns a restore function that clears the flag.
2812
+ * @private
2813
+ */
2814
+ suppressOptimization() {
2815
+ const w = this._wallet;
2816
+ const was = w.optimizationInProgress;
2817
+ w.optimizationInProgress = true;
2818
+ return () => {
2819
+ w.optimizationInProgress = was;
2820
+ };
2821
+ }
2822
+ /**
2823
+ * Insta-claim: listen for the wallet's stream event that fires when
2824
+ * the coordinator broadcasts the transfer. The stream auto-claims
2825
+ * incoming transfers, so no polling is needed.
2826
+ *
2827
+ * After claim, refreshes the leaf cache from the coordinator to
2828
+ * ensure the balance is current.
2829
+ *
2830
+ * Caller is responsible for suppressing optimization around this call
2831
+ * if the claimed leaves must not be swapped before spending.
2832
+ * @private
2833
+ */
2834
+ async instaClaimTransfer(transferId, timeoutMs) {
2835
+ const w = this._wallet;
2836
+ const claimed = await new Promise((resolve) => {
2837
+ let done = false;
2838
+ const finish = (value) => {
2839
+ if (done) {
2840
+ return;
2841
+ }
2842
+ done = true;
2843
+ clearTimeout(timer);
2844
+ try {
2845
+ w.removeListener?.("transfer:claimed", handler);
2846
+ }
2847
+ catch {
2848
+ // Ignore
2849
+ }
2850
+ resolve(value);
2851
+ };
2852
+ const timer = setTimeout(() => finish(false), timeoutMs);
2853
+ const handler = (claimedId) => {
2854
+ if (claimedId === transferId) {
2855
+ finish(true);
2856
+ }
2857
+ };
2858
+ // The wallet's background gRPC stream auto-claims transfers.
2859
+ // We just listen for the event.
2860
+ if (typeof w.on === "function") {
2861
+ w.on("transfer:claimed", handler);
2862
+ }
2863
+ else {
2864
+ // No event support, fall back to passive polling
2865
+ clearTimeout(timer);
2866
+ this.pollForTransferCompletion(transferId, timeoutMs).then(resolve);
2867
+ }
2868
+ });
2869
+ if (claimed) {
2870
+ const leaves = await this._wallet.getLeaves(true);
2871
+ w.leaves = leaves;
2872
+ }
2873
+ return claimed;
2874
+ }
2783
2875
  /**
2784
2876
  * Get Lightning fee estimate for an invoice
2785
2877
  * @private
@@ -2808,64 +2900,24 @@ class FlashnetClient {
2808
2900
  }
2809
2901
  /**
2810
2902
  * Decode the amount from a Lightning invoice (in sats)
2903
+ * Uses light-bolt11-decoder (same library as Spark SDK) for reliable parsing.
2811
2904
  * @private
2812
2905
  */
2813
2906
  async decodeInvoiceAmount(invoice) {
2814
- // Extract amount from BOLT11 invoice
2815
- // Format: ln[network][amount][multiplier]...
2816
- // Amount multipliers: m = milli (0.001), u = micro (0.000001), n = nano, p = pico
2817
- const lowerInvoice = invoice.toLowerCase();
2818
- // Find where the amount starts (after network prefix)
2819
- let amountStart = 0;
2820
- if (lowerInvoice.startsWith("lnbc")) {
2821
- amountStart = 4;
2822
- }
2823
- else if (lowerInvoice.startsWith("lntb")) {
2824
- amountStart = 4;
2825
- }
2826
- else if (lowerInvoice.startsWith("lnbcrt")) {
2827
- amountStart = 6;
2828
- }
2829
- else if (lowerInvoice.startsWith("lntbs")) {
2830
- amountStart = 5;
2907
+ try {
2908
+ const decoded = lightBolt11Decoder.decode(invoice);
2909
+ const amountSection = decoded.sections.find((s) => s.name === "amount");
2910
+ if (!amountSection?.value) {
2911
+ return 0; // Zero-amount invoice
2912
+ }
2913
+ // The library returns amount in millisatoshis as a string
2914
+ const amountMSats = BigInt(amountSection.value);
2915
+ return Number(amountMSats / 1000n);
2831
2916
  }
2832
- else {
2833
- // Unknown format, try to find amount
2834
- const match = lowerInvoice.match(/^ln[a-z]+/);
2835
- if (match) {
2836
- amountStart = match[0].length;
2837
- }
2838
- }
2839
- // Extract amount and multiplier
2840
- const afterPrefix = lowerInvoice.substring(amountStart);
2841
- const amountMatch = afterPrefix.match(/^(\d+)([munp]?)/);
2842
- if (!amountMatch || !amountMatch[1]) {
2843
- return 0; // Zero-amount invoice
2844
- }
2845
- const amount = parseInt(amountMatch[1], 10);
2846
- const multiplier = amountMatch[2] ?? "";
2847
- // Convert to satoshis (1 BTC = 100,000,000 sats)
2848
- // Invoice amounts are in BTC by default
2849
- let btcAmount;
2850
- switch (multiplier) {
2851
- case "m": // milli-BTC (0.001 BTC)
2852
- btcAmount = amount * 0.001;
2853
- break;
2854
- case "u": // micro-BTC (0.000001 BTC)
2855
- btcAmount = amount * 0.000001;
2856
- break;
2857
- case "n": // nano-BTC (0.000000001 BTC)
2858
- btcAmount = amount * 0.000000001;
2859
- break;
2860
- case "p": // pico-BTC (0.000000000001 BTC)
2861
- btcAmount = amount * 0.000000000001;
2862
- break;
2863
- default: // BTC
2864
- btcAmount = amount;
2865
- break;
2917
+ catch {
2918
+ // Fallback: if library fails, return 0 (treated as zero-amount invoice)
2919
+ return 0;
2866
2920
  }
2867
- // Convert BTC to sats
2868
- return Math.round(btcAmount * 100000000);
2869
2921
  }
2870
2922
  /**
2871
2923
  * Clean up wallet connections
@@ -2934,7 +2986,7 @@ class FlashnetClient {
2934
2986
  continue;
2935
2987
  }
2936
2988
  const key = item.asset_identifier.toLowerCase();
2937
- const value = FlashnetClient.safeBigInt(item.min_amount);
2989
+ const value = bigint.safeBigInt(item.min_amount);
2938
2990
  map.set(key, value);
2939
2991
  }
2940
2992
  }
@@ -2956,8 +3008,8 @@ class FlashnetClient {
2956
3008
  const outHex = this.getHexAddress(params.assetOutAddress);
2957
3009
  const minIn = minMap.get(inHex);
2958
3010
  const minOut = minMap.get(outHex);
2959
- const amountIn = FlashnetClient.safeBigInt(params.amountIn);
2960
- const minAmountOut = FlashnetClient.safeBigInt(params.minAmountOut);
3011
+ const amountIn = BigInt(params.amountIn);
3012
+ const minAmountOut = BigInt(params.minAmountOut);
2961
3013
  if (minIn && minOut) {
2962
3014
  if (amountIn < minIn) {
2963
3015
  throw new Error(`Minimum amount not met for input asset. Required \
@@ -2991,13 +3043,13 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2991
3043
  const aMin = minMap.get(aHex);
2992
3044
  const bMin = minMap.get(bHex);
2993
3045
  if (aMin) {
2994
- const aAmt = FlashnetClient.safeBigInt(params.assetAAmount);
3046
+ const aAmt = BigInt(params.assetAAmount);
2995
3047
  if (aAmt < aMin) {
2996
3048
  throw new Error(`Minimum amount not met for Asset A. Required ${aMin.toString()}, provided ${aAmt.toString()}`);
2997
3049
  }
2998
3050
  }
2999
3051
  if (bMin) {
3000
- const bAmt = FlashnetClient.safeBigInt(params.assetBAmount);
3052
+ const bAmt = BigInt(params.assetBAmount);
3001
3053
  if (bAmt < bMin) {
3002
3054
  throw new Error(`Minimum amount not met for Asset B. Required ${bMin.toString()}, provided ${bAmt.toString()}`);
3003
3055
  }
@@ -3019,14 +3071,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3019
3071
  const aMin = minMap.get(aHex);
3020
3072
  const bMin = minMap.get(bHex);
3021
3073
  if (aMin) {
3022
- const predictedAOut = FlashnetClient.safeBigInt(simulation.assetAAmount);
3074
+ const predictedAOut = bigint.safeBigInt(simulation.assetAAmount);
3023
3075
  const relaxedA = aMin / 2n; // apply 50% relaxation for outputs
3024
3076
  if (predictedAOut < relaxedA) {
3025
3077
  throw new Error(`Minimum amount not met for Asset A on withdrawal. Required at least ${relaxedA.toString()} (50% relaxed), predicted ${predictedAOut.toString()}`);
3026
3078
  }
3027
3079
  }
3028
3080
  if (bMin) {
3029
- const predictedBOut = FlashnetClient.safeBigInt(simulation.assetBAmount);
3081
+ const predictedBOut = bigint.safeBigInt(simulation.assetBAmount);
3030
3082
  const relaxedB = bMin / 2n;
3031
3083
  if (predictedBOut < relaxedB) {
3032
3084
  throw new Error(`Minimum amount not met for Asset B on withdrawal. Required at least ${relaxedB.toString()} (50% relaxed), predicted ${predictedBOut.toString()}`);
@@ -3139,7 +3191,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3139
3191
  const transferIds = [];
3140
3192
  // Transfer assets if not using free balance
3141
3193
  if (!params.useFreeBalance) {
3142
- if (FlashnetClient.safeBigInt(params.amountADesired) > 0n) {
3194
+ if (BigInt(params.amountADesired) > 0n) {
3143
3195
  assetATransferId = await this.transferAsset({
3144
3196
  receiverSparkAddress: lpSparkAddress,
3145
3197
  assetAddress: pool.assetAAddress,
@@ -3147,7 +3199,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3147
3199
  }, "Insufficient balance for adding V3 liquidity (Asset A): ", params.useAvailableBalance);
3148
3200
  transferIds.push(assetATransferId);
3149
3201
  }
3150
- if (FlashnetClient.safeBigInt(params.amountBDesired) > 0n) {
3202
+ if (BigInt(params.amountBDesired) > 0n) {
3151
3203
  assetBTransferId = await this.transferAsset({
3152
3204
  receiverSparkAddress: lpSparkAddress,
3153
3205
  assetAddress: pool.assetBAddress,
@@ -3544,7 +3596,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3544
3596
  let assetBTransferId = "";
3545
3597
  const transferIds = [];
3546
3598
  // Transfer assets to pool
3547
- if (FlashnetClient.safeBigInt(params.amountA) > 0n) {
3599
+ if (BigInt(params.amountA) > 0n) {
3548
3600
  assetATransferId = await this.transferAsset({
3549
3601
  receiverSparkAddress: lpSparkAddress,
3550
3602
  assetAddress: pool.assetAAddress,
@@ -3552,7 +3604,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3552
3604
  }, "Insufficient balance for depositing to V3 pool (Asset A): ", params.useAvailableBalance);
3553
3605
  transferIds.push(assetATransferId);
3554
3606
  }
3555
- if (FlashnetClient.safeBigInt(params.amountB) > 0n) {
3607
+ if (BigInt(params.amountB) > 0n) {
3556
3608
  assetBTransferId = await this.transferAsset({
3557
3609
  receiverSparkAddress: lpSparkAddress,
3558
3610
  assetAddress: pool.assetBAddress,