@flashnet/sdk 0.5.3 → 0.5.5
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/src/client/FlashnetClient.d.ts +68 -5
- package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/cjs/src/client/FlashnetClient.js +483 -119
- package/dist/cjs/src/client/FlashnetClient.js.map +1 -1
- package/dist/cjs/src/utils/bigint.d.ts +7 -0
- package/dist/cjs/src/utils/bigint.d.ts.map +1 -0
- package/dist/cjs/src/utils/bigint.js +24 -0
- package/dist/cjs/src/utils/bigint.js.map +1 -0
- package/dist/cjs/src/utils/index.d.ts +1 -0
- package/dist/cjs/src/utils/index.d.ts.map +1 -1
- package/dist/cjs/src/utils/intents.d.ts.map +1 -1
- package/dist/cjs/src/utils/intents.js +6 -4
- package/dist/cjs/src/utils/intents.js.map +1 -1
- package/dist/esm/src/client/FlashnetClient.d.ts +68 -5
- package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/esm/src/client/FlashnetClient.js +483 -119
- package/dist/esm/src/client/FlashnetClient.js.map +1 -1
- package/dist/esm/src/utils/bigint.d.ts +7 -0
- package/dist/esm/src/utils/bigint.d.ts.map +1 -0
- package/dist/esm/src/utils/bigint.js +22 -0
- package/dist/esm/src/utils/bigint.js.map +1 -0
- package/dist/esm/src/utils/index.d.ts +1 -0
- package/dist/esm/src/utils/index.d.ts.map +1 -1
- package/dist/esm/src/utils/intents.d.ts.map +1 -1
- package/dist/esm/src/utils/intents.js +6 -4
- package/dist/esm/src/utils/intents.js.map +1 -1
- package/package.json +3 -3
|
@@ -11,6 +11,7 @@ var hex = require('../utils/hex.js');
|
|
|
11
11
|
var intents = require('../utils/intents.js');
|
|
12
12
|
var sparkAddress = require('../utils/spark-address.js');
|
|
13
13
|
var tokenAddress = require('../utils/tokenAddress.js');
|
|
14
|
+
var bigint = require('../utils/bigint.js');
|
|
14
15
|
var errors = require('../types/errors.js');
|
|
15
16
|
|
|
16
17
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -245,7 +246,8 @@ class FlashnetClient {
|
|
|
245
246
|
const tokenIdentifierHex = hex.getHexFromUint8Array(info.rawTokenIdentifier);
|
|
246
247
|
const tokenAddress$1 = tokenAddress.encodeSparkHumanReadableTokenIdentifier(info.rawTokenIdentifier, this.sparkNetwork);
|
|
247
248
|
tokenBalances.set(tokenPubkey, {
|
|
248
|
-
balance:
|
|
249
|
+
balance: FlashnetClient.safeBigInt(tokenData.ownedBalance),
|
|
250
|
+
availableToSendBalance: FlashnetClient.safeBigInt(tokenData.availableToSendBalance),
|
|
249
251
|
tokenInfo: {
|
|
250
252
|
tokenIdentifier: tokenIdentifierHex,
|
|
251
253
|
tokenAddress: tokenAddress$1,
|
|
@@ -258,7 +260,7 @@ class FlashnetClient {
|
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
262
|
return {
|
|
261
|
-
balance:
|
|
263
|
+
balance: FlashnetClient.safeBigInt(balance.balance),
|
|
262
264
|
tokenBalances,
|
|
263
265
|
};
|
|
264
266
|
}
|
|
@@ -266,17 +268,17 @@ class FlashnetClient {
|
|
|
266
268
|
* Check if wallet has sufficient balance for an operation
|
|
267
269
|
*/
|
|
268
270
|
async checkBalance(params) {
|
|
269
|
-
const balance = await this.getBalance();
|
|
271
|
+
const balance = params.walletBalance ?? (await this.getBalance());
|
|
270
272
|
// Check balance
|
|
271
273
|
const requirements = {
|
|
272
274
|
tokens: new Map(),
|
|
273
275
|
};
|
|
274
276
|
for (const balance of params.balancesToCheck) {
|
|
275
277
|
if (balance.assetAddress === index$1.BTC_ASSET_PUBKEY) {
|
|
276
|
-
requirements.btc =
|
|
278
|
+
requirements.btc = FlashnetClient.safeBigInt(balance.amount);
|
|
277
279
|
}
|
|
278
280
|
else {
|
|
279
|
-
requirements.tokens?.set(balance.assetAddress,
|
|
281
|
+
requirements.tokens?.set(balance.assetAddress, FlashnetClient.safeBigInt(balance.amount));
|
|
280
282
|
}
|
|
281
283
|
}
|
|
282
284
|
// Check BTC balance
|
|
@@ -294,7 +296,9 @@ class FlashnetClient {
|
|
|
294
296
|
const hrKey = this.toHumanReadableTokenIdentifier(tokenPubkey);
|
|
295
297
|
const effectiveTokenBalance = balance.tokenBalances.get(tokenPubkey) ??
|
|
296
298
|
balance.tokenBalances.get(hrKey);
|
|
297
|
-
const available =
|
|
299
|
+
const available = params.useAvailableBalance
|
|
300
|
+
? (effectiveTokenBalance?.availableToSendBalance ?? 0n)
|
|
301
|
+
: (effectiveTokenBalance?.balance ?? 0n);
|
|
298
302
|
if (available < requiredAmount) {
|
|
299
303
|
throw new Error([
|
|
300
304
|
params.errorPrefix ?? "",
|
|
@@ -356,6 +360,7 @@ class FlashnetClient {
|
|
|
356
360
|
},
|
|
357
361
|
],
|
|
358
362
|
errorPrefix: "Insufficient balance for initial liquidity: ",
|
|
363
|
+
useAvailableBalance: params.useAvailableBalance,
|
|
359
364
|
});
|
|
360
365
|
}
|
|
361
366
|
const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
|
|
@@ -415,6 +420,12 @@ class FlashnetClient {
|
|
|
415
420
|
throw new Error(`${name} must be positive integer`);
|
|
416
421
|
}
|
|
417
422
|
}
|
|
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
|
+
}
|
|
418
429
|
/**
|
|
419
430
|
* Calculates virtual reserves for a bonding curve AMM.
|
|
420
431
|
*
|
|
@@ -435,7 +446,7 @@ class FlashnetClient {
|
|
|
435
446
|
}
|
|
436
447
|
const supply = FlashnetClient.parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
|
|
437
448
|
const targetB = FlashnetClient.parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
|
|
438
|
-
const graduationThresholdPct =
|
|
449
|
+
const graduationThresholdPct = FlashnetClient.safeBigInt(params.graduationThresholdPct);
|
|
439
450
|
// Align bounds with Rust AMM (20%..95%), then check feasibility for g=1 (requires >50%).
|
|
440
451
|
const MIN_PCT = 20n;
|
|
441
452
|
const MAX_PCT = 95n;
|
|
@@ -485,6 +496,7 @@ class FlashnetClient {
|
|
|
485
496
|
},
|
|
486
497
|
],
|
|
487
498
|
errorPrefix: "Insufficient balance for pool creation: ",
|
|
499
|
+
useAvailableBalance: params.useAvailableBalance,
|
|
488
500
|
});
|
|
489
501
|
const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
|
|
490
502
|
// Generate intent
|
|
@@ -605,10 +617,14 @@ class FlashnetClient {
|
|
|
605
617
|
});
|
|
606
618
|
// If using free balance (V3 pools only), skip the Spark transfer
|
|
607
619
|
if (params.useFreeBalance) {
|
|
608
|
-
|
|
620
|
+
const swapResponse = await this.executeSwapIntent({
|
|
609
621
|
...params,
|
|
610
622
|
// No transferId - triggers free balance mode
|
|
611
623
|
});
|
|
624
|
+
return {
|
|
625
|
+
...swapResponse,
|
|
626
|
+
inboundSparkTransferId: swapResponse.requestId,
|
|
627
|
+
};
|
|
612
628
|
}
|
|
613
629
|
// Transfer assets to pool using new address encoding
|
|
614
630
|
const lpSparkAddress = sparkAddress.encodeSparkAddressNew({
|
|
@@ -619,12 +635,13 @@ class FlashnetClient {
|
|
|
619
635
|
receiverSparkAddress: lpSparkAddress,
|
|
620
636
|
assetAddress: params.assetInAddress,
|
|
621
637
|
amount: params.amountIn,
|
|
622
|
-
}, "Insufficient balance for swap: ");
|
|
638
|
+
}, "Insufficient balance for swap: ", params.useAvailableBalance);
|
|
623
639
|
// Execute with auto-clawback on failure
|
|
624
|
-
|
|
640
|
+
const swapResponse = await this.executeWithAutoClawback(() => this.executeSwapIntent({
|
|
625
641
|
...params,
|
|
626
642
|
transferId,
|
|
627
643
|
}), [transferId], params.poolId);
|
|
644
|
+
return { ...swapResponse, inboundSparkTransferId: transferId };
|
|
628
645
|
}
|
|
629
646
|
/**
|
|
630
647
|
* Execute a swap with a pre-created transfer or using free balance.
|
|
@@ -754,7 +771,7 @@ class FlashnetClient {
|
|
|
754
771
|
receiverSparkAddress: lpSparkAddress,
|
|
755
772
|
assetAddress: params.initialAssetAddress,
|
|
756
773
|
amount: params.inputAmount,
|
|
757
|
-
}, "Insufficient balance for route swap: ");
|
|
774
|
+
}, "Insufficient balance for route swap: ", params.useAvailableBalance);
|
|
758
775
|
// Execute with auto-clawback on failure
|
|
759
776
|
return this.executeWithAutoClawback(async () => {
|
|
760
777
|
// Prepare hops for validation
|
|
@@ -877,7 +894,7 @@ class FlashnetClient {
|
|
|
877
894
|
assetAddress: pool.assetBAddress,
|
|
878
895
|
amount: params.assetBAmount,
|
|
879
896
|
},
|
|
880
|
-
], "Insufficient balance for adding liquidity: ");
|
|
897
|
+
], "Insufficient balance for adding liquidity: ", params.useAvailableBalance);
|
|
881
898
|
// Execute with auto-clawback on failure
|
|
882
899
|
return this.executeWithAutoClawback(async () => {
|
|
883
900
|
// Generate add liquidity intent
|
|
@@ -1183,6 +1200,7 @@ class FlashnetClient {
|
|
|
1183
1200
|
depositAddress: createResponse.depositAddress,
|
|
1184
1201
|
assetId: params.assetId,
|
|
1185
1202
|
assetAmount: params.assetAmount,
|
|
1203
|
+
useAvailableBalance: params.useAvailableBalance,
|
|
1186
1204
|
});
|
|
1187
1205
|
}
|
|
1188
1206
|
/**
|
|
@@ -1200,6 +1218,7 @@ class FlashnetClient {
|
|
|
1200
1218
|
{ assetAddress: params.assetId, amount: params.assetAmount },
|
|
1201
1219
|
],
|
|
1202
1220
|
errorPrefix: "Insufficient balance to fund escrow: ",
|
|
1221
|
+
useAvailableBalance: params.useAvailableBalance,
|
|
1203
1222
|
});
|
|
1204
1223
|
// 2. Perform transfer
|
|
1205
1224
|
const escrowSparkAddress = sparkAddress.encodeSparkAddressNew({
|
|
@@ -1714,19 +1733,20 @@ class FlashnetClient {
|
|
|
1714
1733
|
/**
|
|
1715
1734
|
* Performs asset transfer using generalized asset address for both BTC and tokens.
|
|
1716
1735
|
*/
|
|
1717
|
-
async transferAsset(recipient, checkBalanceErrorPrefix) {
|
|
1718
|
-
const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix);
|
|
1736
|
+
async transferAsset(recipient, checkBalanceErrorPrefix, useAvailableBalance) {
|
|
1737
|
+
const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix, useAvailableBalance);
|
|
1719
1738
|
return transferIds[0];
|
|
1720
1739
|
}
|
|
1721
1740
|
/**
|
|
1722
1741
|
* Performs asset transfers using generalized asset addresses for both BTC and tokens.
|
|
1723
1742
|
* Supports optional generic to hardcode recipients length so output list can be typed with same length.
|
|
1724
1743
|
*/
|
|
1725
|
-
async transferAssets(recipients, checkBalanceErrorPrefix) {
|
|
1744
|
+
async transferAssets(recipients, checkBalanceErrorPrefix, useAvailableBalance) {
|
|
1726
1745
|
if (checkBalanceErrorPrefix) {
|
|
1727
1746
|
await this.checkBalance({
|
|
1728
1747
|
balancesToCheck: recipients,
|
|
1729
1748
|
errorPrefix: checkBalanceErrorPrefix,
|
|
1749
|
+
useAvailableBalance,
|
|
1730
1750
|
});
|
|
1731
1751
|
}
|
|
1732
1752
|
const transferIds = [];
|
|
@@ -1741,7 +1761,7 @@ class FlashnetClient {
|
|
|
1741
1761
|
else {
|
|
1742
1762
|
const transferId = await this._wallet.transferTokens({
|
|
1743
1763
|
tokenIdentifier: this.toHumanReadableTokenIdentifier(recipient.assetAddress),
|
|
1744
|
-
tokenAmount:
|
|
1764
|
+
tokenAmount: FlashnetClient.safeBigInt(recipient.amount),
|
|
1745
1765
|
receiverSparkAddress: recipient.receiverSparkAddress,
|
|
1746
1766
|
});
|
|
1747
1767
|
transferIds.push(transferId);
|
|
@@ -1825,38 +1845,37 @@ class FlashnetClient {
|
|
|
1825
1845
|
await this.ensureInitialized();
|
|
1826
1846
|
// Decode the invoice to get the amount
|
|
1827
1847
|
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
1848
|
+
// Zero-amount invoice: forward-direction quoting using caller-specified tokenAmount
|
|
1828
1849
|
if (!invoiceAmountSats || invoiceAmountSats <= 0) {
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1850
|
+
const tokenAmount = options?.tokenAmount;
|
|
1851
|
+
if (!tokenAmount || BigInt(tokenAmount) <= 0n) {
|
|
1852
|
+
throw new errors.FlashnetError("Zero-amount invoice requires tokenAmount in options.", {
|
|
1853
|
+
response: {
|
|
1854
|
+
errorCode: "FSAG-1002",
|
|
1855
|
+
errorCategory: "Validation",
|
|
1856
|
+
message: "Zero-amount invoice requires tokenAmount in options.",
|
|
1857
|
+
requestId: "",
|
|
1858
|
+
timestamp: new Date().toISOString(),
|
|
1859
|
+
service: "sdk",
|
|
1860
|
+
severity: "Error",
|
|
1861
|
+
remediation: "Provide tokenAmount when using a zero-amount invoice.",
|
|
1862
|
+
},
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
return this.getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options);
|
|
1841
1866
|
}
|
|
1842
1867
|
// Get Lightning fee estimate
|
|
1843
1868
|
const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
|
|
1844
|
-
// Total BTC needed = invoice amount + lightning fee
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
// that after masking we still have the required amount.
|
|
1849
|
-
// Example: 5014 -> masked to 4992, but if we request 5056, masked stays 5056
|
|
1850
|
-
const BTC_VARIABLE_FEE_BITS = 6n;
|
|
1851
|
-
const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
|
|
1852
|
-
const totalBtcNeeded = ((baseBtcNeeded + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
|
|
1853
|
-
BTC_VARIABLE_FEE_MASK;
|
|
1869
|
+
// Total BTC needed = invoice amount + lightning fee (unmasked).
|
|
1870
|
+
// Bitmasking for V2 pools is handled inside findBestPoolForTokenToBtc.
|
|
1871
|
+
const baseBtcNeeded = FlashnetClient.safeBigInt(invoiceAmountSats) +
|
|
1872
|
+
FlashnetClient.safeBigInt(lightningFeeEstimate);
|
|
1854
1873
|
// Check Flashnet minimum amounts early to provide clear error messages
|
|
1855
1874
|
const minAmounts = await this.getEnabledMinAmountsMap();
|
|
1856
1875
|
// Check BTC minimum (output from swap)
|
|
1857
1876
|
const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
|
|
1858
|
-
if (btcMinAmount &&
|
|
1859
|
-
const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${
|
|
1877
|
+
if (btcMinAmount && baseBtcNeeded < btcMinAmount) {
|
|
1878
|
+
const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${baseBtcNeeded} sats.`;
|
|
1860
1879
|
throw new errors.FlashnetError(msg, {
|
|
1861
1880
|
response: {
|
|
1862
1881
|
errorCode: "FSAG-1003",
|
|
@@ -1870,13 +1889,14 @@ class FlashnetClient {
|
|
|
1870
1889
|
},
|
|
1871
1890
|
});
|
|
1872
1891
|
}
|
|
1873
|
-
// Find the best pool to swap token -> BTC
|
|
1874
|
-
|
|
1892
|
+
// Find the best pool to swap token -> BTC.
|
|
1893
|
+
// Bitmasking is applied per-pool inside this function (V2 pools get masked, V3 pools don't).
|
|
1894
|
+
const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded.toString(), options?.integratorFeeRateBps);
|
|
1875
1895
|
// Check token minimum (input to swap)
|
|
1876
1896
|
const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
|
|
1877
1897
|
const tokenMinAmount = minAmounts.get(tokenHex);
|
|
1878
1898
|
if (tokenMinAmount &&
|
|
1879
|
-
|
|
1899
|
+
FlashnetClient.safeBigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
|
|
1880
1900
|
const msg = `Token amount too small. Minimum input is ${tokenMinAmount} units, but calculated amount is only ${poolQuote.tokenAmountRequired} units.`;
|
|
1881
1901
|
throw new errors.FlashnetError(msg, {
|
|
1882
1902
|
response: {
|
|
@@ -1891,13 +1911,14 @@ class FlashnetClient {
|
|
|
1891
1911
|
},
|
|
1892
1912
|
});
|
|
1893
1913
|
}
|
|
1894
|
-
//
|
|
1895
|
-
|
|
1914
|
+
// BTC variable fee adjustment: difference between what the pool targets and unmasked base.
|
|
1915
|
+
// For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
|
|
1916
|
+
const btcVariableFeeAdjustment = Number(FlashnetClient.safeBigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
|
|
1896
1917
|
return {
|
|
1897
1918
|
poolId: poolQuote.poolId,
|
|
1898
1919
|
tokenAddress: this.toHexTokenIdentifier(tokenAddress),
|
|
1899
1920
|
tokenAmountRequired: poolQuote.tokenAmountRequired,
|
|
1900
|
-
btcAmountRequired:
|
|
1921
|
+
btcAmountRequired: poolQuote.btcAmountUsed,
|
|
1901
1922
|
invoiceAmountSats: invoiceAmountSats,
|
|
1902
1923
|
estimatedAmmFee: poolQuote.estimatedAmmFee,
|
|
1903
1924
|
estimatedLightningFee: lightningFeeEstimate,
|
|
@@ -1907,6 +1928,138 @@ class FlashnetClient {
|
|
|
1907
1928
|
tokenIsAssetA: poolQuote.tokenIsAssetA,
|
|
1908
1929
|
poolReserves: poolQuote.poolReserves,
|
|
1909
1930
|
warningMessage: poolQuote.warningMessage,
|
|
1931
|
+
curveType: poolQuote.curveType,
|
|
1932
|
+
isZeroAmountInvoice: false,
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Generate a quote for a zero-amount invoice.
|
|
1937
|
+
* Forward-direction: simulate swapping tokenAmount and pick the pool with the best BTC output.
|
|
1938
|
+
* @private
|
|
1939
|
+
*/
|
|
1940
|
+
async getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options) {
|
|
1941
|
+
const tokenHex = this.toHexTokenIdentifier(tokenAddress);
|
|
1942
|
+
const btcHex = index$1.BTC_ASSET_PUBKEY;
|
|
1943
|
+
// Discover all token/BTC pools
|
|
1944
|
+
const [poolsWithTokenAsA, poolsWithTokenAsB] = await Promise.all([
|
|
1945
|
+
this.listPools({ assetAAddress: tokenHex, assetBAddress: btcHex }),
|
|
1946
|
+
this.listPools({ assetAAddress: btcHex, assetBAddress: tokenHex }),
|
|
1947
|
+
]);
|
|
1948
|
+
const poolMap = new Map();
|
|
1949
|
+
for (const p of [...poolsWithTokenAsA.pools, ...poolsWithTokenAsB.pools]) {
|
|
1950
|
+
if (!poolMap.has(p.lpPublicKey)) {
|
|
1951
|
+
const tokenIsAssetA = p.assetAAddress?.toLowerCase() === tokenHex.toLowerCase();
|
|
1952
|
+
poolMap.set(p.lpPublicKey, { pool: p, tokenIsAssetA });
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
const allPools = Array.from(poolMap.values());
|
|
1956
|
+
if (allPools.length === 0) {
|
|
1957
|
+
throw new errors.FlashnetError(`No liquidity pool found for token ${tokenAddress} paired with BTC`, {
|
|
1958
|
+
response: {
|
|
1959
|
+
errorCode: "FSAG-4001",
|
|
1960
|
+
errorCategory: "Business",
|
|
1961
|
+
message: `No liquidity pool found for token ${tokenAddress} paired with BTC`,
|
|
1962
|
+
requestId: "",
|
|
1963
|
+
timestamp: new Date().toISOString(),
|
|
1964
|
+
service: "sdk",
|
|
1965
|
+
severity: "Error",
|
|
1966
|
+
},
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
// Simulate each pool with tokenAmount as input, pick highest BTC output
|
|
1970
|
+
let bestResult = null;
|
|
1971
|
+
let bestBtcOut = 0n;
|
|
1972
|
+
for (const { pool, tokenIsAssetA } of allPools) {
|
|
1973
|
+
try {
|
|
1974
|
+
const poolDetails = await this.getPool(pool.lpPublicKey);
|
|
1975
|
+
const assetInAddress = tokenIsAssetA
|
|
1976
|
+
? poolDetails.assetAAddress
|
|
1977
|
+
: poolDetails.assetBAddress;
|
|
1978
|
+
const assetOutAddress = tokenIsAssetA
|
|
1979
|
+
? poolDetails.assetBAddress
|
|
1980
|
+
: poolDetails.assetAAddress;
|
|
1981
|
+
const simulation = await this.simulateSwap({
|
|
1982
|
+
poolId: pool.lpPublicKey,
|
|
1983
|
+
assetInAddress,
|
|
1984
|
+
assetOutAddress,
|
|
1985
|
+
amountIn: tokenAmount,
|
|
1986
|
+
integratorBps: options?.integratorFeeRateBps,
|
|
1987
|
+
});
|
|
1988
|
+
const btcOut = FlashnetClient.safeBigInt(simulation.amountOut);
|
|
1989
|
+
if (btcOut > bestBtcOut) {
|
|
1990
|
+
bestBtcOut = btcOut;
|
|
1991
|
+
bestResult = {
|
|
1992
|
+
poolId: pool.lpPublicKey,
|
|
1993
|
+
tokenIsAssetA,
|
|
1994
|
+
simulation,
|
|
1995
|
+
curveType: poolDetails.curveType,
|
|
1996
|
+
poolReserves: {
|
|
1997
|
+
assetAReserve: poolDetails.assetAReserve,
|
|
1998
|
+
assetBReserve: poolDetails.assetBReserve,
|
|
1999
|
+
},
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
catch {
|
|
2004
|
+
// Skip pools that fail simulation
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
if (!bestResult || bestBtcOut <= 0n) {
|
|
2008
|
+
throw new errors.FlashnetError("No pool can produce BTC output for the given token amount", {
|
|
2009
|
+
response: {
|
|
2010
|
+
errorCode: "FSAG-4201",
|
|
2011
|
+
errorCategory: "Business",
|
|
2012
|
+
message: "No pool can produce BTC output for the given token amount",
|
|
2013
|
+
requestId: "",
|
|
2014
|
+
timestamp: new Date().toISOString(),
|
|
2015
|
+
service: "sdk",
|
|
2016
|
+
severity: "Error",
|
|
2017
|
+
remediation: "Try a larger token amount.",
|
|
2018
|
+
},
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
// Estimate lightning fee from the BTC output
|
|
2022
|
+
let lightningFeeEstimate;
|
|
2023
|
+
try {
|
|
2024
|
+
lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
|
|
2025
|
+
}
|
|
2026
|
+
catch {
|
|
2027
|
+
lightningFeeEstimate = Math.max(5, Math.ceil(Number(bestBtcOut) * 0.0017));
|
|
2028
|
+
}
|
|
2029
|
+
// Check minimum amounts
|
|
2030
|
+
const minAmounts = await this.getEnabledMinAmountsMap();
|
|
2031
|
+
const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
|
|
2032
|
+
if (btcMinAmount && bestBtcOut < btcMinAmount) {
|
|
2033
|
+
const msg = `BTC output too small. Minimum is ${btcMinAmount} sats, but swap would produce only ${bestBtcOut} sats.`;
|
|
2034
|
+
throw new errors.FlashnetError(msg, {
|
|
2035
|
+
response: {
|
|
2036
|
+
errorCode: "FSAG-1003",
|
|
2037
|
+
errorCategory: "Validation",
|
|
2038
|
+
message: msg,
|
|
2039
|
+
requestId: "",
|
|
2040
|
+
timestamp: new Date().toISOString(),
|
|
2041
|
+
service: "sdk",
|
|
2042
|
+
severity: "Error",
|
|
2043
|
+
remediation: "Use a larger token amount.",
|
|
2044
|
+
},
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
return {
|
|
2048
|
+
poolId: bestResult.poolId,
|
|
2049
|
+
tokenAddress: tokenHex,
|
|
2050
|
+
tokenAmountRequired: tokenAmount,
|
|
2051
|
+
btcAmountRequired: bestBtcOut.toString(),
|
|
2052
|
+
invoiceAmountSats: 0,
|
|
2053
|
+
estimatedAmmFee: bestResult.simulation.feePaidAssetIn || "0",
|
|
2054
|
+
estimatedLightningFee: lightningFeeEstimate,
|
|
2055
|
+
btcVariableFeeAdjustment: 0,
|
|
2056
|
+
executionPrice: bestResult.simulation.executionPrice || "0",
|
|
2057
|
+
priceImpactPct: bestResult.simulation.priceImpactPct || "0",
|
|
2058
|
+
tokenIsAssetA: bestResult.tokenIsAssetA,
|
|
2059
|
+
poolReserves: bestResult.poolReserves,
|
|
2060
|
+
warningMessage: bestResult.simulation.warningMessage,
|
|
2061
|
+
curveType: bestResult.curveType,
|
|
2062
|
+
isZeroAmountInvoice: true,
|
|
1910
2063
|
};
|
|
1911
2064
|
}
|
|
1912
2065
|
/**
|
|
@@ -1918,14 +2071,15 @@ class FlashnetClient {
|
|
|
1918
2071
|
*/
|
|
1919
2072
|
async payLightningWithToken(options) {
|
|
1920
2073
|
await this.ensureInitialized();
|
|
1921
|
-
const { invoice, tokenAddress, maxSlippageBps = 500, // 5% default
|
|
2074
|
+
const { invoice, tokenAddress, tokenAmount, maxSlippageBps = 500, // 5% default
|
|
1922
2075
|
maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 30000, // 30s default
|
|
1923
|
-
rollbackOnFailure = false, useExistingBtcBalance = false, } = options;
|
|
2076
|
+
rollbackOnFailure = false, useExistingBtcBalance = false, useAvailableBalance = false, } = options;
|
|
1924
2077
|
try {
|
|
1925
2078
|
// Step 1: Get a quote for the payment
|
|
1926
2079
|
const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
|
|
1927
2080
|
maxSlippageBps,
|
|
1928
2081
|
integratorFeeRateBps,
|
|
2082
|
+
tokenAmount,
|
|
1929
2083
|
});
|
|
1930
2084
|
// Step 2: Check token balance (always required)
|
|
1931
2085
|
await this.checkBalance({
|
|
@@ -1936,17 +2090,8 @@ class FlashnetClient {
|
|
|
1936
2090
|
},
|
|
1937
2091
|
],
|
|
1938
2092
|
errorPrefix: "Insufficient token balance for Lightning payment: ",
|
|
2093
|
+
useAvailableBalance,
|
|
1939
2094
|
});
|
|
1940
|
-
// Determine if we can pay immediately using existing BTC balance
|
|
1941
|
-
let canPayImmediately = false;
|
|
1942
|
-
if (useExistingBtcBalance) {
|
|
1943
|
-
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
1944
|
-
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
1945
|
-
const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
|
|
1946
|
-
// Check if we have enough BTC (don't throw if not, just fall back to waiting)
|
|
1947
|
-
const balance = await this.getBalance();
|
|
1948
|
-
canPayImmediately = balance.balance >= BigInt(btcNeededForPayment);
|
|
1949
|
-
}
|
|
1950
2095
|
// Step 3: Get pool details
|
|
1951
2096
|
const pool = await this.getPool(quote.poolId);
|
|
1952
2097
|
// Step 4: Determine swap direction and execute
|
|
@@ -1968,6 +2113,7 @@ class FlashnetClient {
|
|
|
1968
2113
|
minAmountOut: minBtcOut,
|
|
1969
2114
|
integratorFeeRateBps,
|
|
1970
2115
|
integratorPublicKey,
|
|
2116
|
+
useAvailableBalance,
|
|
1971
2117
|
});
|
|
1972
2118
|
if (!swapResponse.accepted || !swapResponse.outboundTransferId) {
|
|
1973
2119
|
return {
|
|
@@ -1977,10 +2123,20 @@ class FlashnetClient {
|
|
|
1977
2123
|
btcAmountReceived: "0",
|
|
1978
2124
|
swapTransferId: swapResponse.outboundTransferId || "",
|
|
1979
2125
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2126
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
1980
2127
|
error: swapResponse.error || "Swap was not accepted",
|
|
1981
2128
|
};
|
|
1982
2129
|
}
|
|
1983
|
-
// Step 5: Wait for
|
|
2130
|
+
// Step 5: Wait for transfer (skip useExistingBtcBalance for zero-amount invoices)
|
|
2131
|
+
let canPayImmediately = false;
|
|
2132
|
+
if (!quote.isZeroAmountInvoice && useExistingBtcBalance) {
|
|
2133
|
+
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
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
|
+
}
|
|
1984
2140
|
if (!canPayImmediately) {
|
|
1985
2141
|
const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
|
|
1986
2142
|
if (!transferComplete) {
|
|
@@ -1991,20 +2147,57 @@ class FlashnetClient {
|
|
|
1991
2147
|
btcAmountReceived: swapResponse.amountOut || "0",
|
|
1992
2148
|
swapTransferId: swapResponse.outboundTransferId,
|
|
1993
2149
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2150
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
1994
2151
|
error: "Transfer did not complete within timeout",
|
|
1995
2152
|
};
|
|
1996
2153
|
}
|
|
1997
2154
|
}
|
|
1998
|
-
// Step 6: Calculate Lightning fee
|
|
2155
|
+
// Step 6: Calculate Lightning fee and payment amount
|
|
1999
2156
|
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
2000
|
-
// Step 7: Pay the Lightning invoice
|
|
2001
2157
|
const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
|
|
2158
|
+
// Step 7: Pay the Lightning invoice
|
|
2002
2159
|
try {
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2160
|
+
let lightningPayment;
|
|
2161
|
+
let invoiceAmountPaid;
|
|
2162
|
+
if (quote.isZeroAmountInvoice) {
|
|
2163
|
+
// Zero-amount invoice: pay whatever BTC we received minus lightning fee
|
|
2164
|
+
const actualBtc = FlashnetClient.safeBigInt(btcReceived);
|
|
2165
|
+
const lnFee = FlashnetClient.safeBigInt(effectiveMaxLightningFee);
|
|
2166
|
+
const amountToPay = actualBtc - lnFee;
|
|
2167
|
+
if (amountToPay <= 0n) {
|
|
2168
|
+
return {
|
|
2169
|
+
success: false,
|
|
2170
|
+
poolId: quote.poolId,
|
|
2171
|
+
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2172
|
+
btcAmountReceived: btcReceived,
|
|
2173
|
+
swapTransferId: swapResponse.outboundTransferId,
|
|
2174
|
+
ammFeePaid: quote.estimatedAmmFee,
|
|
2175
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2176
|
+
error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
invoiceAmountPaid = Number(amountToPay);
|
|
2180
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2181
|
+
invoice,
|
|
2182
|
+
amountSats: invoiceAmountPaid,
|
|
2183
|
+
maxFeeSats: effectiveMaxLightningFee,
|
|
2184
|
+
preferSpark,
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
else {
|
|
2188
|
+
// Standard invoice: pay the specified amount
|
|
2189
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2190
|
+
invoice,
|
|
2191
|
+
maxFeeSats: effectiveMaxLightningFee,
|
|
2192
|
+
preferSpark,
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
// Extract the Spark transfer ID from the lightning payment result.
|
|
2196
|
+
// payLightningInvoice returns LightningSendRequest | WalletTransfer:
|
|
2197
|
+
// - LightningSendRequest has .transfer?.sparkId (the Sparkscan-visible transfer ID)
|
|
2198
|
+
// - WalletTransfer (Spark-to-Spark) has .id directly as the transfer ID
|
|
2199
|
+
// Note: lightningPayment.id (the SSP request ID) is already returned as lightningPaymentId
|
|
2200
|
+
const sparkLightningTransferId = lightningPayment.transfer?.sparkId;
|
|
2008
2201
|
return {
|
|
2009
2202
|
success: true,
|
|
2010
2203
|
poolId: quote.poolId,
|
|
@@ -2014,6 +2207,9 @@ class FlashnetClient {
|
|
|
2014
2207
|
lightningPaymentId: lightningPayment.id,
|
|
2015
2208
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2016
2209
|
lightningFeePaid: effectiveMaxLightningFee,
|
|
2210
|
+
invoiceAmountPaid,
|
|
2211
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2212
|
+
sparkLightningTransferId,
|
|
2017
2213
|
};
|
|
2018
2214
|
}
|
|
2019
2215
|
catch (lightningError) {
|
|
@@ -2033,6 +2229,7 @@ class FlashnetClient {
|
|
|
2033
2229
|
btcAmountReceived: "0",
|
|
2034
2230
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2035
2231
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2232
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2036
2233
|
error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
|
|
2037
2234
|
};
|
|
2038
2235
|
}
|
|
@@ -2048,6 +2245,7 @@ class FlashnetClient {
|
|
|
2048
2245
|
btcAmountReceived: btcReceived,
|
|
2049
2246
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2050
2247
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2248
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2051
2249
|
error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
|
|
2052
2250
|
};
|
|
2053
2251
|
}
|
|
@@ -2059,6 +2257,7 @@ class FlashnetClient {
|
|
|
2059
2257
|
btcAmountReceived: btcReceived,
|
|
2060
2258
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2061
2259
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2260
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2062
2261
|
error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
|
|
2063
2262
|
};
|
|
2064
2263
|
}
|
|
@@ -2119,7 +2318,7 @@ class FlashnetClient {
|
|
|
2119
2318
|
* Find the best pool for swapping a token to BTC
|
|
2120
2319
|
* @private
|
|
2121
2320
|
*/
|
|
2122
|
-
async findBestPoolForTokenToBtc(tokenAddress,
|
|
2321
|
+
async findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded, integratorFeeRateBps) {
|
|
2123
2322
|
const tokenHex = this.toHexTokenIdentifier(tokenAddress);
|
|
2124
2323
|
const btcHex = index$1.BTC_ASSET_PUBKEY;
|
|
2125
2324
|
// Find all pools that have this token paired with BTC
|
|
@@ -2163,8 +2362,8 @@ class FlashnetClient {
|
|
|
2163
2362
|
const minAmounts = await this.getMinAmountsMap();
|
|
2164
2363
|
const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
|
|
2165
2364
|
// Check if the BTC amount needed is below the minimum
|
|
2166
|
-
if (btcMinAmount && BigInt(
|
|
2167
|
-
const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${
|
|
2365
|
+
if (btcMinAmount && BigInt(baseBtcNeeded) < btcMinAmount) {
|
|
2366
|
+
const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${baseBtcNeeded} sats.`;
|
|
2168
2367
|
throw new errors.FlashnetError(msg, {
|
|
2169
2368
|
response: {
|
|
2170
2369
|
errorCode: "FSAG-1003",
|
|
@@ -2178,60 +2377,99 @@ class FlashnetClient {
|
|
|
2178
2377
|
},
|
|
2179
2378
|
});
|
|
2180
2379
|
}
|
|
2380
|
+
// Compute V2 masked BTC amount (round up to next multiple of 64 for bit masking)
|
|
2381
|
+
const baseBtc = BigInt(baseBtcNeeded);
|
|
2382
|
+
const BTC_VARIABLE_FEE_BITS = 6n;
|
|
2383
|
+
const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
|
|
2384
|
+
const maskedBtc = ((baseBtc + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
|
|
2385
|
+
BTC_VARIABLE_FEE_MASK;
|
|
2181
2386
|
// Find the best pool (lowest token cost for the required BTC)
|
|
2182
2387
|
let bestPool = null;
|
|
2183
2388
|
let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
|
|
2389
|
+
let bestBtcTarget = 0n;
|
|
2390
|
+
let bestCurveType = "";
|
|
2184
2391
|
let bestSimulation = null;
|
|
2185
2392
|
// Track errors for each pool to provide better diagnostics
|
|
2186
2393
|
const poolErrors = [];
|
|
2187
2394
|
for (const pool of allPools) {
|
|
2188
2395
|
try {
|
|
2189
|
-
// Get pool details for reserves
|
|
2396
|
+
// Get pool details for reserves and curve type
|
|
2190
2397
|
const poolDetails = await this.getPool(pool.lpPublicKey);
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
|
|
2398
|
+
const isV3 = poolDetails.curveType === "V3_CONCENTRATED";
|
|
2399
|
+
// V3 pools use exact BTC amount, V2 pools use masked amount
|
|
2400
|
+
const btcTarget = isV3 ? baseBtc : maskedBtc;
|
|
2401
|
+
const assetInAddress = pool.tokenIsAssetA
|
|
2402
|
+
? poolDetails.assetAAddress
|
|
2403
|
+
: poolDetails.assetBAddress;
|
|
2404
|
+
const assetOutAddress = pool.tokenIsAssetA
|
|
2405
|
+
? poolDetails.assetBAddress
|
|
2406
|
+
: poolDetails.assetAAddress;
|
|
2407
|
+
let tokenAmount;
|
|
2408
|
+
let fee;
|
|
2409
|
+
let executionPrice;
|
|
2410
|
+
let priceImpactPct;
|
|
2411
|
+
let warningMessage;
|
|
2412
|
+
if (isV3) {
|
|
2413
|
+
// V3: binary search with simulateSwap
|
|
2414
|
+
const v3Result = await this.findV3TokenAmountForBtcOutput({
|
|
2415
|
+
poolId: pool.lpPublicKey,
|
|
2416
|
+
assetInAddress,
|
|
2417
|
+
assetOutAddress,
|
|
2418
|
+
desiredBtcOut: btcTarget,
|
|
2419
|
+
currentPriceAInB: poolDetails.currentPriceAInB,
|
|
2420
|
+
tokenIsAssetA: pool.tokenIsAssetA,
|
|
2421
|
+
integratorBps: integratorFeeRateBps,
|
|
2422
|
+
});
|
|
2423
|
+
tokenAmount = FlashnetClient.safeBigInt(v3Result.amountIn);
|
|
2424
|
+
fee = v3Result.totalFee;
|
|
2425
|
+
executionPrice = v3Result.simulation.executionPrice || "0";
|
|
2426
|
+
priceImpactPct = v3Result.simulation.priceImpactPct || "0";
|
|
2427
|
+
warningMessage = v3Result.simulation.warningMessage;
|
|
2428
|
+
}
|
|
2429
|
+
else {
|
|
2430
|
+
// V2: constant product math + simulation verification
|
|
2431
|
+
const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
|
|
2432
|
+
tokenAmount = FlashnetClient.safeBigInt(calculation.amountIn);
|
|
2196
2433
|
// Verify with simulation
|
|
2197
2434
|
const simulation = await this.simulateSwap({
|
|
2198
2435
|
poolId: pool.lpPublicKey,
|
|
2199
|
-
assetInAddress
|
|
2200
|
-
|
|
2201
|
-
: poolDetails.assetBAddress,
|
|
2202
|
-
assetOutAddress: pool.tokenIsAssetA
|
|
2203
|
-
? poolDetails.assetBAddress
|
|
2204
|
-
: poolDetails.assetAAddress,
|
|
2436
|
+
assetInAddress,
|
|
2437
|
+
assetOutAddress,
|
|
2205
2438
|
amountIn: calculation.amountIn,
|
|
2206
2439
|
integratorBps: integratorFeeRateBps,
|
|
2207
2440
|
});
|
|
2208
|
-
|
|
2209
|
-
if (BigInt(simulation.amountOut) >= BigInt(btcAmountNeeded)) {
|
|
2210
|
-
bestPool = pool;
|
|
2211
|
-
bestTokenAmount = tokenAmount;
|
|
2212
|
-
bestSimulation = {
|
|
2213
|
-
amountIn: calculation.amountIn,
|
|
2214
|
-
fee: calculation.totalFee,
|
|
2215
|
-
executionPrice: simulation.executionPrice || "0",
|
|
2216
|
-
priceImpactPct: simulation.priceImpactPct || "0",
|
|
2217
|
-
warningMessage: simulation.warningMessage,
|
|
2218
|
-
};
|
|
2219
|
-
}
|
|
2220
|
-
else {
|
|
2221
|
-
// Simulation output was insufficient
|
|
2441
|
+
if (FlashnetClient.safeBigInt(simulation.amountOut) < btcTarget) {
|
|
2222
2442
|
const btcReserve = pool.tokenIsAssetA
|
|
2223
2443
|
? poolDetails.assetBReserve
|
|
2224
2444
|
: poolDetails.assetAReserve;
|
|
2225
2445
|
poolErrors.push({
|
|
2226
2446
|
poolId: pool.lpPublicKey,
|
|
2227
|
-
error: `Simulation output (${simulation.amountOut} sats) < required (${
|
|
2447
|
+
error: `Simulation output (${simulation.amountOut} sats) < required (${btcTarget} sats)`,
|
|
2228
2448
|
btcReserve,
|
|
2229
2449
|
});
|
|
2450
|
+
continue;
|
|
2230
2451
|
}
|
|
2452
|
+
fee = calculation.totalFee;
|
|
2453
|
+
executionPrice = simulation.executionPrice || "0";
|
|
2454
|
+
priceImpactPct = simulation.priceImpactPct || "0";
|
|
2455
|
+
warningMessage = simulation.warningMessage;
|
|
2456
|
+
}
|
|
2457
|
+
// Check if this pool offers a better rate
|
|
2458
|
+
if (tokenAmount < bestTokenAmount) {
|
|
2459
|
+
bestPool = pool;
|
|
2460
|
+
bestTokenAmount = tokenAmount;
|
|
2461
|
+
bestBtcTarget = btcTarget;
|
|
2462
|
+
bestCurveType = poolDetails.curveType;
|
|
2463
|
+
bestSimulation = {
|
|
2464
|
+
amountIn: tokenAmount.toString(),
|
|
2465
|
+
fee,
|
|
2466
|
+
executionPrice,
|
|
2467
|
+
priceImpactPct,
|
|
2468
|
+
warningMessage,
|
|
2469
|
+
};
|
|
2231
2470
|
}
|
|
2232
2471
|
}
|
|
2233
2472
|
catch (e) {
|
|
2234
|
-
// Capture pool errors for diagnostics
|
|
2235
2473
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2236
2474
|
poolErrors.push({
|
|
2237
2475
|
poolId: pool.lpPublicKey,
|
|
@@ -2240,7 +2478,7 @@ class FlashnetClient {
|
|
|
2240
2478
|
}
|
|
2241
2479
|
}
|
|
2242
2480
|
if (!bestPool || !bestSimulation) {
|
|
2243
|
-
let errorMessage = `No pool has sufficient liquidity for ${
|
|
2481
|
+
let errorMessage = `No pool has sufficient liquidity for ${baseBtcNeeded} sats`;
|
|
2244
2482
|
if (poolErrors.length > 0) {
|
|
2245
2483
|
const details = poolErrors
|
|
2246
2484
|
.map((pe) => {
|
|
@@ -2278,6 +2516,8 @@ class FlashnetClient {
|
|
|
2278
2516
|
assetBReserve: poolDetails.assetBReserve,
|
|
2279
2517
|
},
|
|
2280
2518
|
warningMessage: bestSimulation.warningMessage,
|
|
2519
|
+
btcAmountUsed: bestBtcTarget.toString(),
|
|
2520
|
+
curveType: bestCurveType,
|
|
2281
2521
|
};
|
|
2282
2522
|
}
|
|
2283
2523
|
/**
|
|
@@ -2286,9 +2526,9 @@ class FlashnetClient {
|
|
|
2286
2526
|
* @private
|
|
2287
2527
|
*/
|
|
2288
2528
|
calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
|
|
2289
|
-
const amountOut =
|
|
2290
|
-
const resA =
|
|
2291
|
-
const resB =
|
|
2529
|
+
const amountOut = FlashnetClient.safeBigInt(btcAmountOut);
|
|
2530
|
+
const resA = FlashnetClient.safeBigInt(reserveA);
|
|
2531
|
+
const resB = FlashnetClient.safeBigInt(reserveB);
|
|
2292
2532
|
const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
|
|
2293
2533
|
const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
|
|
2294
2534
|
// Token is the input asset
|
|
@@ -2345,12 +2585,133 @@ class FlashnetClient {
|
|
|
2345
2585
|
};
|
|
2346
2586
|
}
|
|
2347
2587
|
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Find the token amount needed to get a specific BTC output from a V3 concentrated liquidity pool.
|
|
2590
|
+
* Uses binary search with simulateSwap since V3 tick-based math can't be inverted locally.
|
|
2591
|
+
* @private
|
|
2592
|
+
*/
|
|
2593
|
+
async findV3TokenAmountForBtcOutput(params) {
|
|
2594
|
+
const { poolId, assetInAddress, assetOutAddress, desiredBtcOut, currentPriceAInB, tokenIsAssetA, integratorBps, } = params;
|
|
2595
|
+
// Step 1: Compute initial estimate from pool price
|
|
2596
|
+
let estimate;
|
|
2597
|
+
if (currentPriceAInB && currentPriceAInB !== "0") {
|
|
2598
|
+
const price = Number(currentPriceAInB);
|
|
2599
|
+
if (tokenIsAssetA) {
|
|
2600
|
+
// priceAInB = how much B (BTC) per 1 A (token), so tokenNeeded = btcOut / price
|
|
2601
|
+
estimate = BigInt(Math.ceil(Number(desiredBtcOut) / price));
|
|
2602
|
+
}
|
|
2603
|
+
else {
|
|
2604
|
+
// priceAInB = how much B (token) per 1 A (BTC), so tokenNeeded = btcOut * price
|
|
2605
|
+
estimate = BigInt(Math.ceil(Number(desiredBtcOut) * price));
|
|
2606
|
+
}
|
|
2607
|
+
// Ensure non-zero
|
|
2608
|
+
if (estimate <= 0n) {
|
|
2609
|
+
estimate = desiredBtcOut * 2n;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
else {
|
|
2613
|
+
estimate = desiredBtcOut * 2n;
|
|
2614
|
+
}
|
|
2615
|
+
// Step 2: Find upper bound by simulating with estimate + 10% buffer
|
|
2616
|
+
let upperBound = (estimate * 110n) / 100n;
|
|
2617
|
+
if (upperBound <= 0n) {
|
|
2618
|
+
upperBound = 1n;
|
|
2619
|
+
}
|
|
2620
|
+
let upperSim = null;
|
|
2621
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2622
|
+
const sim = await this.simulateSwap({
|
|
2623
|
+
poolId,
|
|
2624
|
+
assetInAddress,
|
|
2625
|
+
assetOutAddress,
|
|
2626
|
+
amountIn: upperBound.toString(),
|
|
2627
|
+
integratorBps,
|
|
2628
|
+
});
|
|
2629
|
+
if (FlashnetClient.safeBigInt(sim.amountOut) >= desiredBtcOut) {
|
|
2630
|
+
upperSim = sim;
|
|
2631
|
+
break;
|
|
2632
|
+
}
|
|
2633
|
+
// Double the upper bound
|
|
2634
|
+
upperBound = upperBound * 2n;
|
|
2635
|
+
}
|
|
2636
|
+
if (!upperSim) {
|
|
2637
|
+
throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
|
|
2638
|
+
}
|
|
2639
|
+
// Step 3: Refine estimate via linear interpolation
|
|
2640
|
+
const upperOut = FlashnetClient.safeBigInt(upperSim.amountOut);
|
|
2641
|
+
// Scale proportionally: if upperBound produced upperOut, we need roughly
|
|
2642
|
+
// (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
|
|
2643
|
+
let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
|
|
2644
|
+
if (refined <= 0n) {
|
|
2645
|
+
refined = 1n;
|
|
2646
|
+
}
|
|
2647
|
+
let bestAmountIn = upperBound;
|
|
2648
|
+
let bestSim = upperSim;
|
|
2649
|
+
// Check if the refined estimate is tighter
|
|
2650
|
+
if (refined < upperBound) {
|
|
2651
|
+
const refinedSim = await this.simulateSwap({
|
|
2652
|
+
poolId,
|
|
2653
|
+
assetInAddress,
|
|
2654
|
+
assetOutAddress,
|
|
2655
|
+
amountIn: refined.toString(),
|
|
2656
|
+
integratorBps,
|
|
2657
|
+
});
|
|
2658
|
+
if (FlashnetClient.safeBigInt(refinedSim.amountOut) >= desiredBtcOut) {
|
|
2659
|
+
bestAmountIn = refined;
|
|
2660
|
+
bestSim = refinedSim;
|
|
2661
|
+
}
|
|
2662
|
+
else {
|
|
2663
|
+
// Refined estimate was slightly too low. Keep upperBound as best,
|
|
2664
|
+
// and let binary search narrow between refined (too low) and upperBound (sufficient).
|
|
2665
|
+
bestAmountIn = upperBound;
|
|
2666
|
+
bestSim = upperSim;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
// Step 4: Binary search to converge on minimum amountIn
|
|
2670
|
+
// Use a tight range: the interpolation is close, so search between 99.5% and 100% of best
|
|
2671
|
+
let lo = bestAmountIn === upperBound
|
|
2672
|
+
? refined < upperBound
|
|
2673
|
+
? refined
|
|
2674
|
+
: (bestAmountIn * 99n) / 100n
|
|
2675
|
+
: (bestAmountIn * 999n) / 1000n;
|
|
2676
|
+
if (lo <= 0n) {
|
|
2677
|
+
lo = 1n;
|
|
2678
|
+
}
|
|
2679
|
+
let hi = bestAmountIn;
|
|
2680
|
+
for (let i = 0; i < 6; i++) {
|
|
2681
|
+
if (hi - lo <= 1n) {
|
|
2682
|
+
break;
|
|
2683
|
+
}
|
|
2684
|
+
const mid = (lo + hi) / 2n;
|
|
2685
|
+
const midSim = await this.simulateSwap({
|
|
2686
|
+
poolId,
|
|
2687
|
+
assetInAddress,
|
|
2688
|
+
assetOutAddress,
|
|
2689
|
+
amountIn: mid.toString(),
|
|
2690
|
+
integratorBps,
|
|
2691
|
+
});
|
|
2692
|
+
if (FlashnetClient.safeBigInt(midSim.amountOut) >= desiredBtcOut) {
|
|
2693
|
+
hi = mid;
|
|
2694
|
+
bestAmountIn = mid;
|
|
2695
|
+
bestSim = midSim;
|
|
2696
|
+
}
|
|
2697
|
+
else {
|
|
2698
|
+
lo = mid;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
// Compute fee from the best simulation
|
|
2702
|
+
const totalFee = bestSim.feePaidAssetIn || "0";
|
|
2703
|
+
return {
|
|
2704
|
+
amountIn: bestAmountIn.toString(),
|
|
2705
|
+
totalFee,
|
|
2706
|
+
simulation: bestSim,
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2348
2709
|
/**
|
|
2349
2710
|
* Calculate minimum amount out with slippage protection
|
|
2350
2711
|
* @private
|
|
2351
2712
|
*/
|
|
2352
2713
|
calculateMinAmountOut(expectedAmount, slippageBps) {
|
|
2353
|
-
const amount =
|
|
2714
|
+
const amount = FlashnetClient.safeBigInt(expectedAmount);
|
|
2354
2715
|
const slippageFactor = BigInt(10000 - slippageBps);
|
|
2355
2716
|
const minAmount = (amount * slippageFactor) / 10000n;
|
|
2356
2717
|
return minAmount.toString();
|
|
@@ -2569,8 +2930,11 @@ class FlashnetClient {
|
|
|
2569
2930
|
const map = new Map();
|
|
2570
2931
|
for (const item of config) {
|
|
2571
2932
|
if (item.enabled) {
|
|
2933
|
+
if (item.min_amount == null) {
|
|
2934
|
+
continue;
|
|
2935
|
+
}
|
|
2572
2936
|
const key = item.asset_identifier.toLowerCase();
|
|
2573
|
-
const value =
|
|
2937
|
+
const value = FlashnetClient.safeBigInt(item.min_amount);
|
|
2574
2938
|
map.set(key, value);
|
|
2575
2939
|
}
|
|
2576
2940
|
}
|
|
@@ -2592,8 +2956,8 @@ class FlashnetClient {
|
|
|
2592
2956
|
const outHex = this.getHexAddress(params.assetOutAddress);
|
|
2593
2957
|
const minIn = minMap.get(inHex);
|
|
2594
2958
|
const minOut = minMap.get(outHex);
|
|
2595
|
-
const amountIn =
|
|
2596
|
-
const minAmountOut =
|
|
2959
|
+
const amountIn = FlashnetClient.safeBigInt(params.amountIn);
|
|
2960
|
+
const minAmountOut = FlashnetClient.safeBigInt(params.minAmountOut);
|
|
2597
2961
|
if (minIn && minOut) {
|
|
2598
2962
|
if (amountIn < minIn) {
|
|
2599
2963
|
throw new Error(`Minimum amount not met for input asset. Required \
|
|
@@ -2627,13 +2991,13 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
2627
2991
|
const aMin = minMap.get(aHex);
|
|
2628
2992
|
const bMin = minMap.get(bHex);
|
|
2629
2993
|
if (aMin) {
|
|
2630
|
-
const aAmt =
|
|
2994
|
+
const aAmt = FlashnetClient.safeBigInt(params.assetAAmount);
|
|
2631
2995
|
if (aAmt < aMin) {
|
|
2632
2996
|
throw new Error(`Minimum amount not met for Asset A. Required ${aMin.toString()}, provided ${aAmt.toString()}`);
|
|
2633
2997
|
}
|
|
2634
2998
|
}
|
|
2635
2999
|
if (bMin) {
|
|
2636
|
-
const bAmt =
|
|
3000
|
+
const bAmt = FlashnetClient.safeBigInt(params.assetBAmount);
|
|
2637
3001
|
if (bAmt < bMin) {
|
|
2638
3002
|
throw new Error(`Minimum amount not met for Asset B. Required ${bMin.toString()}, provided ${bAmt.toString()}`);
|
|
2639
3003
|
}
|
|
@@ -2655,14 +3019,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
2655
3019
|
const aMin = minMap.get(aHex);
|
|
2656
3020
|
const bMin = minMap.get(bHex);
|
|
2657
3021
|
if (aMin) {
|
|
2658
|
-
const predictedAOut =
|
|
3022
|
+
const predictedAOut = FlashnetClient.safeBigInt(simulation.assetAAmount);
|
|
2659
3023
|
const relaxedA = aMin / 2n; // apply 50% relaxation for outputs
|
|
2660
3024
|
if (predictedAOut < relaxedA) {
|
|
2661
3025
|
throw new Error(`Minimum amount not met for Asset A on withdrawal. Required at least ${relaxedA.toString()} (50% relaxed), predicted ${predictedAOut.toString()}`);
|
|
2662
3026
|
}
|
|
2663
3027
|
}
|
|
2664
3028
|
if (bMin) {
|
|
2665
|
-
const predictedBOut =
|
|
3029
|
+
const predictedBOut = FlashnetClient.safeBigInt(simulation.assetBAmount);
|
|
2666
3030
|
const relaxedB = bMin / 2n;
|
|
2667
3031
|
if (predictedBOut < relaxedB) {
|
|
2668
3032
|
throw new Error(`Minimum amount not met for Asset B on withdrawal. Required at least ${relaxedB.toString()} (50% relaxed), predicted ${predictedBOut.toString()}`);
|
|
@@ -2775,20 +3139,20 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
2775
3139
|
const transferIds = [];
|
|
2776
3140
|
// Transfer assets if not using free balance
|
|
2777
3141
|
if (!params.useFreeBalance) {
|
|
2778
|
-
if (
|
|
3142
|
+
if (FlashnetClient.safeBigInt(params.amountADesired) > 0n) {
|
|
2779
3143
|
assetATransferId = await this.transferAsset({
|
|
2780
3144
|
receiverSparkAddress: lpSparkAddress,
|
|
2781
3145
|
assetAddress: pool.assetAAddress,
|
|
2782
3146
|
amount: params.amountADesired,
|
|
2783
|
-
}, "Insufficient balance for adding V3 liquidity (Asset A): ");
|
|
3147
|
+
}, "Insufficient balance for adding V3 liquidity (Asset A): ", params.useAvailableBalance);
|
|
2784
3148
|
transferIds.push(assetATransferId);
|
|
2785
3149
|
}
|
|
2786
|
-
if (
|
|
3150
|
+
if (FlashnetClient.safeBigInt(params.amountBDesired) > 0n) {
|
|
2787
3151
|
assetBTransferId = await this.transferAsset({
|
|
2788
3152
|
receiverSparkAddress: lpSparkAddress,
|
|
2789
3153
|
assetAddress: pool.assetBAddress,
|
|
2790
3154
|
amount: params.amountBDesired,
|
|
2791
|
-
}, "Insufficient balance for adding V3 liquidity (Asset B): ");
|
|
3155
|
+
}, "Insufficient balance for adding V3 liquidity (Asset B): ", params.useAvailableBalance);
|
|
2792
3156
|
transferIds.push(assetBTransferId);
|
|
2793
3157
|
}
|
|
2794
3158
|
}
|
|
@@ -2981,14 +3345,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
2981
3345
|
receiverSparkAddress: lpSparkAddress,
|
|
2982
3346
|
assetAddress: pool.assetAAddress,
|
|
2983
3347
|
amount: params.additionalAmountA,
|
|
2984
|
-
}, "Insufficient balance for rebalance (Asset A): ");
|
|
3348
|
+
}, "Insufficient balance for rebalance (Asset A): ", params.useAvailableBalance);
|
|
2985
3349
|
}
|
|
2986
3350
|
if (params.additionalAmountB && BigInt(params.additionalAmountB) > 0n) {
|
|
2987
3351
|
assetBTransferId = await this.transferAsset({
|
|
2988
3352
|
receiverSparkAddress: lpSparkAddress,
|
|
2989
3353
|
assetAddress: pool.assetBAddress,
|
|
2990
3354
|
amount: params.additionalAmountB,
|
|
2991
|
-
}, "Insufficient balance for rebalance (Asset B): ");
|
|
3355
|
+
}, "Insufficient balance for rebalance (Asset B): ", params.useAvailableBalance);
|
|
2992
3356
|
}
|
|
2993
3357
|
// Collect transfer IDs for potential clawback
|
|
2994
3358
|
const transferIds = [];
|
|
@@ -3180,20 +3544,20 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
3180
3544
|
let assetBTransferId = "";
|
|
3181
3545
|
const transferIds = [];
|
|
3182
3546
|
// Transfer assets to pool
|
|
3183
|
-
if (
|
|
3547
|
+
if (FlashnetClient.safeBigInt(params.amountA) > 0n) {
|
|
3184
3548
|
assetATransferId = await this.transferAsset({
|
|
3185
3549
|
receiverSparkAddress: lpSparkAddress,
|
|
3186
3550
|
assetAddress: pool.assetAAddress,
|
|
3187
3551
|
amount: params.amountA,
|
|
3188
|
-
}, "Insufficient balance for depositing to V3 pool (Asset A): ");
|
|
3552
|
+
}, "Insufficient balance for depositing to V3 pool (Asset A): ", params.useAvailableBalance);
|
|
3189
3553
|
transferIds.push(assetATransferId);
|
|
3190
3554
|
}
|
|
3191
|
-
if (
|
|
3555
|
+
if (FlashnetClient.safeBigInt(params.amountB) > 0n) {
|
|
3192
3556
|
assetBTransferId = await this.transferAsset({
|
|
3193
3557
|
receiverSparkAddress: lpSparkAddress,
|
|
3194
3558
|
assetAddress: pool.assetBAddress,
|
|
3195
3559
|
amount: params.amountB,
|
|
3196
|
-
}, "Insufficient balance for depositing to V3 pool (Asset B): ");
|
|
3560
|
+
}, "Insufficient balance for depositing to V3 pool (Asset B): ", params.useAvailableBalance);
|
|
3197
3561
|
transferIds.push(assetBTransferId);
|
|
3198
3562
|
}
|
|
3199
3563
|
const executeDeposit = async () => {
|