@flashnet/sdk 0.5.2 → 0.5.4
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 +28 -7
- package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/cjs/src/client/FlashnetClient.js +475 -109
- package/dist/cjs/src/client/FlashnetClient.js.map +1 -1
- package/dist/esm/src/client/FlashnetClient.d.ts +28 -7
- package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
- package/dist/esm/src/client/FlashnetClient.js +476 -110
- package/dist/esm/src/client/FlashnetClient.js.map +1 -1
- package/package.json +3 -3
|
@@ -1825,38 +1825,36 @@ class FlashnetClient {
|
|
|
1825
1825
|
await this.ensureInitialized();
|
|
1826
1826
|
// Decode the invoice to get the amount
|
|
1827
1827
|
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
1828
|
+
// Zero-amount invoice: forward-direction quoting using caller-specified tokenAmount
|
|
1828
1829
|
if (!invoiceAmountSats || invoiceAmountSats <= 0) {
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1830
|
+
const tokenAmount = options?.tokenAmount;
|
|
1831
|
+
if (!tokenAmount || BigInt(tokenAmount) <= 0n) {
|
|
1832
|
+
throw new errors.FlashnetError("Zero-amount invoice requires tokenAmount in options.", {
|
|
1833
|
+
response: {
|
|
1834
|
+
errorCode: "FSAG-1002",
|
|
1835
|
+
errorCategory: "Validation",
|
|
1836
|
+
message: "Zero-amount invoice requires tokenAmount in options.",
|
|
1837
|
+
requestId: "",
|
|
1838
|
+
timestamp: new Date().toISOString(),
|
|
1839
|
+
service: "sdk",
|
|
1840
|
+
severity: "Error",
|
|
1841
|
+
remediation: "Provide tokenAmount when using a zero-amount invoice.",
|
|
1842
|
+
},
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
return this.getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options);
|
|
1841
1846
|
}
|
|
1842
1847
|
// Get Lightning fee estimate
|
|
1843
1848
|
const lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
|
|
1844
|
-
// Total BTC needed = invoice amount + lightning fee
|
|
1849
|
+
// Total BTC needed = invoice amount + lightning fee (unmasked).
|
|
1850
|
+
// Bitmasking for V2 pools is handled inside findBestPoolForTokenToBtc.
|
|
1845
1851
|
const baseBtcNeeded = BigInt(invoiceAmountSats) + BigInt(lightningFeeEstimate);
|
|
1846
|
-
// Round up to next multiple of 64 (2^6) to account for BTC variable fee bit masking.
|
|
1847
|
-
// The AMM zeroes the lowest 6 bits of BTC output, so we need to request enough
|
|
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;
|
|
1854
1852
|
// Check Flashnet minimum amounts early to provide clear error messages
|
|
1855
1853
|
const minAmounts = await this.getEnabledMinAmountsMap();
|
|
1856
1854
|
// Check BTC minimum (output from swap)
|
|
1857
1855
|
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 ${
|
|
1856
|
+
if (btcMinAmount && baseBtcNeeded < btcMinAmount) {
|
|
1857
|
+
const msg = `Invoice amount too small. Minimum BTC output is ${btcMinAmount} sats, but invoice + lightning fee totals only ${baseBtcNeeded} sats.`;
|
|
1860
1858
|
throw new errors.FlashnetError(msg, {
|
|
1861
1859
|
response: {
|
|
1862
1860
|
errorCode: "FSAG-1003",
|
|
@@ -1870,8 +1868,9 @@ class FlashnetClient {
|
|
|
1870
1868
|
},
|
|
1871
1869
|
});
|
|
1872
1870
|
}
|
|
1873
|
-
// Find the best pool to swap token -> BTC
|
|
1874
|
-
|
|
1871
|
+
// Find the best pool to swap token -> BTC.
|
|
1872
|
+
// Bitmasking is applied per-pool inside this function (V2 pools get masked, V3 pools don't).
|
|
1873
|
+
const poolQuote = await this.findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded.toString(), options?.integratorFeeRateBps);
|
|
1875
1874
|
// Check token minimum (input to swap)
|
|
1876
1875
|
const tokenHex = this.toHexTokenIdentifier(tokenAddress).toLowerCase();
|
|
1877
1876
|
const tokenMinAmount = minAmounts.get(tokenHex);
|
|
@@ -1891,13 +1890,14 @@ class FlashnetClient {
|
|
|
1891
1890
|
},
|
|
1892
1891
|
});
|
|
1893
1892
|
}
|
|
1894
|
-
//
|
|
1895
|
-
|
|
1893
|
+
// BTC variable fee adjustment: difference between what the pool targets and unmasked base.
|
|
1894
|
+
// For V3 pools this is 0 (no masking). For V2 it's the rounding overhead.
|
|
1895
|
+
const btcVariableFeeAdjustment = Number(BigInt(poolQuote.btcAmountUsed) - baseBtcNeeded);
|
|
1896
1896
|
return {
|
|
1897
1897
|
poolId: poolQuote.poolId,
|
|
1898
1898
|
tokenAddress: this.toHexTokenIdentifier(tokenAddress),
|
|
1899
1899
|
tokenAmountRequired: poolQuote.tokenAmountRequired,
|
|
1900
|
-
btcAmountRequired:
|
|
1900
|
+
btcAmountRequired: poolQuote.btcAmountUsed,
|
|
1901
1901
|
invoiceAmountSats: invoiceAmountSats,
|
|
1902
1902
|
estimatedAmmFee: poolQuote.estimatedAmmFee,
|
|
1903
1903
|
estimatedLightningFee: lightningFeeEstimate,
|
|
@@ -1907,6 +1907,138 @@ class FlashnetClient {
|
|
|
1907
1907
|
tokenIsAssetA: poolQuote.tokenIsAssetA,
|
|
1908
1908
|
poolReserves: poolQuote.poolReserves,
|
|
1909
1909
|
warningMessage: poolQuote.warningMessage,
|
|
1910
|
+
curveType: poolQuote.curveType,
|
|
1911
|
+
isZeroAmountInvoice: false,
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Generate a quote for a zero-amount invoice.
|
|
1916
|
+
* Forward-direction: simulate swapping tokenAmount and pick the pool with the best BTC output.
|
|
1917
|
+
* @private
|
|
1918
|
+
*/
|
|
1919
|
+
async getZeroAmountInvoiceQuote(invoice, tokenAddress, tokenAmount, options) {
|
|
1920
|
+
const tokenHex = this.toHexTokenIdentifier(tokenAddress);
|
|
1921
|
+
const btcHex = index$1.BTC_ASSET_PUBKEY;
|
|
1922
|
+
// Discover all token/BTC pools
|
|
1923
|
+
const [poolsWithTokenAsA, poolsWithTokenAsB] = await Promise.all([
|
|
1924
|
+
this.listPools({ assetAAddress: tokenHex, assetBAddress: btcHex }),
|
|
1925
|
+
this.listPools({ assetAAddress: btcHex, assetBAddress: tokenHex }),
|
|
1926
|
+
]);
|
|
1927
|
+
const poolMap = new Map();
|
|
1928
|
+
for (const p of [...poolsWithTokenAsA.pools, ...poolsWithTokenAsB.pools]) {
|
|
1929
|
+
if (!poolMap.has(p.lpPublicKey)) {
|
|
1930
|
+
const tokenIsAssetA = p.assetAAddress?.toLowerCase() === tokenHex.toLowerCase();
|
|
1931
|
+
poolMap.set(p.lpPublicKey, { pool: p, tokenIsAssetA });
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
const allPools = Array.from(poolMap.values());
|
|
1935
|
+
if (allPools.length === 0) {
|
|
1936
|
+
throw new errors.FlashnetError(`No liquidity pool found for token ${tokenAddress} paired with BTC`, {
|
|
1937
|
+
response: {
|
|
1938
|
+
errorCode: "FSAG-4001",
|
|
1939
|
+
errorCategory: "Business",
|
|
1940
|
+
message: `No liquidity pool found for token ${tokenAddress} paired with BTC`,
|
|
1941
|
+
requestId: "",
|
|
1942
|
+
timestamp: new Date().toISOString(),
|
|
1943
|
+
service: "sdk",
|
|
1944
|
+
severity: "Error",
|
|
1945
|
+
},
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
// Simulate each pool with tokenAmount as input, pick highest BTC output
|
|
1949
|
+
let bestResult = null;
|
|
1950
|
+
let bestBtcOut = 0n;
|
|
1951
|
+
for (const { pool, tokenIsAssetA } of allPools) {
|
|
1952
|
+
try {
|
|
1953
|
+
const poolDetails = await this.getPool(pool.lpPublicKey);
|
|
1954
|
+
const assetInAddress = tokenIsAssetA
|
|
1955
|
+
? poolDetails.assetAAddress
|
|
1956
|
+
: poolDetails.assetBAddress;
|
|
1957
|
+
const assetOutAddress = tokenIsAssetA
|
|
1958
|
+
? poolDetails.assetBAddress
|
|
1959
|
+
: poolDetails.assetAAddress;
|
|
1960
|
+
const simulation = await this.simulateSwap({
|
|
1961
|
+
poolId: pool.lpPublicKey,
|
|
1962
|
+
assetInAddress,
|
|
1963
|
+
assetOutAddress,
|
|
1964
|
+
amountIn: tokenAmount,
|
|
1965
|
+
integratorBps: options?.integratorFeeRateBps,
|
|
1966
|
+
});
|
|
1967
|
+
const btcOut = BigInt(simulation.amountOut);
|
|
1968
|
+
if (btcOut > bestBtcOut) {
|
|
1969
|
+
bestBtcOut = btcOut;
|
|
1970
|
+
bestResult = {
|
|
1971
|
+
poolId: pool.lpPublicKey,
|
|
1972
|
+
tokenIsAssetA,
|
|
1973
|
+
simulation,
|
|
1974
|
+
curveType: poolDetails.curveType,
|
|
1975
|
+
poolReserves: {
|
|
1976
|
+
assetAReserve: poolDetails.assetAReserve,
|
|
1977
|
+
assetBReserve: poolDetails.assetBReserve,
|
|
1978
|
+
},
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
catch {
|
|
1983
|
+
// Skip pools that fail simulation
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (!bestResult || bestBtcOut <= 0n) {
|
|
1987
|
+
throw new errors.FlashnetError("No pool can produce BTC output for the given token amount", {
|
|
1988
|
+
response: {
|
|
1989
|
+
errorCode: "FSAG-4201",
|
|
1990
|
+
errorCategory: "Business",
|
|
1991
|
+
message: "No pool can produce BTC output for the given token amount",
|
|
1992
|
+
requestId: "",
|
|
1993
|
+
timestamp: new Date().toISOString(),
|
|
1994
|
+
service: "sdk",
|
|
1995
|
+
severity: "Error",
|
|
1996
|
+
remediation: "Try a larger token amount.",
|
|
1997
|
+
},
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
// Estimate lightning fee from the BTC output
|
|
2001
|
+
let lightningFeeEstimate;
|
|
2002
|
+
try {
|
|
2003
|
+
lightningFeeEstimate = await this.getLightningFeeEstimate(invoice);
|
|
2004
|
+
}
|
|
2005
|
+
catch {
|
|
2006
|
+
lightningFeeEstimate = Math.max(5, Math.ceil(Number(bestBtcOut) * 0.0017));
|
|
2007
|
+
}
|
|
2008
|
+
// Check minimum amounts
|
|
2009
|
+
const minAmounts = await this.getEnabledMinAmountsMap();
|
|
2010
|
+
const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
|
|
2011
|
+
if (btcMinAmount && bestBtcOut < btcMinAmount) {
|
|
2012
|
+
const msg = `BTC output too small. Minimum is ${btcMinAmount} sats, but swap would produce only ${bestBtcOut} sats.`;
|
|
2013
|
+
throw new errors.FlashnetError(msg, {
|
|
2014
|
+
response: {
|
|
2015
|
+
errorCode: "FSAG-1003",
|
|
2016
|
+
errorCategory: "Validation",
|
|
2017
|
+
message: msg,
|
|
2018
|
+
requestId: "",
|
|
2019
|
+
timestamp: new Date().toISOString(),
|
|
2020
|
+
service: "sdk",
|
|
2021
|
+
severity: "Error",
|
|
2022
|
+
remediation: "Use a larger token amount.",
|
|
2023
|
+
},
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
poolId: bestResult.poolId,
|
|
2028
|
+
tokenAddress: tokenHex,
|
|
2029
|
+
tokenAmountRequired: tokenAmount,
|
|
2030
|
+
btcAmountRequired: bestBtcOut.toString(),
|
|
2031
|
+
invoiceAmountSats: 0,
|
|
2032
|
+
estimatedAmmFee: bestResult.simulation.feePaidAssetIn || "0",
|
|
2033
|
+
estimatedLightningFee: lightningFeeEstimate,
|
|
2034
|
+
btcVariableFeeAdjustment: 0,
|
|
2035
|
+
executionPrice: bestResult.simulation.executionPrice || "0",
|
|
2036
|
+
priceImpactPct: bestResult.simulation.priceImpactPct || "0",
|
|
2037
|
+
tokenIsAssetA: bestResult.tokenIsAssetA,
|
|
2038
|
+
poolReserves: bestResult.poolReserves,
|
|
2039
|
+
warningMessage: bestResult.simulation.warningMessage,
|
|
2040
|
+
curveType: bestResult.curveType,
|
|
2041
|
+
isZeroAmountInvoice: true,
|
|
1910
2042
|
};
|
|
1911
2043
|
}
|
|
1912
2044
|
/**
|
|
@@ -1918,7 +2050,7 @@ class FlashnetClient {
|
|
|
1918
2050
|
*/
|
|
1919
2051
|
async payLightningWithToken(options) {
|
|
1920
2052
|
await this.ensureInitialized();
|
|
1921
|
-
const { invoice, tokenAddress, maxSlippageBps = 500, // 5% default
|
|
2053
|
+
const { invoice, tokenAddress, tokenAmount, maxSlippageBps = 500, // 5% default
|
|
1922
2054
|
maxLightningFeeSats, preferSpark = true, integratorFeeRateBps, integratorPublicKey, transferTimeoutMs = 30000, // 30s default
|
|
1923
2055
|
rollbackOnFailure = false, useExistingBtcBalance = false, } = options;
|
|
1924
2056
|
try {
|
|
@@ -1926,6 +2058,7 @@ class FlashnetClient {
|
|
|
1926
2058
|
const quote = await this.getPayLightningWithTokenQuote(invoice, tokenAddress, {
|
|
1927
2059
|
maxSlippageBps,
|
|
1928
2060
|
integratorFeeRateBps,
|
|
2061
|
+
tokenAmount,
|
|
1929
2062
|
});
|
|
1930
2063
|
// Step 2: Check token balance (always required)
|
|
1931
2064
|
await this.checkBalance({
|
|
@@ -1937,16 +2070,6 @@ class FlashnetClient {
|
|
|
1937
2070
|
],
|
|
1938
2071
|
errorPrefix: "Insufficient token balance for Lightning payment: ",
|
|
1939
2072
|
});
|
|
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
2073
|
// Step 3: Get pool details
|
|
1951
2074
|
const pool = await this.getPool(quote.poolId);
|
|
1952
2075
|
// Step 4: Determine swap direction and execute
|
|
@@ -1980,7 +2103,15 @@ class FlashnetClient {
|
|
|
1980
2103
|
error: swapResponse.error || "Swap was not accepted",
|
|
1981
2104
|
};
|
|
1982
2105
|
}
|
|
1983
|
-
// Step 5: Wait for
|
|
2106
|
+
// Step 5: Wait for transfer (skip useExistingBtcBalance for zero-amount invoices)
|
|
2107
|
+
let canPayImmediately = false;
|
|
2108
|
+
if (!quote.isZeroAmountInvoice && useExistingBtcBalance) {
|
|
2109
|
+
const invoiceAmountSats = await this.decodeInvoiceAmount(invoice);
|
|
2110
|
+
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
2111
|
+
const btcNeededForPayment = invoiceAmountSats + effectiveMaxLightningFee;
|
|
2112
|
+
const balance = await this.getBalance();
|
|
2113
|
+
canPayImmediately = balance.balance >= BigInt(btcNeededForPayment);
|
|
2114
|
+
}
|
|
1984
2115
|
if (!canPayImmediately) {
|
|
1985
2116
|
const transferComplete = await this.waitForTransferCompletion(swapResponse.outboundTransferId, transferTimeoutMs);
|
|
1986
2117
|
if (!transferComplete) {
|
|
@@ -1995,16 +2126,45 @@ class FlashnetClient {
|
|
|
1995
2126
|
};
|
|
1996
2127
|
}
|
|
1997
2128
|
}
|
|
1998
|
-
// Step 6: Calculate Lightning fee
|
|
2129
|
+
// Step 6: Calculate Lightning fee and payment amount
|
|
1999
2130
|
const effectiveMaxLightningFee = maxLightningFeeSats ?? quote.estimatedLightningFee;
|
|
2000
|
-
// Step 7: Pay the Lightning invoice
|
|
2001
2131
|
const btcReceived = swapResponse.amountOut || quote.btcAmountRequired;
|
|
2132
|
+
// Step 7: Pay the Lightning invoice
|
|
2002
2133
|
try {
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2134
|
+
let lightningPayment;
|
|
2135
|
+
let invoiceAmountPaid;
|
|
2136
|
+
if (quote.isZeroAmountInvoice) {
|
|
2137
|
+
// Zero-amount invoice: pay whatever BTC we received minus lightning fee
|
|
2138
|
+
const actualBtc = BigInt(btcReceived);
|
|
2139
|
+
const lnFee = BigInt(effectiveMaxLightningFee);
|
|
2140
|
+
const amountToPay = actualBtc - lnFee;
|
|
2141
|
+
if (amountToPay <= 0n) {
|
|
2142
|
+
return {
|
|
2143
|
+
success: false,
|
|
2144
|
+
poolId: quote.poolId,
|
|
2145
|
+
tokenAmountSpent: quote.tokenAmountRequired,
|
|
2146
|
+
btcAmountReceived: btcReceived,
|
|
2147
|
+
swapTransferId: swapResponse.outboundTransferId,
|
|
2148
|
+
ammFeePaid: quote.estimatedAmmFee,
|
|
2149
|
+
error: `BTC received (${btcReceived} sats) is not enough to cover lightning fee (${effectiveMaxLightningFee} sats).`,
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
invoiceAmountPaid = Number(amountToPay);
|
|
2153
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2154
|
+
invoice,
|
|
2155
|
+
amountSats: invoiceAmountPaid,
|
|
2156
|
+
maxFeeSats: effectiveMaxLightningFee,
|
|
2157
|
+
preferSpark,
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
else {
|
|
2161
|
+
// Standard invoice: pay the specified amount
|
|
2162
|
+
lightningPayment = await this._wallet.payLightningInvoice({
|
|
2163
|
+
invoice,
|
|
2164
|
+
maxFeeSats: effectiveMaxLightningFee,
|
|
2165
|
+
preferSpark,
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2008
2168
|
return {
|
|
2009
2169
|
success: true,
|
|
2010
2170
|
poolId: quote.poolId,
|
|
@@ -2014,6 +2174,7 @@ class FlashnetClient {
|
|
|
2014
2174
|
lightningPaymentId: lightningPayment.id,
|
|
2015
2175
|
ammFeePaid: quote.estimatedAmmFee,
|
|
2016
2176
|
lightningFeePaid: effectiveMaxLightningFee,
|
|
2177
|
+
invoiceAmountPaid,
|
|
2017
2178
|
};
|
|
2018
2179
|
}
|
|
2019
2180
|
catch (lightningError) {
|
|
@@ -2119,7 +2280,7 @@ class FlashnetClient {
|
|
|
2119
2280
|
* Find the best pool for swapping a token to BTC
|
|
2120
2281
|
* @private
|
|
2121
2282
|
*/
|
|
2122
|
-
async findBestPoolForTokenToBtc(tokenAddress,
|
|
2283
|
+
async findBestPoolForTokenToBtc(tokenAddress, baseBtcNeeded, integratorFeeRateBps) {
|
|
2123
2284
|
const tokenHex = this.toHexTokenIdentifier(tokenAddress);
|
|
2124
2285
|
const btcHex = index$1.BTC_ASSET_PUBKEY;
|
|
2125
2286
|
// Find all pools that have this token paired with BTC
|
|
@@ -2163,8 +2324,8 @@ class FlashnetClient {
|
|
|
2163
2324
|
const minAmounts = await this.getMinAmountsMap();
|
|
2164
2325
|
const btcMinAmount = minAmounts.get(index$1.BTC_ASSET_PUBKEY.toLowerCase());
|
|
2165
2326
|
// 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 ${
|
|
2327
|
+
if (btcMinAmount && BigInt(baseBtcNeeded) < btcMinAmount) {
|
|
2328
|
+
const msg = `Invoice amount too small. Minimum ${btcMinAmount} sats required, but invoice only requires ${baseBtcNeeded} sats.`;
|
|
2168
2329
|
throw new errors.FlashnetError(msg, {
|
|
2169
2330
|
response: {
|
|
2170
2331
|
errorCode: "FSAG-1003",
|
|
@@ -2178,60 +2339,99 @@ class FlashnetClient {
|
|
|
2178
2339
|
},
|
|
2179
2340
|
});
|
|
2180
2341
|
}
|
|
2342
|
+
// Compute V2 masked BTC amount (round up to next multiple of 64 for bit masking)
|
|
2343
|
+
const baseBtc = BigInt(baseBtcNeeded);
|
|
2344
|
+
const BTC_VARIABLE_FEE_BITS = 6n;
|
|
2345
|
+
const BTC_VARIABLE_FEE_MASK = 1n << BTC_VARIABLE_FEE_BITS; // 64
|
|
2346
|
+
const maskedBtc = ((baseBtc + BTC_VARIABLE_FEE_MASK - 1n) / BTC_VARIABLE_FEE_MASK) *
|
|
2347
|
+
BTC_VARIABLE_FEE_MASK;
|
|
2181
2348
|
// Find the best pool (lowest token cost for the required BTC)
|
|
2182
2349
|
let bestPool = null;
|
|
2183
2350
|
let bestTokenAmount = BigInt(Number.MAX_SAFE_INTEGER);
|
|
2351
|
+
let bestBtcTarget = 0n;
|
|
2352
|
+
let bestCurveType = "";
|
|
2184
2353
|
let bestSimulation = null;
|
|
2185
2354
|
// Track errors for each pool to provide better diagnostics
|
|
2186
2355
|
const poolErrors = [];
|
|
2187
2356
|
for (const pool of allPools) {
|
|
2188
2357
|
try {
|
|
2189
|
-
// Get pool details for reserves
|
|
2358
|
+
// Get pool details for reserves and curve type
|
|
2190
2359
|
const poolDetails = await this.getPool(pool.lpPublicKey);
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
|
|
2360
|
+
const isV3 = poolDetails.curveType === "V3_CONCENTRATED";
|
|
2361
|
+
// V3 pools use exact BTC amount, V2 pools use masked amount
|
|
2362
|
+
const btcTarget = isV3 ? baseBtc : maskedBtc;
|
|
2363
|
+
const assetInAddress = pool.tokenIsAssetA
|
|
2364
|
+
? poolDetails.assetAAddress
|
|
2365
|
+
: poolDetails.assetBAddress;
|
|
2366
|
+
const assetOutAddress = pool.tokenIsAssetA
|
|
2367
|
+
? poolDetails.assetBAddress
|
|
2368
|
+
: poolDetails.assetAAddress;
|
|
2369
|
+
let tokenAmount;
|
|
2370
|
+
let fee;
|
|
2371
|
+
let executionPrice;
|
|
2372
|
+
let priceImpactPct;
|
|
2373
|
+
let warningMessage;
|
|
2374
|
+
if (isV3) {
|
|
2375
|
+
// V3: binary search with simulateSwap
|
|
2376
|
+
const v3Result = await this.findV3TokenAmountForBtcOutput({
|
|
2377
|
+
poolId: pool.lpPublicKey,
|
|
2378
|
+
assetInAddress,
|
|
2379
|
+
assetOutAddress,
|
|
2380
|
+
desiredBtcOut: btcTarget,
|
|
2381
|
+
currentPriceAInB: poolDetails.currentPriceAInB,
|
|
2382
|
+
tokenIsAssetA: pool.tokenIsAssetA,
|
|
2383
|
+
integratorBps: integratorFeeRateBps,
|
|
2384
|
+
});
|
|
2385
|
+
tokenAmount = BigInt(v3Result.amountIn);
|
|
2386
|
+
fee = v3Result.totalFee;
|
|
2387
|
+
executionPrice = v3Result.simulation.executionPrice || "0";
|
|
2388
|
+
priceImpactPct = v3Result.simulation.priceImpactPct || "0";
|
|
2389
|
+
warningMessage = v3Result.simulation.warningMessage;
|
|
2390
|
+
}
|
|
2391
|
+
else {
|
|
2392
|
+
// V2: constant product math + simulation verification
|
|
2393
|
+
const calculation = this.calculateTokenAmountForBtcOutput(btcTarget.toString(), poolDetails.assetAReserve, poolDetails.assetBReserve, poolDetails.lpFeeBps, poolDetails.hostFeeBps, pool.tokenIsAssetA, integratorFeeRateBps);
|
|
2394
|
+
tokenAmount = BigInt(calculation.amountIn);
|
|
2196
2395
|
// Verify with simulation
|
|
2197
2396
|
const simulation = await this.simulateSwap({
|
|
2198
2397
|
poolId: pool.lpPublicKey,
|
|
2199
|
-
assetInAddress
|
|
2200
|
-
|
|
2201
|
-
: poolDetails.assetBAddress,
|
|
2202
|
-
assetOutAddress: pool.tokenIsAssetA
|
|
2203
|
-
? poolDetails.assetBAddress
|
|
2204
|
-
: poolDetails.assetAAddress,
|
|
2398
|
+
assetInAddress,
|
|
2399
|
+
assetOutAddress,
|
|
2205
2400
|
amountIn: calculation.amountIn,
|
|
2206
2401
|
integratorBps: integratorFeeRateBps,
|
|
2207
2402
|
});
|
|
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
|
|
2403
|
+
if (BigInt(simulation.amountOut) < btcTarget) {
|
|
2222
2404
|
const btcReserve = pool.tokenIsAssetA
|
|
2223
2405
|
? poolDetails.assetBReserve
|
|
2224
2406
|
: poolDetails.assetAReserve;
|
|
2225
2407
|
poolErrors.push({
|
|
2226
2408
|
poolId: pool.lpPublicKey,
|
|
2227
|
-
error: `Simulation output (${simulation.amountOut} sats) < required (${
|
|
2409
|
+
error: `Simulation output (${simulation.amountOut} sats) < required (${btcTarget} sats)`,
|
|
2228
2410
|
btcReserve,
|
|
2229
2411
|
});
|
|
2412
|
+
continue;
|
|
2230
2413
|
}
|
|
2414
|
+
fee = calculation.totalFee;
|
|
2415
|
+
executionPrice = simulation.executionPrice || "0";
|
|
2416
|
+
priceImpactPct = simulation.priceImpactPct || "0";
|
|
2417
|
+
warningMessage = simulation.warningMessage;
|
|
2418
|
+
}
|
|
2419
|
+
// Check if this pool offers a better rate
|
|
2420
|
+
if (tokenAmount < bestTokenAmount) {
|
|
2421
|
+
bestPool = pool;
|
|
2422
|
+
bestTokenAmount = tokenAmount;
|
|
2423
|
+
bestBtcTarget = btcTarget;
|
|
2424
|
+
bestCurveType = poolDetails.curveType;
|
|
2425
|
+
bestSimulation = {
|
|
2426
|
+
amountIn: tokenAmount.toString(),
|
|
2427
|
+
fee,
|
|
2428
|
+
executionPrice,
|
|
2429
|
+
priceImpactPct,
|
|
2430
|
+
warningMessage,
|
|
2431
|
+
};
|
|
2231
2432
|
}
|
|
2232
2433
|
}
|
|
2233
2434
|
catch (e) {
|
|
2234
|
-
// Capture pool errors for diagnostics
|
|
2235
2435
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2236
2436
|
poolErrors.push({
|
|
2237
2437
|
poolId: pool.lpPublicKey,
|
|
@@ -2240,7 +2440,7 @@ class FlashnetClient {
|
|
|
2240
2440
|
}
|
|
2241
2441
|
}
|
|
2242
2442
|
if (!bestPool || !bestSimulation) {
|
|
2243
|
-
let errorMessage = `No pool has sufficient liquidity for ${
|
|
2443
|
+
let errorMessage = `No pool has sufficient liquidity for ${baseBtcNeeded} sats`;
|
|
2244
2444
|
if (poolErrors.length > 0) {
|
|
2245
2445
|
const details = poolErrors
|
|
2246
2446
|
.map((pe) => {
|
|
@@ -2278,6 +2478,8 @@ class FlashnetClient {
|
|
|
2278
2478
|
assetBReserve: poolDetails.assetBReserve,
|
|
2279
2479
|
},
|
|
2280
2480
|
warningMessage: bestSimulation.warningMessage,
|
|
2481
|
+
btcAmountUsed: bestBtcTarget.toString(),
|
|
2482
|
+
curveType: bestCurveType,
|
|
2281
2483
|
};
|
|
2282
2484
|
}
|
|
2283
2485
|
/**
|
|
@@ -2345,6 +2547,127 @@ class FlashnetClient {
|
|
|
2345
2547
|
};
|
|
2346
2548
|
}
|
|
2347
2549
|
}
|
|
2550
|
+
/**
|
|
2551
|
+
* Find the token amount needed to get a specific BTC output from a V3 concentrated liquidity pool.
|
|
2552
|
+
* Uses binary search with simulateSwap since V3 tick-based math can't be inverted locally.
|
|
2553
|
+
* @private
|
|
2554
|
+
*/
|
|
2555
|
+
async findV3TokenAmountForBtcOutput(params) {
|
|
2556
|
+
const { poolId, assetInAddress, assetOutAddress, desiredBtcOut, currentPriceAInB, tokenIsAssetA, integratorBps, } = params;
|
|
2557
|
+
// Step 1: Compute initial estimate from pool price
|
|
2558
|
+
let estimate;
|
|
2559
|
+
if (currentPriceAInB && currentPriceAInB !== "0") {
|
|
2560
|
+
const price = Number(currentPriceAInB);
|
|
2561
|
+
if (tokenIsAssetA) {
|
|
2562
|
+
// priceAInB = how much B (BTC) per 1 A (token), so tokenNeeded = btcOut / price
|
|
2563
|
+
estimate = BigInt(Math.ceil(Number(desiredBtcOut) / price));
|
|
2564
|
+
}
|
|
2565
|
+
else {
|
|
2566
|
+
// priceAInB = how much B (token) per 1 A (BTC), so tokenNeeded = btcOut * price
|
|
2567
|
+
estimate = BigInt(Math.ceil(Number(desiredBtcOut) * price));
|
|
2568
|
+
}
|
|
2569
|
+
// Ensure non-zero
|
|
2570
|
+
if (estimate <= 0n) {
|
|
2571
|
+
estimate = desiredBtcOut * 2n;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
else {
|
|
2575
|
+
estimate = desiredBtcOut * 2n;
|
|
2576
|
+
}
|
|
2577
|
+
// Step 2: Find upper bound by simulating with estimate + 10% buffer
|
|
2578
|
+
let upperBound = (estimate * 110n) / 100n;
|
|
2579
|
+
if (upperBound <= 0n) {
|
|
2580
|
+
upperBound = 1n;
|
|
2581
|
+
}
|
|
2582
|
+
let upperSim = null;
|
|
2583
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2584
|
+
const sim = await this.simulateSwap({
|
|
2585
|
+
poolId,
|
|
2586
|
+
assetInAddress,
|
|
2587
|
+
assetOutAddress,
|
|
2588
|
+
amountIn: upperBound.toString(),
|
|
2589
|
+
integratorBps,
|
|
2590
|
+
});
|
|
2591
|
+
if (BigInt(sim.amountOut) >= desiredBtcOut) {
|
|
2592
|
+
upperSim = sim;
|
|
2593
|
+
break;
|
|
2594
|
+
}
|
|
2595
|
+
// Double the upper bound
|
|
2596
|
+
upperBound = upperBound * 2n;
|
|
2597
|
+
}
|
|
2598
|
+
if (!upperSim) {
|
|
2599
|
+
throw new Error(`V3 pool ${poolId} has insufficient liquidity for ${desiredBtcOut} sats`);
|
|
2600
|
+
}
|
|
2601
|
+
// Step 3: Refine estimate via linear interpolation
|
|
2602
|
+
const upperOut = BigInt(upperSim.amountOut);
|
|
2603
|
+
// Scale proportionally: if upperBound produced upperOut, we need roughly
|
|
2604
|
+
// (upperBound * desiredBtcOut / upperOut). Add +1 to avoid undershoot from truncation.
|
|
2605
|
+
let refined = (upperBound * desiredBtcOut) / upperOut + 1n;
|
|
2606
|
+
if (refined <= 0n) {
|
|
2607
|
+
refined = 1n;
|
|
2608
|
+
}
|
|
2609
|
+
let bestAmountIn = upperBound;
|
|
2610
|
+
let bestSim = upperSim;
|
|
2611
|
+
// Check if the refined estimate is tighter
|
|
2612
|
+
if (refined < upperBound) {
|
|
2613
|
+
const refinedSim = await this.simulateSwap({
|
|
2614
|
+
poolId,
|
|
2615
|
+
assetInAddress,
|
|
2616
|
+
assetOutAddress,
|
|
2617
|
+
amountIn: refined.toString(),
|
|
2618
|
+
integratorBps,
|
|
2619
|
+
});
|
|
2620
|
+
if (BigInt(refinedSim.amountOut) >= desiredBtcOut) {
|
|
2621
|
+
bestAmountIn = refined;
|
|
2622
|
+
bestSim = refinedSim;
|
|
2623
|
+
}
|
|
2624
|
+
else {
|
|
2625
|
+
// Refined estimate was slightly too low. Keep upperBound as best,
|
|
2626
|
+
// and let binary search narrow between refined (too low) and upperBound (sufficient).
|
|
2627
|
+
bestAmountIn = upperBound;
|
|
2628
|
+
bestSim = upperSim;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
// Step 4: Binary search to converge on minimum amountIn
|
|
2632
|
+
// Use a tight range: the interpolation is close, so search between 99.5% and 100% of best
|
|
2633
|
+
let lo = bestAmountIn === upperBound
|
|
2634
|
+
? refined < upperBound
|
|
2635
|
+
? refined
|
|
2636
|
+
: (bestAmountIn * 99n) / 100n
|
|
2637
|
+
: (bestAmountIn * 999n) / 1000n;
|
|
2638
|
+
if (lo <= 0n) {
|
|
2639
|
+
lo = 1n;
|
|
2640
|
+
}
|
|
2641
|
+
let hi = bestAmountIn;
|
|
2642
|
+
for (let i = 0; i < 6; i++) {
|
|
2643
|
+
if (hi - lo <= 1n) {
|
|
2644
|
+
break;
|
|
2645
|
+
}
|
|
2646
|
+
const mid = (lo + hi) / 2n;
|
|
2647
|
+
const midSim = await this.simulateSwap({
|
|
2648
|
+
poolId,
|
|
2649
|
+
assetInAddress,
|
|
2650
|
+
assetOutAddress,
|
|
2651
|
+
amountIn: mid.toString(),
|
|
2652
|
+
integratorBps,
|
|
2653
|
+
});
|
|
2654
|
+
if (BigInt(midSim.amountOut) >= desiredBtcOut) {
|
|
2655
|
+
hi = mid;
|
|
2656
|
+
bestAmountIn = mid;
|
|
2657
|
+
bestSim = midSim;
|
|
2658
|
+
}
|
|
2659
|
+
else {
|
|
2660
|
+
lo = mid;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
// Compute fee from the best simulation
|
|
2664
|
+
const totalFee = bestSim.feePaidAssetIn || "0";
|
|
2665
|
+
return {
|
|
2666
|
+
amountIn: bestAmountIn.toString(),
|
|
2667
|
+
totalFee,
|
|
2668
|
+
simulation: bestSim,
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2348
2671
|
/**
|
|
2349
2672
|
* Calculate minimum amount out with slippage protection
|
|
2350
2673
|
* @private
|
|
@@ -3159,49 +3482,92 @@ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toStri
|
|
|
3159
3482
|
* Deposits assets to your free balance in a V3 concentrated liquidity pool.
|
|
3160
3483
|
*
|
|
3161
3484
|
* Free balance can be used for adding liquidity to positions without requiring
|
|
3162
|
-
* additional Spark transfers. The
|
|
3163
|
-
* assets being deposited.
|
|
3485
|
+
* additional Spark transfers. The SDK handles the Spark transfers internally.
|
|
3164
3486
|
*
|
|
3165
3487
|
* @param params - Deposit parameters
|
|
3166
3488
|
* @param params.poolId - The pool identifier (LP identity public key)
|
|
3167
3489
|
* @param params.amountA - Amount of asset A to deposit (use "0" to skip)
|
|
3168
3490
|
* @param params.amountB - Amount of asset B to deposit (use "0" to skip)
|
|
3169
|
-
* @param params.assetASparkTransferId - Spark transfer ID for asset A (use "" to skip)
|
|
3170
|
-
* @param params.assetBSparkTransferId - Spark transfer ID for asset B (use "" to skip)
|
|
3171
3491
|
* @returns Promise resolving to deposit response with updated balances
|
|
3172
3492
|
* @throws Error if the deposit is rejected
|
|
3173
3493
|
*/
|
|
3174
3494
|
async depositConcentratedBalance(params) {
|
|
3175
3495
|
await this.ensureInitialized();
|
|
3176
|
-
//
|
|
3177
|
-
const
|
|
3178
|
-
const
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
assetASparkTransferId: params.assetASparkTransferId,
|
|
3182
|
-
assetBSparkTransferId: params.assetBSparkTransferId,
|
|
3183
|
-
amountA: params.amountA,
|
|
3184
|
-
amountB: params.amountB,
|
|
3185
|
-
nonce,
|
|
3496
|
+
// Get pool details to know asset addresses
|
|
3497
|
+
const pool = await this.getPool(params.poolId);
|
|
3498
|
+
const lpSparkAddress = sparkAddress.encodeSparkAddressNew({
|
|
3499
|
+
identityPublicKey: params.poolId,
|
|
3500
|
+
network: this.sparkNetwork,
|
|
3186
3501
|
});
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
const
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3502
|
+
let assetATransferId = "";
|
|
3503
|
+
let assetBTransferId = "";
|
|
3504
|
+
const transferIds = [];
|
|
3505
|
+
// Transfer assets to pool
|
|
3506
|
+
if (BigInt(params.amountA) > 0n) {
|
|
3507
|
+
assetATransferId = await this.transferAsset({
|
|
3508
|
+
receiverSparkAddress: lpSparkAddress,
|
|
3509
|
+
assetAddress: pool.assetAAddress,
|
|
3510
|
+
amount: params.amountA,
|
|
3511
|
+
}, "Insufficient balance for depositing to V3 pool (Asset A): ");
|
|
3512
|
+
transferIds.push(assetATransferId);
|
|
3513
|
+
}
|
|
3514
|
+
if (BigInt(params.amountB) > 0n) {
|
|
3515
|
+
assetBTransferId = await this.transferAsset({
|
|
3516
|
+
receiverSparkAddress: lpSparkAddress,
|
|
3517
|
+
assetAddress: pool.assetBAddress,
|
|
3518
|
+
amount: params.amountB,
|
|
3519
|
+
}, "Insufficient balance for depositing to V3 pool (Asset B): ");
|
|
3520
|
+
transferIds.push(assetBTransferId);
|
|
3521
|
+
}
|
|
3522
|
+
const executeDeposit = async () => {
|
|
3523
|
+
// Generate intent
|
|
3524
|
+
const nonce = index$2.generateNonce();
|
|
3525
|
+
const intentMessage = intents.generateDepositBalanceIntentMessage({
|
|
3526
|
+
userPublicKey: this.publicKey,
|
|
3527
|
+
lpIdentityPublicKey: params.poolId,
|
|
3528
|
+
assetASparkTransferId: assetATransferId,
|
|
3529
|
+
assetBSparkTransferId: assetBTransferId,
|
|
3530
|
+
amountA: params.amountA,
|
|
3531
|
+
amountB: params.amountB,
|
|
3532
|
+
nonce,
|
|
3533
|
+
});
|
|
3534
|
+
// Sign intent
|
|
3535
|
+
const messageHash = sha256__default.default(intentMessage);
|
|
3536
|
+
const signature = await this._wallet.config.signer.signMessageWithIdentityKey(messageHash, true);
|
|
3537
|
+
const request = {
|
|
3538
|
+
poolId: params.poolId,
|
|
3539
|
+
amountA: params.amountA,
|
|
3540
|
+
amountB: params.amountB,
|
|
3541
|
+
assetASparkTransferId: assetATransferId,
|
|
3542
|
+
assetBSparkTransferId: assetBTransferId,
|
|
3543
|
+
nonce,
|
|
3544
|
+
signature: hex.getHexFromUint8Array(signature),
|
|
3545
|
+
};
|
|
3546
|
+
const response = await this.typedApi.depositConcentratedBalance(request);
|
|
3547
|
+
if (!response.accepted) {
|
|
3548
|
+
const errorMessage = response.error || "Deposit balance rejected by the AMM";
|
|
3549
|
+
throw new errors.FlashnetError(errorMessage, {
|
|
3550
|
+
response: {
|
|
3551
|
+
errorCode: "UNKNOWN",
|
|
3552
|
+
errorCategory: "System",
|
|
3553
|
+
message: errorMessage,
|
|
3554
|
+
requestId: "",
|
|
3555
|
+
timestamp: new Date().toISOString(),
|
|
3556
|
+
service: "amm-gateway",
|
|
3557
|
+
severity: "Error",
|
|
3558
|
+
},
|
|
3559
|
+
httpStatus: 400,
|
|
3560
|
+
transferIds,
|
|
3561
|
+
lpIdentityPublicKey: params.poolId,
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
return response;
|
|
3198
3565
|
};
|
|
3199
|
-
|
|
3200
|
-
if (
|
|
3201
|
-
|
|
3202
|
-
throw new Error(errorMessage);
|
|
3566
|
+
// Execute with auto-clawback if we made transfers
|
|
3567
|
+
if (transferIds.length > 0) {
|
|
3568
|
+
return this.executeWithAutoClawback(executeDeposit, transferIds, params.poolId);
|
|
3203
3569
|
}
|
|
3204
|
-
return
|
|
3570
|
+
return executeDeposit();
|
|
3205
3571
|
}
|
|
3206
3572
|
}
|
|
3207
3573
|
|