@atomiqlabs/lp-lib 17.3.0 → 17.4.0

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/index.d.ts CHANGED
@@ -38,5 +38,6 @@ export * from "./wallets/ILightningWallet";
38
38
  export * from "./wallets/ISpvVaultSigner";
39
39
  export * from "./swaps/spv_vault_swap/SpvVaults";
40
40
  export * from "./swaps/spv_vault_swap/SpvVault";
41
+ export * from "./swaps/spv_vault_swap/StickyAddress";
41
42
  export * from "./swaps/spv_vault_swap/SpvVaultSwap";
42
43
  export * from "./swaps/spv_vault_swap/SpvVaultSwapHandler";
package/dist/index.js CHANGED
@@ -54,5 +54,6 @@ __exportStar(require("./wallets/ILightningWallet"), exports);
54
54
  __exportStar(require("./wallets/ISpvVaultSigner"), exports);
55
55
  __exportStar(require("./swaps/spv_vault_swap/SpvVaults"), exports);
56
56
  __exportStar(require("./swaps/spv_vault_swap/SpvVault"), exports);
57
+ __exportStar(require("./swaps/spv_vault_swap/StickyAddress"), exports);
57
58
  __exportStar(require("./swaps/spv_vault_swap/SpvVaultSwap"), exports);
58
59
  __exportStar(require("./swaps/spv_vault_swap/SpvVaultSwapHandler"), exports);
@@ -59,8 +59,8 @@ export interface IPlugin {
59
59
  author: string;
60
60
  description: string;
61
61
  onEnable(chainsData: MultichainData, bitcoinRpc: BitcoinRpc<any>, bitcoinWallet: IBitcoinWallet, lightningWallet: ILightningWallet, swapPricing: ISwapPrice, tokens: {
62
- [ticker: string]: {
63
- [chainId: string]: {
62
+ [chainId: string]: {
63
+ [ticker: string]: {
64
64
  address: string;
65
65
  decimals: number;
66
66
  };
@@ -28,8 +28,8 @@ export declare class PluginManager {
28
28
  static registerPlugin(name: string, plugin: IPlugin): void;
29
29
  static unregisterPlugin(name: string): boolean;
30
30
  static enable<T extends SwapData>(chainsData: MultichainData, bitcoinRpc: BitcoinRpc<any>, bitcoinWallet: IBitcoinWallet, lightningWallet: ILightningWallet, swapPricing: ISwapPrice, tokens: {
31
- [ticker: string]: {
32
- [chainId: string]: {
31
+ [chainId: string]: {
32
+ [ticker: string]: {
33
33
  address: string;
34
34
  decimals: number;
35
35
  };
@@ -24,8 +24,8 @@ class FromBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
24
24
  AmountAssertions_1.AmountAssertions.handlePluginErrorResponses(res);
25
25
  if ((0, IPlugin_1.isQuoteSetFees)(res)) {
26
26
  return {
27
- baseFee: res.baseFee || this.config.baseFee,
28
- feePPM: res.feePPM || this.config.feePPM,
27
+ baseFee: res.baseFee ?? this.config.baseFee,
28
+ feePPM: res.feePPM ?? this.config.feePPM,
29
29
  securityDepositApyPPM: res.securityDepositApyPPM,
30
30
  securityDepositBaseMultiplierPPM: res.securityDepositBaseMultiplierPPM
31
31
  };
@@ -19,8 +19,8 @@ class ToBtcAmountAssertions extends AmountAssertions_1.AmountAssertions {
19
19
  AmountAssertions_1.AmountAssertions.handlePluginErrorResponses(res);
20
20
  if ((0, IPlugin_1.isQuoteSetFees)(res)) {
21
21
  return {
22
- baseFee: res.baseFee || this.config.baseFee,
23
- feePPM: res.feePPM || this.config.feePPM
22
+ baseFee: res.baseFee ?? this.config.baseFee,
23
+ feePPM: res.feePPM ?? this.config.feePPM
24
24
  };
25
25
  }
26
26
  }
@@ -38,6 +38,8 @@ export declare class SpvVaultSwap extends SwapHandlerSwap<SpvVaultSwapState> {
38
38
  readonly token: string;
39
39
  readonly gasToken: string;
40
40
  btcTxId: string;
41
+ saveStickyAddress?: boolean;
42
+ hasStickyAddress?: boolean;
41
43
  constructor(chainIdentifier: string, quoteId: string, expiry: number, vault: SpvVault, vaultUtxo: string, btcAddress: string, btcFeeRate: number, recipient: string, amountBtc: bigint, amountToken: bigint, amountGasToken: bigint, swapFee: bigint, swapFeeInToken: bigint, gasSwapFee: bigint, gasSwapFeeInToken: bigint, callerFeeShare: bigint, frontingFeeShare: bigint, executionFeeShare: bigint, token: string, gasToken: string);
42
44
  constructor(data: any);
43
45
  serialize(): any;
@@ -75,6 +75,8 @@ class SpvVaultSwap extends SwapHandlerSwap_1.SwapHandlerSwap {
75
75
  this.tokenMultiplier = (0, Utils_1.deserializeBN)(chainIdentifierOrObj.tokenMultiplier);
76
76
  this.gasTokenMultiplier = (0, Utils_1.deserializeBN)(chainIdentifierOrObj.gasTokenMultiplier);
77
77
  this.btcTxId = chainIdentifierOrObj.btcTxId;
78
+ this.hasStickyAddress = chainIdentifierOrObj.hasStickyAddress;
79
+ this.saveStickyAddress = chainIdentifierOrObj.saveStickyAddress;
78
80
  }
79
81
  this.type = SwapHandler_1.SwapHandlerType.FROM_BTC_SPV;
80
82
  }
@@ -106,7 +108,9 @@ class SpvVaultSwap extends SwapHandlerSwap_1.SwapHandlerSwap {
106
108
  gasToken: this.gasToken,
107
109
  tokenMultiplier: (0, Utils_1.serializeBN)(this.tokenMultiplier),
108
110
  gasTokenMultiplier: (0, Utils_1.serializeBN)(this.gasTokenMultiplier),
109
- btcTxId: this.btcTxId
111
+ btcTxId: this.btcTxId,
112
+ hasStickyAddress: this.hasStickyAddress,
113
+ saveStickyAddress: this.saveStickyAddress
110
114
  };
111
115
  }
112
116
  getIdentifierHash() {
@@ -9,6 +9,7 @@ import { ISpvVaultSigner } from "../../wallets/ISpvVaultSigner";
9
9
  import { SpvVault } from "./SpvVault";
10
10
  import { FromBtcAmountAssertions } from "../assertions/FromBtcAmountAssertions";
11
11
  import { SpvVaults } from "./SpvVaults";
12
+ import { StickyAddress } from "./StickyAddress";
12
13
  export type SpvVaultSwapHandlerConfig = SwapBaseConfig & {
13
14
  vaultsCheckInterval: number;
14
15
  gasTokenMax: {
@@ -40,7 +41,10 @@ export declare class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVa
40
41
  readonly AmountAssertions: FromBtcAmountAssertions;
41
42
  readonly Vaults: SpvVaults;
42
43
  config: SpvVaultSwapHandlerConfig;
43
- constructor(storageDirectory: IIntermediaryStorage<SpvVaultSwap>, vaultStorage: IStorageManager<SpvVault>, path: string, chainsData: MultichainData, swapPricing: ISwapPrice, bitcoin: IBitcoinWallet, bitcoinRpc: BitcoinRpc<BtcBlock>, spvVaultSigner: ISpvVaultSigner, config: SpvVaultSwapHandlerConfig);
44
+ readonly stickyAddresses?: IStorageManager<StickyAddress>;
45
+ constructor(storageDirectory: IIntermediaryStorage<SpvVaultSwap>, vaultStorage: IStorageManager<SpvVault>, path: string, chainsData: MultichainData, swapPricing: ISwapPrice, bitcoin: IBitcoinWallet, bitcoinRpc: BitcoinRpc<BtcBlock>, spvVaultSigner: ISpvVaultSigner, config: SpvVaultSwapHandlerConfig, stickyAddresses?: IStorageManager<StickyAddress>);
46
+ private getStickyAddress;
47
+ addStickyAddress(chainId: string, address: string, btcAddress: string): Promise<void>;
44
48
  protected processClaimEvent(swap: SpvVaultSwap | null, event: SpvVaultClaimEvent): Promise<void>;
45
49
  /**
46
50
  * Chain event processor
@@ -15,9 +15,10 @@ const SpvVaults_1 = require("./SpvVaults");
15
15
  const BitcoinUtils_1 = require("../../utils/BitcoinUtils");
16
16
  const AmountAssertions_1 = require("../assertions/AmountAssertions");
17
17
  const IPlugin_1 = require("../../plugins/IPlugin");
18
+ const StickyAddress_1 = require("./StickyAddress");
18
19
  const TX_MAX_VSIZE = 16 * 1024;
19
20
  class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
20
- constructor(storageDirectory, vaultStorage, path, chainsData, swapPricing, bitcoin, bitcoinRpc, spvVaultSigner, config) {
21
+ constructor(storageDirectory, vaultStorage, path, chainsData, swapPricing, bitcoin, bitcoinRpc, spvVaultSigner, config, stickyAddresses) {
21
22
  super(storageDirectory, path, chainsData, swapPricing);
22
23
  this.type = SwapHandler_1.SwapHandlerType.FROM_BTC_SPV;
23
24
  this.inflightSwapStates = new Set([SpvVaultSwap_1.SpvVaultSwapState.SIGNED, SpvVaultSwap_1.SpvVaultSwapState.SENT, SpvVaultSwap_1.SpvVaultSwapState.BTC_CONFIRMED]);
@@ -28,6 +29,43 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
28
29
  this.config = config;
29
30
  this.AmountAssertions = new FromBtcAmountAssertions_1.FromBtcAmountAssertions(config, swapPricing);
30
31
  this.Vaults = new SpvVaults_1.SpvVaults(vaultStorage, bitcoin, spvVaultSigner, bitcoinRpc, this.chains, config);
32
+ this.stickyAddresses = stickyAddresses;
33
+ }
34
+ async getStickyAddress(chainId, address) {
35
+ if (this.stickyAddresses == null)
36
+ throw new Error("Sticky addresses are not supported!");
37
+ const { chainInterface } = this.getChain(chainId);
38
+ const normalizedAddress = chainInterface.normalizeAddress(address);
39
+ const addressIdentifier = chainId + "-" + normalizedAddress;
40
+ const result = this.stickyAddresses.data[addressIdentifier];
41
+ if (result == null)
42
+ return;
43
+ const btcAddress = result.address;
44
+ if (this.bitcoin.isOwnedAddress != null) {
45
+ if (!(await this.bitcoin.isOwnedAddress(btcAddress))) {
46
+ this.logger.warn(`getStickyAddress(): Failed to get sticky address, address ${btcAddress} is not controlled by our bitcoin wallet!`);
47
+ return;
48
+ }
49
+ }
50
+ return btcAddress;
51
+ }
52
+ async addStickyAddress(chainId, address, btcAddress) {
53
+ if (this.stickyAddresses == null)
54
+ throw new Error("Sticky addresses are not supported!");
55
+ const { chainInterface } = this.getChain(chainId);
56
+ const normalizedAddress = chainInterface.normalizeAddress(address);
57
+ if (this.bitcoin.isOwnedAddress != null) {
58
+ if (!(await this.bitcoin.isOwnedAddress(btcAddress))) {
59
+ this.logger.warn(`addStickyAddress(): Failed to create sticky address, address ${btcAddress} is not controlled by our bitcoin wallet!`);
60
+ return;
61
+ }
62
+ }
63
+ const addressIdentifier = chainId + "-" + normalizedAddress;
64
+ if (this.stickyAddresses.data[addressIdentifier] != null) {
65
+ this.logger.warn(`addStickyAddress(): Failed to create sticky address, address sticky address already exists for ${addressIdentifier}!`);
66
+ return;
67
+ }
68
+ await this.stickyAddresses.saveData(addressIdentifier, new StickyAddress_1.StickyAddress(btcAddress));
31
69
  }
32
70
  async processClaimEvent(swap, event) {
33
71
  if (swap == null)
@@ -35,6 +73,13 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
35
73
  //Update swap
36
74
  swap.txIds.claim = event.meta?.txId;
37
75
  await this.removeSwapData(swap, SpvVaultSwap_1.SpvVaultSwapState.CLAIMED);
76
+ if (swap.saveStickyAddress)
77
+ try {
78
+ await this.addStickyAddress(swap.chainIdentifier, swap.recipient, swap.btcAddress);
79
+ }
80
+ catch (e) {
81
+ this.logger.error(`processClaimEvent(): Failed to create the sticky address for swap ${swap.getIdentifier()}`);
82
+ }
38
83
  }
39
84
  /**
40
85
  * Chain event processor
@@ -91,6 +136,10 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
91
136
  this.btcTxIdIndex.set(swap.btcTxId, swap);
92
137
  }
93
138
  await this.Vaults.init();
139
+ if (this.stickyAddresses != null) {
140
+ await this.stickyAddresses.init();
141
+ await this.stickyAddresses.loadData(StickyAddress_1.StickyAddress);
142
+ }
94
143
  this.subscribeToEvents();
95
144
  await PluginManager_1.PluginManager.serviceInitialize(this);
96
145
  }
@@ -98,7 +147,8 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
98
147
  if (swap.state === SpvVaultSwap_1.SpvVaultSwapState.CREATED) {
99
148
  if (swap.expiry < Date.now() / 1000) {
100
149
  await this.removeSwapData(swap, SpvVaultSwap_1.SpvVaultSwapState.EXPIRED);
101
- await this.bitcoin.addUnusedAddress(swap.btcAddress);
150
+ if (!swap.hasStickyAddress)
151
+ await this.bitcoin.addUnusedAddress(swap.btcAddress);
102
152
  }
103
153
  }
104
154
  if (swap.state === SpvVaultSwap_1.SpvVaultSwapState.SIGNED) {
@@ -182,10 +232,14 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
182
232
  const { signer, chainInterface, spvVaultContract } = this.getChain(chainIdentifier);
183
233
  metadata.times.requestReceived = Date.now();
184
234
  /**
235
+ * address: string smart chain address of the recipient
185
236
  * token: string Desired token to use
186
237
  * gasToken: string
187
238
  */
188
239
  const preFetchParsedBody = await req.paramReader.getParams({
240
+ address: (val) => val != null &&
241
+ typeof (val) === "string" &&
242
+ chainInterface.isValidAddress(val, true) ? val : null,
189
243
  token: (val) => val != null &&
190
244
  typeof (val) === "string" &&
191
245
  this.isTokenSupported(chainIdentifier, val) ? val : null,
@@ -198,6 +252,10 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
198
252
  code: 20100,
199
253
  msg: "Invalid request body"
200
254
  };
255
+ const stickyAddressObject = req.paramReader.getExistingParamsOrNull({
256
+ stickyAddress: SchemaVerifier_1.FieldTypeEnum.BooleanOptional
257
+ });
258
+ const useStickyAddress = stickyAddressObject?.stickyAddress;
201
259
  //Create abortController for parallel prefetches
202
260
  const responseStream = res.responseStream;
203
261
  const abortController = (0, Utils_1.getAbortController)(responseStream);
@@ -210,7 +268,13 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
210
268
  });
211
269
  //Listener that re-adds the returned bitcoin address to the unused address list if request fails or closes
212
270
  let abortAddUnusedAddressListener;
213
- const bitcoinAddressPrefetch = this.bitcoin.getAddress().then(value => {
271
+ const bitcoinAddressPrefetch = (async () => {
272
+ if (useStickyAddress) {
273
+ const result = await this.getStickyAddress(chainIdentifier, preFetchParsedBody.address);
274
+ if (result != null)
275
+ return { address: result, isStickyAddress: true };
276
+ }
277
+ const value = await this.bitcoin.getAddress();
214
278
  //Already aborted
215
279
  if (abortController.signal.aborted) {
216
280
  this.bitcoin.addUnusedAddress(value);
@@ -220,13 +284,12 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
220
284
  abortController.signal.addEventListener("abort", abortAddUnusedAddressListener = () => {
221
285
  this.bitcoin.addUnusedAddress(value);
222
286
  });
223
- return value;
224
- }).catch(e => {
287
+ return { address: value, isStickyAddress: false };
288
+ })().catch(e => {
225
289
  abortController.abort(e);
226
290
  return null;
227
291
  });
228
292
  /**
229
- * address: string smart chain address of the recipient
230
293
  * amount: string amount (in sats)
231
294
  * gasAmount: string Desired amount in gas token to also get
232
295
  * exactOut: boolean Whether the swap should be an exact out instead of exact in swap
@@ -234,9 +297,6 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
234
297
  * frontingFeeRate: string Fronting fee (in output token) to assign to the swap
235
298
  */
236
299
  const actualParsedBody = await req.paramReader.getParams({
237
- address: (val) => val != null &&
238
- typeof (val) === "string" &&
239
- chainInterface.isValidAddress(val, true) ? val : null,
240
300
  amount: SchemaVerifier_1.FieldTypeEnum.BigInt,
241
301
  gasAmount: SchemaVerifier_1.FieldTypeEnum.BigInt,
242
302
  exactOut: SchemaVerifier_1.FieldTypeEnum.BooleanOptional,
@@ -304,9 +364,11 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
304
364
  metadata.times.vaultPicked = Date.now();
305
365
  //Create swap receive bitcoin address
306
366
  const btcFeeRate = await btcFeeRatePrefetch;
307
- const receiveAddress = await bitcoinAddressPrefetch;
367
+ const btcAddressObject = await bitcoinAddressPrefetch;
308
368
  abortController.signal.throwIfAborted();
309
369
  metadata.times.addressCreated = Date.now();
370
+ const receiveAddress = btcAddressObject.address;
371
+ const hasStickyAddress = btcAddressObject.isStickyAddress;
310
372
  //Adjust the amounts based on passed fees
311
373
  if (parsedBody.exactOut) {
312
374
  totalInToken = parsedBody.amount;
@@ -327,6 +389,8 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
327
389
  const quoteId = (0, crypto_1.randomBytes)(32).toString("hex");
328
390
  const swap = new SpvVaultSwap_1.SpvVaultSwap(chainIdentifier, quoteId, expiry, vault, utxo, receiveAddress, btcFeeRate, parsedBody.address, totalBtcOutput, totalInToken, totalInGasToken, swapFee, swapFeeInToken, gasSwapFee, gasSwapFeeInToken, callerFeeShare, frontingFeeShare, executionFeeShare, useToken, gasToken);
329
391
  swap.metadata = metadata;
392
+ swap.saveStickyAddress = useStickyAddress && !hasStickyAddress;
393
+ swap.hasStickyAddress = hasStickyAddress;
330
394
  //We can remove the listener to add unused address now, as we are about to save the swap
331
395
  abortController.signal.removeEventListener("abort", abortAddUnusedAddressListener);
332
396
  await PluginManager_1.PluginManager.swapCreate(swap);
@@ -0,0 +1,6 @@
1
+ import { StorageObject } from "@atomiqlabs/base";
2
+ export declare class StickyAddress implements StorageObject {
3
+ address: string;
4
+ constructor(addressOrSerialized: string | any);
5
+ serialize(): any;
6
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StickyAddress = void 0;
4
+ class StickyAddress {
5
+ constructor(addressOrSerialized) {
6
+ if (typeof (addressOrSerialized) === "string") {
7
+ this.address = addressOrSerialized;
8
+ }
9
+ else {
10
+ this.address = addressOrSerialized.address;
11
+ }
12
+ }
13
+ serialize() {
14
+ return {
15
+ address: this.address
16
+ };
17
+ }
18
+ }
19
+ exports.StickyAddress = StickyAddress;
@@ -63,6 +63,11 @@ export declare abstract class IBitcoinWallet {
63
63
  * Returns an unused address suitable for receiving
64
64
  */
65
65
  abstract getAddress(): Promise<string>;
66
+ /**
67
+ * Whether a provided address is controlled by this wallet
68
+ * @param address
69
+ */
70
+ isOwnedAddress?(address: string): Promise<boolean>;
66
71
  /**
67
72
  * Adds previously returned address (with getAddress call), to the pool of unused addresses
68
73
  * @param address
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/lp-lib",
3
- "version": "17.3.0",
3
+ "version": "17.4.0",
4
4
  "description": "Main functionality implementation for atomiq LP node",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
package/src/index.ts CHANGED
@@ -49,5 +49,6 @@ export * from "./wallets/ISpvVaultSigner";
49
49
 
50
50
  export * from "./swaps/spv_vault_swap/SpvVaults";
51
51
  export * from "./swaps/spv_vault_swap/SpvVault";
52
+ export * from "./swaps/spv_vault_swap/StickyAddress";
52
53
  export * from "./swaps/spv_vault_swap/SpvVaultSwap";
53
54
  export * from "./swaps/spv_vault_swap/SpvVaultSwapHandler";
@@ -96,8 +96,8 @@ export interface IPlugin {
96
96
 
97
97
  swapPricing: ISwapPrice,
98
98
  tokens: {
99
- [ticker: string]: {
100
- [chainId: string]: {
99
+ [chainId: string]: {
100
+ [ticker: string]: {
101
101
  address: string,
102
102
  decimals: number
103
103
  }
@@ -74,8 +74,8 @@ export class PluginManager {
74
74
 
75
75
  swapPricing: ISwapPrice,
76
76
  tokens: {
77
- [ticker: string]: {
78
- [chainId: string]: {
77
+ [chainId: string]: {
78
+ [ticker: string]: {
79
79
  address: string,
80
80
  decimals: number
81
81
  }
@@ -55,8 +55,8 @@ export class FromBtcAmountAssertions extends AmountAssertions {
55
55
  AmountAssertions.handlePluginErrorResponses(res);
56
56
  if(isQuoteSetFees(res)) {
57
57
  return {
58
- baseFee: res.baseFee || this.config.baseFee,
59
- feePPM: res.feePPM || this.config.feePPM,
58
+ baseFee: res.baseFee ?? this.config.baseFee,
59
+ feePPM: res.feePPM ?? this.config.feePPM,
60
60
  securityDepositApyPPM: res.securityDepositApyPPM,
61
61
  securityDepositBaseMultiplierPPM: res.securityDepositBaseMultiplierPPM
62
62
  }
@@ -33,8 +33,8 @@ export class ToBtcAmountAssertions extends AmountAssertions {
33
33
  AmountAssertions.handlePluginErrorResponses(res);
34
34
  if(isQuoteSetFees(res)) {
35
35
  return {
36
- baseFee: res.baseFee || this.config.baseFee,
37
- feePPM: res.feePPM || this.config.feePPM
36
+ baseFee: res.baseFee ?? this.config.baseFee,
37
+ feePPM: res.feePPM ?? this.config.feePPM
38
38
  }
39
39
  }
40
40
  }
@@ -53,6 +53,9 @@ export class SpvVaultSwap extends SwapHandlerSwap<SpvVaultSwapState> {
53
53
 
54
54
  btcTxId: string;
55
55
 
56
+ saveStickyAddress?: boolean;
57
+ hasStickyAddress?: boolean;
58
+
56
59
  constructor(
57
60
  chainIdentifier: string, quoteId: string, expiry: number,
58
61
  vault: SpvVault, vaultUtxo: string,
@@ -129,6 +132,8 @@ export class SpvVaultSwap extends SwapHandlerSwap<SpvVaultSwapState> {
129
132
  this.tokenMultiplier = deserializeBN(chainIdentifierOrObj.tokenMultiplier);
130
133
  this.gasTokenMultiplier = deserializeBN(chainIdentifierOrObj.gasTokenMultiplier);
131
134
  this.btcTxId = chainIdentifierOrObj.btcTxId;
135
+ this.hasStickyAddress = chainIdentifierOrObj.hasStickyAddress;
136
+ this.saveStickyAddress = chainIdentifierOrObj.saveStickyAddress;
132
137
  }
133
138
  this.type = SwapHandlerType.FROM_BTC_SPV;
134
139
  }
@@ -161,7 +166,9 @@ export class SpvVaultSwap extends SwapHandlerSwap<SpvVaultSwapState> {
161
166
  gasToken: this.gasToken,
162
167
  tokenMultiplier: serializeBN(this.tokenMultiplier),
163
168
  gasTokenMultiplier: serializeBN(this.gasTokenMultiplier),
164
- btcTxId: this.btcTxId
169
+ btcTxId: this.btcTxId,
170
+ hasStickyAddress: this.hasStickyAddress,
171
+ saveStickyAddress: this.saveStickyAddress
165
172
  };
166
173
  }
167
174
 
@@ -38,6 +38,7 @@ import {SpvVaults, VAULT_DUST_AMOUNT} from "./SpvVaults";
38
38
  import {isLegacyInput} from "../../utils/BitcoinUtils";
39
39
  import {AmountAssertions} from "../assertions/AmountAssertions";
40
40
  import {isQuoteThrow} from "../../plugins/IPlugin";
41
+ import {StickyAddress} from "./StickyAddress";
41
42
 
42
43
  export type SpvVaultSwapHandlerConfig = SwapBaseConfig & {
43
44
  vaultsCheckInterval: number,
@@ -78,6 +79,8 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
78
79
 
79
80
  config: SpvVaultSwapHandlerConfig;
80
81
 
82
+ readonly stickyAddresses?: IStorageManager<StickyAddress>;
83
+
81
84
  constructor(
82
85
  storageDirectory: IIntermediaryStorage<SpvVaultSwap>,
83
86
  vaultStorage: IStorageManager<SpvVault>,
@@ -87,7 +90,8 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
87
90
  bitcoin: IBitcoinWallet,
88
91
  bitcoinRpc: BitcoinRpc<BtcBlock>,
89
92
  spvVaultSigner: ISpvVaultSigner,
90
- config: SpvVaultSwapHandlerConfig
93
+ config: SpvVaultSwapHandlerConfig,
94
+ stickyAddresses?: IStorageManager<StickyAddress>
91
95
  ) {
92
96
  super(storageDirectory, path, chainsData, swapPricing);
93
97
  this.bitcoinRpc = bitcoinRpc;
@@ -96,6 +100,52 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
96
100
  this.config = config;
97
101
  this.AmountAssertions = new FromBtcAmountAssertions(config, swapPricing);
98
102
  this.Vaults = new SpvVaults(vaultStorage, bitcoin, spvVaultSigner, bitcoinRpc, this.chains, config);
103
+ this.stickyAddresses = stickyAddresses;
104
+ }
105
+
106
+ private async getStickyAddress(chainId: string, address: string): Promise<string | undefined> {
107
+ if(this.stickyAddresses==null) throw new Error("Sticky addresses are not supported!");
108
+
109
+ const {chainInterface} = this.getChain(chainId);
110
+ const normalizedAddress = chainInterface.normalizeAddress(address);
111
+
112
+ const addressIdentifier = chainId+"-"+normalizedAddress;
113
+
114
+ const result = this.stickyAddresses.data[addressIdentifier];
115
+ if(result==null) return;
116
+
117
+ const btcAddress = result.address;
118
+
119
+ if(this.bitcoin.isOwnedAddress!=null) {
120
+ if(!(await this.bitcoin.isOwnedAddress(btcAddress))) {
121
+ this.logger.warn(`getStickyAddress(): Failed to get sticky address, address ${btcAddress} is not controlled by our bitcoin wallet!`);
122
+ return;
123
+ }
124
+ }
125
+
126
+ return btcAddress;
127
+ }
128
+
129
+ async addStickyAddress(chainId: string, address: string, btcAddress: string) {
130
+ if(this.stickyAddresses==null) throw new Error("Sticky addresses are not supported!");
131
+
132
+ const {chainInterface} = this.getChain(chainId);
133
+ const normalizedAddress = chainInterface.normalizeAddress(address);
134
+
135
+ if(this.bitcoin.isOwnedAddress!=null) {
136
+ if(!(await this.bitcoin.isOwnedAddress(btcAddress))) {
137
+ this.logger.warn(`addStickyAddress(): Failed to create sticky address, address ${btcAddress} is not controlled by our bitcoin wallet!`);
138
+ return;
139
+ }
140
+ }
141
+
142
+ const addressIdentifier = chainId+"-"+normalizedAddress;
143
+
144
+ if(this.stickyAddresses.data[addressIdentifier]!=null) {
145
+ this.logger.warn(`addStickyAddress(): Failed to create sticky address, address sticky address already exists for ${addressIdentifier}!`);
146
+ return;
147
+ }
148
+ await this.stickyAddresses.saveData(addressIdentifier, new StickyAddress(btcAddress));
99
149
  }
100
150
 
101
151
  protected async processClaimEvent(swap: SpvVaultSwap | null, event: SpvVaultClaimEvent): Promise<void> {
@@ -103,6 +153,11 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
103
153
  //Update swap
104
154
  swap.txIds.claim = event.meta?.txId;
105
155
  await this.removeSwapData(swap, SpvVaultSwapState.CLAIMED);
156
+ if(swap.saveStickyAddress) try {
157
+ await this.addStickyAddress(swap.chainIdentifier, swap.recipient, swap.btcAddress);
158
+ } catch (e) {
159
+ this.logger.error(`processClaimEvent(): Failed to create the sticky address for swap ${swap.getIdentifier()}`);
160
+ }
106
161
  }
107
162
 
108
163
  /**
@@ -161,6 +216,10 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
161
216
  if(swap.btcTxId!=null) this.btcTxIdIndex.set(swap.btcTxId, swap);
162
217
  }
163
218
  await this.Vaults.init();
219
+ if(this.stickyAddresses!=null) {
220
+ await this.stickyAddresses.init();
221
+ await this.stickyAddresses.loadData(StickyAddress);
222
+ }
164
223
  this.subscribeToEvents();
165
224
  await PluginManager.serviceInitialize(this);
166
225
  }
@@ -169,7 +228,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
169
228
  if(swap.state===SpvVaultSwapState.CREATED) {
170
229
  if(swap.expiry < Date.now()/1000) {
171
230
  await this.removeSwapData(swap, SpvVaultSwapState.EXPIRED);
172
- await this.bitcoin.addUnusedAddress(swap.btcAddress);
231
+ if(!swap.hasStickyAddress) await this.bitcoin.addUnusedAddress(swap.btcAddress);
173
232
  }
174
233
  }
175
234
 
@@ -260,10 +319,14 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
260
319
  metadata.times.requestReceived = Date.now();
261
320
 
262
321
  /**
322
+ * address: string smart chain address of the recipient
263
323
  * token: string Desired token to use
264
324
  * gasToken: string
265
325
  */
266
326
  const preFetchParsedBody = await req.paramReader.getParams({
327
+ address: (val: string) => val!=null &&
328
+ typeof(val)==="string" &&
329
+ chainInterface.isValidAddress(val, true) ? val : null,
267
330
  token: (val: string) => val!=null &&
268
331
  typeof(val)==="string" &&
269
332
  this.isTokenSupported(chainIdentifier, val) ? val : null,
@@ -276,6 +339,11 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
276
339
  msg: "Invalid request body"
277
340
  };
278
341
 
342
+ const stickyAddressObject = req.paramReader.getExistingParamsOrNull({
343
+ stickyAddress: FieldTypeEnum.BooleanOptional
344
+ });
345
+ const useStickyAddress = stickyAddressObject?.stickyAddress;
346
+
279
347
  //Create abortController for parallel prefetches
280
348
  const responseStream = res.responseStream;
281
349
  const abortController = getAbortController(responseStream);
@@ -293,7 +361,15 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
293
361
 
294
362
  //Listener that re-adds the returned bitcoin address to the unused address list if request fails or closes
295
363
  let abortAddUnusedAddressListener: () => void;
296
- const bitcoinAddressPrefetch = this.bitcoin.getAddress().then(value => {
364
+
365
+ const bitcoinAddressPrefetch: Promise<{address: string, isStickyAddress: boolean} | null> = (async () => {
366
+ if(useStickyAddress) {
367
+ const result = await this.getStickyAddress(chainIdentifier, preFetchParsedBody.address);
368
+ if(result!=null) return {address: result, isStickyAddress: true};
369
+ }
370
+
371
+ const value = await this.bitcoin.getAddress();
372
+
297
373
  //Already aborted
298
374
  if(abortController.signal.aborted) {
299
375
  this.bitcoin.addUnusedAddress(value);
@@ -303,14 +379,13 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
303
379
  abortController.signal.addEventListener("abort", abortAddUnusedAddressListener = () => {
304
380
  this.bitcoin.addUnusedAddress(value);
305
381
  });
306
- return value;
307
- }).catch(e => {
382
+ return {address: value, isStickyAddress: false};
383
+ })().catch(e => {
308
384
  abortController.abort(e);
309
385
  return null;
310
386
  });
311
387
 
312
388
  /**
313
- * address: string smart chain address of the recipient
314
389
  * amount: string amount (in sats)
315
390
  * gasAmount: string Desired amount in gas token to also get
316
391
  * exactOut: boolean Whether the swap should be an exact out instead of exact in swap
@@ -318,9 +393,6 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
318
393
  * frontingFeeRate: string Fronting fee (in output token) to assign to the swap
319
394
  */
320
395
  const actualParsedBody = await req.paramReader.getParams({
321
- address: (val: string) => val!=null &&
322
- typeof(val)==="string" &&
323
- chainInterface.isValidAddress(val, true) ? val : null,
324
396
  amount: FieldTypeEnum.BigInt,
325
397
  gasAmount: FieldTypeEnum.BigInt,
326
398
  exactOut: FieldTypeEnum.BooleanOptional,
@@ -411,10 +483,13 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
411
483
 
412
484
  //Create swap receive bitcoin address
413
485
  const btcFeeRate = await btcFeeRatePrefetch;
414
- const receiveAddress = await bitcoinAddressPrefetch;
486
+ const btcAddressObject = await bitcoinAddressPrefetch;
415
487
  abortController.signal.throwIfAborted();
416
488
  metadata.times.addressCreated = Date.now();
417
489
 
490
+ const receiveAddress = btcAddressObject.address;
491
+ const hasStickyAddress = btcAddressObject.isStickyAddress;
492
+
418
493
  //Adjust the amounts based on passed fees
419
494
  if(parsedBody.exactOut) {
420
495
  totalInToken = parsedBody.amount;
@@ -445,6 +520,8 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
445
520
  useToken, gasToken
446
521
  );
447
522
  swap.metadata = metadata;
523
+ swap.saveStickyAddress = useStickyAddress && !hasStickyAddress
524
+ swap.hasStickyAddress = hasStickyAddress;
448
525
 
449
526
  //We can remove the listener to add unused address now, as we are about to save the swap
450
527
  abortController.signal.removeEventListener("abort", abortAddUnusedAddressListener);
@@ -0,0 +1,21 @@
1
+ import {StorageObject} from "@atomiqlabs/base";
2
+
3
+ export class StickyAddress implements StorageObject {
4
+
5
+ address: string;
6
+
7
+ constructor(addressOrSerialized: string | any) {
8
+ if(typeof(addressOrSerialized) === "string") {
9
+ this.address = addressOrSerialized;
10
+ } else {
11
+ this.address = addressOrSerialized.address;
12
+ }
13
+ }
14
+
15
+ serialize(): any {
16
+ return {
17
+ address: this.address
18
+ }
19
+ }
20
+
21
+ }
@@ -154,6 +154,11 @@ export abstract class IBitcoinWallet {
154
154
  * Returns an unused address suitable for receiving
155
155
  */
156
156
  abstract getAddress(): Promise<string>;
157
+ /**
158
+ * Whether a provided address is controlled by this wallet
159
+ * @param address
160
+ */
161
+ isOwnedAddress?(address: string): Promise<boolean>;
157
162
  /**
158
163
  * Adds previously returned address (with getAddress call), to the pool of unused addresses
159
164
  * @param address