@atomiqlabs/sdk 8.7.2 → 8.7.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.
Files changed (38) hide show
  1. package/dist/bitcoin/wallet/SingleAddressBitcoinWallet.js +15 -1
  2. package/dist/intermediaries/apis/IntermediaryAPI.d.ts +1 -0
  3. package/dist/intermediaries/apis/IntermediaryAPI.js +2 -1
  4. package/dist/prices/abstract/ISwapPrice.d.ts +4 -2
  5. package/dist/prices/abstract/ISwapPrice.js +18 -6
  6. package/dist/swapper/SwapperUtils.d.ts +6 -0
  7. package/dist/swapper/SwapperUtils.js +13 -0
  8. package/dist/swaps/ISwap.js +10 -5
  9. package/dist/swaps/ISwapWrapper.d.ts +1 -0
  10. package/dist/swaps/ISwapWrapper.js +2 -2
  11. package/dist/swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper.js +3 -2
  12. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap.js +1 -1
  13. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper.js +6 -3
  14. package/dist/swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper.js +3 -2
  15. package/dist/swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper.js +4 -4
  16. package/dist/swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper.js +4 -4
  17. package/dist/swaps/spv_swaps/SpvFromBTCSwap.js +1 -1
  18. package/dist/swaps/spv_swaps/SpvFromBTCWrapper.d.ts +7 -0
  19. package/dist/swaps/spv_swaps/SpvFromBTCWrapper.js +7 -3
  20. package/dist/swaps/trusted/ln/LnForGasWrapper.js +1 -1
  21. package/dist/swaps/trusted/onchain/OnchainForGasWrapper.js +1 -1
  22. package/package.json +2 -2
  23. package/src/bitcoin/wallet/SingleAddressBitcoinWallet.ts +14 -2
  24. package/src/intermediaries/apis/IntermediaryAPI.ts +4 -2
  25. package/src/prices/abstract/ISwapPrice.ts +19 -6
  26. package/src/swapper/SwapperUtils.ts +12 -0
  27. package/src/swaps/ISwap.ts +17 -6
  28. package/src/swaps/ISwapWrapper.ts +4 -3
  29. package/src/swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper.ts +4 -2
  30. package/src/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap.ts +4 -1
  31. package/src/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper.ts +11 -4
  32. package/src/swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper.ts +4 -2
  33. package/src/swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper.ts +6 -6
  34. package/src/swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper.ts +5 -5
  35. package/src/swaps/spv_swaps/SpvFromBTCSwap.ts +4 -1
  36. package/src/swaps/spv_swaps/SpvFromBTCWrapper.ts +18 -4
  37. package/src/swaps/trusted/ln/LnForGasWrapper.ts +1 -1
  38. package/src/swaps/trusted/onchain/OnchainForGasWrapper.ts +1 -1
@@ -5,10 +5,12 @@ const utils_1 = require("@scure/btc-signer/utils");
5
5
  const btc_signer_1 = require("@scure/btc-signer");
6
6
  const buffer_1 = require("buffer");
7
7
  const BitcoinWallet_1 = require("./BitcoinWallet");
8
+ const base_1 = require("@atomiqlabs/base");
8
9
  const bip32_1 = require("@scure/bip32");
9
10
  const bip39_1 = require("@scure/bip39");
10
11
  const english_js_1 = require("@scure/bip39/wordlists/english.js");
11
12
  const sha2_1 = require("@noble/hashes/sha2");
13
+ const logger = (0, base_1.getLogger)("SingleAddressBitcoinWallet: ");
12
14
  /**
13
15
  * Bitcoin wallet implementation deriving a single address from a WIF encoded private key
14
16
  *
@@ -32,12 +34,24 @@ class SingleAddressBitcoinWallet extends BitcoinWallet_1.BitcoinWallet {
32
34
  if (address == null)
33
35
  throw new Error("Failed to generate p2wpkh address from the provided private key!");
34
36
  this.address = address;
37
+ this.addressType = (0, BitcoinWallet_1.identifyAddressType)(this.address, network);
35
38
  }
36
39
  else {
37
40
  this.address = addressDataOrWIF.address;
41
+ this.addressType = (0, BitcoinWallet_1.identifyAddressType)(this.address, network);
38
42
  this.pubkey = buffer_1.Buffer.from(addressDataOrWIF.publicKey, "hex");
43
+ // Some wallets seem to be returning a full 33-byte compressed pubkey instead of a taproot
44
+ // 32-byte long X-only key. Handle these cases here
45
+ if (this.addressType === "p2tr") {
46
+ if (this.pubkey.length !== 33)
47
+ return;
48
+ const leadingByte = this.pubkey[0];
49
+ if (leadingByte !== 0x03 && leadingByte !== 0x02)
50
+ throw new Error("Invalid public key passed for taproot bitcoin wallet, expected an X-only 32-byte public key, or a compressed 33-byte public key");
51
+ logger.debug(`constructor(): Converting compressed public key ${addressDataOrWIF.publicKey} to taproot X-only 32-byte public key`);
52
+ this.pubkey = this.pubkey.slice(1);
53
+ }
39
54
  }
40
- this.addressType = (0, BitcoinWallet_1.identifyAddressType)(this.address, network);
41
55
  }
42
56
  /**
43
57
  * Returns all the wallet addresses controlled by the wallet
@@ -244,6 +244,7 @@ export type SpvFromBTCPrepare = SwapInit & {
244
244
  exactOut: boolean;
245
245
  callerFeeRate: Promise<bigint>;
246
246
  frontingFeeRate: bigint;
247
+ stickyAddress?: boolean;
247
248
  };
248
249
  declare const SpvFromBTCInitResponseSchema: {
249
250
  readonly txId: FieldTypeEnum.String;
@@ -543,7 +543,8 @@ class IntermediaryAPI {
543
543
  gasAmount: init.gasAmount.toString(10),
544
544
  gasToken: init.gasToken,
545
545
  frontingFeeRate: init.frontingFeeRate.toString(10),
546
- callerFeeRate: init.callerFeeRate.then(val => val.toString(10))
546
+ callerFeeRate: init.callerFeeRate.then(val => val.toString(10)),
547
+ stickyAddress: init.stickyAddress
547
548
  }, {
548
549
  code: SchemaVerifier_1.FieldTypeEnum.Number,
549
550
  msg: SchemaVerifier_1.FieldTypeEnum.String,
@@ -64,8 +64,9 @@ export declare abstract class ISwapPrice<T extends MultiChain = MultiChain> {
64
64
  * @param tokenAddress Token address to be paid
65
65
  * @param abortSignal
66
66
  * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice}
67
+ * @param realSwapFeeSats
67
68
  */
68
- isValidAmountSend<C extends ChainIds<T>>(chainIdentifier: C, amountSats: bigint, satsBaseFee: bigint, feePPM: bigint, paidToken: bigint, tokenAddress: string, abortSignal?: AbortSignal, preFetchedPrice?: bigint | null): Promise<PriceInfoType>;
69
+ isValidAmountSend<C extends ChainIds<T>>(chainIdentifier: C, amountSats: bigint, satsBaseFee: bigint, feePPM: bigint, paidToken: bigint, tokenAddress: string, abortSignal?: AbortSignal, preFetchedPrice?: bigint | null, realSwapFeeSats?: bigint): Promise<PriceInfoType>;
69
70
  /**
70
71
  * Recomputes pricing info without fetching the current price
71
72
  *
@@ -88,8 +89,9 @@ export declare abstract class ISwapPrice<T extends MultiChain = MultiChain> {
88
89
  * @param tokenAddress Token address to be received
89
90
  * @param abortSignal
90
91
  * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice}
92
+ * @param realSwapFeeSats
91
93
  */
92
- isValidAmountReceive<C extends ChainIds<T>>(chainIdentifier: C, amountSats: bigint, satsBaseFee: bigint, feePPM: bigint, receiveToken: bigint, tokenAddress: string, abortSignal?: AbortSignal, preFetchedPrice?: bigint | null): Promise<PriceInfoType>;
94
+ isValidAmountReceive<C extends ChainIds<T>>(chainIdentifier: C, amountSats: bigint, satsBaseFee: bigint, feePPM: bigint, receiveToken: bigint, tokenAddress: string, abortSignal?: AbortSignal, preFetchedPrice?: bigint | null, realSwapFeeSats?: bigint): Promise<PriceInfoType>;
93
95
  /**
94
96
  * Pre-fetches the pricing data for a given token, such that further calls to {@link isValidAmountReceive} or
95
97
  * {@link isValidAmountSend} are quicker and don't need to wait for the price fetch
@@ -59,10 +59,14 @@ class ISwapPrice {
59
59
  * @param tokenAddress Token address to be paid
60
60
  * @param abortSignal
61
61
  * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice}
62
+ * @param realSwapFeeSats
62
63
  */
63
- async isValidAmountSend(chainIdentifier, amountSats, satsBaseFee, feePPM, paidToken, tokenAddress, abortSignal, preFetchedPrice) {
64
- const totalSats = (amountSats * (1000000n + feePPM) / 1000000n)
65
- + satsBaseFee;
64
+ async isValidAmountSend(chainIdentifier, amountSats, satsBaseFee, feePPM, paidToken, tokenAddress, abortSignal, preFetchedPrice, realSwapFeeSats) {
65
+ if (realSwapFeeSats != undefined && realSwapFeeSats < 0)
66
+ throw new Error("Invalid swap fee! Swap fee cannot be negative!");
67
+ const totalSats = realSwapFeeSats != undefined
68
+ ? amountSats + realSwapFeeSats
69
+ : (amountSats * (1000000n + feePPM) / 1000000n) + satsBaseFee;
66
70
  const totalUSats = totalSats * 1000000n;
67
71
  const swapPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / paidToken;
68
72
  if (this.shouldIgnore(chainIdentifier, tokenAddress))
@@ -122,10 +126,18 @@ class ISwapPrice {
122
126
  * @param tokenAddress Token address to be received
123
127
  * @param abortSignal
124
128
  * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice}
129
+ * @param realSwapFeeSats
125
130
  */
126
- async isValidAmountReceive(chainIdentifier, amountSats, satsBaseFee, feePPM, receiveToken, tokenAddress, abortSignal, preFetchedPrice) {
127
- const totalSats = (amountSats * (1000000n - feePPM) / 1000000n)
128
- - satsBaseFee;
131
+ async isValidAmountReceive(chainIdentifier, amountSats, satsBaseFee, feePPM, receiveToken, tokenAddress, abortSignal, preFetchedPrice, realSwapFeeSats) {
132
+ if (realSwapFeeSats != undefined) {
133
+ if (realSwapFeeSats >= amountSats)
134
+ throw new Error("Invalid swap fee! Larger than or equal to total output amount!");
135
+ if (realSwapFeeSats < 0)
136
+ throw new Error("Invalid swap fee! Must be non-negative!");
137
+ }
138
+ const totalSats = realSwapFeeSats != undefined
139
+ ? amountSats - realSwapFeeSats
140
+ : (amountSats * (1000000n - feePPM) / 1000000n) - satsBaseFee;
129
141
  const totalUSats = totalSats * 1000000n;
130
142
  const swapPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / receiveToken;
131
143
  if (this.shouldIgnore(chainIdentifier, tokenAddress))
@@ -138,6 +138,12 @@ export declare class SwapperUtils<T extends MultiChain> {
138
138
  * Returns the address of the native currency of the smart chain
139
139
  */
140
140
  getNativeToken<ChainIdentifier extends ChainIds<T>>(chainIdentifier: ChainIdentifier): SCToken<ChainIdentifier>;
141
+ /**
142
+ * Returns whether when swapping to the provided token a gas drop can be requested
143
+ *
144
+ * @param token
145
+ */
146
+ destinationTokenSupportsGasDrop<ChainIdentifier extends ChainIds<T>>(token: SCToken<ChainIdentifier>): boolean;
141
147
  /**
142
148
  * Returns a random signer for a given smart chain
143
149
  *
@@ -373,6 +373,19 @@ class SwapperUtils {
373
373
  throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
374
374
  return this.root._tokens[chainIdentifier][this.root._chains[chainIdentifier].chainInterface.getNativeCurrencyAddress()];
375
375
  }
376
+ /**
377
+ * Returns whether when swapping to the provided token a gas drop can be requested
378
+ *
379
+ * @param token
380
+ */
381
+ destinationTokenSupportsGasDrop(token) {
382
+ if (this.root._chains[token.chainId] == null)
383
+ throw new Error("Invalid chain identifier! Unknown chain: " + token.chainId);
384
+ const { chainInterface } = this.root._chains[token.chainId];
385
+ if (chainInterface.shouldGetNativeTokenDrop != null)
386
+ return chainInterface.shouldGetNativeTokenDrop(token.address);
387
+ return chainInterface.getNativeCurrencyAddress() === token.address;
388
+ }
376
389
  /**
377
390
  * Returns a random signer for a given smart chain
378
391
  *
@@ -166,16 +166,21 @@ class ISwap {
166
166
  if (this.pricingInfo == null)
167
167
  return;
168
168
  const priceUsdPerBtc = this.pricingInfo.realPriceUsdPerBitcoin;
169
- const input = this.getInput();
170
169
  const output = this.getOutput();
171
- if (input.isUnknown || output.isUnknown)
170
+ if (output.isUnknown)
172
171
  return;
173
- if ((0, Token_1.isSCToken)(input.token) && this.getDirection() === SwapDirection_1.SwapDirection.TO_BTC) {
174
- this.pricingInfo = await this.wrapper._prices.isValidAmountSend(this.chainIdentifier, output.rawAmount, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, input.rawAmount, input.token.address);
172
+ if ((0, Token_1.isSCToken)(this.getInputToken()) && this.getDirection() === SwapDirection_1.SwapDirection.TO_BTC) {
173
+ const input = this.getInputWithoutFee();
174
+ if (input.isUnknown)
175
+ return;
176
+ this.pricingInfo = await this.wrapper._prices.isValidAmountSend(this.chainIdentifier, output.rawAmount, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, input.rawAmount + this.swapFee, input.token.address, undefined, undefined, this.swapFeeBtc);
175
177
  this.pricingInfo.realPriceUsdPerBitcoin = priceUsdPerBtc;
176
178
  }
177
179
  else if ((0, Token_1.isSCToken)(output.token) && this.getDirection() === SwapDirection_1.SwapDirection.FROM_BTC) {
178
- this.pricingInfo = await this.wrapper._prices.isValidAmountReceive(this.chainIdentifier, input.rawAmount, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, output.rawAmount, output.token.address);
180
+ const input = this.getInput();
181
+ if (input.isUnknown)
182
+ return;
183
+ this.pricingInfo = await this.wrapper._prices.isValidAmountReceive(this.chainIdentifier, input.rawAmount, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, output.rawAmount, output.token.address, undefined, undefined, this.swapFeeBtc);
179
184
  this.pricingInfo.realPriceUsdPerBitcoin = priceUsdPerBtc;
180
185
  }
181
186
  }
@@ -194,6 +194,7 @@ export declare abstract class ISwapWrapper<T extends ChainType, D extends SwapTy
194
194
  swapFeePPM: number;
195
195
  }, send: boolean, amountSats: bigint, amountToken: bigint, token: string, feeData: {
196
196
  networkFee?: bigint;
197
+ swapFeeBtc?: bigint;
197
198
  }, pricePrefetchPromise?: Promise<bigint | undefined>, usdPricePrefetchPromise?: Promise<number | undefined>, abortSignal?: AbortSignal): Promise<PriceInfoType>;
198
199
  /**
199
200
  * Processes a single smart chain on-chain event
@@ -114,8 +114,8 @@ class ISwapWrapper {
114
114
  amountToken = amountToken - feeData.networkFee;
115
115
  const [isValidAmount, usdPrice] = await Promise.all([
116
116
  send ?
117
- this._prices.isValidAmountSend(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise) :
118
- this._prices.isValidAmountReceive(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise),
117
+ this._prices.isValidAmountSend(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise, feeData.swapFeeBtc) :
118
+ this._prices.isValidAmountReceive(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise, feeData.swapFeeBtc),
119
119
  usdPricePrefetchPromise.then(value => {
120
120
  if (value != null)
121
121
  return value;
@@ -221,10 +221,11 @@ class FromBTCLNWrapper extends IFromBTCLNWrapper_1.IFromBTCLNWrapper {
221
221
  if (decodedPr.timeExpireDate == null)
222
222
  throw new IntermediaryError_1.IntermediaryError("Invalid returned swap invoice, no expiry date field");
223
223
  const amountIn = (BigInt(decodedPr.millisatoshis) + 999n) / 1000n;
224
+ const swapFeeBtc = resp.swapFee * amountIn / (resp.total - resp.swapFee);
224
225
  try {
225
226
  this.verifyReturnedData(resp, amountData, lp, _options, decodedPr, paymentHash);
226
227
  const [pricingInfo] = await Promise.all([
227
- this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.FROM_BTCLN], false, amountIn, resp.total, amountData.token, {}, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal),
228
+ this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.FROM_BTCLN], false, amountIn, resp.total, amountData.token, { swapFeeBtc }, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal),
228
229
  this.verifyIntermediaryLiquidity(resp.total, (0, Utils_1.throwIfUndefined)(liquidityPromise)),
229
230
  lnCapacityPromise != null ? this.verifyLnNodeCapacity(lp, decodedPr, lnCapacityPromise, abortController.signal) : Promise.resolve()
230
231
  ]);
@@ -233,7 +234,7 @@ class FromBTCLNWrapper extends IFromBTCLNWrapper_1.IFromBTCLNWrapper {
233
234
  url: lp.url,
234
235
  expiry: decodedPr.timeExpireDate * 1000,
235
236
  swapFee: resp.swapFee,
236
- swapFeeBtc: resp.swapFee * amountIn / (resp.total - resp.swapFee),
237
+ swapFeeBtc,
237
238
  feeRate: (await _preFetches.feeRatePromise[version]),
238
239
  initialSwapData: await this._contract(version).createSwapData(base_1.ChainSwapType.HTLC, lp.getAddress(this.chainIdentifier), recipient, amountData.token, resp.total, _hash[version], this.getRandomSequence(), BigInt(Math.floor(Date.now() / 1000)), false, true, resp.securityDeposit, 0n, nativeTokenAddress),
239
240
  pr: resp.pr,
@@ -206,7 +206,7 @@ class FromBTCLNAutoSwap extends IEscrowSwap_1.IEscrowSwap {
206
206
  if (this.pricingInfo == null || this.btcAmountSwap == null)
207
207
  return;
208
208
  const usdPricePerBtc = this.pricingInfo.realPriceUsdPerBitcoin;
209
- this.pricingInfo = await this.wrapper._prices.isValidAmountReceive(this.chainIdentifier, this.btcAmountSwap, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, this.getOutputAmountWithoutFee(), this.getSwapData().getToken());
209
+ this.pricingInfo = await this.wrapper._prices.isValidAmountReceive(this.chainIdentifier, this.btcAmountSwap, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, this.getOutputAmountWithoutFee(), this.getSwapData().getToken(), undefined, undefined, this.swapFeeBtc);
210
210
  this.pricingInfo.realPriceUsdPerBitcoin = usdPricePerBtc;
211
211
  }
212
212
  //////////////////////////////
@@ -231,7 +231,10 @@ class FromBTCLNAutoWrapper extends IFromBTCLNWrapper_1.IFromBTCLNWrapper {
231
231
  description: options?.description,
232
232
  descriptionHash: (0, Utils_1.parseHashValueExact32Bytes)(options?.descriptionHash, "description hash")
233
233
  };
234
- if (amountData.token === this._chain.getNativeCurrencyAddress() && _options.gasAmount !== 0n)
234
+ if (_options.gasAmount !== 0n &&
235
+ (this._chain.shouldGetNativeTokenDrop != null
236
+ ? !this._chain.shouldGetNativeTokenDrop(amountData.token)
237
+ : amountData.token === this._chain.getNativeCurrencyAddress()))
235
238
  throw new UserError_1.UserError("Cannot specify `gasAmount` for swaps to a native token!");
236
239
  if (_options.description != null && buffer_1.Buffer.byteLength(_options.description, "utf8") > 500)
237
240
  throw new UserError_1.UserError("Invalid description length");
@@ -301,9 +304,9 @@ class FromBTCLNAutoWrapper extends IFromBTCLNWrapper_1.IFromBTCLNWrapper {
301
304
  try {
302
305
  this.verifyReturnedData(resp, amountData, lp, _options, decodedPr, paymentHash, claimerBounty);
303
306
  const [pricingInfo, gasPricingInfo] = await Promise.all([
304
- this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.FROM_BTCLN_AUTO], false, resp.btcAmountSwap, resp.total, amountData.token, {}, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal),
307
+ this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.FROM_BTCLN_AUTO], false, resp.btcAmountSwap, resp.total, amountData.token, { swapFeeBtc: resp.swapFeeBtc }, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal),
305
308
  _options.gasAmount === 0n ? Promise.resolve(undefined) : this.verifyReturnedPrice({ ...lp.services[SwapType_1.SwapType.FROM_BTCLN_AUTO], swapBaseFee: 0 }, //Base fee should be charged only on the amount, not on gas
306
- false, resp.btcAmountGas, resp.totalGas + resp.claimerBounty, nativeTokenAddress, {}, _preFetches.gasTokenPricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal),
309
+ false, resp.btcAmountGas, resp.totalGas + resp.claimerBounty, nativeTokenAddress, { swapFeeBtc: resp.gasSwapFeeBtc }, _preFetches.gasTokenPricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal),
307
310
  this.verifyIntermediaryLiquidity(resp.total, (0, Utils_1.throwIfUndefined)(liquidityPromise)),
308
311
  _options.unsafeSkipLnNodeCheck ? Promise.resolve() : this.verifyLnNodeCapacity(lp, decodedPr, lnCapacityPromise, abortController.signal)
309
312
  ]);
@@ -335,10 +335,11 @@ class FromBTCWrapper extends IFromBTCWrapper_1.IFromBTCWrapper {
335
335
  }, undefined, e => e instanceof RequestError_1.RequestError, abortController.signal);
336
336
  const data = new (this._swapDataDeserializer(version))(resp.data);
337
337
  data.setClaimer(recipient);
338
+ const swapFeeBtc = resp.swapFee * resp.amount / (data.getAmount() - resp.swapFee);
338
339
  this.verifyReturnedData(recipient, resp, amountData, lp, _options, data, sequence, (await claimerBountyPrefetchPromise[version]), nativeTokenAddress);
339
340
  const [pricingInfo, signatureExpiry] = await Promise.all([
340
341
  //Get intermediary's liquidity
341
- this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.FROM_BTC], false, resp.amount, resp.total, amountData.token, {}, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal),
342
+ this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.FROM_BTC], false, resp.amount, resp.total, amountData.token, { swapFeeBtc }, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal),
342
343
  this.verifyReturnedSignature(recipient, data, resp, feeRatePromise[version], signDataPromise, version, abortController.signal),
343
344
  this.verifyIntermediaryLiquidity(data.getAmount(), (0, Utils_1.throwIfUndefined)(liquidityPromise)),
344
345
  ]);
@@ -347,7 +348,7 @@ class FromBTCWrapper extends IFromBTCWrapper_1.IFromBTCWrapper {
347
348
  url: lp.url,
348
349
  expiry: signatureExpiry,
349
350
  swapFee: resp.swapFee,
350
- swapFeeBtc: resp.swapFee * resp.amount / (data.getAmount() - resp.swapFee),
351
+ swapFeeBtc,
351
352
  feeRate: (await feeRatePromise[version]),
352
353
  signatureData: resp,
353
354
  data,
@@ -181,15 +181,15 @@ class ToBTCLNWrapper extends IToBTCWrapper_1.IToBTCWrapper {
181
181
  const data = new (this._swapDataDeserializer(version))(resp.data);
182
182
  data.setOfferer(signer);
183
183
  await this.verifyReturnedData(signer, resp, parsedPr, amountData.token, lp, calculatedOptions, data);
184
+ const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee);
184
185
  const [pricingInfo, signatureExpiry, reputation] = await Promise.all([
185
- this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.TO_BTCLN], true, amountOut, data.getAmount(), amountData.token, { networkFee: resp.maxFee }, preFetches.pricePreFetchPromise, preFetches.usdPricePrefetchPromise, abortController.signal),
186
+ this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.TO_BTCLN], true, amountOut, data.getAmount(), amountData.token, { networkFee: resp.maxFee, swapFeeBtc }, preFetches.pricePreFetchPromise, preFetches.usdPricePrefetchPromise, abortController.signal),
186
187
  this.verifyReturnedSignature(signer, data, resp, preFetches.feeRatePromise[version], signDataPromise, version, abortController.signal),
187
188
  reputationPromise
188
189
  ]);
189
190
  abortController.signal.throwIfAborted();
190
191
  if (reputation != null)
191
192
  lp.reputation[amountData.token.toString()] = reputation;
192
- const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee);
193
193
  const quote = new ToBTCLNSwap_1.ToBTCLNSwap(this, {
194
194
  pricingInfo,
195
195
  url: lp.url,
@@ -339,15 +339,15 @@ class ToBTCLNWrapper extends IToBTCWrapper_1.IToBTCWrapper {
339
339
  const data = new (this._swapDataDeserializer(version))(resp.data);
340
340
  data.setOfferer(signer);
341
341
  await this.verifyReturnedData(signer, resp, parsedInvoice, amountData.token, lp, calculatedOptions, data, amountData.amount);
342
+ const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee);
342
343
  const [pricingInfo, signatureExpiry, reputation] = await Promise.all([
343
- this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.TO_BTCLN], true, prepareResp.amount, data.getAmount(), amountData.token, { networkFee: resp.maxFee }, preFetches.pricePreFetchPromise, preFetches.usdPricePrefetchPromise, abortSignal),
344
+ this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.TO_BTCLN], true, prepareResp.amount, data.getAmount(), amountData.token, { networkFee: resp.maxFee, swapFeeBtc }, preFetches.pricePreFetchPromise, preFetches.usdPricePrefetchPromise, abortSignal),
344
345
  this.verifyReturnedSignature(signer, data, resp, preFetches.feeRatePromise[version], signDataPromise, version, abortController.signal),
345
346
  reputationPromise
346
347
  ]);
347
348
  abortController.signal.throwIfAborted();
348
349
  if (reputation != null)
349
350
  lp.reputation[amountData.token.toString()] = reputation;
350
- const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee);
351
351
  const quote = new ToBTCLNSwap_1.ToBTCLNSwap(this, {
352
352
  pricingInfo,
353
353
  url: lp.url,
@@ -200,18 +200,18 @@ class ToBTCWrapper extends IToBTCWrapper_1.IToBTCWrapper {
200
200
  let hash = _hash?.[version] ?? this._contract(version).getHashForOnchain(outputScript, resp.amount, _options.confirmations, nonce).toString("hex");
201
201
  const data = new (this._swapDataDeserializer(version))(resp.data);
202
202
  data.setOfferer(signer);
203
+ const inputWithoutFees = data.getAmount() - resp.swapFee - resp.networkFee;
204
+ const swapFeeBtc = resp.swapFee * resp.amount / inputWithoutFees;
205
+ const networkFeeBtc = resp.networkFee * resp.amount / inputWithoutFees;
203
206
  this.verifyReturnedData(signer, resp, amountData, lp, _options, data, hash);
204
207
  const [pricingInfo, signatureExpiry, reputation] = await Promise.all([
205
- this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.TO_BTC], true, resp.amount, data.getAmount(), amountData.token, resp, pricePreFetchPromise, usdPricePrefetchPromise, abortController.signal),
208
+ this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.TO_BTC], true, resp.amount, data.getAmount(), amountData.token, { networkFee: resp.networkFee, swapFeeBtc }, pricePreFetchPromise, usdPricePrefetchPromise, abortController.signal),
206
209
  this.verifyReturnedSignature(signer, data, resp, feeRatePromise[version], signDataPromise, version, abortController.signal),
207
210
  reputationPromise
208
211
  ]);
209
212
  abortController.signal.throwIfAborted();
210
213
  if (reputation != null)
211
214
  lp.reputation[amountData.token.toString()] = reputation;
212
- const inputWithoutFees = data.getAmount() - resp.swapFee - resp.networkFee;
213
- const swapFeeBtc = resp.swapFee * resp.amount / inputWithoutFees;
214
- const networkFeeBtc = resp.networkFee * resp.amount / inputWithoutFees;
215
215
  const quote = new ToBTCSwap_1.ToBTCSwap(this, {
216
216
  pricingInfo,
217
217
  url: lp.url,
@@ -255,7 +255,7 @@ class SpvFromBTCSwap extends ISwap_1.ISwap {
255
255
  if (this.pricingInfo == null)
256
256
  return;
257
257
  const usdPricePerBtc = this.pricingInfo.realPriceUsdPerBitcoin;
258
- this.pricingInfo = await this.wrapper._prices.isValidAmountReceive(this.chainIdentifier, this.btcAmountSwap, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, this.getOutputWithoutFee().rawAmount, this.outputSwapToken);
258
+ this.pricingInfo = await this.wrapper._prices.isValidAmountReceive(this.chainIdentifier, this.btcAmountSwap, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, this.getOutputWithoutFee().rawAmount, this.outputSwapToken, undefined, undefined, this.swapFeeBtc);
259
259
  this.pricingInfo.realPriceUsdPerBitcoin = usdPricePerBtc;
260
260
  }
261
261
  //////////////////////////////
@@ -46,6 +46,13 @@ export type SpvFromBTCOptions = {
46
46
  * the settlement gas fee cost)
47
47
  */
48
48
  feeSafetyFactor?: number;
49
+ /**
50
+ * Instruct the LP to create a "sticky address" for your destination wallet address. After the first successful
51
+ * swap with that LP, the used bitcoin address will be permanently linked to your destination wallet address. So
52
+ * all subsequent swaps to the same address will yield the same LP deposit bitcoin address. Useful for corporate
53
+ * whitelist-only wallets
54
+ */
55
+ stickyAddress?: boolean;
49
56
  /**
50
57
  * @deprecated Use `maxAllowedBitcoinFeeRate` instead!
51
58
  */
@@ -466,7 +466,10 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
466
466
  feeSafetyFactor: options?.feeSafetyFactor ?? 1.25,
467
467
  maxAllowedBitcoinFeeRate: options?.maxAllowedBitcoinFeeRate ?? options?.maxAllowedNetworkFeeRate ?? Infinity
468
468
  };
469
- if (amountData.token === this._chain.getNativeCurrencyAddress() && _options.gasAmount !== 0n)
469
+ if (_options.gasAmount !== 0n &&
470
+ (this._chain.shouldGetNativeTokenDrop != null
471
+ ? !this._chain.shouldGetNativeTokenDrop(amountData.token)
472
+ : amountData.token === this._chain.getNativeCurrencyAddress()))
470
473
  throw new UserError_1.UserError("Cannot specify `gasAmount` for swaps to a native token!");
471
474
  const lpVersions = Intermediary_1.Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
472
475
  const _abortController = (0, Utils_1.extendAbortController)(abortSignal);
@@ -505,15 +508,16 @@ class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
505
508
  gasAmount: _options.gasAmount,
506
509
  callerFeeRate: (0, Utils_1.throwIfUndefined)(callerFeePrefetchPromise[version], "Caller fee prefetch failed!"),
507
510
  frontingFeeRate: 0n,
511
+ stickyAddress: options?.stickyAddress,
508
512
  additionalParams
509
513
  }, this._options.postRequestTimeout, abortController.signal, retryCount > 0 ? false : undefined);
510
514
  }, undefined, e => e instanceof RequestError_1.RequestError, abortController.signal);
511
515
  this.logger.debug("create(" + lp.url + "): LP response: ", resp);
512
516
  const callerFeeShare = (await callerFeePrefetchPromise[version]);
513
517
  const [pricingInfo, gasPricingInfo, { vault, vaultUtxoValue }] = await Promise.all([
514
- this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.SPV_VAULT_FROM_BTC], false, resp.btcAmountSwap, resp.total * (100000n + callerFeeShare) / 100000n, amountData.token, {}, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal),
518
+ this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.SPV_VAULT_FROM_BTC], false, resp.btcAmountSwap, resp.total * (100000n + callerFeeShare) / 100000n, amountData.token, { swapFeeBtc: resp.swapFeeBtc }, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal),
515
519
  _options.gasAmount === 0n ? Promise.resolve(undefined) : this.verifyReturnedPrice({ ...lp.services[SwapType_1.SwapType.SPV_VAULT_FROM_BTC], swapBaseFee: 0 }, //Base fee should be charged only on the amount, not on gas
516
- false, resp.btcAmountGas, resp.totalGas * (100000n + callerFeeShare) / 100000n, nativeTokenAddress, {}, gasTokenPricePrefetchPromise, usdPricePrefetchPromise, abortController.signal),
520
+ false, resp.btcAmountGas, resp.totalGas * (100000n + callerFeeShare) / 100000n, nativeTokenAddress, { swapFeeBtc: resp.gasSwapFeeBtc }, gasTokenPricePrefetchPromise, usdPricePrefetchPromise, abortController.signal),
517
521
  this.verifyReturnedData(resp, amountData, lp, _options, callerFeeShare, bitcoinFeeRatePromise, abortController.signal)
518
522
  ]);
519
523
  const swapInit = {
@@ -62,7 +62,7 @@ class LnForGasWrapper extends ISwapWrapper_1.ISwapWrapper {
62
62
  throw new IntermediaryError_1.IntermediaryError("Invalid total returned");
63
63
  const pricingInfo = await this.verifyReturnedPrice(typeof (lpOrUrl) === "string" || lpOrUrl.services[SwapType_1.SwapType.TRUSTED_FROM_BTCLN] == null ?
64
64
  { swapFeePPM: 10000, swapBaseFee: 10 } :
65
- lpOrUrl.services[SwapType_1.SwapType.TRUSTED_FROM_BTCLN], false, amountIn, amount, token, {});
65
+ lpOrUrl.services[SwapType_1.SwapType.TRUSTED_FROM_BTCLN], false, amountIn, amount, token, { swapFeeBtc: resp.swapFeeSats });
66
66
  const quoteInit = {
67
67
  pr: resp.pr,
68
68
  outputAmount: resp.total,
@@ -69,7 +69,7 @@ class OnchainForGasWrapper extends ISwapWrapper_1.ISwapWrapper {
69
69
  throw new IntermediaryError_1.IntermediaryError("Invalid total returned");
70
70
  const pricingInfo = await this.verifyReturnedPrice(typeof (lpOrUrl) === "string" || lpOrUrl.services[SwapType_1.SwapType.TRUSTED_FROM_BTC] == null ?
71
71
  { swapFeePPM: 10000, swapBaseFee: 10 } :
72
- lpOrUrl.services[SwapType_1.SwapType.TRUSTED_FROM_BTC], false, resp.amountSats, amount, this._chain.getNativeCurrencyAddress(), {});
72
+ lpOrUrl.services[SwapType_1.SwapType.TRUSTED_FROM_BTC], false, resp.amountSats, amount, this._chain.getNativeCurrencyAddress(), { swapFeeBtc: resp.swapFeeSats });
73
73
  const quote = new OnchainForGasSwap_1.OnchainForGasSwap(this, {
74
74
  paymentHash: resp.paymentHash,
75
75
  sequence: resp.sequence,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/sdk",
3
- "version": "8.7.2",
3
+ "version": "8.7.4",
4
4
  "description": "atomiq labs SDK for cross-chain swaps between smart chains and bitcoin",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "author": "adambor",
24
24
  "license": "ISC",
25
25
  "dependencies": {
26
- "@atomiqlabs/base": "^13.5.0",
26
+ "@atomiqlabs/base": "^13.5.2",
27
27
  "@atomiqlabs/bolt11": "1.6.1",
28
28
  "@atomiqlabs/btc-mempool": "^1.0.4",
29
29
  "@atomiqlabs/messenger-nostr": "^2.0.0",
@@ -3,12 +3,14 @@ import {BTC_NETWORK, NETWORK, pubECDSA, randomPrivateKeyBytes, TEST_NETWORK} fro
3
3
  import {getAddress, Transaction, WIF} from "@scure/btc-signer";
4
4
  import {Buffer} from "buffer";
5
5
  import {identifyAddressType, BitcoinWallet} from "./BitcoinWallet";
6
- import {BitcoinNetwork, BitcoinRpcWithAddressIndex} from "@atomiqlabs/base";
6
+ import {BitcoinNetwork, BitcoinRpcWithAddressIndex, getLogger} from "@atomiqlabs/base";
7
7
  import {HDKey} from "@scure/bip32";
8
8
  import {entropyToMnemonic, generateMnemonic, mnemonicToSeed} from "@scure/bip39";
9
9
  import {wordlist} from "@scure/bip39/wordlists/english.js";
10
10
  import {sha256} from "@noble/hashes/sha2";
11
11
 
12
+ const logger = getLogger("SingleAddressBitcoinWallet: ");
13
+
12
14
  /**
13
15
  * Bitcoin wallet implementation deriving a single address from a WIF encoded private key
14
16
  *
@@ -42,11 +44,21 @@ export class SingleAddressBitcoinWallet extends BitcoinWallet {
42
44
  const address = getAddress("wpkh", this.privKey, network);
43
45
  if(address==null) throw new Error("Failed to generate p2wpkh address from the provided private key!");
44
46
  this.address = address;
47
+ this.addressType = identifyAddressType(this.address, network);
45
48
  } else {
46
49
  this.address = addressDataOrWIF.address;
50
+ this.addressType = identifyAddressType(this.address, network);
47
51
  this.pubkey = Buffer.from(addressDataOrWIF.publicKey, "hex");
52
+ // Some wallets seem to be returning a full 33-byte compressed pubkey instead of a taproot
53
+ // 32-byte long X-only key. Handle these cases here
54
+ if(this.addressType==="p2tr") {
55
+ if(this.pubkey.length!==33) return;
56
+ const leadingByte = this.pubkey[0];
57
+ if(leadingByte!==0x03 && leadingByte!==0x02) throw new Error("Invalid public key passed for taproot bitcoin wallet, expected an X-only 32-byte public key, or a compressed 33-byte public key");
58
+ logger.debug(`constructor(): Converting compressed public key ${addressDataOrWIF.publicKey} to taproot X-only 32-byte public key`);
59
+ this.pubkey = this.pubkey.slice(1);
60
+ }
48
61
  }
49
- this.addressType = identifyAddressType(this.address, network);
50
62
  }
51
63
 
52
64
  /**
@@ -310,7 +310,8 @@ export type SpvFromBTCPrepare = SwapInit & {
310
310
  gasToken: string,
311
311
  exactOut: boolean,
312
312
  callerFeeRate: Promise<bigint>,
313
- frontingFeeRate: bigint
313
+ frontingFeeRate: bigint,
314
+ stickyAddress?: boolean
314
315
  }
315
316
 
316
317
  const SpvFromBTCInitResponseSchema = {
@@ -878,7 +879,8 @@ export class IntermediaryAPI {
878
879
  gasAmount: init.gasAmount.toString(10),
879
880
  gasToken: init.gasToken,
880
881
  frontingFeeRate: init.frontingFeeRate.toString(10),
881
- callerFeeRate: init.callerFeeRate.then(val => val.toString(10))
882
+ callerFeeRate: init.callerFeeRate.then(val => val.toString(10)),
883
+ stickyAddress: init.stickyAddress
882
884
  }, {
883
885
  code: FieldTypeEnum.Number,
884
886
  msg: FieldTypeEnum.String,
@@ -100,6 +100,7 @@ export abstract class ISwapPrice<T extends MultiChain = MultiChain> {
100
100
  * @param tokenAddress Token address to be paid
101
101
  * @param abortSignal
102
102
  * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice}
103
+ * @param realSwapFeeSats
103
104
  */
104
105
  public async isValidAmountSend<C extends ChainIds<T>>(
105
106
  chainIdentifier: C,
@@ -109,11 +110,15 @@ export abstract class ISwapPrice<T extends MultiChain = MultiChain> {
109
110
  paidToken: bigint,
110
111
  tokenAddress: string,
111
112
  abortSignal?: AbortSignal,
112
- preFetchedPrice?: bigint | null
113
+ preFetchedPrice?: bigint | null,
114
+ realSwapFeeSats?: bigint
113
115
  ): Promise<PriceInfoType> {
114
- const totalSats = (amountSats * (1000000n + feePPM) / 1000000n)
115
- + satsBaseFee;
116
+ if(realSwapFeeSats!=undefined && realSwapFeeSats<0) throw new Error("Invalid swap fee! Swap fee cannot be negative!");
117
+ const totalSats = realSwapFeeSats!=undefined
118
+ ? amountSats + realSwapFeeSats
119
+ : (amountSats * (1000000n + feePPM) / 1000000n) + satsBaseFee;
116
120
  const totalUSats = totalSats * 1000000n;
121
+
117
122
  const swapPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / paidToken;
118
123
 
119
124
  if(this.shouldIgnore(chainIdentifier, tokenAddress)) return {
@@ -185,6 +190,7 @@ export abstract class ISwapPrice<T extends MultiChain = MultiChain> {
185
190
  * @param tokenAddress Token address to be received
186
191
  * @param abortSignal
187
192
  * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice}
193
+ * @param realSwapFeeSats
188
194
  */
189
195
  public async isValidAmountReceive<C extends ChainIds<T>>(
190
196
  chainIdentifier: C,
@@ -194,11 +200,18 @@ export abstract class ISwapPrice<T extends MultiChain = MultiChain> {
194
200
  receiveToken: bigint,
195
201
  tokenAddress: string,
196
202
  abortSignal?: AbortSignal,
197
- preFetchedPrice?: bigint | null
203
+ preFetchedPrice?: bigint | null,
204
+ realSwapFeeSats?: bigint
198
205
  ): Promise<PriceInfoType> {
199
- const totalSats = (amountSats * (1000000n - feePPM) / 1000000n)
200
- - satsBaseFee;
206
+ if(realSwapFeeSats!=undefined) {
207
+ if(realSwapFeeSats>=amountSats) throw new Error("Invalid swap fee! Larger than or equal to total output amount!");
208
+ if(realSwapFeeSats<0) throw new Error("Invalid swap fee! Must be non-negative!");
209
+ }
210
+ const totalSats = realSwapFeeSats!=undefined
211
+ ? amountSats - realSwapFeeSats
212
+ : (amountSats * (1000000n - feePPM) / 1000000n) - satsBaseFee;
201
213
  const totalUSats = totalSats * 1000000n;
214
+
202
215
  const swapPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / receiveToken;
203
216
 
204
217
  if(this.shouldIgnore(chainIdentifier, tokenAddress)) return {
@@ -454,6 +454,18 @@ export class SwapperUtils<T extends MultiChain> {
454
454
  return this.root._tokens[chainIdentifier][this.root._chains[chainIdentifier].chainInterface.getNativeCurrencyAddress()] as SCToken<ChainIdentifier>;
455
455
  }
456
456
 
457
+ /**
458
+ * Returns whether when swapping to the provided token a gas drop can be requested
459
+ *
460
+ * @param token
461
+ */
462
+ destinationTokenSupportsGasDrop<ChainIdentifier extends ChainIds<T>>(token: SCToken<ChainIdentifier>): boolean {
463
+ if(this.root._chains[token.chainId]==null) throw new Error("Invalid chain identifier! Unknown chain: "+token.chainId);
464
+ const {chainInterface} = this.root._chains[token.chainId];
465
+ if(chainInterface.shouldGetNativeTokenDrop!=null) return chainInterface.shouldGetNativeTokenDrop(token.address);
466
+ return chainInterface.getNativeCurrencyAddress() !== token.address;
467
+ }
468
+
457
469
  /**
458
470
  * Returns a random signer for a given smart chain
459
471
  *
@@ -322,28 +322,39 @@ export abstract class ISwap<
322
322
  public async refreshPriceData(): Promise<void> {
323
323
  if(this.pricingInfo==null) return;
324
324
  const priceUsdPerBtc = this.pricingInfo.realPriceUsdPerBitcoin;
325
- const input = this.getInput();
326
325
  const output = this.getOutput();
327
- if(input.isUnknown || output.isUnknown) return;
326
+ if(output.isUnknown) return;
327
+
328
+ if(isSCToken(this.getInputToken()) && this.getDirection()===SwapDirection.TO_BTC) {
329
+ const input = this.getInputWithoutFee();
330
+ if(input.isUnknown) return;
328
331
 
329
- if(isSCToken(input.token) && this.getDirection()===SwapDirection.TO_BTC) {
330
332
  this.pricingInfo = await this.wrapper._prices.isValidAmountSend(
331
333
  this.chainIdentifier,
332
334
  output.rawAmount!,
333
335
  this.pricingInfo.satsBaseFee,
334
336
  this.pricingInfo.feePPM,
335
- input.rawAmount!,
336
- input.token.address
337
+ input.rawAmount! + this.swapFee,
338
+ input.token.address,
339
+ undefined,
340
+ undefined,
341
+ this.swapFeeBtc
337
342
  );
338
343
  this.pricingInfo.realPriceUsdPerBitcoin = priceUsdPerBtc;
339
344
  } else if(isSCToken(output.token) && this.getDirection()===SwapDirection.FROM_BTC) {
345
+ const input = this.getInput();
346
+ if(input.isUnknown) return;
347
+
340
348
  this.pricingInfo = await this.wrapper._prices.isValidAmountReceive(
341
349
  this.chainIdentifier,
342
350
  input.rawAmount!,
343
351
  this.pricingInfo.satsBaseFee,
344
352
  this.pricingInfo.feePPM,
345
353
  output.rawAmount!,
346
- output.token.address
354
+ output.token.address,
355
+ undefined,
356
+ undefined,
357
+ this.swapFeeBtc
347
358
  );
348
359
  this.pricingInfo.realPriceUsdPerBitcoin = priceUsdPerBtc;
349
360
  }
@@ -259,7 +259,8 @@ export abstract class ISwapWrapper<
259
259
  amountToken: bigint,
260
260
  token: string,
261
261
  feeData: {
262
- networkFee?: bigint
262
+ networkFee?: bigint,
263
+ swapFeeBtc?: bigint
263
264
  },
264
265
  pricePrefetchPromise: Promise<bigint | undefined> = Promise.resolve(undefined),
265
266
  usdPricePrefetchPromise: Promise<number | undefined> = Promise.resolve(undefined),
@@ -271,8 +272,8 @@ export abstract class ISwapWrapper<
271
272
 
272
273
  const [isValidAmount, usdPrice] = await Promise.all([
273
274
  send ?
274
- this._prices.isValidAmountSend(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise) :
275
- this._prices.isValidAmountReceive(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise),
275
+ this._prices.isValidAmountSend(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise, feeData.swapFeeBtc) :
276
+ this._prices.isValidAmountReceive(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise, feeData.swapFeeBtc),
276
277
  usdPricePrefetchPromise.then(value => {
277
278
  if(value!=null) return value;
278
279
  return this._prices.preFetchUsdPrice(abortSignal);
@@ -342,12 +342,14 @@ export class FromBTCLNWrapper<
342
342
  if(decodedPr.timeExpireDate==null) throw new IntermediaryError("Invalid returned swap invoice, no expiry date field");
343
343
  const amountIn = (BigInt(decodedPr.millisatoshis) + 999n) / 1000n;
344
344
 
345
+ const swapFeeBtc = resp.swapFee * amountIn / (resp.total - resp.swapFee);
346
+
345
347
  try {
346
348
  this.verifyReturnedData(resp, amountData, lp, _options, decodedPr, paymentHash);
347
349
  const [pricingInfo] = await Promise.all([
348
350
  this.verifyReturnedPrice(
349
351
  lp.services[SwapType.FROM_BTCLN], false, amountIn, resp.total,
350
- amountData.token, {}, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal
352
+ amountData.token, {swapFeeBtc}, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal
351
353
  ),
352
354
  this.verifyIntermediaryLiquidity(resp.total, throwIfUndefined(liquidityPromise)),
353
355
  lnCapacityPromise!=null ? this.verifyLnNodeCapacity(lp, decodedPr, lnCapacityPromise, abortController.signal) : Promise.resolve()
@@ -358,7 +360,7 @@ export class FromBTCLNWrapper<
358
360
  url: lp.url,
359
361
  expiry: decodedPr.timeExpireDate*1000,
360
362
  swapFee: resp.swapFee,
361
- swapFeeBtc: resp.swapFee * amountIn / (resp.total - resp.swapFee),
363
+ swapFeeBtc,
362
364
  feeRate: (await _preFetches.feeRatePromise[version])!,
363
365
  initialSwapData: await this._contract(version).createSwapData(
364
366
  ChainSwapType.HTLC, lp.getAddress(this.chainIdentifier), recipient, amountData.token,
@@ -309,7 +309,10 @@ export class FromBTCLNAutoSwap<T extends ChainType = ChainType>
309
309
  this.pricingInfo.satsBaseFee,
310
310
  this.pricingInfo.feePPM,
311
311
  this.getOutputAmountWithoutFee(),
312
- this.getSwapData().getToken()
312
+ this.getSwapData().getToken(),
313
+ undefined,
314
+ undefined,
315
+ this.swapFeeBtc
313
316
  );
314
317
  this.pricingInfo.realPriceUsdPerBitcoin = usdPricePerBtc;
315
318
  }
@@ -382,8 +382,15 @@ export class FromBTCLNAutoWrapper<
382
382
  descriptionHash: parseHashValueExact32Bytes(options?.descriptionHash, "description hash")
383
383
  };
384
384
 
385
- if(amountData.token===this._chain.getNativeCurrencyAddress() && _options.gasAmount!==0n)
386
- throw new UserError("Cannot specify `gasAmount` for swaps to a native token!");
385
+
386
+ if(
387
+ _options.gasAmount!==0n &&
388
+ (
389
+ this._chain.shouldGetNativeTokenDrop!=null
390
+ ? !this._chain.shouldGetNativeTokenDrop(amountData.token)
391
+ : amountData.token===this._chain.getNativeCurrencyAddress()
392
+ )
393
+ ) throw new UserError("Cannot specify `gasAmount` for swaps to a native token!");
387
394
 
388
395
  if(_options.description!=null && Buffer.byteLength(_options.description, "utf8") > 500)
389
396
  throw new UserError("Invalid description length");
@@ -468,13 +475,13 @@ export class FromBTCLNAutoWrapper<
468
475
  lp.services[SwapType.FROM_BTCLN_AUTO],
469
476
  false, resp.btcAmountSwap,
470
477
  resp.total,
471
- amountData.token, {}, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal
478
+ amountData.token, {swapFeeBtc: resp.swapFeeBtc}, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal
472
479
  ),
473
480
  _options.gasAmount===0n ? Promise.resolve(undefined) : this.verifyReturnedPrice(
474
481
  {...lp.services[SwapType.FROM_BTCLN_AUTO], swapBaseFee: 0}, //Base fee should be charged only on the amount, not on gas
475
482
  false, resp.btcAmountGas,
476
483
  resp.totalGas + resp.claimerBounty,
477
- nativeTokenAddress, {}, _preFetches.gasTokenPricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal
484
+ nativeTokenAddress, {swapFeeBtc: resp.gasSwapFeeBtc}, _preFetches.gasTokenPricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal
478
485
  ),
479
486
  this.verifyIntermediaryLiquidity(resp.total, throwIfUndefined(liquidityPromise)),
480
487
  _options.unsafeSkipLnNodeCheck ? Promise.resolve() : this.verifyLnNodeCapacity(lp, decodedPr, lnCapacityPromise, abortController.signal)
@@ -528,12 +528,14 @@ export class FromBTCWrapper<
528
528
  const data: T["Data"] = new (this._swapDataDeserializer(version))(resp.data);
529
529
  data.setClaimer(recipient);
530
530
 
531
+ const swapFeeBtc = resp.swapFee * resp.amount / (data.getAmount() - resp.swapFee);
532
+
531
533
  this.verifyReturnedData(recipient, resp, amountData, lp, _options, data, sequence, (await claimerBountyPrefetchPromise[version])!, nativeTokenAddress);
532
534
  const [pricingInfo, signatureExpiry] = await Promise.all([
533
535
  //Get intermediary's liquidity
534
536
  this.verifyReturnedPrice(
535
537
  lp.services[SwapType.FROM_BTC], false, resp.amount, resp.total,
536
- amountData.token, {}, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
538
+ amountData.token, {swapFeeBtc}, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
537
539
  ),
538
540
  this.verifyReturnedSignature(recipient, data, resp, feeRatePromise[version], signDataPromise, version, abortController.signal),
539
541
  this.verifyIntermediaryLiquidity(data.getAmount(), throwIfUndefined(liquidityPromise)),
@@ -544,7 +546,7 @@ export class FromBTCWrapper<
544
546
  url: lp.url,
545
547
  expiry: signatureExpiry,
546
548
  swapFee: resp.swapFee,
547
- swapFeeBtc: resp.swapFee * resp.amount / (data.getAmount() - resp.swapFee),
549
+ swapFeeBtc,
548
550
  feeRate: (await feeRatePromise[version])!,
549
551
  signatureData: resp,
550
552
  data,
@@ -315,10 +315,12 @@ export class ToBTCLNWrapper<T extends ChainType> extends IToBTCWrapper<T, ToBTCL
315
315
 
316
316
  await this.verifyReturnedData(signer, resp, parsedPr, amountData.token, lp, calculatedOptions, data);
317
317
 
318
+ const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee);
319
+
318
320
  const [pricingInfo, signatureExpiry, reputation] = await Promise.all([
319
321
  this.verifyReturnedPrice(
320
322
  lp.services[SwapType.TO_BTCLN], true, amountOut, data.getAmount(),
321
- amountData.token, {networkFee: resp.maxFee},
323
+ amountData.token, {networkFee: resp.maxFee, swapFeeBtc},
322
324
  preFetches.pricePreFetchPromise, preFetches.usdPricePrefetchPromise, abortController.signal
323
325
  ),
324
326
  this.verifyReturnedSignature(
@@ -330,8 +332,6 @@ export class ToBTCLNWrapper<T extends ChainType> extends IToBTCWrapper<T, ToBTCL
330
332
 
331
333
  if(reputation!=null) lp.reputation[amountData.token.toString()] = reputation;
332
334
 
333
- const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee);
334
-
335
335
  const quote = new ToBTCLNSwap<T>(this, {
336
336
  pricingInfo,
337
337
  url: lp.url,
@@ -528,10 +528,12 @@ export class ToBTCLNWrapper<T extends ChainType> extends IToBTCWrapper<T, ToBTCL
528
528
 
529
529
  await this.verifyReturnedData(signer, resp, parsedInvoice, amountData.token, lp, calculatedOptions, data, amountData.amount);
530
530
 
531
+ const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee);
532
+
531
533
  const [pricingInfo, signatureExpiry, reputation] = await Promise.all([
532
534
  this.verifyReturnedPrice(
533
535
  lp.services[SwapType.TO_BTCLN], true, prepareResp.amount, data.getAmount(),
534
- amountData.token, {networkFee: resp.maxFee},
536
+ amountData.token, {networkFee: resp.maxFee, swapFeeBtc},
535
537
  preFetches.pricePreFetchPromise, preFetches.usdPricePrefetchPromise, abortSignal
536
538
  ),
537
539
  this.verifyReturnedSignature(
@@ -543,8 +545,6 @@ export class ToBTCLNWrapper<T extends ChainType> extends IToBTCWrapper<T, ToBTCL
543
545
 
544
546
  if(reputation!=null) lp.reputation[amountData.token.toString()] = reputation;
545
547
 
546
- const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee);
547
-
548
548
  const quote = new ToBTCLNSwap<T>(this, {
549
549
  pricingInfo,
550
550
  url: lp.url,
@@ -304,11 +304,15 @@ export class ToBTCWrapper<T extends ChainType> extends IToBTCWrapper<T, ToBTCDef
304
304
  const data: T["Data"] = new (this._swapDataDeserializer(version))(resp.data);
305
305
  data.setOfferer(signer);
306
306
 
307
+ const inputWithoutFees = data.getAmount() - resp.swapFee - resp.networkFee;
308
+ const swapFeeBtc = resp.swapFee * resp.amount / inputWithoutFees;
309
+ const networkFeeBtc = resp.networkFee * resp.amount / inputWithoutFees;
310
+
307
311
  this.verifyReturnedData(signer, resp, amountData, lp, _options, data, hash);
308
312
  const [pricingInfo, signatureExpiry, reputation] = await Promise.all([
309
313
  this.verifyReturnedPrice(
310
314
  lp.services[SwapType.TO_BTC], true, resp.amount, data.getAmount(),
311
- amountData.token, resp, pricePreFetchPromise, usdPricePrefetchPromise, abortController.signal
315
+ amountData.token, {networkFee: resp.networkFee, swapFeeBtc}, pricePreFetchPromise, usdPricePrefetchPromise, abortController.signal
312
316
  ),
313
317
  this.verifyReturnedSignature(signer, data, resp, feeRatePromise[version], signDataPromise, version, abortController.signal),
314
318
  reputationPromise
@@ -317,10 +321,6 @@ export class ToBTCWrapper<T extends ChainType> extends IToBTCWrapper<T, ToBTCDef
317
321
 
318
322
  if(reputation!=null) lp.reputation[amountData.token.toString()] = reputation;
319
323
 
320
- const inputWithoutFees = data.getAmount() - resp.swapFee - resp.networkFee;
321
- const swapFeeBtc = resp.swapFee * resp.amount / inputWithoutFees;
322
- const networkFeeBtc = resp.networkFee * resp.amount / inputWithoutFees
323
-
324
324
  const quote = new ToBTCSwap<T>(this, {
325
325
  pricingInfo,
326
326
  url: lp.url,
@@ -402,7 +402,10 @@ export class SpvFromBTCSwap<T extends ChainType>
402
402
  this.pricingInfo.satsBaseFee,
403
403
  this.pricingInfo.feePPM,
404
404
  this.getOutputWithoutFee().rawAmount,
405
- this.outputSwapToken
405
+ this.outputSwapToken,
406
+ undefined,
407
+ undefined,
408
+ this.swapFeeBtc
406
409
  );
407
410
  this.pricingInfo.realPriceUsdPerBitcoin = usdPricePerBtc;
408
411
  }
@@ -68,6 +68,13 @@ export type SpvFromBTCOptions = {
68
68
  * the settlement gas fee cost)
69
69
  */
70
70
  feeSafetyFactor?: number,
71
+ /**
72
+ * Instruct the LP to create a "sticky address" for your destination wallet address. After the first successful
73
+ * swap with that LP, the used bitcoin address will be permanently linked to your destination wallet address. So
74
+ * all subsequent swaps to the same address will yield the same LP deposit bitcoin address. Useful for corporate
75
+ * whitelist-only wallets
76
+ */
77
+ stickyAddress?: boolean,
71
78
 
72
79
  /**
73
80
  * @deprecated Use `maxAllowedBitcoinFeeRate` instead!
@@ -636,8 +643,14 @@ export class SpvFromBTCWrapper<
636
643
  maxAllowedBitcoinFeeRate: options?.maxAllowedBitcoinFeeRate ?? options?.maxAllowedNetworkFeeRate ?? Infinity
637
644
  };
638
645
 
639
- if(amountData.token===this._chain.getNativeCurrencyAddress() && _options.gasAmount!==0n)
640
- throw new UserError("Cannot specify `gasAmount` for swaps to a native token!");
646
+ if(
647
+ _options.gasAmount!==0n &&
648
+ (
649
+ this._chain.shouldGetNativeTokenDrop!=null
650
+ ? !this._chain.shouldGetNativeTokenDrop(amountData.token)
651
+ : amountData.token===this._chain.getNativeCurrencyAddress()
652
+ )
653
+ ) throw new UserError("Cannot specify `gasAmount` for swaps to a native token!");
641
654
 
642
655
  const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
643
656
 
@@ -681,6 +694,7 @@ export class SpvFromBTCWrapper<
681
694
  gasAmount: _options.gasAmount,
682
695
  callerFeeRate: throwIfUndefined(callerFeePrefetchPromise[version], "Caller fee prefetch failed!"),
683
696
  frontingFeeRate: 0n,
697
+ stickyAddress: options?.stickyAddress,
684
698
  additionalParams
685
699
  },
686
700
  this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined
@@ -700,13 +714,13 @@ export class SpvFromBTCWrapper<
700
714
  lp.services[SwapType.SPV_VAULT_FROM_BTC],
701
715
  false, resp.btcAmountSwap,
702
716
  resp.total * (100_000n + callerFeeShare) / 100_000n,
703
- amountData.token, {}, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
717
+ amountData.token, {swapFeeBtc: resp.swapFeeBtc}, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
704
718
  ),
705
719
  _options.gasAmount===0n ? Promise.resolve(undefined) : this.verifyReturnedPrice(
706
720
  {...lp.services[SwapType.SPV_VAULT_FROM_BTC], swapBaseFee: 0}, //Base fee should be charged only on the amount, not on gas
707
721
  false, resp.btcAmountGas,
708
722
  resp.totalGas * (100_000n + callerFeeShare) / 100_000n,
709
- nativeTokenAddress, {}, gasTokenPricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
723
+ nativeTokenAddress, {swapFeeBtc: resp.gasSwapFeeBtc}, gasTokenPricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
710
724
  ),
711
725
  this.verifyReturnedData(resp, amountData, lp, _options, callerFeeShare, bitcoinFeeRatePromise, abortController.signal)
712
726
  ]);
@@ -68,7 +68,7 @@ export class LnForGasWrapper<T extends ChainType> extends ISwapWrapper<T, LnForG
68
68
  {swapFeePPM: 10000, swapBaseFee: 10} :
69
69
  lpOrUrl.services[SwapType.TRUSTED_FROM_BTCLN],
70
70
  false, amountIn,
71
- amount, token, {}
71
+ amount, token, {swapFeeBtc: resp.swapFeeSats}
72
72
  );
73
73
 
74
74
  const quoteInit: LnForGasSwapInit = {
@@ -105,7 +105,7 @@ export class OnchainForGasWrapper<T extends ChainType> extends ISwapWrapper<T, O
105
105
  {swapFeePPM: 10000, swapBaseFee: 10} :
106
106
  lpOrUrl.services[SwapType.TRUSTED_FROM_BTC],
107
107
  false, resp.amountSats,
108
- amount, this._chain.getNativeCurrencyAddress(), {}
108
+ amount, this._chain.getNativeCurrencyAddress(), {swapFeeBtc: resp.swapFeeSats}
109
109
  );
110
110
 
111
111
  const quote = new OnchainForGasSwap(this, {