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