@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
|
@@ -9,6 +9,7 @@ import { getHexFromUint8Array } from '../utils/hex.js';
|
|
|
9
9
|
import { generateConstantProductPoolInitializationIntentMessage, generatePoolInitializationIntentMessage, generatePoolConfirmInitialDepositIntentMessage, generatePoolSwapIntentMessage, generateRouteSwapIntentMessage, generateAddLiquidityIntentMessage, generateRemoveLiquidityIntentMessage, generateRegisterHostIntentMessage, generateWithdrawHostFeesIntentMessage, generateWithdrawIntegratorFeesIntentMessage, generateCreateEscrowIntentMessage, generateFundEscrowIntentMessage, generateClaimEscrowIntentMessage, generateClawbackIntentMessage, generateCreateConcentratedPoolIntentMessage, generateDecreaseLiquidityIntentMessage, generateCollectFeesIntentMessage, generateWithdrawBalanceIntentMessage, generateIncreaseLiquidityIntentMessage, generateRebalancePositionIntentMessage, generateDepositBalanceIntentMessage } from '../utils/intents.js';
|
|
10
10
|
import { getSparkNetworkFromAddress, encodeSparkAddressNew } from '../utils/spark-address.js';
|
|
11
11
|
import { encodeSparkHumanReadableTokenIdentifier, decodeSparkHumanReadableTokenIdentifier } from '../utils/tokenAddress.js';
|
|
12
|
+
import { safeBigInt } from '../utils/bigint.js';
|
|
12
13
|
import { FlashnetError } from '../types/errors.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -239,7 +240,8 @@ class FlashnetClient {
|
|
|
239
240
|
const tokenIdentifierHex = getHexFromUint8Array(info.rawTokenIdentifier);
|
|
240
241
|
const tokenAddress = encodeSparkHumanReadableTokenIdentifier(info.rawTokenIdentifier, this.sparkNetwork);
|
|
241
242
|
tokenBalances.set(tokenPubkey, {
|
|
242
|
-
balance:
|
|
243
|
+
balance: FlashnetClient.safeBigInt(tokenData.ownedBalance),
|
|
244
|
+
availableToSendBalance: FlashnetClient.safeBigInt(tokenData.availableToSendBalance),
|
|
243
245
|
tokenInfo: {
|
|
244
246
|
tokenIdentifier: tokenIdentifierHex,
|
|
245
247
|
tokenAddress,
|
|
@@ -252,7 +254,7 @@ class FlashnetClient {
|
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
return {
|
|
255
|
-
balance:
|
|
257
|
+
balance: FlashnetClient.safeBigInt(balance.balance),
|
|
256
258
|
tokenBalances,
|
|
257
259
|
};
|
|
258
260
|
}
|
|
@@ -260,17 +262,17 @@ class FlashnetClient {
|
|
|
260
262
|
* Check if wallet has sufficient balance for an operation
|
|
261
263
|
*/
|
|
262
264
|
async checkBalance(params) {
|
|
263
|
-
const balance = await this.getBalance();
|
|
265
|
+
const balance = params.walletBalance ?? (await this.getBalance());
|
|
264
266
|
// Check balance
|
|
265
267
|
const requirements = {
|
|
266
268
|
tokens: new Map(),
|
|
267
269
|
};
|
|
268
270
|
for (const balance of params.balancesToCheck) {
|
|
269
271
|
if (balance.assetAddress === BTC_ASSET_PUBKEY) {
|
|
270
|
-
requirements.btc =
|
|
272
|
+
requirements.btc = FlashnetClient.safeBigInt(balance.amount);
|
|
271
273
|
}
|
|
272
274
|
else {
|
|
273
|
-
requirements.tokens?.set(balance.assetAddress,
|
|
275
|
+
requirements.tokens?.set(balance.assetAddress, FlashnetClient.safeBigInt(balance.amount));
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
// Check BTC balance
|
|
@@ -288,7 +290,9 @@ class FlashnetClient {
|
|
|
288
290
|
const hrKey = this.toHumanReadableTokenIdentifier(tokenPubkey);
|
|
289
291
|
const effectiveTokenBalance = balance.tokenBalances.get(tokenPubkey) ??
|
|
290
292
|
balance.tokenBalances.get(hrKey);
|
|
291
|
-
const available =
|
|
293
|
+
const available = params.useAvailableBalance
|
|
294
|
+
? (effectiveTokenBalance?.availableToSendBalance ?? 0n)
|
|
295
|
+
: (effectiveTokenBalance?.balance ?? 0n);
|
|
292
296
|
if (available < requiredAmount) {
|
|
293
297
|
throw new Error([
|
|
294
298
|
params.errorPrefix ?? "",
|
|
@@ -350,6 +354,7 @@ class FlashnetClient {
|
|
|
350
354
|
},
|
|
351
355
|
],
|
|
352
356
|
errorPrefix: "Insufficient balance for initial liquidity: ",
|
|
357
|
+
useAvailableBalance: params.useAvailableBalance,
|
|
353
358
|
});
|
|
354
359
|
}
|
|
355
360
|
const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
|
|
@@ -409,6 +414,12 @@ class FlashnetClient {
|
|
|
409
414
|
throw new Error(`${name} must be positive integer`);
|
|
410
415
|
}
|
|
411
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Safely convert a value to BigInt. Delegates to the shared `safeBigInt` utility.
|
|
419
|
+
*/
|
|
420
|
+
static safeBigInt(value, fallback = 0n) {
|
|
421
|
+
return safeBigInt(value, fallback);
|
|
422
|
+
}
|
|
412
423
|
/**
|
|
413
424
|
* Calculates virtual reserves for a bonding curve AMM.
|
|
414
425
|
*
|
|
@@ -429,7 +440,7 @@ class FlashnetClient {
|
|
|
429
440
|
}
|
|
430
441
|
const supply = FlashnetClient.parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
|
|
431
442
|
const targetB = FlashnetClient.parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
|
|
432
|
-
const graduationThresholdPct =
|
|
443
|
+
const graduationThresholdPct = FlashnetClient.safeBigInt(params.graduationThresholdPct);
|
|
433
444
|
// Align bounds with Rust AMM (20%..95%), then check feasibility for g=1 (requires >50%).
|
|
434
445
|
const MIN_PCT = 20n;
|
|
435
446
|
const MAX_PCT = 95n;
|
|
@@ -479,6 +490,7 @@ class FlashnetClient {
|
|
|
479
490
|
},
|
|
480
491
|
],
|
|
481
492
|
errorPrefix: "Insufficient balance for pool creation: ",
|
|
493
|
+
useAvailableBalance: params.useAvailableBalance,
|
|
482
494
|
});
|
|
483
495
|
const poolOwnerPublicKey = params.poolOwnerPublicKey ?? this.publicKey;
|
|
484
496
|
// Generate intent
|
|
@@ -599,10 +611,14 @@ class FlashnetClient {
|
|
|
599
611
|
});
|
|
600
612
|
// If using free balance (V3 pools only), skip the Spark transfer
|
|
601
613
|
if (params.useFreeBalance) {
|
|
602
|
-
|
|
614
|
+
const swapResponse = await this.executeSwapIntent({
|
|
603
615
|
...params,
|
|
604
616
|
// No transferId - triggers free balance mode
|
|
605
617
|
});
|
|
618
|
+
return {
|
|
619
|
+
...swapResponse,
|
|
620
|
+
inboundSparkTransferId: swapResponse.requestId,
|
|
621
|
+
};
|
|
606
622
|
}
|
|
607
623
|
// Transfer assets to pool using new address encoding
|
|
608
624
|
const lpSparkAddress = encodeSparkAddressNew({
|
|
@@ -613,12 +629,13 @@ class FlashnetClient {
|
|
|
613
629
|
receiverSparkAddress: lpSparkAddress,
|
|
614
630
|
assetAddress: params.assetInAddress,
|
|
615
631
|
amount: params.amountIn,
|
|
616
|
-
}, "Insufficient balance for swap: ");
|
|
632
|
+
}, "Insufficient balance for swap: ", params.useAvailableBalance);
|
|
617
633
|
// Execute with auto-clawback on failure
|
|
618
|
-
|
|
634
|
+
const swapResponse = await this.executeWithAutoClawback(() => this.executeSwapIntent({
|
|
619
635
|
...params,
|
|
620
636
|
transferId,
|
|
621
637
|
}), [transferId], params.poolId);
|
|
638
|
+
return { ...swapResponse, inboundSparkTransferId: transferId };
|
|
622
639
|
}
|
|
623
640
|
/**
|
|
624
641
|
* Execute a swap with a pre-created transfer or using free balance.
|
|
@@ -748,7 +765,7 @@ class FlashnetClient {
|
|
|
748
765
|
receiverSparkAddress: lpSparkAddress,
|
|
749
766
|
assetAddress: params.initialAssetAddress,
|
|
750
767
|
amount: params.inputAmount,
|
|
751
|
-
}, "Insufficient balance for route swap: ");
|
|
768
|
+
}, "Insufficient balance for route swap: ", params.useAvailableBalance);
|
|
752
769
|
// Execute with auto-clawback on failure
|
|
753
770
|
return this.executeWithAutoClawback(async () => {
|
|
754
771
|
// Prepare hops for validation
|
|
@@ -871,7 +888,7 @@ class FlashnetClient {
|
|
|
871
888
|
assetAddress: pool.assetBAddress,
|
|
872
889
|
amount: params.assetBAmount,
|
|
873
890
|
},
|
|
874
|
-
], "Insufficient balance for adding liquidity: ");
|
|
891
|
+
], "Insufficient balance for adding liquidity: ", params.useAvailableBalance);
|
|
875
892
|
// Execute with auto-clawback on failure
|
|
876
893
|
return this.executeWithAutoClawback(async () => {
|
|
877
894
|
// Generate add liquidity intent
|
|
@@ -1177,6 +1194,7 @@ class FlashnetClient {
|
|
|
1177
1194
|
depositAddress: createResponse.depositAddress,
|
|
1178
1195
|
assetId: params.assetId,
|
|
1179
1196
|
assetAmount: params.assetAmount,
|
|
1197
|
+
useAvailableBalance: params.useAvailableBalance,
|
|
1180
1198
|
});
|
|
1181
1199
|
}
|
|
1182
1200
|
/**
|
|
@@ -1194,6 +1212,7 @@ class FlashnetClient {
|
|
|
1194
1212
|
{ assetAddress: params.assetId, amount: params.assetAmount },
|
|
1195
1213
|
],
|
|
1196
1214
|
errorPrefix: "Insufficient balance to fund escrow: ",
|
|
1215
|
+
useAvailableBalance: params.useAvailableBalance,
|
|
1197
1216
|
});
|
|
1198
1217
|
// 2. Perform transfer
|
|
1199
1218
|
const escrowSparkAddress = encodeSparkAddressNew({
|
|
@@ -1708,19 +1727,20 @@ class FlashnetClient {
|
|
|
1708
1727
|
/**
|
|
1709
1728
|
* Performs asset transfer using generalized asset address for both BTC and tokens.
|
|
1710
1729
|
*/
|
|
1711
|
-
async transferAsset(recipient, checkBalanceErrorPrefix) {
|
|
1712
|
-
const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix);
|
|
1730
|
+
async transferAsset(recipient, checkBalanceErrorPrefix, useAvailableBalance) {
|
|
1731
|
+
const transferIds = await this.transferAssets([recipient], checkBalanceErrorPrefix, useAvailableBalance);
|
|
1713
1732
|
return transferIds[0];
|
|
1714
1733
|
}
|
|
1715
1734
|
/**
|
|
1716
1735
|
* Performs asset transfers using generalized asset addresses for both BTC and tokens.
|
|
1717
1736
|
* Supports optional generic to hardcode recipients length so output list can be typed with same length.
|
|
1718
1737
|
*/
|
|
1719
|
-
async transferAssets(recipients, checkBalanceErrorPrefix) {
|
|
1738
|
+
async transferAssets(recipients, checkBalanceErrorPrefix, useAvailableBalance) {
|
|
1720
1739
|
if (checkBalanceErrorPrefix) {
|
|
1721
1740
|
await this.checkBalance({
|
|
1722
1741
|
balancesToCheck: recipients,
|
|
1723
1742
|
errorPrefix: checkBalanceErrorPrefix,
|
|
1743
|
+
useAvailableBalance,
|
|
1724
1744
|
});
|
|
1725
1745
|
}
|
|
1726
1746
|
const transferIds = [];
|
|
@@ -1735,7 +1755,7 @@ class FlashnetClient {
|
|
|
1735
1755
|
else {
|
|
1736
1756
|
const transferId = await this._wallet.transferTokens({
|
|
1737
1757
|
tokenIdentifier: this.toHumanReadableTokenIdentifier(recipient.assetAddress),
|
|
1738
|
-
tokenAmount:
|
|
1758
|
+
tokenAmount: FlashnetClient.safeBigInt(recipient.amount),
|
|
1739
1759
|
receiverSparkAddress: recipient.receiverSparkAddress,
|
|
1740
1760
|
});
|
|
1741
1761
|
transferIds.push(transferId);
|
|
@@ -1819,38 +1839,37 @@ class FlashnetClient {
|
|
|
1819
1839
|
await this.ensureInitialized();
|
|
1820
1840
|
// Decode the invoice to get the amount
|
|
1821
1841
|
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
1842
|
+
// Zero-amount invoice: forward-direction quoting using caller-specified tokenAmount
|
|
1822
1843
|
if (!invoiceAmountSats || invoiceAmountSats <= 0) {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1844
|
+
const tokenAmount = options?.tokenAmount;
|
|
1845
|
+
if (!tokenAmount || BigInt(tokenAmount) <= 0n) {
|
|
1846
|
+
throw new FlashnetError("Zero-amount invoice requires tokenAmount in options.", {
|
|
1847
|
+
response: {
|
|
1848
|
+
errorCode: "FSAG-1002",
|
|
1849
|
+
errorCategory: "Validation",
|
|
1850
|
+
message: "Zero-amount invoice requires tokenAmount in options.",
|
|
1851
|
+
requestId: "",
|
|
1852
|
+
timestamp: new Date().toISOString(),
|
|
1853
|
+
service: "sdk",
|
|
1854
|
+
severity: "Error",
|
|
1855
|
+
remediation: "Provide tokenAmount when using a zero-amount invoice.",
|
|
1856
|
+
},
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
return this.getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options);
|
|
1835
1860
|
}
|
|
1836
1861
|
// Get Lightning fee estimate
|
|
1837
1862
|
const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
|
|
1838
|
-
// Total BTC needed = invoice amount + lightning fee
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
// that after masking we still have the required amount.
|
|
1843
|
-
// Example: 5014 -> masked to 4992, but if we request 5056, masked stays 5056
|
|
1844
|
-
const BTC_VARIABLE_FEE_BITS = 6n;
|
|
1845
|
-
const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
|
|
1846
|
-
const totalBtcNeeded = ((baseBtcNeeded + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
|
|
1847
|
-
BTC_VARIABLE_FEE_MASK;
|
|
1863
|
+
// Total BTC needed = invoice amount + lightning fee (unmasked).
|
|
1864
|
+
// Bitmasking for V2 pools is handled inside findBestPoolForTokenToBtc.
|
|
1865
|
+
const baseBtcNeeded = FlashnetClient.safeBigInt(invoiceAmountSats) +
|
|
1866
|
+
FlashnetClient.safeBigInt(lightningFeeEstimate);
|
|
1848
1867
|
// Check Flashnet minimum amounts early to provide clear error messages
|
|
1849
1868
|
const minAmounts = await this.getEnabledMinAmountsMap();
|
|
1850
1869
|
// Check BTC minimum (output from swap)
|
|
1851
1870
|
const btcMinAmount = minAmounts.get(BTC_ASSET_PUBKEY.toLowerCase());
|
|
1852
|
-
if (btcMinAmount &&
|
|
1853
|
-
const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${
|
|
1871
|
+
if (btcMinAmount && baseBtcNeeded < btcMinAmount) {
|
|
1872
|
+
const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${baseBtcNeeded} sats.`;
|
|
1854
1873
|
throw new FlashnetError(msg, {
|
|
1855
1874
|
response: {
|
|
1856
1875
|
errorCode: "FSAG-1003",
|
|
@@ -1864,13 +1883,14 @@ class FlashnetClient {
|
|
|
1864
1883
|
},
|
|
1865
1884
|
});
|
|
1866
1885
|
}
|
|
1867
|
-
// Find the best pool to swap token -> BTC
|
|
1868
|
-
|
|
1886
|
+
// Find the best pool to swap token -> BTC.
|
|
1887
|
+
// Bitmasking is applied per-pool inside this function (V2 pools get masked, V3 pools don't).
|
|
1888
|
+
const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded.toString(), options?.integratorFeeRateBps);
|
|
1869
1889
|
// Check token minimum (input to swap)
|
|
1870
1890
|
const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
|
|
1871
1891
|
const tokenMinAmount = minAmounts.get(tokenHex);
|
|
1872
1892
|
if (tokenMinAmount &&
|
|
1873
|
-
|
|
1893
|
+
FlashnetClient.safeBigInt(poolQuote.tokenAmountRequired) < tokenMinAmount) {
|
|
1874
1894
|
const msg = `Token amount too small. Minimum input is ${tokenMinAmount} units, but calculated amount is only ${poolQuote.tokenAmountRequired} units.`;
|
|
1875
1895
|
throw new FlashnetError(msg, {
|
|
1876
1896
|
response: {
|
|
@@ -1885,13 +1905,14 @@ class FlashnetClient {
|
|
|
1885
1905
|
},
|
|
1886
1906
|
});
|
|
1887
1907
|
}
|
|
1888
|
-
//
|
|
1889
|
-
|
|
1908
|
+
// BTC variable fee adjustment: difference between what the pool targets and unmasked base.
|
|
1909
|
+
// For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
|
|
1910
|
+
const btcVariableFeeAdjustment = Number(FlashnetClient.safeBigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
|
|
1890
1911
|
return {
|
|
1891
1912
|
poolId: poolQuote.poolId,
|
|
1892
1913
|
tokenAddress: this.toHexTokenIdentifier(tokenAddress),
|
|
1893
1914
|
tokenAmountRequired: poolQuote.tokenAmountRequired,
|
|
1894
|
-
btcAmountRequired:
|
|
1915
|
+
btcAmountRequired: poolQuote.btcAmountUsed,
|
|
1895
1916
|
invoiceAmountSats: invoiceAmountSats,
|
|
1896
1917
|
estimatedAmmFee: poolQuote.estimatedAmmFee,
|
|
1897
1918
|
estimatedLightningFee: lightningFeeEstimate,
|
|
@@ -1901,6 +1922,138 @@ class FlashnetClient {
|
|
|
1901
1922
|
tokenIsAssetA: poolQuote.tokenIsAssetA,
|
|
1902
1923
|
poolReserves: poolQuote.poolReserves,
|
|
1903
1924
|
warningMessage: poolQuote.warningMessage,
|
|
1925
|
+
curveType: poolQuote.curveType,
|
|
1926
|
+
isZeroAmountInvoice: false,
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Generate a quote for a zero-amount invoice.
|
|
1931
|
+
* Forward-direction: simulate swapping tokenAmount and pick the pool with the best BTC output.
|
|
1932
|
+
* @private
|
|
1933
|
+
*/
|
|
1934
|
+
async getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options) {
|
|
1935
|
+
const tokenHex = this.toHexTokenIdentifier(tokenAddress);
|
|
1936
|
+
const btcHex = BTC_ASSET_PUBKEY;
|
|
1937
|
+
// Discover all token/BTC pools
|
|
1938
|
+
const [poolsWithTokenAsA, poolsWithTokenAsB] = await Promise.all([
|
|
1939
|
+
this.listPools({ assetAAddress: tokenHex, assetBAddress: btcHex }),
|
|
1940
|
+
this.listPools({ assetAAddress: btcHex, assetBAddress: tokenHex }),
|
|
1941
|
+
]);
|
|
1942
|
+
const poolMap = new Map();
|
|
1943
|
+
for (const p of [...poolsWithTokenAsA.pools, ...poolsWithTokenAsB.pools]) {
|
|
1944
|
+
if (!poolMap.has(p.lpPublicKey)) {
|
|
1945
|
+
const tokenIsAssetA = p.assetAAddress?.toLowerCase() === tokenHex.toLowerCase();
|
|
1946
|
+
poolMap.set(p.lpPublicKey, { pool: p, tokenIsAssetA });
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
const allPools = Array.from(poolMap.values());
|
|
1950
|
+
if (allPools.length === 0) {
|
|
1951
|
+
throw new FlashnetError(`No liquidity pool found for token ${tokenAddress} paired with BTC`, {
|
|
1952
|
+
response: {
|
|
1953
|
+
errorCode: "FSAG-4001",
|
|
1954
|
+
errorCategory: "Business",
|
|
1955
|
+
message: `No liquidity pool found for token ${tokenAddress} paired with BTC`,
|
|
1956
|
+
requestId: "",
|
|
1957
|
+
timestamp: new Date().toISOString(),
|
|
1958
|
+
service: "sdk",
|
|
1959
|
+
severity: "Error",
|
|
1960
|
+
},
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
// Simulate each pool with tokenAmount as input, pick highest BTC output
|
|
1964
|
+
let bestResult = null;
|
|
1965
|
+
let bestBtcOut = 0n;
|
|
1966
|
+
for (const { pool, tokenIsAssetA } of allPools) {
|
|
1967
|
+
try {
|
|
1968
|
+
const poolDetails = await this.getPool(pool.lpPublicKey);
|
|
1969
|
+
const assetInAddress = tokenIsAssetA
|
|
1970
|
+
? poolDetails.assetAAddress
|
|
1971
|
+
: poolDetails.assetBAddress;
|
|
1972
|
+
const assetOutAddress = tokenIsAssetA
|
|
1973
|
+
? poolDetails.assetBAddress
|
|
1974
|
+
: poolDetails.assetAAddress;
|
|
1975
|
+
const simulation = await this.simulateSwap({
|
|
1976
|
+
poolId: pool.lpPublicKey,
|
|
1977
|
+
assetInAddress,
|
|
1978
|
+
assetOutAddress,
|
|
1979
|
+
amountIn: tokenAmount,
|
|
1980
|
+
integratorBps: options?.integratorFeeRateBps,
|
|
1981
|
+
});
|
|
1982
|
+
const btcOut = FlashnetClient.safeBigInt(simulation.amountOut);
|
|
1983
|
+
if (btcOut > bestBtcOut) {
|
|
1984
|
+
bestBtcOut = btcOut;
|
|
1985
|
+
bestResult = {
|
|
1986
|
+
poolId: pool.lpPublicKey,
|
|
1987
|
+
tokenIsAssetA,
|
|
1988
|
+
simulation,
|
|
1989
|
+
curveType: poolDetails.curveType,
|
|
1990
|
+
poolReserves: {
|
|
1991
|
+
assetAReserve: poolDetails.assetAReserve,
|
|
1992
|
+
assetBReserve: poolDetails.assetBReserve,
|
|
1993
|
+
},
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
catch {
|
|
1998
|
+
// Skip pools that fail simulation
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
if (!bestResult || bestBtcOut <= 0n) {
|
|
2002
|
+
throw new FlashnetError("No pool can produce BTC output for the given token amount", {
|
|
2003
|
+
response: {
|
|
2004
|
+
errorCode: "FSAG-4201",
|
|
2005
|
+
errorCategory: "Business",
|
|
2006
|
+
message: "No pool can produce BTC output for the given token amount",
|
|
2007
|
+
requestId: "",
|
|
2008
|
+
timestamp: new Date().toISOString(),
|
|
2009
|
+
service: "sdk",
|
|
2010
|
+
severity: "Error",
|
|
2011
|
+
remediation: "Try a larger token amount.",
|
|
2012
|
+
},
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
// Estimate lightning fee from the BTC output
|
|
2016
|
+
let lightningFeeEstimate;
|
|
2017
|
+
try {
|
|
2018
|
+
lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
|
|
2019
|
+
}
|
|
2020
|
+
catch {
|
|
2021
|
+
lightningFeeEstimate = Math.max(5, Math.ceil(Number(bestBtcOut) * 0.0017));
|
|
2022
|
+
}
|
|
2023
|
+
// Check minimum amounts
|
|
2024
|
+
const minAmounts = await this.getEnabledMinAmountsMap();
|
|
2025
|
+
const btcMinAmount = minAmounts.get(BTC_ASSET_PUBKEY.toLowerCase());
|
|
2026
|
+
if (btcMinAmount && bestBtcOut < btcMinAmount) {
|
|
2027
|
+
const msg = `BTC output too small. Minimum is ${btcMinAmount} sats, but swap would produce only ${bestBtcOut} sats.`;
|
|
2028
|
+
throw new FlashnetError(msg, {
|
|
2029
|
+
response: {
|
|
2030
|
+
errorCode: "FSAG-1003",
|
|
2031
|
+
errorCategory: "Validation",
|
|
2032
|
+
message: msg,
|
|
2033
|
+
requestId: "",
|
|
2034
|
+
timestamp: new Date().toISOString(),
|
|
2035
|
+
service: "sdk",
|
|
2036
|
+
severity: "Error",
|
|
2037
|
+
remediation: "Use a larger token amount.",
|
|
2038
|
+
},
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
return {
|
|
2042
|
+
poolId: bestResult.poolId,
|
|
2043
|
+
tokenAddress: tokenHex,
|
|
2044
|
+
tokenAmountRequired: tokenAmount,
|
|
2045
|
+
btcAmountRequired: bestBtcOut.toString(),
|
|
2046
|
+
invoiceAmountSats: 0,
|
|
2047
|
+
estimatedAmmFee: bestResult.simulation.feePaidAssetIn || "0",
|
|
2048
|
+
estimatedLightningFee: lightningFeeEstimate,
|
|
2049
|
+
btcVariableFeeAdjustment: 0,
|
|
2050
|
+
executionPrice: bestResult.simulation.executionPrice || "0",
|
|
2051
|
+
priceImpactPct: bestResult.simulation.priceImpactPct || "0",
|
|
2052
|
+
tokenIsAssetA: bestResult.tokenIsAssetA,
|
|
2053
|
+
poolReserves: bestResult.poolReserves,
|
|
2054
|
+
warningMessage: bestResult.simulation.warningMessage,
|
|
2055
|
+
curveType: bestResult.curveType,
|
|
2056
|
+
isZeroAmountInvoice: true,
|
|
1904
2057
|
};
|
|
1905
2058
|
}
|
|
1906
2059
|
/**
|
|
@@ -1912,14 +2065,15 @@ class FlashnetClient {
|
|
|
1912
2065
|
*/
|
|
1913
2066
|
async payLightningWithToken(options) {
|
|
1914
2067
|
await this.ensureInitialized();
|
|
1915
|
-
const { invoice, tokenAddress, maxSlippageBps = 500, // 5% default
|
|
2068
|
+
const { invoice, tokenAddress, tokenAmount, maxSlippageBps = 500, // 5% default
|
|
1916
2069
|
maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 30000, // 30s default
|
|
1917
|
-
rollbackOnFailure = false, useExistingBtcBalance = false, } = options;
|
|
2070
|
+
rollbackOnFailure = false, useExistingBtcBalance = false, useAvailableBalance = false, } = options;
|
|
1918
2071
|
try {
|
|
1919
2072
|
// Step 1: Get a quote for the payment
|
|
1920
2073
|
const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
|
|
1921
2074
|
maxSlippageBps,
|
|
1922
2075
|
integratorFeeRateBps,
|
|
2076
|
+
tokenAmount,
|
|
1923
2077
|
});
|
|
1924
2078
|
// Step 2: Check token balance (always required)
|
|
1925
2079
|
await this.checkBalance({
|
|
@@ -1930,17 +2084,8 @@ class FlashnetClient {
|
|
|
1930
2084
|
},
|
|
1931
2085
|
],
|
|
1932
2086
|
errorPrefix: "Insufficient token balance for Lightning payment: ",
|
|
2087
|
+
useAvailableBalance,
|
|
1933
2088
|
});
|
|
1934
|
-
// Determine if we can pay immediately using existing BTC balance
|
|
1935
|
-
let canPayImmediately = false;
|
|
1936
|
-
if (useExistingBtcBalance) {
|
|
1937
|
-
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
1938
|
-
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
1939
|
-
const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
|
|
1940
|
-
// Check if we have enough BTC (don't throw if not, just fall back to waiting)
|
|
1941
|
-
const balance = await this.getBalance();
|
|
1942
|
-
canPayImmediately = balance.balance >= BigInt(btcNeededForPayment);
|
|
1943
|
-
}
|
|
1944
2089
|
// Step 3: Get pool details
|
|
1945
2090
|
const pool = await this.getPool(quote.poolId);
|
|
1946
2091
|
// Step 4: Determine swap direction and execute
|
|
@@ -1962,6 +2107,7 @@ class FlashnetClient {
|
|
|
1962
2107
|
minAmountOut: minBtcOut,
|
|
1963
2108
|
integratorFeeRateBps,
|
|
1964
2109
|
integratorPublicKey,
|
|
2110
|
+
useAvailableBalance,
|
|
1965
2111
|
});
|
|
1966
2112
|
if (!swapResponse.accepted || !swapResponse.outboundTransferId) {
|
|
1967
2113
|
return {
|
|
@@ -1971,10 +2117,20 @@ class FlashnetClient {
|
|
|
1971
2117
|
btcAmountReceived: "0",
|
|
1972
2118
|
swapTransferId: swapResponse.outboundTransferId || "",
|
|
1973
2119
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2120
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
1974
2121
|
error: swapResponse.error || "Swap was not accepted",
|
|
1975
2122
|
};
|
|
1976
2123
|
}
|
|
1977
|
-
// Step 5: Wait for
|
|
2124
|
+
// Step 5: Wait for transfer (skip useExistingBtcBalance for zero-amount invoices)
|
|
2125
|
+
let canPayImmediately = false;
|
|
2126
|
+
if (!quote.isZeroAmountInvoice && useExistingBtcBalance) {
|
|
2127
|
+
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
2128
|
+
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
2129
|
+
const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
|
|
2130
|
+
const balance = await this.getBalance();
|
|
2131
|
+
canPayImmediately =
|
|
2132
|
+
balance.balance >= FlashnetClient.safeBigInt(btcNeededForPayment);
|
|
2133
|
+
}
|
|
1978
2134
|
if (!canPayImmediately) {
|
|
1979
2135
|
const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
|
|
1980
2136
|
if (!transferComplete) {
|
|
@@ -1985,20 +2141,57 @@ class FlashnetClient {
|
|
|
1985
2141
|
btcAmountReceived: swapResponse.amountOut || "0",
|
|
1986
2142
|
swapTransferId: swapResponse.outboundTransferId,
|
|
1987
2143
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2144
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
1988
2145
|
error: "Transfer did not complete within timeout",
|
|
1989
2146
|
};
|
|
1990
2147
|
}
|
|
1991
2148
|
}
|
|
1992
|
-
// Step 6: Calculate Lightning fee
|
|
2149
|
+
// Step 6: Calculate Lightning fee and payment amount
|
|
1993
2150
|
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
1994
|
-
// Step 7: Pay the Lightning invoice
|
|
1995
2151
|
const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
|
|
2152
|
+
// Step 7: Pay the Lightning invoice
|
|
1996
2153
|
try {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2154
|
+
let lightningPayment;
|
|
2155
|
+
let invoiceAmountPaid;
|
|
2156
|
+
if (quote.isZeroAmountInvoice) {
|
|
2157
|
+
// Zero-amount invoice: pay whatever BTC we received minus lightning fee
|
|
2158
|
+
const actualBtc = FlashnetClient.safeBigInt(btcReceived);
|
|
2159
|
+
const lnFee = FlashnetClient.safeBigInt(effectiveMaxLightningFee);
|
|
2160
|
+
const amountToPay = actualBtc - lnFee;
|
|
2161
|
+
if (amountToPay <= 0n) {
|
|
2162
|
+
return {
|
|
2163
|
+
success: false,
|
|
2164
|
+
poolId: quote.poolId,
|
|
2165
|
+
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2166
|
+
btcAmountReceived: btcReceived,
|
|
2167
|
+
swapTransferId: swapResponse.outboundTransferId,
|
|
2168
|
+
ammFeePaid: quote.estimatedAmmFee,
|
|
2169
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2170
|
+
error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
invoiceAmountPaid = Number(amountToPay);
|
|
2174
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2175
|
+
invoice,
|
|
2176
|
+
amountSats: invoiceAmountPaid,
|
|
2177
|
+
maxFeeSats: effectiveMaxLightningFee,
|
|
2178
|
+
preferSpark,
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
else {
|
|
2182
|
+
// Standard invoice: pay the specified amount
|
|
2183
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2184
|
+
invoice,
|
|
2185
|
+
maxFeeSats: effectiveMaxLightningFee,
|
|
2186
|
+
preferSpark,
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
// Extract the Spark transfer ID from the lightning payment result.
|
|
2190
|
+
// payLightningInvoice returns LightningSendRequest | WalletTransfer:
|
|
2191
|
+
// - LightningSendRequest has .transfer?.sparkId (the Sparkscan-visible transfer ID)
|
|
2192
|
+
// - WalletTransfer (Spark-to-Spark) has .id directly as the transfer ID
|
|
2193
|
+
// Note: lightningPayment.id (the SSP request ID) is already returned as lightningPaymentId
|
|
2194
|
+
const sparkLightningTransferId = lightningPayment.transfer?.sparkId;
|
|
2002
2195
|
return {
|
|
2003
2196
|
success: true,
|
|
2004
2197
|
poolId: quote.poolId,
|
|
@@ -2008,6 +2201,9 @@ class FlashnetClient {
|
|
|
2008
2201
|
lightningPaymentId: lightningPayment.id,
|
|
2009
2202
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2010
2203
|
lightningFeePaid: effectiveMaxLightningFee,
|
|
2204
|
+
invoiceAmountPaid,
|
|
2205
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2206
|
+
sparkLightningTransferId,
|
|
2011
2207
|
};
|
|
2012
2208
|
}
|
|
2013
2209
|
catch (lightningError) {
|
|
@@ -2027,6 +2223,7 @@ class FlashnetClient {
|
|
|
2027
2223
|
btcAmountReceived: "0",
|
|
2028
2224
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2029
2225
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2226
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2030
2227
|
error: `Lightning payment failed: ${lightningErrorMessage}. Funds rolled back to ${rollbackResult.tokenAmount} tokens.`,
|
|
2031
2228
|
};
|
|
2032
2229
|
}
|
|
@@ -2042,6 +2239,7 @@ class FlashnetClient {
|
|
|
2042
2239
|
btcAmountReceived: btcReceived,
|
|
2043
2240
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2044
2241
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2242
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2045
2243
|
error: `Lightning payment failed: ${lightningErrorMessage}. Rollback also failed: ${rollbackErrorMessage}. BTC remains in wallet.`,
|
|
2046
2244
|
};
|
|
2047
2245
|
}
|
|
@@ -2053,6 +2251,7 @@ class FlashnetClient {
|
|
|
2053
2251
|
btcAmountReceived: btcReceived,
|
|
2054
2252
|
swapTransferId: swapResponse.outboundTransferId,
|
|
2055
2253
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2254
|
+
sparkTokenTransferId: swapResponse.inboundSparkTransferId,
|
|
2056
2255
|
error: `Lightning payment failed: ${lightningErrorMessage}. BTC (${btcReceived} sats) remains in wallet.`,
|
|
2057
2256
|
};
|
|
2058
2257
|
}
|
|
@@ -2113,7 +2312,7 @@ class FlashnetClient {
|
|
|
2113
2312
|
* Find the best pool for swapping a token to BTC
|
|
2114
2313
|
* @private
|
|
2115
2314
|
*/
|
|
2116
|
-
async findBestPoolForTokenToBtc(tokenAddress,
|
|
2315
|
+
async findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded, integratorFeeRateBps) {
|
|
2117
2316
|
const tokenHex = this.toHexTokenIdentifier(tokenAddress);
|
|
2118
2317
|
const btcHex = BTC_ASSET_PUBKEY;
|
|
2119
2318
|
// Find all pools that have this token paired with BTC
|
|
@@ -2157,8 +2356,8 @@ class FlashnetClient {
|
|
|
2157
2356
|
const minAmounts = await this.getMinAmountsMap();
|
|
2158
2357
|
const btcMinAmount = minAmounts.get(BTC_ASSET_PUBKEY.toLowerCase());
|
|
2159
2358
|
// Check if the BTC amount needed is below the minimum
|
|
2160
|
-
if (btcMinAmount && BigInt(
|
|
2161
|
-
const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${
|
|
2359
|
+
if (btcMinAmount && BigInt(baseBtcNeeded) < btcMinAmount) {
|
|
2360
|
+
const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${baseBtcNeeded} sats.`;
|
|
2162
2361
|
throw new FlashnetError(msg, {
|
|
2163
2362
|
response: {
|
|
2164
2363
|
errorCode: "FSAG-1003",
|
|
@@ -2172,60 +2371,99 @@ class FlashnetClient {
|
|
|
2172
2371
|
},
|
|
2173
2372
|
});
|
|
2174
2373
|
}
|
|
2374
|
+
// Compute V2 masked BTC amount (round up to next multiple of 64 for bit masking)
|
|
2375
|
+
const baseBtc = BigInt(baseBtcNeeded);
|
|
2376
|
+
const BTC_VARIABLE_FEE_BITS = 6n;
|
|
2377
|
+
const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
|
|
2378
|
+
const maskedBtc = ((baseBtc + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
|
|
2379
|
+
BTC_VARIABLE_FEE_MASK;
|
|
2175
2380
|
// Find the best pool (lowest token cost for the required BTC)
|
|
2176
2381
|
let bestPool = null;
|
|
2177
2382
|
let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
|
|
2383
|
+
let bestBtcTarget = 0n;
|
|
2384
|
+
let bestCurveType = "";
|
|
2178
2385
|
let bestSimulation = null;
|
|
2179
2386
|
// Track errors for each pool to provide better diagnostics
|
|
2180
2387
|
const poolErrors = [];
|
|
2181
2388
|
for (const pool of allPools) {
|
|
2182
2389
|
try {
|
|
2183
|
-
// Get pool details for reserves
|
|
2390
|
+
// Get pool details for reserves and curve type
|
|
2184
2391
|
const poolDetails = await this.getPool(pool.lpPublicKey);
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2392
|
+
const isV3 = poolDetails.curveType === "V3_CONCENTRATED";
|
|
2393
|
+
// V3 pools use exact BTC amount, V2 pools use masked amount
|
|
2394
|
+
const btcTarget = isV3 ? baseBtc : maskedBtc;
|
|
2395
|
+
const assetInAddress = pool.tokenIsAssetA
|
|
2396
|
+
? poolDetails.assetAAddress
|
|
2397
|
+
: poolDetails.assetBAddress;
|
|
2398
|
+
const assetOutAddress = pool.tokenIsAssetA
|
|
2399
|
+
? poolDetails.assetBAddress
|
|
2400
|
+
: poolDetails.assetAAddress;
|
|
2401
|
+
let tokenAmount;
|
|
2402
|
+
let fee;
|
|
2403
|
+
let executionPrice;
|
|
2404
|
+
let priceImpactPct;
|
|
2405
|
+
let warningMessage;
|
|
2406
|
+
if (isV3) {
|
|
2407
|
+
// V3: binary search with simulateSwap
|
|
2408
|
+
const v3Result = await this.findV3TokenAmountForBtcOutput({
|
|
2409
|
+
poolId: pool.lpPublicKey,
|
|
2410
|
+
assetInAddress,
|
|
2411
|
+
assetOutAddress,
|
|
2412
|
+
desiredBtcOut: btcTarget,
|
|
2413
|
+
currentPriceAInB: poolDetails.currentPriceAInB,
|
|
2414
|
+
tokenIsAssetA: pool.tokenIsAssetA,
|
|
2415
|
+
integratorBps: integratorFeeRateBps,
|
|
2416
|
+
});
|
|
2417
|
+
tokenAmount = FlashnetClient.safeBigInt(v3Result.amountIn);
|
|
2418
|
+
fee = v3Result.totalFee;
|
|
2419
|
+
executionPrice = v3Result.simulation.executionPrice || "0";
|
|
2420
|
+
priceImpactPct = v3Result.simulation.priceImpactPct || "0";
|
|
2421
|
+
warningMessage = v3Result.simulation.warningMessage;
|
|
2422
|
+
}
|
|
2423
|
+
else {
|
|
2424
|
+
// V2: constant product math + simulation verification
|
|
2425
|
+
const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
|
|
2426
|
+
tokenAmount = FlashnetClient.safeBigInt(calculation.amountIn);
|
|
2190
2427
|
// Verify with simulation
|
|
2191
2428
|
const simulation = await this.simulateSwap({
|
|
2192
2429
|
poolId: pool.lpPublicKey,
|
|
2193
|
-
assetInAddress
|
|
2194
|
-
|
|
2195
|
-
: poolDetails.assetBAddress,
|
|
2196
|
-
assetOutAddress: pool.tokenIsAssetA
|
|
2197
|
-
? poolDetails.assetBAddress
|
|
2198
|
-
: poolDetails.assetAAddress,
|
|
2430
|
+
assetInAddress,
|
|
2431
|
+
assetOutAddress,
|
|
2199
2432
|
amountIn: calculation.amountIn,
|
|
2200
2433
|
integratorBps: integratorFeeRateBps,
|
|
2201
2434
|
});
|
|
2202
|
-
|
|
2203
|
-
if (BigInt(simulation.amountOut) >= BigInt(btcAmountNeeded)) {
|
|
2204
|
-
bestPool = pool;
|
|
2205
|
-
bestTokenAmount = tokenAmount;
|
|
2206
|
-
bestSimulation = {
|
|
2207
|
-
amountIn: calculation.amountIn,
|
|
2208
|
-
fee: calculation.totalFee,
|
|
2209
|
-
executionPrice: simulation.executionPrice || "0",
|
|
2210
|
-
priceImpactPct: simulation.priceImpactPct || "0",
|
|
2211
|
-
warningMessage: simulation.warningMessage,
|
|
2212
|
-
};
|
|
2213
|
-
}
|
|
2214
|
-
else {
|
|
2215
|
-
// Simulation output was insufficient
|
|
2435
|
+
if (FlashnetClient.safeBigInt(simulation.amountOut) < btcTarget) {
|
|
2216
2436
|
const btcReserve = pool.tokenIsAssetA
|
|
2217
2437
|
? poolDetails.assetBReserve
|
|
2218
2438
|
: poolDetails.assetAReserve;
|
|
2219
2439
|
poolErrors.push({
|
|
2220
2440
|
poolId: pool.lpPublicKey,
|
|
2221
|
-
error: `Simulation output (${simulation.amountOut} sats) < required (${
|
|
2441
|
+
error: `Simulation output (${simulation.amountOut} sats) < required (${btcTarget} sats)`,
|
|
2222
2442
|
btcReserve,
|
|
2223
2443
|
});
|
|
2444
|
+
continue;
|
|
2224
2445
|
}
|
|
2446
|
+
fee = calculation.totalFee;
|
|
2447
|
+
executionPrice = simulation.executionPrice || "0";
|
|
2448
|
+
priceImpactPct = simulation.priceImpactPct || "0";
|
|
2449
|
+
warningMessage = simulation.warningMessage;
|
|
2450
|
+
}
|
|
2451
|
+
// Check if this pool offers a better rate
|
|
2452
|
+
if (tokenAmount < bestTokenAmount) {
|
|
2453
|
+
bestPool = pool;
|
|
2454
|
+
bestTokenAmount = tokenAmount;
|
|
2455
|
+
bestBtcTarget = btcTarget;
|
|
2456
|
+
bestCurveType = poolDetails.curveType;
|
|
2457
|
+
bestSimulation = {
|
|
2458
|
+
amountIn: tokenAmount.toString(),
|
|
2459
|
+
fee,
|
|
2460
|
+
executionPrice,
|
|
2461
|
+
priceImpactPct,
|
|
2462
|
+
warningMessage,
|
|
2463
|
+
};
|
|
2225
2464
|
}
|
|
2226
2465
|
}
|
|
2227
2466
|
catch (e) {
|
|
2228
|
-
// Capture pool errors for diagnostics
|
|
2229
2467
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2230
2468
|
poolErrors.push({
|
|
2231
2469
|
poolId: pool.lpPublicKey,
|
|
@@ -2234,7 +2472,7 @@ class FlashnetClient {
|
|
|
2234
2472
|
}
|
|
2235
2473
|
}
|
|
2236
2474
|
if (!bestPool || !bestSimulation) {
|
|
2237
|
-
let errorMessage = `No pool has sufficient liquidity for ${
|
|
2475
|
+
let errorMessage = `No pool has sufficient liquidity for ${baseBtcNeeded} sats`;
|
|
2238
2476
|
if (poolErrors.length > 0) {
|
|
2239
2477
|
const details = poolErrors
|
|
2240
2478
|
.map((pe) => {
|
|
@@ -2272,6 +2510,8 @@ class FlashnetClient {
|
|
|
2272
2510
|
assetBReserve: poolDetails.assetBReserve,
|
|
2273
2511
|
},
|
|
2274
2512
|
warningMessage: bestSimulation.warningMessage,
|
|
2513
|
+
btcAmountUsed: bestBtcTarget.toString(),
|
|
2514
|
+
curveType: bestCurveType,
|
|
2275
2515
|
};
|
|
2276
2516
|
}
|
|
2277
2517
|
/**
|
|
@@ -2280,9 +2520,9 @@ class FlashnetClient {
|
|
|
2280
2520
|
* @private
|
|
2281
2521
|
*/
|
|
2282
2522
|
calculateTokenAmountForBtcOutput(btcAmountOut, reserveA, reserveB, lpFeeBps, hostFeeBps, tokenIsAssetA, integratorFeeBps) {
|
|
2283
|
-
const amountOut =
|
|
2284
|
-
const resA =
|
|
2285
|
-
const resB =
|
|
2523
|
+
const amountOut = FlashnetClient.safeBigInt(btcAmountOut);
|
|
2524
|
+
const resA = FlashnetClient.safeBigInt(reserveA);
|
|
2525
|
+
const resB = FlashnetClient.safeBigInt(reserveB);
|
|
2286
2526
|
const totalFeeBps = lpFeeBps + hostFeeBps + (integratorFeeBps || 0);
|
|
2287
2527
|
const feeRate = Number(totalFeeBps) / 10000; // Convert bps to decimal
|
|
2288
2528
|
// Token is the input asset
|
|
@@ -2339,12 +2579,133 @@ class FlashnetClient {
|
|
|
2339
2579
|
};
|
|
2340
2580
|
}
|
|
2341
2581
|
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Find the token amount needed to get a specific BTC output from a V3 concentrated liquidity pool.
|
|
2584
|
+
* Uses binary search with simulateSwap since V3 tick-based math can't be inverted locally.
|
|
2585
|
+
* @private
|
|
2586
|
+
*/
|
|
2587
|
+
async findV3TokenAmountForBtcOutput(params) {
|
|
2588
|
+
const { poolId, assetInAddress, assetOutAddress, desiredBtcOut, currentPriceAInB, tokenIsAssetA, integratorBps, } = params;
|
|
2589
|
+
// Step 1: Compute initial estimate from pool price
|
|
2590
|
+
let estimate;
|
|
2591
|
+
if (currentPriceAInB && currentPriceAInB !== "0") {
|
|
2592
|
+
const price = Number(currentPriceAInB);
|
|
2593
|
+
if (tokenIsAssetA) {
|
|
2594
|
+
// priceAInB = how much B (BTC) per 1 A (token), so tokenNeeded = btcOut / price
|
|
2595
|
+
estimate = BigInt(Math.ceil(Number(desiredBtcOut) / price));
|
|
2596
|
+
}
|
|
2597
|
+
else {
|
|
2598
|
+
// priceAInB = how much B (token) per 1 A (BTC), so tokenNeeded = btcOut * price
|
|
2599
|
+
estimate = BigInt(Math.ceil(Number(desiredBtcOut) * price));
|
|
2600
|
+
}
|
|
2601
|
+
// Ensure non-zero
|
|
2602
|
+
if (estimate <= 0n) {
|
|
2603
|
+
estimate = desiredBtcOut * 2n;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
else {
|
|
2607
|
+
estimate = desiredBtcOut * 2n;
|
|
2608
|
+
}
|
|
2609
|
+
// Step 2: Find upper bound by simulating with estimate + 10% buffer
|
|
2610
|
+
let upperBound = (estimate * 110n) / 100n;
|
|
2611
|
+
if (upperBound <= 0n) {
|
|
2612
|
+
upperBound = 1n;
|
|
2613
|
+
}
|
|
2614
|
+
let upperSim = null;
|
|
2615
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2616
|
+
const sim = await this.simulateSwap({
|
|
2617
|
+
poolId,
|
|
2618
|
+
assetInAddress,
|
|
2619
|
+
assetOutAddress,
|
|
2620
|
+
amountIn: upperBound.toString(),
|
|
2621
|
+
integratorBps,
|
|
2622
|
+
});
|
|
2623
|
+
if (FlashnetClient.safeBigInt(sim.amountOut) >= desiredBtcOut) {
|
|
2624
|
+
upperSim = sim;
|
|
2625
|
+
break;
|
|
2626
|
+
}
|
|
2627
|
+
// Double the upper bound
|
|
2628
|
+
upperBound = upperBound * 2n;
|
|
2629
|
+
}
|
|
2630
|
+
if (!upperSim) {
|
|
2631
|
+
throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
|
|
2632
|
+
}
|
|
2633
|
+
// Step 3: Refine estimate via linear interpolation
|
|
2634
|
+
const upperOut = FlashnetClient.safeBigInt(upperSim.amountOut);
|
|
2635
|
+
// Scale proportionally: if upperBound produced upperOut, we need roughly
|
|
2636
|
+
// (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
|
|
2637
|
+
let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
|
|
2638
|
+
if (refined <= 0n) {
|
|
2639
|
+
refined = 1n;
|
|
2640
|
+
}
|
|
2641
|
+
let bestAmountIn = upperBound;
|
|
2642
|
+
let bestSim = upperSim;
|
|
2643
|
+
// Check if the refined estimate is tighter
|
|
2644
|
+
if (refined < upperBound) {
|
|
2645
|
+
const refinedSim = await this.simulateSwap({
|
|
2646
|
+
poolId,
|
|
2647
|
+
assetInAddress,
|
|
2648
|
+
assetOutAddress,
|
|
2649
|
+
amountIn: refined.toString(),
|
|
2650
|
+
integratorBps,
|
|
2651
|
+
});
|
|
2652
|
+
if (FlashnetClient.safeBigInt(refinedSim.amountOut) >= desiredBtcOut) {
|
|
2653
|
+
bestAmountIn = refined;
|
|
2654
|
+
bestSim = refinedSim;
|
|
2655
|
+
}
|
|
2656
|
+
else {
|
|
2657
|
+
// Refined estimate was slightly too low. Keep upperBound as best,
|
|
2658
|
+
// and let binary search narrow between refined (too low) and upperBound (sufficient).
|
|
2659
|
+
bestAmountIn = upperBound;
|
|
2660
|
+
bestSim = upperSim;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
// Step 4: Binary search to converge on minimum amountIn
|
|
2664
|
+
// Use a tight range: the interpolation is close, so search between 99.5% and 100% of best
|
|
2665
|
+
let lo = bestAmountIn === upperBound
|
|
2666
|
+
? refined < upperBound
|
|
2667
|
+
? refined
|
|
2668
|
+
: (bestAmountIn * 99n) / 100n
|
|
2669
|
+
: (bestAmountIn * 999n) / 1000n;
|
|
2670
|
+
if (lo <= 0n) {
|
|
2671
|
+
lo = 1n;
|
|
2672
|
+
}
|
|
2673
|
+
let hi = bestAmountIn;
|
|
2674
|
+
for (let i = 0; i < 6; i++) {
|
|
2675
|
+
if (hi - lo <= 1n) {
|
|
2676
|
+
break;
|
|
2677
|
+
}
|
|
2678
|
+
const mid = (lo + hi) / 2n;
|
|
2679
|
+
const midSim = await this.simulateSwap({
|
|
2680
|
+
poolId,
|
|
2681
|
+
assetInAddress,
|
|
2682
|
+
assetOutAddress,
|
|
2683
|
+
amountIn: mid.toString(),
|
|
2684
|
+
integratorBps,
|
|
2685
|
+
});
|
|
2686
|
+
if (FlashnetClient.safeBigInt(midSim.amountOut) >= desiredBtcOut) {
|
|
2687
|
+
hi = mid;
|
|
2688
|
+
bestAmountIn = mid;
|
|
2689
|
+
bestSim = midSim;
|
|
2690
|
+
}
|
|
2691
|
+
else {
|
|
2692
|
+
lo = mid;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
// Compute fee from the best simulation
|
|
2696
|
+
const totalFee = bestSim.feePaidAssetIn || "0";
|
|
2697
|
+
return {
|
|
2698
|
+
amountIn: bestAmountIn.toString(),
|
|
2699
|
+
totalFee,
|
|
2700
|
+
simulation: bestSim,
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2342
2703
|
/**
|
|
2343
2704
|
* Calculate minimum amount out with slippage protection
|
|
2344
2705
|
* @private
|
|
2345
2706
|
*/
|
|
2346
2707
|
calculateMinAmountOut(expectedAmount, slippageBps) {
|
|
2347
|
-
const amount =
|
|
2708
|
+
const amount = FlashnetClient.safeBigInt(expectedAmount);
|
|
2348
2709
|
const slippageFactor = BigInt(10000 - slippageBps);
|
|
2349
2710
|
const minAmount = (amount * slippageFactor) / 10000n;
|
|
2350
2711
|
return minAmount.toString();
|
|
@@ -2563,8 +2924,11 @@ class FlashnetClient {
|
|
|
2563
2924
|
const map = new Map();
|
|
2564
2925
|
for (const item of config) {
|
|
2565
2926
|
if (item.enabled) {
|
|
2927
|
+
if (item.min_amount == null) {
|
|
2928
|
+
continue;
|
|
2929
|
+
}
|
|
2566
2930
|
const key = item.asset_identifier.toLowerCase();
|
|
2567
|
-
const value =
|
|
2931
|
+
const value = FlashnetClient.safeBigInt(item.min_amount);
|
|
2568
2932
|
map.set(key, value);
|
|
2569
2933
|
}
|
|
2570
2934
|
}
|
|
@@ -2586,8 +2950,8 @@ class FlashnetClient {
|
|
|
2586
2950
|
const outHex = this.getHexAddress(params.assetOutAddress);
|
|
2587
2951
|
const minIn = minMap.get(inHex);
|
|
2588
2952
|
const minOut = minMap.get(outHex);
|
|
2589
|
-
const amountIn =
|
|
2590
|
-
const minAmountOut =
|
|
2953
|
+
const amountIn = FlashnetClient.safeBigInt(params.amountIn);
|
|
2954
|
+
const minAmountOut = FlashnetClient.safeBigInt(params.minAmountOut);
|
|
2591
2955
|
if (minIn && minOut) {
|
|
2592
2956
|
if (amountIn < minIn) {
|
|
2593
2957
|
throw new Error(`Minimum amount not met for input asset. Required \
|
|
@@ -2621,13 +2985,13 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
2621
2985
|
const aMin = minMap.get(aHex);
|
|
2622
2986
|
const bMin = minMap.get(bHex);
|
|
2623
2987
|
if (aMin) {
|
|
2624
|
-
const aAmt =
|
|
2988
|
+
const aAmt = FlashnetClient.safeBigInt(params.assetAAmount);
|
|
2625
2989
|
if (aAmt < aMin) {
|
|
2626
2990
|
throw new Error(`Minimum amount not met for Asset A. Required ${aMin.toString()}, provided ${aAmt.toString()}`);
|
|
2627
2991
|
}
|
|
2628
2992
|
}
|
|
2629
2993
|
if (bMin) {
|
|
2630
|
-
const bAmt =
|
|
2994
|
+
const bAmt = FlashnetClient.safeBigInt(params.assetBAmount);
|
|
2631
2995
|
if (bAmt < bMin) {
|
|
2632
2996
|
throw new Error(`Minimum amount not met for Asset B. Required ${bMin.toString()}, provided ${bAmt.toString()}`);
|
|
2633
2997
|
}
|
|
@@ -2649,14 +3013,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
2649
3013
|
const aMin = minMap.get(aHex);
|
|
2650
3014
|
const bMin = minMap.get(bHex);
|
|
2651
3015
|
if (aMin) {
|
|
2652
|
-
const predictedAOut =
|
|
3016
|
+
const predictedAOut = FlashnetClient.safeBigInt(simulation.assetAAmount);
|
|
2653
3017
|
const relaxedA = aMin / 2n; // apply 50% relaxation for outputs
|
|
2654
3018
|
if (predictedAOut < relaxedA) {
|
|
2655
3019
|
throw new Error(`Minimum amount not met for Asset A on withdrawal. Required at least ${relaxedA.toString()} (50% relaxed), predicted ${predictedAOut.toString()}`);
|
|
2656
3020
|
}
|
|
2657
3021
|
}
|
|
2658
3022
|
if (bMin) {
|
|
2659
|
-
const predictedBOut =
|
|
3023
|
+
const predictedBOut = FlashnetClient.safeBigInt(simulation.assetBAmount);
|
|
2660
3024
|
const relaxedB = bMin / 2n;
|
|
2661
3025
|
if (predictedBOut < relaxedB) {
|
|
2662
3026
|
throw new Error(`Minimum amount not met for Asset B on withdrawal. Required at least ${relaxedB.toString()} (50% relaxed), predicted ${predictedBOut.toString()}`);
|
|
@@ -2769,20 +3133,20 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
2769
3133
|
const transferIds = [];
|
|
2770
3134
|
// Transfer assets if not using free balance
|
|
2771
3135
|
if (!params.useFreeBalance) {
|
|
2772
|
-
if (
|
|
3136
|
+
if (FlashnetClient.safeBigInt(params.amountADesired) > 0n) {
|
|
2773
3137
|
assetATransferId = await this.transferAsset({
|
|
2774
3138
|
receiverSparkAddress: lpSparkAddress,
|
|
2775
3139
|
assetAddress: pool.assetAAddress,
|
|
2776
3140
|
amount: params.amountADesired,
|
|
2777
|
-
}, "Insufficient balance for adding V3 liquidity (Asset A): ");
|
|
3141
|
+
}, "Insufficient balance for adding V3 liquidity (Asset A): ", params.useAvailableBalance);
|
|
2778
3142
|
transferIds.push(assetATransferId);
|
|
2779
3143
|
}
|
|
2780
|
-
if (
|
|
3144
|
+
if (FlashnetClient.safeBigInt(params.amountBDesired) > 0n) {
|
|
2781
3145
|
assetBTransferId = await this.transferAsset({
|
|
2782
3146
|
receiverSparkAddress: lpSparkAddress,
|
|
2783
3147
|
assetAddress: pool.assetBAddress,
|
|
2784
3148
|
amount: params.amountBDesired,
|
|
2785
|
-
}, "Insufficient balance for adding V3 liquidity (Asset B): ");
|
|
3149
|
+
}, "Insufficient balance for adding V3 liquidity (Asset B): ", params.useAvailableBalance);
|
|
2786
3150
|
transferIds.push(assetBTransferId);
|
|
2787
3151
|
}
|
|
2788
3152
|
}
|
|
@@ -2975,14 +3339,14 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
2975
3339
|
receiverSparkAddress: lpSparkAddress,
|
|
2976
3340
|
assetAddress: pool.assetAAddress,
|
|
2977
3341
|
amount: params.additionalAmountA,
|
|
2978
|
-
}, "Insufficient balance for rebalance (Asset A): ");
|
|
3342
|
+
}, "Insufficient balance for rebalance (Asset A): ", params.useAvailableBalance);
|
|
2979
3343
|
}
|
|
2980
3344
|
if (params.additionalAmountB && BigInt(params.additionalAmountB) > 0n) {
|
|
2981
3345
|
assetBTransferId = await this.transferAsset({
|
|
2982
3346
|
receiverSparkAddress: lpSparkAddress,
|
|
2983
3347
|
assetAddress: pool.assetBAddress,
|
|
2984
3348
|
amount: params.additionalAmountB,
|
|
2985
|
-
}, "Insufficient balance for rebalance (Asset B): ");
|
|
3349
|
+
}, "Insufficient balance for rebalance (Asset B): ", params.useAvailableBalance);
|
|
2986
3350
|
}
|
|
2987
3351
|
// Collect transfer IDs for potential clawback
|
|
2988
3352
|
const transferIds = [];
|
|
@@ -3174,20 +3538,20 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
3174
3538
|
let assetBTransferId = "";
|
|
3175
3539
|
const transferIds = [];
|
|
3176
3540
|
// Transfer assets to pool
|
|
3177
|
-
if (
|
|
3541
|
+
if (FlashnetClient.safeBigInt(params.amountA) > 0n) {
|
|
3178
3542
|
assetATransferId = await this.transferAsset({
|
|
3179
3543
|
receiverSparkAddress: lpSparkAddress,
|
|
3180
3544
|
assetAddress: pool.assetAAddress,
|
|
3181
3545
|
amount: params.amountA,
|
|
3182
|
-
}, "Insufficient balance for depositing to V3 pool (Asset A): ");
|
|
3546
|
+
}, "Insufficient balance for depositing to V3 pool (Asset A): ", params.useAvailableBalance);
|
|
3183
3547
|
transferIds.push(assetATransferId);
|
|
3184
3548
|
}
|
|
3185
|
-
if (
|
|
3549
|
+
if (FlashnetClient.safeBigInt(params.amountB) > 0n) {
|
|
3186
3550
|
assetBTransferId = await this.transferAsset({
|
|
3187
3551
|
receiverSparkAddress: lpSparkAddress,
|
|
3188
3552
|
assetAddress: pool.assetBAddress,
|
|
3189
3553
|
amount: params.amountB,
|
|
3190
|
-
}, "Insufficient balance for depositing to V3 pool (Asset B): ");
|
|
3554
|
+
}, "Insufficient balance for depositing to V3 pool (Asset B): ", params.useAvailableBalance);
|
|
3191
3555
|
transferIds.push(assetBTransferId);
|
|
3192
3556
|
}
|
|
3193
3557
|
const executeDeposit = async () => {
|