@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.
- package/dist/cjs/node_modules/@buildonspark/spark-sdk/dist/buffer-o9LR0ip9.d.ts +2 -0
- package/dist/cjs/node_modules/@buildonspark/spark-sdk/dist/buffer-o9LR0ip9.d.ts.map +1 -0
- package/dist/cjs/src/client/FlashnetClient.d.ts +21 -4
- package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/cjs/src/client/FlashnetClient.js +259 -207
- package/dist/cjs/src/client/FlashnetClient.js.map +1 -1
- package/dist/cjs/src/utils/intents.d.ts.map +1 -1
- package/dist/cjs/src/utils/intents.js +4 -6
- package/dist/cjs/src/utils/intents.js.map +1 -1
- package/dist/esm/node_modules/@buildonspark/spark-sdk/dist/buffer-o9LR0ip9.d.ts +2 -0
- package/dist/esm/node_modules/@buildonspark/spark-sdk/dist/buffer-o9LR0ip9.d.ts.map +1 -0
- package/dist/esm/src/client/FlashnetClient.d.ts +21 -4
- package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/esm/src/client/FlashnetClient.js +259 -207
- package/dist/esm/src/client/FlashnetClient.js.map +1 -1
- package/dist/esm/src/utils/intents.d.ts.map +1 -1
- package/dist/esm/src/utils/intents.js +4 -6
- package/dist/esm/src/utils/intents.js.map +1 -1
- package/package.json +11 -5
- package/dist/cjs/node_modules/@buildonspark/spark-sdk/dist/buffer-DK3I_h9P.d.ts +0 -2
- package/dist/cjs/node_modules/@buildonspark/spark-sdk/dist/buffer-DK3I_h9P.d.ts.map +0 -1
- package/dist/esm/node_modules/@buildonspark/spark-sdk/dist/buffer-DK3I_h9P.d.ts +0 -2
- package/dist/esm/node_modules/@buildonspark/spark-sdk/dist/buffer-DK3I_h9P.d.ts.map +0 -1
|
@@ -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:
|
|
244
|
-
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:
|
|
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 =
|
|
275
|
+
requirements.btc = BigInt(balance.amount);
|
|
273
276
|
}
|
|
274
277
|
else {
|
|
275
|
-
requirements.tokens?.set(balance.assetAddress,
|
|
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
|
-
//
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
2099
|
-
|
|
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:
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
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
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
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:
|
|
2150
|
+
btcAmountReceived: swapResponse.amountOut || "0",
|
|
2167
2151
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2168
2152
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2169
2153
|
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2170
|
-
error:
|
|
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
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
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
|
-
//
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
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:
|
|
2223
|
-
btcAmountReceived:
|
|
2187
|
+
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2188
|
+
btcAmountReceived: btcReceived,
|
|
2224
2189
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2225
2190
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2226
2191
|
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2227
|
-
error: `
|
|
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
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
:
|
|
2235
|
-
|
|
2236
|
-
|
|
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
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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 =
|
|
2524
|
-
const resA =
|
|
2525
|
-
const resB =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
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
|
-
|
|
2827
|
-
//
|
|
2828
|
-
|
|
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 =
|
|
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 =
|
|
2954
|
-
const 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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
3601
|
+
if (BigInt(params.amountB) > 0n) {
|
|
3550
3602
|
assetBTransferId = await this.transferAsset({
|
|
3551
3603
|
receiverSparkAddress: lpSparkAddress,
|
|
3552
3604
|
assetAddress: pool.assetBAddress,
|