@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,4 +1,5 @@
1
1
  import sha256 from 'fast-sha256';
2
+ import { decode } from 'light-bolt11-decoder';
2
3
  import { ApiClient } from '../api/client.js';
3
4
  import { TypedAmmApi } from '../api/typed-endpoints.js';
4
5
  import { getClientEnvironmentName, resolveClientNetworkConfig, getClientNetworkConfig, BTC_ASSET_PUBKEY } from '../config/index.js';
@@ -240,8 +241,10 @@ class FlashnetClient {
240
241
  const tokenIdentifierHex = getHexFromUint8Array(info.rawTokenIdentifier);
241
242
  const tokenAddress = encodeSparkHumanReadableTokenIdentifier(info.rawTokenIdentifier, this.sparkNetwork);
242
243
  tokenBalances.set(tokenPubkey, {
243
- balance: FlashnetClient.safeBigInt(tokenData.ownedBalance),
244
- availableToSendBalance: FlashnetClient.safeBigInt(tokenData.availableToSendBalance),
244
+ balance: safeBigInt(tokenData.ownedBalance ?? tokenData.balance),
245
+ availableToSendBalance: safeBigInt(tokenData.availableToSendBalance ??
246
+ tokenData.ownedBalance ??
247
+ tokenData.balance),
245
248
  tokenInfo: {
246
249
  tokenIdentifier: tokenIdentifierHex,
247
250
  tokenAddress,
@@ -254,7 +257,7 @@ class FlashnetClient {
254
257
  }
255
258
  }
256
259
  return {
257
- balance: FlashnetClient.safeBigInt(balance.balance),
260
+ balance: safeBigInt(balance.balance),
258
261
  tokenBalances,
259
262
  };
260
263
  }
@@ -269,10 +272,10 @@ class FlashnetClient {
269
272
  };
270
273
  for (const balance of params.balancesToCheck) {
271
274
  if (balance.assetAddress === BTC_ASSET_PUBKEY) {
272
- requirements.btc = FlashnetClient.safeBigInt(balance.amount);
275
+ requirements.btc = BigInt(balance.amount);
273
276
  }
274
277
  else {
275
- requirements.tokens?.set(balance.assetAddress, FlashnetClient.safeBigInt(balance.amount));
278
+ requirements.tokens?.set(balance.assetAddress, BigInt(balance.amount));
276
279
  }
277
280
  }
278
281
  // Check BTC balance
@@ -286,10 +289,12 @@ class FlashnetClient {
286
289
  // Check token balances
287
290
  if (requirements.tokens) {
288
291
  for (const [tokenPubkey, requiredAmount,] of requirements.tokens.entries()) {
289
- // If direct lookup fails (possible representation mismatch), try the human-readable form
292
+ // Support both hex and Bech32m token identifiers by trying all representations
290
293
  const hrKey = this.toHumanReadableTokenIdentifier(tokenPubkey);
294
+ const hexKey = this.toHexTokenIdentifier(tokenPubkey);
291
295
  const effectiveTokenBalance = balance.tokenBalances.get(tokenPubkey) ??
292
- balance.tokenBalances.get(hrKey);
296
+ balance.tokenBalances.get(hrKey) ??
297
+ balance.tokenBalances.get(hexKey);
293
298
  const available = params.useAvailableBalance
294
299
  ? (effectiveTokenBalance?.availableToSendBalance ?? 0n)
295
300
  : (effectiveTokenBalance?.balance ?? 0n);
@@ -414,12 +419,6 @@ class FlashnetClient {
414
419
  throw new Error(`${name} must be positive integer`);
415
420
  }
416
421
  }
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
- }
423
422
  /**
424
423
  * Calculates virtual reserves for a bonding curve AMM.
425
424
  *
@@ -440,7 +439,7 @@ class FlashnetClient {
440
439
  }
441
440
  const supply = FlashnetClient.parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
442
441
  const targetB = FlashnetClient.parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
443
- const graduationThresholdPct = FlashnetClient.safeBigInt(params.graduationThresholdPct);
442
+ const graduationThresholdPct = BigInt(params.graduationThresholdPct);
444
443
  // Align bounds with Rust AMM (20%..95%), then check feasibility for g=1 (requires >50%).
445
444
  const MIN_PCT = 20n;
446
445
  const MAX_PCT = 95n;
@@ -1755,7 +1754,7 @@ class FlashnetClient {
1755
1754
  else {
1756
1755
  const transferId = await this._wallet.transferTokens({
1757
1756
  tokenIdentifier: this.toHumanReadableTokenIdentifier(recipient.assetAddress),
1758
- tokenAmount: FlashnetClient.safeBigInt(recipient.amount),
1757
+ tokenAmount: BigInt(recipient.amount),
1759
1758
  receiverSparkAddress: recipient.receiverSparkAddress,
1760
1759
  });
1761
1760
  transferIds.push(transferId);
@@ -1862,8 +1861,7 @@ class FlashnetClient {
1862
1861
  const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
1863
1862
  // Total BTC needed = invoice amount + lightning fee (unmasked).
1864
1863
  // Bitmasking for V2 pools is handled inside findBestPoolForTokenToBtc.
1865
- const baseBtcNeeded = FlashnetClient.safeBigInt(invoiceAmountSats) +
1866
- FlashnetClient.safeBigInt(lightningFeeEstimate);
1864
+ const baseBtcNeeded = BigInt(invoiceAmountSats) + BigInt(lightningFeeEstimate);
1867
1865
  // Check Flashnet minimum amounts early to provide clear error messages
1868
1866
  const minAmounts = await this.getEnabledMinAmountsMap();
1869
1867
  // Check BTC minimum (output from swap)
@@ -1890,7 +1888,7 @@ class FlashnetClient {
1890
1888
  const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
1891
1889
  const tokenMinAmount = minAmounts.get(tokenHex);
1892
1890
  if (tokenMinAmount &&
1893
- FlashnetClient.safeBigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1891
+ safeBigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1894
1892
  const msg = `Token amount too small. Minimum input is ${tokenMinAmount} units, but calculated amount is only ${poolQuote.tokenAmountRequired} units.`;
1895
1893
  throw new FlashnetError(msg, {
1896
1894
  response: {
@@ -1907,7 +1905,7 @@ class FlashnetClient {
1907
1905
  }
1908
1906
  // BTC variable fee adjustment: difference between what the pool targets and unmasked base.
1909
1907
  // For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
1910
- const btcVariableFeeAdjustment = Number(FlashnetClient.safeBigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
1908
+ const btcVariableFeeAdjustment = Number(safeBigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
1911
1909
  return {
1912
1910
  poolId: poolQuote.poolId,
1913
1911
  tokenAddress: this.toHexTokenIdentifier(tokenAddress),
@@ -1979,7 +1977,7 @@ class FlashnetClient {
1979
1977
  amountIn: tokenAmount,
1980
1978
  integratorBps: options?.integratorFeeRateBps,
1981
1979
  });
1982
- const btcOut = FlashnetClient.safeBigInt(simulation.amountOut);
1980
+ const btcOut = safeBigInt(simulation.amountOut);
1983
1981
  if (btcOut > bestBtcOut) {
1984
1982
  bestBtcOut = btcOut;
1985
1983
  bestResult = {
@@ -2095,8 +2093,16 @@ class FlashnetClient {
2095
2093
  const assetOutAddress = quote.tokenIsAssetA
2096
2094
  ? pool.assetBAddress
2097
2095
  : pool.assetAAddress;
2098
- // Calculate min amount out with slippage protection
2099
- const minBtcOut = this.calculateMinAmountOut(quote.btcAmountRequired, maxSlippageBps);
2096
+ const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2097
+ // Floor minAmountOut at invoiceAmount + fee so the swap never returns
2098
+ // less BTC than the lightning payment requires.
2099
+ const slippageMin = this.calculateMinAmountOut(quote.btcAmountRequired, maxSlippageBps);
2100
+ const baseBtcNeeded = !quote.isZeroAmountInvoice
2101
+ ? BigInt(quote.invoiceAmountSats) + BigInt(effectiveMaxLightningFee)
2102
+ : 0n;
2103
+ const minBtcOut = BigInt(slippageMin) >= baseBtcNeeded
2104
+ ? slippageMin
2105
+ : baseBtcNeeded.toString();
2100
2106
  // Execute the swap
2101
2107
  const swapResponse = await this.executeSwap({
2102
2108
  poolId: quote.poolId,
@@ -2121,139 +2127,158 @@ class FlashnetClient {
2121
2127
  error: swapResponse.error || "Swap was not accepted",
2122
2128
  };
2123
2129
  }
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
- }
2134
- if (!canPayImmediately) {
2135
- const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
2136
- if (!transferComplete) {
2137
- return {
2138
- success: false,
2139
- poolId: quote.poolId,
2140
- tokenAmountSpent: quote.tokenAmountRequired,
2141
- btcAmountReceived: swapResponse.amountOut || "0",
2142
- swapTransferId: swapResponse.outboundTransferId,
2143
- ammFeePaid: quote.estimatedAmmFee,
2144
- sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2145
- error: "Transfer did not complete within timeout",
2146
- };
2147
- }
2148
- }
2149
- // Step 6: Calculate Lightning fee and payment amount
2150
- const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2151
- const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
2152
- // Step 7: Pay the Lightning invoice
2130
+ // Step 5: Claim the swap output and refresh wallet state.
2131
+ // Suppress leaf optimization for the entire claim-to-pay window so
2132
+ // the SSP cannot swap away the leaves we need for lightning payment.
2133
+ const restoreOptimization = this.suppressOptimization();
2153
2134
  try {
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) {
2135
+ let canPayImmediately = false;
2136
+ if (!quote.isZeroAmountInvoice && useExistingBtcBalance) {
2137
+ const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
2138
+ const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
2139
+ const balance = await this.getBalance();
2140
+ canPayImmediately =
2141
+ balance.balance >= safeBigInt(btcNeededForPayment);
2142
+ }
2143
+ if (!canPayImmediately) {
2144
+ const claimed = await this.instaClaimTransfer(swapResponse.outboundTransferId, transferTimeoutMs);
2145
+ if (!claimed) {
2162
2146
  return {
2163
2147
  success: false,
2164
2148
  poolId: quote.poolId,
2165
2149
  tokenAmountSpent: quote.tokenAmountRequired,
2166
- btcAmountReceived: btcReceived,
2150
+ btcAmountReceived: swapResponse.amountOut || "0",
2167
2151
  swapTransferId: swapResponse.outboundTransferId,
2168
2152
  ammFeePaid: quote.estimatedAmmFee,
2169
2153
  sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2170
- error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
2154
+ error: "Transfer did not complete within timeout",
2171
2155
  };
2172
2156
  }
2173
- invoiceAmountPaid = Number(amountToPay);
2174
- lightningPayment = await this._wallet.payLightningInvoice({
2175
- invoice,
2176
- amountSats: invoiceAmountPaid,
2177
- maxFeeSats: effectiveMaxLightningFee,
2178
- preferSpark,
2179
- });
2180
2157
  }
2181
- else {
2182
- // Standard invoice: pay the specified amount
2183
- lightningPayment = await this._wallet.payLightningInvoice({
2184
- invoice,
2185
- maxFeeSats: effectiveMaxLightningFee,
2186
- preferSpark,
2187
- });
2158
+ // Step 6: Calculate payment amount
2159
+ const requestedMaxLightningFee = effectiveMaxLightningFee;
2160
+ const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
2161
+ // Cap the lightning fee budget to what the wallet can actually cover.
2162
+ // The swap output may be slightly less than quoted due to rounding or
2163
+ // price movement between quote and execution. The Spark SDK requires
2164
+ // invoiceAmount + maxFeeSats <= balance, so we adjust maxFeeSats down
2165
+ // when the actual BTC received is less than expected.
2166
+ let cappedMaxLightningFee = requestedMaxLightningFee;
2167
+ if (!quote.isZeroAmountInvoice) {
2168
+ const actualBtc = safeBigInt(btcReceived);
2169
+ const invoiceAmount = safeBigInt(quote.invoiceAmountSats);
2170
+ const available = actualBtc - invoiceAmount;
2171
+ if (available > 0n && available < safeBigInt(cappedMaxLightningFee)) {
2172
+ cappedMaxLightningFee = Number(available);
2173
+ }
2188
2174
  }
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;
2195
- return {
2196
- success: true,
2197
- poolId: quote.poolId,
2198
- tokenAmountSpent: quote.tokenAmountRequired,
2199
- btcAmountReceived: btcReceived,
2200
- swapTransferId: swapResponse.outboundTransferId,
2201
- lightningPaymentId: lightningPayment.id,
2202
- ammFeePaid: quote.estimatedAmmFee,
2203
- lightningFeePaid: effectiveMaxLightningFee,
2204
- invoiceAmountPaid,
2205
- sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2206
- sparkLightningTransferId,
2207
- };
2208
- }
2209
- catch (lightningError) {
2210
- // Lightning payment failed after swap succeeded
2211
- const lightningErrorMessage = lightningError instanceof Error
2212
- ? lightningError.message
2213
- : String(lightningError);
2214
- // Attempt rollback if requested
2215
- if (rollbackOnFailure) {
2216
- try {
2217
- const rollbackResult = await this.rollbackSwap(quote.poolId, btcReceived, tokenAddress, maxSlippageBps);
2218
- if (rollbackResult.success) {
2175
+ // Step 7: Pay the Lightning invoice
2176
+ try {
2177
+ let lightningPayment;
2178
+ let invoiceAmountPaid;
2179
+ if (quote.isZeroAmountInvoice) {
2180
+ const actualBtc = safeBigInt(btcReceived);
2181
+ const lnFee = safeBigInt(cappedMaxLightningFee);
2182
+ const amountToPay = actualBtc - lnFee;
2183
+ if (amountToPay <= 0n) {
2219
2184
  return {
2220
2185
  success: false,
2221
2186
  poolId: quote.poolId,
2222
- tokenAmountSpent: "0", // Rolled back
2223
- btcAmountReceived: "0",
2187
+ tokenAmountSpent: quote.tokenAmountRequired,
2188
+ btcAmountReceived: btcReceived,
2224
2189
  swapTransferId: swapResponse.outboundTransferId,
2225
2190
  ammFeePaid: quote.estimatedAmmFee,
2226
2191
  sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2227
- error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
2192
+ error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${cappedMaxLightningFee} sats).`,
2228
2193
  };
2229
2194
  }
2195
+ invoiceAmountPaid = Number(amountToPay);
2196
+ lightningPayment = await this._wallet.payLightningInvoice({
2197
+ invoice,
2198
+ amountSats: invoiceAmountPaid,
2199
+ maxFeeSats: cappedMaxLightningFee,
2200
+ preferSpark,
2201
+ });
2230
2202
  }
2231
- catch (rollbackError) {
2232
- const rollbackErrorMessage = rollbackError instanceof Error
2233
- ? rollbackError.message
2234
- : String(rollbackError);
2235
- return {
2236
- success: false,
2237
- poolId: quote.poolId,
2238
- tokenAmountSpent: quote.tokenAmountRequired,
2239
- btcAmountReceived: btcReceived,
2240
- swapTransferId: swapResponse.outboundTransferId,
2241
- ammFeePaid: quote.estimatedAmmFee,
2242
- sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2243
- error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
2244
- };
2203
+ else {
2204
+ lightningPayment = await this._wallet.payLightningInvoice({
2205
+ invoice,
2206
+ maxFeeSats: cappedMaxLightningFee,
2207
+ preferSpark,
2208
+ });
2245
2209
  }
2210
+ // Extract the Spark transfer ID from the lightning payment result.
2211
+ // payLightningInvoice returns LightningSendRequest | WalletTransfer:
2212
+ // - LightningSendRequest has .transfer?.sparkId (the Sparkscan-visible transfer ID)
2213
+ // - WalletTransfer (Spark-to-Spark) has .id directly as the transfer ID
2214
+ // Note: lightningPayment.id (the SSP request ID) is already returned as lightningPaymentId
2215
+ const sparkLightningTransferId = lightningPayment.transfer?.sparkId;
2216
+ return {
2217
+ success: true,
2218
+ poolId: quote.poolId,
2219
+ tokenAmountSpent: quote.tokenAmountRequired,
2220
+ btcAmountReceived: btcReceived,
2221
+ swapTransferId: swapResponse.outboundTransferId,
2222
+ lightningPaymentId: lightningPayment.id,
2223
+ ammFeePaid: quote.estimatedAmmFee,
2224
+ lightningFeePaid: cappedMaxLightningFee,
2225
+ invoiceAmountPaid,
2226
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2227
+ sparkLightningTransferId,
2228
+ };
2246
2229
  }
2247
- return {
2248
- success: false,
2249
- poolId: quote.poolId,
2250
- tokenAmountSpent: quote.tokenAmountRequired,
2251
- btcAmountReceived: btcReceived,
2252
- swapTransferId: swapResponse.outboundTransferId,
2253
- ammFeePaid: quote.estimatedAmmFee,
2254
- sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2255
- error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
2256
- };
2230
+ catch (lightningError) {
2231
+ // Lightning payment failed after swap succeeded
2232
+ const lightningErrorMessage = lightningError instanceof Error
2233
+ ? lightningError.message
2234
+ : String(lightningError);
2235
+ // Attempt rollback if requested
2236
+ if (rollbackOnFailure) {
2237
+ try {
2238
+ const rollbackResult = await this.rollbackSwap(quote.poolId, btcReceived, tokenAddress, maxSlippageBps);
2239
+ if (rollbackResult.success) {
2240
+ return {
2241
+ success: false,
2242
+ poolId: quote.poolId,
2243
+ tokenAmountSpent: "0", // Rolled back
2244
+ btcAmountReceived: "0",
2245
+ swapTransferId: swapResponse.outboundTransferId,
2246
+ ammFeePaid: quote.estimatedAmmFee,
2247
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2248
+ error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
2249
+ };
2250
+ }
2251
+ }
2252
+ catch (rollbackError) {
2253
+ const rollbackErrorMessage = rollbackError instanceof Error
2254
+ ? rollbackError.message
2255
+ : String(rollbackError);
2256
+ return {
2257
+ success: false,
2258
+ poolId: quote.poolId,
2259
+ tokenAmountSpent: quote.tokenAmountRequired,
2260
+ btcAmountReceived: btcReceived,
2261
+ swapTransferId: swapResponse.outboundTransferId,
2262
+ ammFeePaid: quote.estimatedAmmFee,
2263
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2264
+ error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
2265
+ };
2266
+ }
2267
+ }
2268
+ return {
2269
+ success: false,
2270
+ poolId: quote.poolId,
2271
+ tokenAmountSpent: quote.tokenAmountRequired,
2272
+ btcAmountReceived: btcReceived,
2273
+ swapTransferId: swapResponse.outboundTransferId,
2274
+ ammFeePaid: quote.estimatedAmmFee,
2275
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2276
+ error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
2277
+ };
2278
+ }
2279
+ }
2280
+ finally {
2281
+ restoreOptimization();
2257
2282
  }
2258
2283
  }
2259
2284
  catch (error) {
@@ -2414,7 +2439,7 @@ class FlashnetClient {
2414
2439
  tokenIsAssetA: pool.tokenIsAssetA,
2415
2440
  integratorBps: integratorFeeRateBps,
2416
2441
  });
2417
- tokenAmount = FlashnetClient.safeBigInt(v3Result.amountIn);
2442
+ tokenAmount = safeBigInt(v3Result.amountIn);
2418
2443
  fee = v3Result.totalFee;
2419
2444
  executionPrice = v3Result.simulation.executionPrice || "0";
2420
2445
  priceImpactPct = v3Result.simulation.priceImpactPct || "0";
@@ -2423,7 +2448,7 @@ class FlashnetClient {
2423
2448
  else {
2424
2449
  // V2: constant product math + simulation verification
2425
2450
  const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
2426
- tokenAmount = FlashnetClient.safeBigInt(calculation.amountIn);
2451
+ tokenAmount = safeBigInt(calculation.amountIn);
2427
2452
  // Verify with simulation
2428
2453
  const simulation = await this.simulateSwap({
2429
2454
  poolId: pool.lpPublicKey,
@@ -2432,7 +2457,7 @@ class FlashnetClient {
2432
2457
  amountIn: calculation.amountIn,
2433
2458
  integratorBps: integratorFeeRateBps,
2434
2459
  });
2435
- if (FlashnetClient.safeBigInt(simulation.amountOut) < btcTarget) {
2460
+ if (safeBigInt(simulation.amountOut) < btcTarget) {
2436
2461
  const btcReserve = pool.tokenIsAssetA
2437
2462
  ? poolDetails.assetBReserve
2438
2463
  : poolDetails.assetAReserve;
@@ -2520,9 +2545,9 @@ class FlashnetClient {
2520
2545
  * @private
2521
2546
  */
2522
2547
  calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
2523
- const amountOut = FlashnetClient.safeBigInt(btcAmountOut);
2524
- const resA = FlashnetClient.safeBigInt(reserveA);
2525
- const resB = FlashnetClient.safeBigInt(reserveB);
2548
+ const amountOut = safeBigInt(btcAmountOut);
2549
+ const resA = safeBigInt(reserveA);
2550
+ const resB = safeBigInt(reserveB);
2526
2551
  const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
2527
2552
  const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
2528
2553
  // Token is the input asset
@@ -2620,7 +2645,7 @@ class FlashnetClient {
2620
2645
  amountIn: upperBound.toString(),
2621
2646
  integratorBps,
2622
2647
  });
2623
- if (FlashnetClient.safeBigInt(sim.amountOut) >= desiredBtcOut) {
2648
+ if (safeBigInt(sim.amountOut) >= desiredBtcOut) {
2624
2649
  upperSim = sim;
2625
2650
  break;
2626
2651
  }
@@ -2631,7 +2656,7 @@ class FlashnetClient {
2631
2656
  throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
2632
2657
  }
2633
2658
  // Step 3: Refine estimate via linear interpolation
2634
- const upperOut = FlashnetClient.safeBigInt(upperSim.amountOut);
2659
+ const upperOut = safeBigInt(upperSim.amountOut);
2635
2660
  // Scale proportionally: if upperBound produced upperOut, we need roughly
2636
2661
  // (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
2637
2662
  let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
@@ -2649,7 +2674,7 @@ class FlashnetClient {
2649
2674
  amountIn: refined.toString(),
2650
2675
  integratorBps,
2651
2676
  });
2652
- if (FlashnetClient.safeBigInt(refinedSim.amountOut) >= desiredBtcOut) {
2677
+ if (safeBigInt(refinedSim.amountOut) >= desiredBtcOut) {
2653
2678
  bestAmountIn = refined;
2654
2679
  bestSim = refinedSim;
2655
2680
  }
@@ -2683,7 +2708,7 @@ class FlashnetClient {
2683
2708
  amountIn: mid.toString(),
2684
2709
  integratorBps,
2685
2710
  });
2686
- if (FlashnetClient.safeBigInt(midSim.amountOut) >= desiredBtcOut) {
2711
+ if (safeBigInt(midSim.amountOut) >= desiredBtcOut) {
2687
2712
  hi = mid;
2688
2713
  bestAmountIn = mid;
2689
2714
  bestSim = midSim;
@@ -2705,7 +2730,7 @@ class FlashnetClient {
2705
2730
  * @private
2706
2731
  */
2707
2732
  calculateMinAmountOut(expectedAmount, slippageBps) {
2708
- const amount = FlashnetClient.safeBigInt(expectedAmount);
2733
+ const amount = BigInt(expectedAmount);
2709
2734
  const slippageFactor = BigInt(10000 - slippageBps);
2710
2735
  const minAmount = (amount * slippageFactor) / 10000n;
2711
2736
  return minAmount.toString();
@@ -2774,6 +2799,73 @@ class FlashnetClient {
2774
2799
  }
2775
2800
  return false;
2776
2801
  }
2802
+ /**
2803
+ * Suppress leaf optimization on the wallet. Sets the internal
2804
+ * optimizationInProgress flag so optimizeLeaves() returns immediately.
2805
+ * Returns a restore function that clears the flag.
2806
+ * @private
2807
+ */
2808
+ suppressOptimization() {
2809
+ const w = this._wallet;
2810
+ const was = w.optimizationInProgress;
2811
+ w.optimizationInProgress = true;
2812
+ return () => {
2813
+ w.optimizationInProgress = was;
2814
+ };
2815
+ }
2816
+ /**
2817
+ * Insta-claim: listen for the wallet's stream event that fires when
2818
+ * the coordinator broadcasts the transfer. The stream auto-claims
2819
+ * incoming transfers, so no polling is needed.
2820
+ *
2821
+ * After claim, refreshes the leaf cache from the coordinator to
2822
+ * ensure the balance is current.
2823
+ *
2824
+ * Caller is responsible for suppressing optimization around this call
2825
+ * if the claimed leaves must not be swapped before spending.
2826
+ * @private
2827
+ */
2828
+ async instaClaimTransfer(transferId, timeoutMs) {
2829
+ const w = this._wallet;
2830
+ const claimed = await new Promise((resolve) => {
2831
+ let done = false;
2832
+ const finish = (value) => {
2833
+ if (done) {
2834
+ return;
2835
+ }
2836
+ done = true;
2837
+ clearTimeout(timer);
2838
+ try {
2839
+ w.removeListener?.("transfer:claimed", handler);
2840
+ }
2841
+ catch {
2842
+ // Ignore
2843
+ }
2844
+ resolve(value);
2845
+ };
2846
+ const timer = setTimeout(() => finish(false), timeoutMs);
2847
+ const handler = (claimedId) => {
2848
+ if (claimedId === transferId) {
2849
+ finish(true);
2850
+ }
2851
+ };
2852
+ // The wallet's background gRPC stream auto-claims transfers.
2853
+ // We just listen for the event.
2854
+ if (typeof w.on === "function") {
2855
+ w.on("transfer:claimed", handler);
2856
+ }
2857
+ else {
2858
+ // No event support, fall back to passive polling
2859
+ clearTimeout(timer);
2860
+ this.pollForTransferCompletion(transferId, timeoutMs).then(resolve);
2861
+ }
2862
+ });
2863
+ if (claimed) {
2864
+ const leaves = await this._wallet.getLeaves(true);
2865
+ w.leaves = leaves;
2866
+ }
2867
+ return claimed;
2868
+ }
2777
2869
  /**
2778
2870
  * Get Lightning fee estimate for an invoice
2779
2871
  * @private
@@ -2802,64 +2894,24 @@ class FlashnetClient {
2802
2894
  }
2803
2895
  /**
2804
2896
  * Decode the amount from a Lightning invoice (in sats)
2897
+ * Uses light-bolt11-decoder (same library as Spark SDK) for reliable parsing.
2805
2898
  * @private
2806
2899
  */
2807
2900
  async decodeInvoiceAmount(invoice) {
2808
- // Extract amount from BOLT11 invoice
2809
- // Format: ln[network][amount][multiplier]...
2810
- // Amount multipliers: m = milli (0.001), u = micro (0.000001), n = nano, p = pico
2811
- const lowerInvoice = invoice.toLowerCase();
2812
- // Find where the amount starts (after network prefix)
2813
- let amountStart = 0;
2814
- if (lowerInvoice.startsWith("lnbc")) {
2815
- amountStart = 4;
2816
- }
2817
- else if (lowerInvoice.startsWith("lntb")) {
2818
- amountStart = 4;
2819
- }
2820
- else if (lowerInvoice.startsWith("lnbcrt")) {
2821
- amountStart = 6;
2822
- }
2823
- else if (lowerInvoice.startsWith("lntbs")) {
2824
- amountStart = 5;
2901
+ try {
2902
+ const decoded = decode(invoice);
2903
+ const amountSection = decoded.sections.find((s) => s.name === "amount");
2904
+ if (!amountSection?.value) {
2905
+ return 0; // Zero-amount invoice
2906
+ }
2907
+ // The library returns amount in millisatoshis as a string
2908
+ const amountMSats = BigInt(amountSection.value);
2909
+ return Number(amountMSats / 1000n);
2825
2910
  }
2826
- else {
2827
- // Unknown format, try to find amount
2828
- const match = lowerInvoice.match(/^ln[a-z]+/);
2829
- if (match) {
2830
- amountStart = match[0].length;
2831
- }
2832
- }
2833
- // Extract amount and multiplier
2834
- const afterPrefix = lowerInvoice.substring(amountStart);
2835
- const amountMatch = afterPrefix.match(/^(\d+)([munp]?)/);
2836
- if (!amountMatch || !amountMatch[1]) {
2837
- return 0; // Zero-amount invoice
2838
- }
2839
- const amount = parseInt(amountMatch[1], 10);
2840
- const multiplier = amountMatch[2] ?? "";
2841
- // Convert to satoshis (1 BTC = 100,000,000 sats)
2842
- // Invoice amounts are in BTC by default
2843
- let btcAmount;
2844
- switch (multiplier) {
2845
- case "m": // milli-BTC (0.001 BTC)
2846
- btcAmount = amount * 0.001;
2847
- break;
2848
- case "u": // micro-BTC (0.000001 BTC)
2849
- btcAmount = amount * 0.000001;
2850
- break;
2851
- case "n": // nano-BTC (0.000000001 BTC)
2852
- btcAmount = amount * 0.000000001;
2853
- break;
2854
- case "p": // pico-BTC (0.000000000001 BTC)
2855
- btcAmount = amount * 0.000000000001;
2856
- break;
2857
- default: // BTC
2858
- btcAmount = amount;
2859
- break;
2911
+ catch {
2912
+ // Fallback: if library fails, return 0 (treated as zero-amount invoice)
2913
+ return 0;
2860
2914
  }
2861
- // Convert BTC to sats
2862
- return Math.round(btcAmount * 100000000);
2863
2915
  }
2864
2916
  /**
2865
2917
  * Clean up wallet connections
@@ -2928,7 +2980,7 @@ class FlashnetClient {
2928
2980
  continue;
2929
2981
  }
2930
2982
  const key = item.asset_identifier.toLowerCase();
2931
- const value = FlashnetClient.safeBigInt(item.min_amount);
2983
+ const value = safeBigInt(item.min_amount);
2932
2984
  map.set(key, value);
2933
2985
  }
2934
2986
  }
@@ -2950,8 +3002,8 @@ class FlashnetClient {
2950
3002
  const outHex = this.getHexAddress(params.assetOutAddress);
2951
3003
  const minIn = minMap.get(inHex);
2952
3004
  const minOut = minMap.get(outHex);
2953
- const amountIn = FlashnetClient.safeBigInt(params.amountIn);
2954
- const minAmountOut = FlashnetClient.safeBigInt(params.minAmountOut);
3005
+ const amountIn = BigInt(params.amountIn);
3006
+ const minAmountOut = BigInt(params.minAmountOut);
2955
3007
  if (minIn && minOut) {
2956
3008
  if (amountIn < minIn) {
2957
3009
  throw new Error(`Minimum amount not met for input asset. Required \
@@ -2985,13 +3037,13 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2985
3037
  const aMin = minMap.get(aHex);
2986
3038
  const bMin = minMap.get(bHex);
2987
3039
  if (aMin) {
2988
- const aAmt = FlashnetClient.safeBigInt(params.assetAAmount);
3040
+ const aAmt = BigInt(params.assetAAmount);
2989
3041
  if (aAmt < aMin) {
2990
3042
  throw new Error(`Minimum amount not met for Asset A. Required ${aMin.toString()}, provided ${aAmt.toString()}`);
2991
3043
  }
2992
3044
  }
2993
3045
  if (bMin) {
2994
- const bAmt = FlashnetClient.safeBigInt(params.assetBAmount);
3046
+ const bAmt = BigInt(params.assetBAmount);
2995
3047
  if (bAmt < bMin) {
2996
3048
  throw new Error(`Minimum amount not met for Asset B. Required ${bMin.toString()}, provided ${bAmt.toString()}`);
2997
3049
  }
@@ -3013,14 +3065,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3013
3065
  const aMin = minMap.get(aHex);
3014
3066
  const bMin = minMap.get(bHex);
3015
3067
  if (aMin) {
3016
- const predictedAOut = FlashnetClient.safeBigInt(simulation.assetAAmount);
3068
+ const predictedAOut = safeBigInt(simulation.assetAAmount);
3017
3069
  const relaxedA = aMin / 2n; // apply 50% relaxation for outputs
3018
3070
  if (predictedAOut < relaxedA) {
3019
3071
  throw new Error(`Minimum amount not met for Asset A on withdrawal. Required at least ${relaxedA.toString()} (50% relaxed), predicted ${predictedAOut.toString()}`);
3020
3072
  }
3021
3073
  }
3022
3074
  if (bMin) {
3023
- const predictedBOut = FlashnetClient.safeBigInt(simulation.assetBAmount);
3075
+ const predictedBOut = safeBigInt(simulation.assetBAmount);
3024
3076
  const relaxedB = bMin / 2n;
3025
3077
  if (predictedBOut < relaxedB) {
3026
3078
  throw new Error(`Minimum amount not met for Asset B on withdrawal. Required at least ${relaxedB.toString()} (50% relaxed), predicted ${predictedBOut.toString()}`);
@@ -3133,7 +3185,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3133
3185
  const transferIds = [];
3134
3186
  // Transfer assets if not using free balance
3135
3187
  if (!params.useFreeBalance) {
3136
- if (FlashnetClient.safeBigInt(params.amountADesired) > 0n) {
3188
+ if (BigInt(params.amountADesired) > 0n) {
3137
3189
  assetATransferId = await this.transferAsset({
3138
3190
  receiverSparkAddress: lpSparkAddress,
3139
3191
  assetAddress: pool.assetAAddress,
@@ -3141,7 +3193,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3141
3193
  }, "Insufficient balance for adding V3 liquidity (Asset A): ", params.useAvailableBalance);
3142
3194
  transferIds.push(assetATransferId);
3143
3195
  }
3144
- if (FlashnetClient.safeBigInt(params.amountBDesired) > 0n) {
3196
+ if (BigInt(params.amountBDesired) > 0n) {
3145
3197
  assetBTransferId = await this.transferAsset({
3146
3198
  receiverSparkAddress: lpSparkAddress,
3147
3199
  assetAddress: pool.assetBAddress,
@@ -3538,7 +3590,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3538
3590
  let assetBTransferId = "";
3539
3591
  const transferIds = [];
3540
3592
  // Transfer assets to pool
3541
- if (FlashnetClient.safeBigInt(params.amountA) > 0n) {
3593
+ if (BigInt(params.amountA) > 0n) {
3542
3594
  assetATransferId = await this.transferAsset({
3543
3595
  receiverSparkAddress: lpSparkAddress,
3544
3596
  assetAddress: pool.assetAAddress,
@@ -3546,7 +3598,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3546
3598
  }, "Insufficient balance for depositing to V3 pool (Asset A): ", params.useAvailableBalance);
3547
3599
  transferIds.push(assetATransferId);
3548
3600
  }
3549
- if (FlashnetClient.safeBigInt(params.amountB) > 0n) {
3601
+ if (BigInt(params.amountB) > 0n) {
3550
3602
  assetBTransferId = await this.transferAsset({
3551
3603
  receiverSparkAddress: lpSparkAddress,
3552
3604
  assetAddress: pool.assetBAddress,