@flashnet/sdk 0.5.6 → 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 +20 -0
- package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/cjs/src/client/FlashnetClient.js +206 -111
- package/dist/cjs/src/client/FlashnetClient.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 +20 -0
- package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/esm/src/client/FlashnetClient.js +206 -111
- package/dist/esm/src/client/FlashnetClient.js.map +1 -1
- package/package.json +7 -3
- 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
|
@@ -2093,8 +2093,16 @@ class FlashnetClient {
|
|
|
2093
2093
|
const assetOutAddress = quote.tokenIsAssetA
|
|
2094
2094
|
? pool.assetBAddress
|
|
2095
2095
|
: pool.assetAAddress;
|
|
2096
|
-
|
|
2097
|
-
|
|
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();
|
|
2098
2106
|
// Execute the swap
|
|
2099
2107
|
const swapResponse = await this.executeSwap({
|
|
2100
2108
|
poolId: quote.poolId,
|
|
@@ -2119,138 +2127,158 @@ class FlashnetClient {
|
|
|
2119
2127
|
error: swapResponse.error || "Swap was not accepted",
|
|
2120
2128
|
};
|
|
2121
2129
|
}
|
|
2122
|
-
// Step 5:
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
2127
|
-
const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
|
|
2128
|
-
const balance = await this.getBalance();
|
|
2129
|
-
canPayImmediately = balance.balance >= safeBigInt(btcNeededForPayment);
|
|
2130
|
-
}
|
|
2131
|
-
if (!canPayImmediately) {
|
|
2132
|
-
const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
|
|
2133
|
-
if (!transferComplete) {
|
|
2134
|
-
return {
|
|
2135
|
-
success: false,
|
|
2136
|
-
poolId: quote.poolId,
|
|
2137
|
-
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2138
|
-
btcAmountReceived: swapResponse.amountOut || "0",
|
|
2139
|
-
swapTransferId: swapResponse.outboundTransferId,
|
|
2140
|
-
ammFeePaid: quote.estimatedAmmFee,
|
|
2141
|
-
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2142
|
-
error: "Transfer did not complete within timeout",
|
|
2143
|
-
};
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
// Step 6: Calculate Lightning fee and payment amount
|
|
2147
|
-
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
2148
|
-
const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
|
|
2149
|
-
// 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();
|
|
2150
2134
|
try {
|
|
2151
|
-
let
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
const
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
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) {
|
|
2159
2146
|
return {
|
|
2160
2147
|
success: false,
|
|
2161
2148
|
poolId: quote.poolId,
|
|
2162
2149
|
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2163
|
-
btcAmountReceived:
|
|
2150
|
+
btcAmountReceived: swapResponse.amountOut || "0",
|
|
2164
2151
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2165
2152
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2166
2153
|
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2167
|
-
error:
|
|
2154
|
+
error: "Transfer did not complete within timeout",
|
|
2168
2155
|
};
|
|
2169
2156
|
}
|
|
2170
|
-
invoiceAmountPaid = Number(amountToPay);
|
|
2171
|
-
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2172
|
-
invoice,
|
|
2173
|
-
amountSats: invoiceAmountPaid,
|
|
2174
|
-
maxFeeSats: effectiveMaxLightningFee,
|
|
2175
|
-
preferSpark,
|
|
2176
|
-
});
|
|
2177
2157
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
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
|
+
}
|
|
2185
2174
|
}
|
|
2186
|
-
//
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2196
|
-
btcAmountReceived: btcReceived,
|
|
2197
|
-
swapTransferId: swapResponse.outboundTransferId,
|
|
2198
|
-
lightningPaymentId: lightningPayment.id,
|
|
2199
|
-
ammFeePaid: quote.estimatedAmmFee,
|
|
2200
|
-
lightningFeePaid: effectiveMaxLightningFee,
|
|
2201
|
-
invoiceAmountPaid,
|
|
2202
|
-
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2203
|
-
sparkLightningTransferId,
|
|
2204
|
-
};
|
|
2205
|
-
}
|
|
2206
|
-
catch (lightningError) {
|
|
2207
|
-
// Lightning payment failed after swap succeeded
|
|
2208
|
-
const lightningErrorMessage = lightningError instanceof Error
|
|
2209
|
-
? lightningError.message
|
|
2210
|
-
: String(lightningError);
|
|
2211
|
-
// Attempt rollback if requested
|
|
2212
|
-
if (rollbackOnFailure) {
|
|
2213
|
-
try {
|
|
2214
|
-
const rollbackResult = await this.rollbackSwap(quote.poolId, btcReceived, tokenAddress, maxSlippageBps);
|
|
2215
|
-
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) {
|
|
2216
2184
|
return {
|
|
2217
2185
|
success: false,
|
|
2218
2186
|
poolId: quote.poolId,
|
|
2219
|
-
tokenAmountSpent:
|
|
2220
|
-
btcAmountReceived:
|
|
2187
|
+
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2188
|
+
btcAmountReceived: btcReceived,
|
|
2221
2189
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2222
2190
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2223
2191
|
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2224
|
-
error: `
|
|
2192
|
+
error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${cappedMaxLightningFee} sats).`,
|
|
2225
2193
|
};
|
|
2226
2194
|
}
|
|
2195
|
+
invoiceAmountPaid = Number(amountToPay);
|
|
2196
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2197
|
+
invoice,
|
|
2198
|
+
amountSats: invoiceAmountPaid,
|
|
2199
|
+
maxFeeSats: cappedMaxLightningFee,
|
|
2200
|
+
preferSpark,
|
|
2201
|
+
});
|
|
2227
2202
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
:
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
poolId: quote.poolId,
|
|
2235
|
-
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2236
|
-
btcAmountReceived: btcReceived,
|
|
2237
|
-
swapTransferId: swapResponse.outboundTransferId,
|
|
2238
|
-
ammFeePaid: quote.estimatedAmmFee,
|
|
2239
|
-
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2240
|
-
error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
|
|
2241
|
-
};
|
|
2203
|
+
else {
|
|
2204
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2205
|
+
invoice,
|
|
2206
|
+
maxFeeSats: cappedMaxLightningFee,
|
|
2207
|
+
preferSpark,
|
|
2208
|
+
});
|
|
2242
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
|
+
};
|
|
2243
2229
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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();
|
|
2254
2282
|
}
|
|
2255
2283
|
}
|
|
2256
2284
|
catch (error) {
|
|
@@ -2771,6 +2799,73 @@ class FlashnetClient {
|
|
|
2771
2799
|
}
|
|
2772
2800
|
return false;
|
|
2773
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
|
+
}
|
|
2774
2869
|
/**
|
|
2775
2870
|
* Get Lightning fee estimate for an invoice
|
|
2776
2871
|
* @private
|