@flashnet/sdk 0.5.4 → 0.5.6

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';
@@ -9,6 +10,7 @@ import { getHexFromUint8Array } from '../utils/hex.js';
9
10
  import { generateConstantProductPoolInitializationIntentMessage, generatePoolInitializationIntentMessage, generatePoolConfirmInitialDepositIntentMessage, generatePoolSwapIntentMessage, generateRouteSwapIntentMessage, generateAddLiquidityIntentMessage, generateRemoveLiquidityIntentMessage, generateRegisterHostIntentMessage, generateWithdrawHostFeesIntentMessage, generateWithdrawIntegratorFeesIntentMessage, generateCreateEscrowIntentMessage, generateFundEscrowIntentMessage, generateClaimEscrowIntentMessage, generateClawbackIntentMessage, generateCreateConcentratedPoolIntentMessage, generateDecreaseLiquidityIntentMessage, generateCollectFeesIntentMessage, generateWithdrawBalanceIntentMessage, generateIncreaseLiquidityIntentMessage, generateRebalancePositionIntentMessage, generateDepositBalanceIntentMessage } from '../utils/intents.js';
10
11
  import { getSparkNetworkFromAddress, encodeSparkAddressNew } from '../utils/spark-address.js';
11
12
  import { encodeSparkHumanReadableTokenIdentifier, decodeSparkHumanReadableTokenIdentifier } from '../utils/tokenAddress.js';
13
+ import { safeBigInt } from '../utils/bigint.js';
12
14
  import { FlashnetError } from '../types/errors.js';
13
15
 
14
16
  /**
@@ -239,7 +241,10 @@ class FlashnetClient {
239
241
  const tokenIdentifierHex = getHexFromUint8Array(info.rawTokenIdentifier);
240
242
  const tokenAddress = encodeSparkHumanReadableTokenIdentifier(info.rawTokenIdentifier, this.sparkNetwork);
241
243
  tokenBalances.set(tokenPubkey, {
242
- balance: BigInt(tokenData.balance),
244
+ balance: safeBigInt(tokenData.ownedBalance ?? tokenData.balance),
245
+ availableToSendBalance: safeBigInt(tokenData.availableToSendBalance ??
246
+ tokenData.ownedBalance ??
247
+ tokenData.balance),
243
248
  tokenInfo: {
244
249
  tokenIdentifier: tokenIdentifierHex,
245
250
  tokenAddress,
@@ -252,7 +257,7 @@ class FlashnetClient {
252
257
  }
253
258
  }
254
259
  return {
255
- balance: BigInt(balance.balance),
260
+ balance: safeBigInt(balance.balance),
256
261
  tokenBalances,
257
262
  };
258
263
  }
@@ -260,7 +265,7 @@ class FlashnetClient {
260
265
  * Check if wallet has sufficient balance for an operation
261
266
  */
262
267
  async checkBalance(params) {
263
- const balance = await this.getBalance();
268
+ const balance = params.walletBalance ?? (await this.getBalance());
264
269
  // Check balance
265
270
  const requirements = {
266
271
  tokens: new Map(),
@@ -284,11 +289,15 @@ class FlashnetClient {
284
289
  // Check token balances
285
290
  if (requirements.tokens) {
286
291
  for (const [tokenPubkey, requiredAmount,] of requirements.tokens.entries()) {
287
- // If direct lookup fails (possible representation mismatch), try the human-readable form
292
+ // Support both hex and Bech32m token identifiers by trying all representations
288
293
  const hrKey = this.toHumanReadableTokenIdentifier(tokenPubkey);
294
+ const hexKey = this.toHexTokenIdentifier(tokenPubkey);
289
295
  const effectiveTokenBalance = balance.tokenBalances.get(tokenPubkey) ??
290
- balance.tokenBalances.get(hrKey);
291
- const available = effectiveTokenBalance?.balance ?? 0n;
296
+ balance.tokenBalances.get(hrKey) ??
297
+ balance.tokenBalances.get(hexKey);
298
+ const available = params.useAvailableBalance
299
+ ? (effectiveTokenBalance?.availableToSendBalance ?? 0n)
300
+ : (effectiveTokenBalance?.balance ?? 0n);
292
301
  if (available < requiredAmount) {
293
302
  throw new Error([
294
303
  params.errorPrefix ?? "",
@@ -350,6 +359,7 @@ class FlashnetClient {
350
359
  },
351
360
  ],
352
361
  errorPrefix: "Insufficient balance for initial liquidity: ",
362
+ useAvailableBalance: params.useAvailableBalance,
353
363
  });
354
364
  }
355
365
  const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
@@ -479,6 +489,7 @@ class FlashnetClient {
479
489
  },
480
490
  ],
481
491
  errorPrefix: "Insufficient balance for pool creation: ",
492
+ useAvailableBalance: params.useAvailableBalance,
482
493
  });
483
494
  const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
484
495
  // Generate intent
@@ -599,10 +610,14 @@ class FlashnetClient {
599
610
  });
600
611
  // If using free balance (V3 pools only), skip the Spark transfer
601
612
  if (params.useFreeBalance) {
602
- return this.executeSwapIntent({
613
+ const swapResponse = await this.executeSwapIntent({
603
614
  ...params,
604
615
  // No transferId - triggers free balance mode
605
616
  });
617
+ return {
618
+ ...swapResponse,
619
+ inboundSparkTransferId: swapResponse.requestId,
620
+ };
606
621
  }
607
622
  // Transfer assets to pool using new address encoding
608
623
  const lpSparkAddress = encodeSparkAddressNew({
@@ -613,12 +628,13 @@ class FlashnetClient {
613
628
  receiverSparkAddress: lpSparkAddress,
614
629
  assetAddress: params.assetInAddress,
615
630
  amount: params.amountIn,
616
- }, "Insufficient balance for swap: ");
631
+ }, "Insufficient balance for swap: ", params.useAvailableBalance);
617
632
  // Execute with auto-clawback on failure
618
- return this.executeWithAutoClawback(() => this.executeSwapIntent({
633
+ const swapResponse = await this.executeWithAutoClawback(() => this.executeSwapIntent({
619
634
  ...params,
620
635
  transferId,
621
636
  }), [transferId], params.poolId);
637
+ return { ...swapResponse, inboundSparkTransferId: transferId };
622
638
  }
623
639
  /**
624
640
  * Execute a swap with a pre-created transfer or using free balance.
@@ -748,7 +764,7 @@ class FlashnetClient {
748
764
  receiverSparkAddress: lpSparkAddress,
749
765
  assetAddress: params.initialAssetAddress,
750
766
  amount: params.inputAmount,
751
- }, "Insufficient balance for route swap: ");
767
+ }, "Insufficient balance for route swap: ", params.useAvailableBalance);
752
768
  // Execute with auto-clawback on failure
753
769
  return this.executeWithAutoClawback(async () => {
754
770
  // Prepare hops for validation
@@ -871,7 +887,7 @@ class FlashnetClient {
871
887
  assetAddress: pool.assetBAddress,
872
888
  amount: params.assetBAmount,
873
889
  },
874
- ], "Insufficient balance for adding liquidity: ");
890
+ ], "Insufficient balance for adding liquidity: ", params.useAvailableBalance);
875
891
  // Execute with auto-clawback on failure
876
892
  return this.executeWithAutoClawback(async () => {
877
893
  // Generate add liquidity intent
@@ -1177,6 +1193,7 @@ class FlashnetClient {
1177
1193
  depositAddress: createResponse.depositAddress,
1178
1194
  assetId: params.assetId,
1179
1195
  assetAmount: params.assetAmount,
1196
+ useAvailableBalance: params.useAvailableBalance,
1180
1197
  });
1181
1198
  }
1182
1199
  /**
@@ -1194,6 +1211,7 @@ class FlashnetClient {
1194
1211
  { assetAddress: params.assetId, amount: params.assetAmount },
1195
1212
  ],
1196
1213
  errorPrefix: "Insufficient balance to fund escrow: ",
1214
+ useAvailableBalance: params.useAvailableBalance,
1197
1215
  });
1198
1216
  // 2. Perform transfer
1199
1217
  const escrowSparkAddress = encodeSparkAddressNew({
@@ -1708,19 +1726,20 @@ class FlashnetClient {
1708
1726
  /**
1709
1727
  * Performs asset transfer using generalized asset address for both BTC and tokens.
1710
1728
  */
1711
- async transferAsset(recipient, checkBalanceErrorPrefix) {
1712
- const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix);
1729
+ async transferAsset(recipient, checkBalanceErrorPrefix, useAvailableBalance) {
1730
+ const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix, useAvailableBalance);
1713
1731
  return transferIds[0];
1714
1732
  }
1715
1733
  /**
1716
1734
  * Performs asset transfers using generalized asset addresses for both BTC and tokens.
1717
1735
  * Supports optional generic to hardcode recipients length so output list can be typed with same length.
1718
1736
  */
1719
- async transferAssets(recipients, checkBalanceErrorPrefix) {
1737
+ async transferAssets(recipients, checkBalanceErrorPrefix, useAvailableBalance) {
1720
1738
  if (checkBalanceErrorPrefix) {
1721
1739
  await this.checkBalance({
1722
1740
  balancesToCheck: recipients,
1723
1741
  errorPrefix: checkBalanceErrorPrefix,
1742
+ useAvailableBalance,
1724
1743
  });
1725
1744
  }
1726
1745
  const transferIds = [];
@@ -1869,7 +1888,7 @@ class FlashnetClient {
1869
1888
  const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
1870
1889
  const tokenMinAmount = minAmounts.get(tokenHex);
1871
1890
  if (tokenMinAmount &&
1872
- BigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1891
+ safeBigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
1873
1892
  const msg = `Token amount too small. Minimum input is ${tokenMinAmount} units, but calculated amount is only ${poolQuote.tokenAmountRequired} units.`;
1874
1893
  throw new FlashnetError(msg, {
1875
1894
  response: {
@@ -1886,7 +1905,7 @@ class FlashnetClient {
1886
1905
  }
1887
1906
  // BTC variable fee adjustment: difference between what the pool targets and unmasked base.
1888
1907
  // For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
1889
- const btcVariableFeeAdjustment = Number(BigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
1908
+ const btcVariableFeeAdjustment = Number(safeBigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
1890
1909
  return {
1891
1910
  poolId: poolQuote.poolId,
1892
1911
  tokenAddress: this.toHexTokenIdentifier(tokenAddress),
@@ -1958,7 +1977,7 @@ class FlashnetClient {
1958
1977
  amountIn: tokenAmount,
1959
1978
  integratorBps: options?.integratorFeeRateBps,
1960
1979
  });
1961
- const btcOut = BigInt(simulation.amountOut);
1980
+ const btcOut = safeBigInt(simulation.amountOut);
1962
1981
  if (btcOut > bestBtcOut) {
1963
1982
  bestBtcOut = btcOut;
1964
1983
  bestResult = {
@@ -2046,7 +2065,7 @@ class FlashnetClient {
2046
2065
  await this.ensureInitialized();
2047
2066
  const { invoice, tokenAddress, tokenAmount, maxSlippageBps = 500, // 5% default
2048
2067
  maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 30000, // 30s default
2049
- rollbackOnFailure = false, useExistingBtcBalance = false, } = options;
2068
+ rollbackOnFailure = false, useExistingBtcBalance = false, useAvailableBalance = false, } = options;
2050
2069
  try {
2051
2070
  // Step 1: Get a quote for the payment
2052
2071
  const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
@@ -2063,6 +2082,7 @@ class FlashnetClient {
2063
2082
  },
2064
2083
  ],
2065
2084
  errorPrefix: "Insufficient token balance for Lightning payment: ",
2085
+ useAvailableBalance,
2066
2086
  });
2067
2087
  // Step 3: Get pool details
2068
2088
  const pool = await this.getPool(quote.poolId);
@@ -2085,6 +2105,7 @@ class FlashnetClient {
2085
2105
  minAmountOut: minBtcOut,
2086
2106
  integratorFeeRateBps,
2087
2107
  integratorPublicKey,
2108
+ useAvailableBalance,
2088
2109
  });
2089
2110
  if (!swapResponse.accepted || !swapResponse.outboundTransferId) {
2090
2111
  return {
@@ -2094,6 +2115,7 @@ class FlashnetClient {
2094
2115
  btcAmountReceived: "0",
2095
2116
  swapTransferId: swapResponse.outboundTransferId || "",
2096
2117
  ammFeePaid: quote.estimatedAmmFee,
2118
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2097
2119
  error: swapResponse.error || "Swap was not accepted",
2098
2120
  };
2099
2121
  }
@@ -2104,7 +2126,7 @@ class FlashnetClient {
2104
2126
  const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
2105
2127
  const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
2106
2128
  const balance = await this.getBalance();
2107
- canPayImmediately = balance.balance >= BigInt(btcNeededForPayment);
2129
+ canPayImmediately = balance.balance >= safeBigInt(btcNeededForPayment);
2108
2130
  }
2109
2131
  if (!canPayImmediately) {
2110
2132
  const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
@@ -2116,6 +2138,7 @@ class FlashnetClient {
2116
2138
  btcAmountReceived: swapResponse.amountOut || "0",
2117
2139
  swapTransferId: swapResponse.outboundTransferId,
2118
2140
  ammFeePaid: quote.estimatedAmmFee,
2141
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2119
2142
  error: "Transfer did not complete within timeout",
2120
2143
  };
2121
2144
  }
@@ -2129,8 +2152,8 @@ class FlashnetClient {
2129
2152
  let invoiceAmountPaid;
2130
2153
  if (quote.isZeroAmountInvoice) {
2131
2154
  // Zero-amount invoice: pay whatever BTC we received minus lightning fee
2132
- const actualBtc = BigInt(btcReceived);
2133
- const lnFee = BigInt(effectiveMaxLightningFee);
2155
+ const actualBtc = safeBigInt(btcReceived);
2156
+ const lnFee = safeBigInt(effectiveMaxLightningFee);
2134
2157
  const amountToPay = actualBtc - lnFee;
2135
2158
  if (amountToPay <= 0n) {
2136
2159
  return {
@@ -2140,6 +2163,7 @@ class FlashnetClient {
2140
2163
  btcAmountReceived: btcReceived,
2141
2164
  swapTransferId: swapResponse.outboundTransferId,
2142
2165
  ammFeePaid: quote.estimatedAmmFee,
2166
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2143
2167
  error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
2144
2168
  };
2145
2169
  }
@@ -2159,6 +2183,12 @@ class FlashnetClient {
2159
2183
  preferSpark,
2160
2184
  });
2161
2185
  }
2186
+ // Extract the Spark transfer ID from the lightning payment result.
2187
+ // payLightningInvoice returns LightningSendRequest | WalletTransfer:
2188
+ // - LightningSendRequest has .transfer?.sparkId (the Sparkscan-visible transfer ID)
2189
+ // - WalletTransfer (Spark-to-Spark) has .id directly as the transfer ID
2190
+ // Note: lightningPayment.id (the SSP request ID) is already returned as lightningPaymentId
2191
+ const sparkLightningTransferId = lightningPayment.transfer?.sparkId;
2162
2192
  return {
2163
2193
  success: true,
2164
2194
  poolId: quote.poolId,
@@ -2169,6 +2199,8 @@ class FlashnetClient {
2169
2199
  ammFeePaid: quote.estimatedAmmFee,
2170
2200
  lightningFeePaid: effectiveMaxLightningFee,
2171
2201
  invoiceAmountPaid,
2202
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2203
+ sparkLightningTransferId,
2172
2204
  };
2173
2205
  }
2174
2206
  catch (lightningError) {
@@ -2188,6 +2220,7 @@ class FlashnetClient {
2188
2220
  btcAmountReceived: "0",
2189
2221
  swapTransferId: swapResponse.outboundTransferId,
2190
2222
  ammFeePaid: quote.estimatedAmmFee,
2223
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2191
2224
  error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
2192
2225
  };
2193
2226
  }
@@ -2203,6 +2236,7 @@ class FlashnetClient {
2203
2236
  btcAmountReceived: btcReceived,
2204
2237
  swapTransferId: swapResponse.outboundTransferId,
2205
2238
  ammFeePaid: quote.estimatedAmmFee,
2239
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2206
2240
  error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
2207
2241
  };
2208
2242
  }
@@ -2214,6 +2248,7 @@ class FlashnetClient {
2214
2248
  btcAmountReceived: btcReceived,
2215
2249
  swapTransferId: swapResponse.outboundTransferId,
2216
2250
  ammFeePaid: quote.estimatedAmmFee,
2251
+ sparkTokenTransferId: swapResponse.inboundSparkTransferId,
2217
2252
  error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
2218
2253
  };
2219
2254
  }
@@ -2376,7 +2411,7 @@ class FlashnetClient {
2376
2411
  tokenIsAssetA: pool.tokenIsAssetA,
2377
2412
  integratorBps: integratorFeeRateBps,
2378
2413
  });
2379
- tokenAmount = BigInt(v3Result.amountIn);
2414
+ tokenAmount = safeBigInt(v3Result.amountIn);
2380
2415
  fee = v3Result.totalFee;
2381
2416
  executionPrice = v3Result.simulation.executionPrice || "0";
2382
2417
  priceImpactPct = v3Result.simulation.priceImpactPct || "0";
@@ -2385,7 +2420,7 @@ class FlashnetClient {
2385
2420
  else {
2386
2421
  // V2: constant product math + simulation verification
2387
2422
  const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
2388
- tokenAmount = BigInt(calculation.amountIn);
2423
+ tokenAmount = safeBigInt(calculation.amountIn);
2389
2424
  // Verify with simulation
2390
2425
  const simulation = await this.simulateSwap({
2391
2426
  poolId: pool.lpPublicKey,
@@ -2394,7 +2429,7 @@ class FlashnetClient {
2394
2429
  amountIn: calculation.amountIn,
2395
2430
  integratorBps: integratorFeeRateBps,
2396
2431
  });
2397
- if (BigInt(simulation.amountOut) < btcTarget) {
2432
+ if (safeBigInt(simulation.amountOut) < btcTarget) {
2398
2433
  const btcReserve = pool.tokenIsAssetA
2399
2434
  ? poolDetails.assetBReserve
2400
2435
  : poolDetails.assetAReserve;
@@ -2482,9 +2517,9 @@ class FlashnetClient {
2482
2517
  * @private
2483
2518
  */
2484
2519
  calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
2485
- const amountOut = BigInt(btcAmountOut);
2486
- const resA = BigInt(reserveA);
2487
- const resB = BigInt(reserveB);
2520
+ const amountOut = safeBigInt(btcAmountOut);
2521
+ const resA = safeBigInt(reserveA);
2522
+ const resB = safeBigInt(reserveB);
2488
2523
  const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
2489
2524
  const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
2490
2525
  // Token is the input asset
@@ -2582,7 +2617,7 @@ class FlashnetClient {
2582
2617
  amountIn: upperBound.toString(),
2583
2618
  integratorBps,
2584
2619
  });
2585
- if (BigInt(sim.amountOut) >= desiredBtcOut) {
2620
+ if (safeBigInt(sim.amountOut) >= desiredBtcOut) {
2586
2621
  upperSim = sim;
2587
2622
  break;
2588
2623
  }
@@ -2593,7 +2628,7 @@ class FlashnetClient {
2593
2628
  throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
2594
2629
  }
2595
2630
  // Step 3: Refine estimate via linear interpolation
2596
- const upperOut = BigInt(upperSim.amountOut);
2631
+ const upperOut = safeBigInt(upperSim.amountOut);
2597
2632
  // Scale proportionally: if upperBound produced upperOut, we need roughly
2598
2633
  // (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
2599
2634
  let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
@@ -2611,7 +2646,7 @@ class FlashnetClient {
2611
2646
  amountIn: refined.toString(),
2612
2647
  integratorBps,
2613
2648
  });
2614
- if (BigInt(refinedSim.amountOut) >= desiredBtcOut) {
2649
+ if (safeBigInt(refinedSim.amountOut) >= desiredBtcOut) {
2615
2650
  bestAmountIn = refined;
2616
2651
  bestSim = refinedSim;
2617
2652
  }
@@ -2645,7 +2680,7 @@ class FlashnetClient {
2645
2680
  amountIn: mid.toString(),
2646
2681
  integratorBps,
2647
2682
  });
2648
- if (BigInt(midSim.amountOut) >= desiredBtcOut) {
2683
+ if (safeBigInt(midSim.amountOut) >= desiredBtcOut) {
2649
2684
  hi = mid;
2650
2685
  bestAmountIn = mid;
2651
2686
  bestSim = midSim;
@@ -2764,64 +2799,24 @@ class FlashnetClient {
2764
2799
  }
2765
2800
  /**
2766
2801
  * Decode the amount from a Lightning invoice (in sats)
2802
+ * Uses light-bolt11-decoder (same library as Spark SDK) for reliable parsing.
2767
2803
  * @private
2768
2804
  */
2769
2805
  async decodeInvoiceAmount(invoice) {
2770
- // Extract amount from BOLT11 invoice
2771
- // Format: ln[network][amount][multiplier]...
2772
- // Amount multipliers: m = milli (0.001), u = micro (0.000001), n = nano, p = pico
2773
- const lowerInvoice = invoice.toLowerCase();
2774
- // Find where the amount starts (after network prefix)
2775
- let amountStart = 0;
2776
- if (lowerInvoice.startsWith("lnbc")) {
2777
- amountStart = 4;
2778
- }
2779
- else if (lowerInvoice.startsWith("lntb")) {
2780
- amountStart = 4;
2781
- }
2782
- else if (lowerInvoice.startsWith("lnbcrt")) {
2783
- amountStart = 6;
2784
- }
2785
- else if (lowerInvoice.startsWith("lntbs")) {
2786
- amountStart = 5;
2806
+ try {
2807
+ const decoded = decode(invoice);
2808
+ const amountSection = decoded.sections.find((s) => s.name === "amount");
2809
+ if (!amountSection?.value) {
2810
+ return 0; // Zero-amount invoice
2811
+ }
2812
+ // The library returns amount in millisatoshis as a string
2813
+ const amountMSats = BigInt(amountSection.value);
2814
+ return Number(amountMSats / 1000n);
2787
2815
  }
2788
- else {
2789
- // Unknown format, try to find amount
2790
- const match = lowerInvoice.match(/^ln[a-z]+/);
2791
- if (match) {
2792
- amountStart = match[0].length;
2793
- }
2794
- }
2795
- // Extract amount and multiplier
2796
- const afterPrefix = lowerInvoice.substring(amountStart);
2797
- const amountMatch = afterPrefix.match(/^(\d+)([munp]?)/);
2798
- if (!amountMatch || !amountMatch[1]) {
2799
- return 0; // Zero-amount invoice
2800
- }
2801
- const amount = parseInt(amountMatch[1], 10);
2802
- const multiplier = amountMatch[2] ?? "";
2803
- // Convert to satoshis (1 BTC = 100,000,000 sats)
2804
- // Invoice amounts are in BTC by default
2805
- let btcAmount;
2806
- switch (multiplier) {
2807
- case "m": // milli-BTC (0.001 BTC)
2808
- btcAmount = amount * 0.001;
2809
- break;
2810
- case "u": // micro-BTC (0.000001 BTC)
2811
- btcAmount = amount * 0.000001;
2812
- break;
2813
- case "n": // nano-BTC (0.000000001 BTC)
2814
- btcAmount = amount * 0.000000001;
2815
- break;
2816
- case "p": // pico-BTC (0.000000000001 BTC)
2817
- btcAmount = amount * 0.000000000001;
2818
- break;
2819
- default: // BTC
2820
- btcAmount = amount;
2821
- break;
2816
+ catch {
2817
+ // Fallback: if library fails, return 0 (treated as zero-amount invoice)
2818
+ return 0;
2822
2819
  }
2823
- // Convert BTC to sats
2824
- return Math.round(btcAmount * 100000000);
2825
2820
  }
2826
2821
  /**
2827
2822
  * Clean up wallet connections
@@ -2886,8 +2881,11 @@ class FlashnetClient {
2886
2881
  const map = new Map();
2887
2882
  for (const item of config) {
2888
2883
  if (item.enabled) {
2884
+ if (item.min_amount == null) {
2885
+ continue;
2886
+ }
2889
2887
  const key = item.asset_identifier.toLowerCase();
2890
- const value = BigInt(String(item.min_amount));
2888
+ const value = safeBigInt(item.min_amount);
2891
2889
  map.set(key, value);
2892
2890
  }
2893
2891
  }
@@ -2909,8 +2907,8 @@ class FlashnetClient {
2909
2907
  const outHex = this.getHexAddress(params.assetOutAddress);
2910
2908
  const minIn = minMap.get(inHex);
2911
2909
  const minOut = minMap.get(outHex);
2912
- const amountIn = BigInt(String(params.amountIn));
2913
- const minAmountOut = BigInt(String(params.minAmountOut));
2910
+ const amountIn = BigInt(params.amountIn);
2911
+ const minAmountOut = BigInt(params.minAmountOut);
2914
2912
  if (minIn && minOut) {
2915
2913
  if (amountIn < minIn) {
2916
2914
  throw new Error(`Minimum amount not met for input asset. Required \
@@ -2944,13 +2942,13 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2944
2942
  const aMin = minMap.get(aHex);
2945
2943
  const bMin = minMap.get(bHex);
2946
2944
  if (aMin) {
2947
- const aAmt = BigInt(String(params.assetAAmount));
2945
+ const aAmt = BigInt(params.assetAAmount);
2948
2946
  if (aAmt < aMin) {
2949
2947
  throw new Error(`Minimum amount not met for Asset A. Required ${aMin.toString()}, provided ${aAmt.toString()}`);
2950
2948
  }
2951
2949
  }
2952
2950
  if (bMin) {
2953
- const bAmt = BigInt(String(params.assetBAmount));
2951
+ const bAmt = BigInt(params.assetBAmount);
2954
2952
  if (bAmt < bMin) {
2955
2953
  throw new Error(`Minimum amount not met for Asset B. Required ${bMin.toString()}, provided ${bAmt.toString()}`);
2956
2954
  }
@@ -2972,14 +2970,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
2972
2970
  const aMin = minMap.get(aHex);
2973
2971
  const bMin = minMap.get(bHex);
2974
2972
  if (aMin) {
2975
- const predictedAOut = BigInt(String(simulation.assetAAmount));
2973
+ const predictedAOut = safeBigInt(simulation.assetAAmount);
2976
2974
  const relaxedA = aMin / 2n; // apply 50% relaxation for outputs
2977
2975
  if (predictedAOut < relaxedA) {
2978
2976
  throw new Error(`Minimum amount not met for Asset A on withdrawal. Required at least ${relaxedA.toString()} (50% relaxed), predicted ${predictedAOut.toString()}`);
2979
2977
  }
2980
2978
  }
2981
2979
  if (bMin) {
2982
- const predictedBOut = BigInt(String(simulation.assetBAmount));
2980
+ const predictedBOut = safeBigInt(simulation.assetBAmount);
2983
2981
  const relaxedB = bMin / 2n;
2984
2982
  if (predictedBOut < relaxedB) {
2985
2983
  throw new Error(`Minimum amount not met for Asset B on withdrawal. Required at least ${relaxedB.toString()} (50% relaxed), predicted ${predictedBOut.toString()}`);
@@ -3097,7 +3095,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3097
3095
  receiverSparkAddress: lpSparkAddress,
3098
3096
  assetAddress: pool.assetAAddress,
3099
3097
  amount: params.amountADesired,
3100
- }, "Insufficient balance for adding V3 liquidity (Asset A): ");
3098
+ }, "Insufficient balance for adding V3 liquidity (Asset A): ", params.useAvailableBalance);
3101
3099
  transferIds.push(assetATransferId);
3102
3100
  }
3103
3101
  if (BigInt(params.amountBDesired) > 0n) {
@@ -3105,7 +3103,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3105
3103
  receiverSparkAddress: lpSparkAddress,
3106
3104
  assetAddress: pool.assetBAddress,
3107
3105
  amount: params.amountBDesired,
3108
- }, "Insufficient balance for adding V3 liquidity (Asset B): ");
3106
+ }, "Insufficient balance for adding V3 liquidity (Asset B): ", params.useAvailableBalance);
3109
3107
  transferIds.push(assetBTransferId);
3110
3108
  }
3111
3109
  }
@@ -3298,14 +3296,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3298
3296
  receiverSparkAddress: lpSparkAddress,
3299
3297
  assetAddress: pool.assetAAddress,
3300
3298
  amount: params.additionalAmountA,
3301
- }, "Insufficient balance for rebalance (Asset A): ");
3299
+ }, "Insufficient balance for rebalance (Asset A): ", params.useAvailableBalance);
3302
3300
  }
3303
3301
  if (params.additionalAmountB && BigInt(params.additionalAmountB) > 0n) {
3304
3302
  assetBTransferId = await this.transferAsset({
3305
3303
  receiverSparkAddress: lpSparkAddress,
3306
3304
  assetAddress: pool.assetBAddress,
3307
3305
  amount: params.additionalAmountB,
3308
- }, "Insufficient balance for rebalance (Asset B): ");
3306
+ }, "Insufficient balance for rebalance (Asset B): ", params.useAvailableBalance);
3309
3307
  }
3310
3308
  // Collect transfer IDs for potential clawback
3311
3309
  const transferIds = [];
@@ -3502,7 +3500,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3502
3500
  receiverSparkAddress: lpSparkAddress,
3503
3501
  assetAddress: pool.assetAAddress,
3504
3502
  amount: params.amountA,
3505
- }, "Insufficient balance for depositing to V3 pool (Asset A): ");
3503
+ }, "Insufficient balance for depositing to V3 pool (Asset A): ", params.useAvailableBalance);
3506
3504
  transferIds.push(assetATransferId);
3507
3505
  }
3508
3506
  if (BigInt(params.amountB) > 0n) {
@@ -3510,7 +3508,7 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
3510
3508
  receiverSparkAddress: lpSparkAddress,
3511
3509
  assetAddress: pool.assetBAddress,
3512
3510
  amount: params.amountB,
3513
- }, "Insufficient balance for depositing to V3 pool (Asset B): ");
3511
+ }, "Insufficient balance for depositing to V3 pool (Asset B): ", params.useAvailableBalance);
3514
3512
  transferIds.push(assetBTransferId);
3515
3513
  }
3516
3514
  const executeDeposit = async () => {