@atomiqlabs/lp-lib 10.3.11

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 (138) hide show
  1. package/LICENSE +201 -0
  2. package/dist/fees/IBtcFeeEstimator.d.ts +3 -0
  3. package/dist/fees/IBtcFeeEstimator.js +2 -0
  4. package/dist/fees/OneDollarFeeEstimator.d.ts +16 -0
  5. package/dist/fees/OneDollarFeeEstimator.js +71 -0
  6. package/dist/index.d.ts +33 -0
  7. package/dist/index.js +52 -0
  8. package/dist/info/InfoHandler.d.ts +17 -0
  9. package/dist/info/InfoHandler.js +70 -0
  10. package/dist/plugins/IPlugin.d.ts +118 -0
  11. package/dist/plugins/IPlugin.js +33 -0
  12. package/dist/plugins/PluginManager.d.ts +89 -0
  13. package/dist/plugins/PluginManager.js +263 -0
  14. package/dist/prices/BinanceSwapPrice.d.ts +27 -0
  15. package/dist/prices/BinanceSwapPrice.js +106 -0
  16. package/dist/prices/CoinGeckoSwapPrice.d.ts +31 -0
  17. package/dist/prices/CoinGeckoSwapPrice.js +76 -0
  18. package/dist/storage/IIntermediaryStorage.d.ts +15 -0
  19. package/dist/storage/IIntermediaryStorage.js +2 -0
  20. package/dist/storagemanager/IntermediaryStorageManager.d.ts +15 -0
  21. package/dist/storagemanager/IntermediaryStorageManager.js +113 -0
  22. package/dist/storagemanager/StorageManager.d.ts +12 -0
  23. package/dist/storagemanager/StorageManager.js +74 -0
  24. package/dist/swaps/FromBtcBaseSwap.d.ts +12 -0
  25. package/dist/swaps/FromBtcBaseSwap.js +16 -0
  26. package/dist/swaps/FromBtcBaseSwapHandler.d.ts +118 -0
  27. package/dist/swaps/FromBtcBaseSwapHandler.js +294 -0
  28. package/dist/swaps/FromBtcLnBaseSwapHandler.d.ts +25 -0
  29. package/dist/swaps/FromBtcLnBaseSwapHandler.js +55 -0
  30. package/dist/swaps/ISwapPrice.d.ts +44 -0
  31. package/dist/swaps/ISwapPrice.js +73 -0
  32. package/dist/swaps/SwapHandler.d.ts +186 -0
  33. package/dist/swaps/SwapHandler.js +292 -0
  34. package/dist/swaps/SwapHandlerSwap.d.ts +75 -0
  35. package/dist/swaps/SwapHandlerSwap.js +72 -0
  36. package/dist/swaps/ToBtcBaseSwap.d.ts +35 -0
  37. package/dist/swaps/ToBtcBaseSwap.js +61 -0
  38. package/dist/swaps/ToBtcBaseSwapHandler.d.ts +94 -0
  39. package/dist/swaps/ToBtcBaseSwapHandler.js +233 -0
  40. package/dist/swaps/frombtc_abstract/FromBtcAbs.d.ts +92 -0
  41. package/dist/swaps/frombtc_abstract/FromBtcAbs.js +386 -0
  42. package/dist/swaps/frombtc_abstract/FromBtcSwapAbs.d.ts +26 -0
  43. package/dist/swaps/frombtc_abstract/FromBtcSwapAbs.js +63 -0
  44. package/dist/swaps/frombtc_trusted/FromBtcTrusted.d.ts +55 -0
  45. package/dist/swaps/frombtc_trusted/FromBtcTrusted.js +586 -0
  46. package/dist/swaps/frombtc_trusted/FromBtcTrustedSwap.d.ts +43 -0
  47. package/dist/swaps/frombtc_trusted/FromBtcTrustedSwap.js +99 -0
  48. package/dist/swaps/frombtcln_abstract/FromBtcLnAbs.d.ts +105 -0
  49. package/dist/swaps/frombtcln_abstract/FromBtcLnAbs.js +731 -0
  50. package/dist/swaps/frombtcln_abstract/FromBtcLnSwapAbs.d.ts +29 -0
  51. package/dist/swaps/frombtcln_abstract/FromBtcLnSwapAbs.js +64 -0
  52. package/dist/swaps/frombtcln_trusted/FromBtcLnTrusted.d.ts +79 -0
  53. package/dist/swaps/frombtcln_trusted/FromBtcLnTrusted.js +514 -0
  54. package/dist/swaps/frombtcln_trusted/FromBtcLnTrustedSwap.d.ts +28 -0
  55. package/dist/swaps/frombtcln_trusted/FromBtcLnTrustedSwap.js +66 -0
  56. package/dist/swaps/tobtc_abstract/ToBtcAbs.d.ts +290 -0
  57. package/dist/swaps/tobtc_abstract/ToBtcAbs.js +1056 -0
  58. package/dist/swaps/tobtc_abstract/ToBtcSwapAbs.d.ts +29 -0
  59. package/dist/swaps/tobtc_abstract/ToBtcSwapAbs.js +70 -0
  60. package/dist/swaps/tobtcln_abstract/ToBtcLnAbs.d.ts +246 -0
  61. package/dist/swaps/tobtcln_abstract/ToBtcLnAbs.js +1169 -0
  62. package/dist/swaps/tobtcln_abstract/ToBtcLnSwapAbs.d.ts +27 -0
  63. package/dist/swaps/tobtcln_abstract/ToBtcLnSwapAbs.js +65 -0
  64. package/dist/utils/Utils.d.ts +32 -0
  65. package/dist/utils/Utils.js +109 -0
  66. package/dist/utils/coinselect2/accumulative.d.ts +6 -0
  67. package/dist/utils/coinselect2/accumulative.js +44 -0
  68. package/dist/utils/coinselect2/blackjack.d.ts +6 -0
  69. package/dist/utils/coinselect2/blackjack.js +41 -0
  70. package/dist/utils/coinselect2/index.d.ts +16 -0
  71. package/dist/utils/coinselect2/index.js +40 -0
  72. package/dist/utils/coinselect2/utils.d.ts +64 -0
  73. package/dist/utils/coinselect2/utils.js +121 -0
  74. package/dist/utils/paramcoders/IParamReader.d.ts +5 -0
  75. package/dist/utils/paramcoders/IParamReader.js +2 -0
  76. package/dist/utils/paramcoders/IParamWriter.d.ts +4 -0
  77. package/dist/utils/paramcoders/IParamWriter.js +2 -0
  78. package/dist/utils/paramcoders/LegacyParamEncoder.d.ts +10 -0
  79. package/dist/utils/paramcoders/LegacyParamEncoder.js +33 -0
  80. package/dist/utils/paramcoders/ParamDecoder.d.ts +25 -0
  81. package/dist/utils/paramcoders/ParamDecoder.js +234 -0
  82. package/dist/utils/paramcoders/ParamEncoder.d.ts +9 -0
  83. package/dist/utils/paramcoders/ParamEncoder.js +22 -0
  84. package/dist/utils/paramcoders/SchemaVerifier.d.ts +22 -0
  85. package/dist/utils/paramcoders/SchemaVerifier.js +85 -0
  86. package/dist/utils/paramcoders/server/ServerParamDecoder.d.ts +8 -0
  87. package/dist/utils/paramcoders/server/ServerParamDecoder.js +105 -0
  88. package/dist/utils/paramcoders/server/ServerParamEncoder.d.ts +11 -0
  89. package/dist/utils/paramcoders/server/ServerParamEncoder.js +76 -0
  90. package/package.json +43 -0
  91. package/src/fees/IBtcFeeEstimator.ts +7 -0
  92. package/src/fees/OneDollarFeeEstimator.ts +95 -0
  93. package/src/index.ts +46 -0
  94. package/src/info/InfoHandler.ts +106 -0
  95. package/src/plugins/IPlugin.ts +155 -0
  96. package/src/plugins/PluginManager.ts +310 -0
  97. package/src/prices/BinanceSwapPrice.ts +114 -0
  98. package/src/prices/CoinGeckoSwapPrice.ts +88 -0
  99. package/src/storage/IIntermediaryStorage.ts +21 -0
  100. package/src/storagemanager/IntermediaryStorageManager.ts +101 -0
  101. package/src/storagemanager/StorageManager.ts +68 -0
  102. package/src/swaps/FromBtcBaseSwap.ts +21 -0
  103. package/src/swaps/FromBtcBaseSwapHandler.ts +375 -0
  104. package/src/swaps/FromBtcLnBaseSwapHandler.ts +48 -0
  105. package/src/swaps/ISwapPrice.ts +94 -0
  106. package/src/swaps/SwapHandler.ts +404 -0
  107. package/src/swaps/SwapHandlerSwap.ts +133 -0
  108. package/src/swaps/ToBtcBaseSwap.ts +76 -0
  109. package/src/swaps/ToBtcBaseSwapHandler.ts +309 -0
  110. package/src/swaps/frombtc_abstract/FromBtcAbs.ts +484 -0
  111. package/src/swaps/frombtc_abstract/FromBtcSwapAbs.ts +77 -0
  112. package/src/swaps/frombtc_trusted/FromBtcTrusted.ts +661 -0
  113. package/src/swaps/frombtc_trusted/FromBtcTrustedSwap.ts +158 -0
  114. package/src/swaps/frombtcln_abstract/FromBtcLnAbs.ts +864 -0
  115. package/src/swaps/frombtcln_abstract/FromBtcLnSwapAbs.ts +82 -0
  116. package/src/swaps/frombtcln_trusted/FromBtcLnTrusted.ts +592 -0
  117. package/src/swaps/frombtcln_trusted/FromBtcLnTrustedSwap.ts +90 -0
  118. package/src/swaps/tobtc_abstract/ToBtcAbs.ts +1249 -0
  119. package/src/swaps/tobtc_abstract/ToBtcSwapAbs.ts +112 -0
  120. package/src/swaps/tobtcln_abstract/ToBtcLnAbs.ts +1422 -0
  121. package/src/swaps/tobtcln_abstract/ToBtcLnSwapAbs.ts +87 -0
  122. package/src/utils/Utils.ts +108 -0
  123. package/src/utils/coinselect2/accumulative.js +32 -0
  124. package/src/utils/coinselect2/accumulative.ts +58 -0
  125. package/src/utils/coinselect2/blackjack.js +29 -0
  126. package/src/utils/coinselect2/blackjack.ts +54 -0
  127. package/src/utils/coinselect2/index.js +16 -0
  128. package/src/utils/coinselect2/index.ts +50 -0
  129. package/src/utils/coinselect2/utils.js +110 -0
  130. package/src/utils/coinselect2/utils.ts +183 -0
  131. package/src/utils/paramcoders/IParamReader.ts +8 -0
  132. package/src/utils/paramcoders/IParamWriter.ts +8 -0
  133. package/src/utils/paramcoders/LegacyParamEncoder.ts +28 -0
  134. package/src/utils/paramcoders/ParamDecoder.ts +219 -0
  135. package/src/utils/paramcoders/ParamEncoder.ts +30 -0
  136. package/src/utils/paramcoders/SchemaVerifier.ts +97 -0
  137. package/src/utils/paramcoders/server/ServerParamDecoder.ts +115 -0
  138. package/src/utils/paramcoders/server/ServerParamEncoder.ts +76 -0
@@ -0,0 +1,1249 @@
1
+ import {Express, Request, Response} from "express";
2
+ import * as BN from "bn.js";
3
+ import * as bitcoin from "bitcoinjs-lib";
4
+ import * as lncli from "ln-service";
5
+ import {ToBtcSwapAbs, ToBtcSwapState} from "./ToBtcSwapAbs";
6
+ import {MultichainData, SwapHandlerType} from "../SwapHandler";
7
+ import {ISwapPrice} from "../ISwapPrice";
8
+ import {
9
+ BtcTx,
10
+ ChainSwapType,
11
+ ClaimEvent,
12
+ InitializeEvent,
13
+ RefundEvent,
14
+ SwapCommitStatus,
15
+ SwapData,
16
+ BitcoinRpc,
17
+ BtcBlock
18
+ } from "@atomiqlabs/base";
19
+ import {AuthenticatedLnd} from "lightning";
20
+ import {expressHandlerWrapper, HEX_REGEX, isDefinedRuntimeError} from "../../utils/Utils";
21
+ import {PluginManager} from "../../plugins/PluginManager";
22
+ import {IIntermediaryStorage} from "../../storage/IIntermediaryStorage";
23
+ import {IBtcFeeEstimator} from "../../fees/IBtcFeeEstimator";
24
+ import {coinSelect} from "../../utils/coinselect2";
25
+ import {CoinselectTxInput, CoinselectTxOutput, utils} from "../../utils/coinselect2/utils";
26
+ import {randomBytes} from "crypto";
27
+ import {FieldTypeEnum, verifySchema} from "../../utils/paramcoders/SchemaVerifier";
28
+ import {serverParamDecoder} from "../../utils/paramcoders/server/ServerParamDecoder";
29
+ import {IParamReader} from "../../utils/paramcoders/IParamReader";
30
+ import {ServerParamEncoder} from "../../utils/paramcoders/server/ServerParamEncoder";
31
+ import {ToBtcBaseConfig, ToBtcBaseSwapHandler} from "../ToBtcBaseSwapHandler";
32
+ import {PromiseQueue} from "promise-queue-ts";
33
+
34
+ const OUTPUT_SCRIPT_MAX_LENGTH = 200;
35
+
36
+ type SpendableUtxo = {
37
+ address: string,
38
+ address_format: string,
39
+ confirmation_count: number,
40
+ output_script: string,
41
+ tokens: number,
42
+ transaction_id: string,
43
+ transaction_vout: number
44
+ };
45
+
46
+ export type ToBtcConfig = ToBtcBaseConfig & {
47
+ sendSafetyFactor: BN,
48
+
49
+ bitcoinNetwork: bitcoin.networks.Network,
50
+
51
+ minChainCltv: BN,
52
+
53
+ networkFeeMultiplierPPM: BN,
54
+ minConfirmations: number,
55
+ maxConfirmations: number,
56
+ maxConfTarget: number,
57
+ minConfTarget: number,
58
+
59
+ txCheckInterval: number,
60
+
61
+ feeEstimator?: IBtcFeeEstimator,
62
+ onchainReservedPerChannel?: number
63
+ };
64
+
65
+ export type ToBtcRequestType = {
66
+ address: string,
67
+ amount: BN,
68
+ confirmationTarget: number,
69
+ confirmations: number,
70
+ nonce: BN,
71
+ token: string,
72
+ offerer: string,
73
+ exactIn?: boolean
74
+ };
75
+
76
+ /**
77
+ * Handler for to BTC swaps, utilizing PTLCs (proof-time locked contracts) using btc relay (on-chain bitcoin SPV)
78
+ */
79
+ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState> {
80
+ protected readonly CONFIRMATIONS_REQUIRED = 1;
81
+ protected readonly ADDRESS_FORMAT_MAP = {
82
+ "p2wpkh": "p2wpkh",
83
+ "np2wpkh": "p2sh-p2wpkh",
84
+ "p2tr" : "p2tr"
85
+ };
86
+ protected readonly LND_CHANGE_OUTPUT_TYPE = "p2tr";
87
+ protected readonly UTXO_CACHE_TIMEOUT = 5*1000;
88
+ protected readonly CHANNEL_COUNT_CACHE_TIMEOUT = 30*1000;
89
+
90
+ readonly type = SwapHandlerType.TO_BTC;
91
+
92
+ activeSubscriptions: {[txId: string]: ToBtcSwapAbs} = {};
93
+ cachedUtxos: {
94
+ utxos: (CoinselectTxInput & {confirmations: number})[],
95
+ timestamp: number
96
+ };
97
+ cachedChannelCount: {
98
+ count: number,
99
+ timestamp: number
100
+ };
101
+ bitcoinRpc: BitcoinRpc<BtcBlock>;
102
+ sendBtcQueue: PromiseQueue = new PromiseQueue();
103
+
104
+ readonly config: ToBtcConfig;
105
+
106
+ constructor(
107
+ storageDirectory: IIntermediaryStorage<ToBtcSwapAbs>,
108
+ path: string,
109
+ chainData: MultichainData,
110
+ lnd: AuthenticatedLnd,
111
+ swapPricing: ISwapPrice,
112
+ bitcoinRpc: BitcoinRpc<BtcBlock>,
113
+ config: ToBtcConfig
114
+ ) {
115
+ super(storageDirectory, path, chainData, lnd, swapPricing);
116
+ this.bitcoinRpc = bitcoinRpc;
117
+ this.config = config;
118
+ this.config.onchainReservedPerChannel = this.config.onchainReservedPerChannel || 40000;
119
+ }
120
+
121
+ /**
122
+ * Returns the payment hash of the swap, takes swap nonce into account. Payment hash is chain-specific.
123
+ *
124
+ * @param chainIdentifier
125
+ * @param address
126
+ * @param nonce
127
+ * @param amount
128
+ * @param bitcoinNetwork
129
+ */
130
+ private getHash(chainIdentifier: string, address: string, nonce: BN, amount: BN, bitcoinNetwork: bitcoin.networks.Network): Buffer {
131
+ const parsedOutputScript = bitcoin.address.toOutputScript(address, bitcoinNetwork);
132
+ const {swapContract} = this.getChain(chainIdentifier);
133
+ return swapContract.getHashForOnchain(parsedOutputScript, amount, nonce);
134
+ }
135
+
136
+ /**
137
+ * Returns spendable UTXOs, these are either confirmed UTXOs, or unconfirmed ones that are either whitelisted,
138
+ * or created by our transactions (and therefore only we could doublespend)
139
+ *
140
+ * @private
141
+ */
142
+ protected async getSpendableUtxos(): Promise<SpendableUtxo[]> {
143
+ const resBlockheight = await lncli.getHeight({
144
+ lnd: this.LND
145
+ });
146
+
147
+ const blockheight: number = resBlockheight.current_block_height;
148
+
149
+ const resChainTxns = await lncli.getChainTransactions({
150
+ lnd: this.LND,
151
+ after: blockheight-this.CONFIRMATIONS_REQUIRED
152
+ });
153
+
154
+ const selfUTXOs: Set<string> = PluginManager.getWhitelistedTxIds();
155
+
156
+ const transactions = resChainTxns.transactions;
157
+ for(let tx of transactions) {
158
+ if(tx.is_outgoing) {
159
+ selfUTXOs.add(tx.id);
160
+ }
161
+ }
162
+
163
+ const resUtxos = await lncli.getUtxos({
164
+ lnd: this.LND
165
+ });
166
+
167
+ return resUtxos.utxos.filter(utxo => utxo.confirmation_count>=this.CONFIRMATIONS_REQUIRED || selfUTXOs.has(utxo.transaction_id));
168
+ }
169
+
170
+ /**
171
+ * Returns utxo pool to be used by the coinselection algorithm
172
+ *
173
+ * @private
174
+ */
175
+ protected async getUtxoPool(useCached: boolean = false): Promise<(CoinselectTxInput & {confirmations: number})[]> {
176
+ if(!useCached || this.cachedUtxos==null || this.cachedUtxos.timestamp<Date.now()-this.UTXO_CACHE_TIMEOUT) {
177
+ const utxos = await this.getSpendableUtxos();
178
+
179
+ let totalSpendable = 0;
180
+ const utxoPool = utxos.map(utxo => {
181
+ totalSpendable += utxo.tokens;
182
+ return {
183
+ vout: utxo.transaction_vout,
184
+ txId: utxo.transaction_id,
185
+ value: utxo.tokens,
186
+ type: this.ADDRESS_FORMAT_MAP[utxo.address_format],
187
+ outputScript: Buffer.from(utxo.output_script, "hex"),
188
+ address: utxo.address,
189
+ confirmations: utxo.confirmation_count
190
+ };
191
+ });
192
+
193
+ this.cachedUtxos = {
194
+ utxos: utxoPool,
195
+ timestamp: Date.now()
196
+ };
197
+
198
+ this.logger.info("getUtxoPool(): total spendable value: "+totalSpendable+" num utxos: "+utxoPool.length);
199
+ }
200
+
201
+ return this.cachedUtxos.utxos;
202
+ }
203
+
204
+ /**
205
+ * Checks whether a coinselect result leaves enough funds to cover potential lightning anchor transaction fees
206
+ *
207
+ * @param utxoPool
208
+ * @param obj
209
+ * @param satsPerVbyte
210
+ * @param useCached Whether to use a cached channel count
211
+ * @param initialOutputLength
212
+ * @private
213
+ * @returns true if alright, false if the coinselection doesn't leave enough funds for anchor fees
214
+ */
215
+ protected async isLeavingEnoughForLightningAnchors(
216
+ utxoPool: CoinselectTxInput[],
217
+ obj: {inputs?: CoinselectTxInput[], outputs?: CoinselectTxOutput[]},
218
+ satsPerVbyte: BN,
219
+ useCached: boolean = false,
220
+ initialOutputLength: number = 1
221
+ ): Promise<boolean> {
222
+ if(obj.inputs==null || obj.outputs==null) return false;
223
+ const spentInputs = new Set<string>();
224
+ obj.inputs.forEach(txIn => {
225
+ spentInputs.add(txIn.txId+":"+txIn.vout);
226
+ });
227
+
228
+ let leavesValue: BN = new BN(0);
229
+ utxoPool.forEach(val => {
230
+ const utxoEconomicalValue: BN = new BN(val.value).sub(satsPerVbyte.mul(new BN(utils.inputBytes(val).length)));
231
+ if (
232
+ //Utxo not spent
233
+ !spentInputs.has(val.txId + ":" + val.vout) &&
234
+ //Only economical utxos at current fees
235
+ !utxoEconomicalValue.isNeg()
236
+ ) {
237
+ leavesValue = leavesValue.add(utxoEconomicalValue);
238
+ }
239
+ });
240
+ if(obj.outputs.length>initialOutputLength) {
241
+ const changeUtxo = obj.outputs[obj.outputs.length-1];
242
+ leavesValue = leavesValue.add(
243
+ new BN(changeUtxo.value).sub(satsPerVbyte.mul(new BN(utils.inputBytes(changeUtxo).length)))
244
+ );
245
+ }
246
+
247
+ if(!useCached || this.cachedChannelCount==null || this.cachedChannelCount.timestamp<Date.now()-this.CHANNEL_COUNT_CACHE_TIMEOUT) {
248
+ const {channels} = await lncli.getChannels({lnd: this.LND});
249
+ this.cachedChannelCount = {
250
+ count: channels.length,
251
+ timestamp: Date.now()
252
+ }
253
+ }
254
+
255
+ return leavesValue.gt(new BN(this.config.onchainReservedPerChannel).mul(new BN(this.cachedChannelCount.count)));
256
+ }
257
+
258
+ /**
259
+ * Gets the change address from the underlying LND instance
260
+ *
261
+ * @private
262
+ */
263
+ protected getChangeAddress(): Promise<string> {
264
+ return new Promise((resolve, reject) => {
265
+ this.LND.wallet.nextAddr({
266
+ type: 4,
267
+ change: true
268
+ }, (err, res) => {
269
+ if(err!=null) {
270
+ reject([503, 'UnexpectedErrGettingNextAddr', {err}]);
271
+ return;
272
+ }
273
+ resolve(res.addr);
274
+ });
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Computes bitcoin on-chain network fee, takes channel reserve & network fee multiplier into consideration
280
+ *
281
+ * @param targetAddress Bitcoin address to send the funds to
282
+ * @param targetAmount Amount of funds to send to the address
283
+ * @param estimate Whether the chain fee should be just estimated and therefore cached utxo set could be used
284
+ * @param multiplierPPM Multiplier for the sats/vB returned from the fee estimator in PPM (parts per million)
285
+ * @private
286
+ * @returns Fee estimate & inputs/outputs to use when constructing transaction, or null in case of not enough funds
287
+ */
288
+ private async getChainFee(targetAddress: string, targetAmount: number, estimate: boolean = false, multiplierPPM?: BN): Promise<{
289
+ satsPerVbyte: BN,
290
+ networkFee: BN,
291
+ inputs: CoinselectTxInput[],
292
+ outputs: CoinselectTxOutput[]
293
+ } | null> {
294
+ let feeRate: number | null = this.config.feeEstimator==null
295
+ ? await lncli.getChainFeeRate({lnd: this.LND})
296
+ .then(res => res.tokens_per_vbyte)
297
+ .catch(e => this.logger.error("getChainFee(): LND getChainFeeRate error", e))
298
+ : await this.config.feeEstimator.estimateFee();
299
+
300
+ if(feeRate==null) return null;
301
+
302
+ let satsPerVbyte = new BN(Math.ceil(feeRate));
303
+ if(multiplierPPM!=null) satsPerVbyte = satsPerVbyte.mul(multiplierPPM).div(new BN(1000000));
304
+
305
+ const utxoPool: CoinselectTxInput[] = await this.getUtxoPool(estimate);
306
+
307
+ let obj = coinSelect(utxoPool, [{
308
+ address: targetAddress,
309
+ value: targetAmount,
310
+ script: bitcoin.address.toOutputScript(targetAddress, this.config.bitcoinNetwork)
311
+ }], satsPerVbyte.toNumber(), this.LND_CHANGE_OUTPUT_TYPE);
312
+
313
+ if(obj.inputs==null || obj.outputs==null) return null;
314
+
315
+ if(!await this.isLeavingEnoughForLightningAnchors(utxoPool, obj, satsPerVbyte, estimate)) return null;
316
+
317
+ this.logger.info("getChainFee(): fee estimated,"+
318
+ " target: "+targetAddress+
319
+ " amount: "+targetAmount.toString(10)+
320
+ " fee: "+obj.fee+
321
+ " sats/vB: "+satsPerVbyte+
322
+ " inputs: "+obj.inputs.length+
323
+ " outputs: "+obj.outputs.length+
324
+ " multiplier: "+(multiplierPPM==null ? 1 : multiplierPPM.toNumber()/1000000));
325
+
326
+ return {
327
+ networkFee: new BN(obj.fee),
328
+ satsPerVbyte,
329
+ outputs: obj.outputs,
330
+ inputs: obj.inputs
331
+ };
332
+ }
333
+
334
+ /**
335
+ * Tries to claim the swap after our transaction was confirmed
336
+ *
337
+ * @param tx
338
+ * @param payment
339
+ * @param vout
340
+ */
341
+ private async tryClaimSwap(tx: {blockhash: string, confirmations: number, txid: string, hex: string}, swap: ToBtcSwapAbs, vout: number): Promise<boolean> {
342
+ const {swapContract, signer} = this.getChain(swap.chainIdentifier);
343
+
344
+ const blockHeader = await this.bitcoinRpc.getBlockHeader(tx.blockhash);
345
+
346
+ //Set flag that we are sending the transaction already, so we don't end up with race condition
347
+ const unlock: () => boolean = swap.lock(swapContract.claimWithTxDataTimeout);
348
+ if(unlock==null) return false;
349
+
350
+ try {
351
+ this.swapLogger.debug(swap, "tryClaimSwap(): initiate claim of swap, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout);
352
+ const result = await swapContract.claimWithTxData(signer, swap.data, blockHeader.getHeight(), tx, vout, null, null, false, {
353
+ waitForConfirmation: true
354
+ });
355
+ this.swapLogger.info(swap, "tryClaimSwap(): swap claimed successfully, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout+" address: "+swap.address);
356
+ if(swap.metadata!=null) swap.metadata.times.txClaimed = Date.now();
357
+ unlock();
358
+ return true;
359
+ } catch (e) {
360
+ this.swapLogger.error(swap, "tryClaimSwap(): error occurred claiming swap, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout+" address: "+swap.address, e);
361
+ return false
362
+ }
363
+ }
364
+
365
+ protected async processPastSwap(swap: ToBtcSwapAbs) {
366
+ const {swapContract, signer} = this.getChain(swap.chainIdentifier);
367
+
368
+ const timestamp = new BN(Math.floor(Date.now()/1000)).sub(new BN(this.config.maxSkew));
369
+
370
+ if(swap.state===ToBtcSwapState.SAVED && swap.signatureExpiry!=null) {
371
+ const isSignatureExpired = swap.signatureExpiry.lt(timestamp);
372
+ if(isSignatureExpired) {
373
+ const isCommitted = await swapContract.isCommited(swap.data);
374
+ if(!isCommitted) {
375
+ this.swapLogger.info(swap, "processPastSwap(state=SAVED): authorization expired & swap not committed, cancelling swap, address: "+swap.address);
376
+ await this.removeSwapData(swap, ToBtcSwapState.CANCELED);
377
+ } else {
378
+ this.swapLogger.info(swap, "processPastSwap(state=SAVED): swap committed (detected from processPastSwap), address: "+swap.address);
379
+ await swap.setState(ToBtcSwapState.COMMITED);
380
+ await this.storageManager.saveData(swap.getHash(), swap.data.getSequence(), swap);
381
+ }
382
+ return;
383
+ }
384
+ }
385
+
386
+ if(swap.state===ToBtcSwapState.NON_PAYABLE || swap.state===ToBtcSwapState.SAVED) {
387
+ const isSwapExpired = swap.data.getExpiry().lt(timestamp);
388
+ if(isSwapExpired) {
389
+ this.swapLogger.info(swap, "processPastSwap(state=NON_PAYABLE|SAVED): swap expired, cancelling, address: "+swap.address);
390
+ await this.removeSwapData(swap, ToBtcSwapState.CANCELED);
391
+ return;
392
+ }
393
+ }
394
+
395
+ //Sanity check for sent swaps
396
+ if(swap.state===ToBtcSwapState.BTC_SENT) {
397
+ const isCommited = await swapContract.isCommited(swap.data);
398
+ if(!isCommited) {
399
+ const status = await swapContract.getCommitStatus(signer.getAddress(), swap.data);
400
+ if(status===SwapCommitStatus.PAID) {
401
+ this.swapLogger.info(swap, "processPastSwap(state=BTC_SENT): swap claimed (detected from processPastSwap), address: "+swap.address);
402
+ this.unsubscribePayment(swap);
403
+ await this.removeSwapData(swap, ToBtcSwapState.CLAIMED);
404
+ } else if(status===SwapCommitStatus.EXPIRED) {
405
+ this.swapLogger.warn(swap, "processPastSwap(state=BTC_SENT): swap expired, but bitcoin was probably already sent, txId: "+swap.txId+" address: "+swap.address);
406
+ this.unsubscribePayment(swap);
407
+ await this.removeSwapData(swap, ToBtcSwapState.REFUNDED);
408
+ }
409
+ return;
410
+ }
411
+ }
412
+
413
+ if(swap.state===ToBtcSwapState.COMMITED || swap.state===ToBtcSwapState.BTC_SENDING || swap.state===ToBtcSwapState.BTC_SENT) {
414
+ await this.processInitialized(swap);
415
+ return;
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Checks past swaps, deletes ones that are already expired.
421
+ */
422
+ protected async processPastSwaps() {
423
+ const queriedData = await this.storageManager.query([
424
+ {
425
+ key: "state",
426
+ values: [
427
+ ToBtcSwapState.SAVED,
428
+ ToBtcSwapState.NON_PAYABLE,
429
+ ToBtcSwapState.COMMITED,
430
+ ToBtcSwapState.BTC_SENDING,
431
+ ToBtcSwapState.BTC_SENT,
432
+ ]
433
+ }
434
+ ]);
435
+
436
+ for(let swap of queriedData) {
437
+ await this.processPastSwap(swap);
438
+ }
439
+ }
440
+
441
+ protected async processBtcTx(swap: ToBtcSwapAbs, tx: BtcTx): Promise<boolean> {
442
+ tx.confirmations = tx.confirmations || 0;
443
+
444
+ //Check transaction has enough confirmations
445
+ const hasEnoughConfirmations = tx.confirmations>=swap.data.getConfirmations();
446
+ if(!hasEnoughConfirmations) {
447
+ return false;
448
+ }
449
+
450
+ this.swapLogger.debug(swap, "processBtcTx(): address: "+swap.address+" amount: "+swap.amount.toString(10)+" btcTx: "+tx);
451
+
452
+ //Search for required transaction output (vout)
453
+ const outputScript = bitcoin.address.toOutputScript(swap.address, this.config.bitcoinNetwork);
454
+ const vout = tx.outs.find(e => new BN(e.value).eq(swap.amount) && Buffer.from(e.scriptPubKey.hex, "hex").equals(outputScript));
455
+ if(vout==null) {
456
+ this.swapLogger.warn(swap, "processBtcTx(): cannot find correct vout,"+
457
+ " required output script: "+outputScript.toString("hex")+
458
+ " required amount: "+swap.amount.toString(10)+
459
+ " vouts: ", tx.outs);
460
+ return false;
461
+ }
462
+
463
+ if(swap.metadata!=null) swap.metadata.times.payTxConfirmed = Date.now();
464
+
465
+ const success = await this.tryClaimSwap(tx, swap, vout.n);
466
+
467
+ return success;
468
+ }
469
+
470
+ /**
471
+ * Checks active sent out bitcoin transactions
472
+ */
473
+ private async processBtcTxs() {
474
+ const unsubscribeSwaps: ToBtcSwapAbs[] = [];
475
+
476
+ for(let txId in this.activeSubscriptions) {
477
+ const swap: ToBtcSwapAbs = this.activeSubscriptions[txId];
478
+ //TODO: RBF the transaction if it's already taking too long to confirm
479
+ try {
480
+ let tx: BtcTx = await this.bitcoinRpc.getTransaction(txId);
481
+ if(tx==null) continue;
482
+
483
+ if(await this.processBtcTx(swap, tx)) {
484
+ this.swapLogger.info(swap, "processBtcTxs(): swap claimed successfully, txId: "+tx.txid+" address: "+swap.address);
485
+ unsubscribeSwaps.push(swap);
486
+ }
487
+ } catch (e) {
488
+ this.swapLogger.error(swap, "processBtcTxs(): error processing btc transaction", e);
489
+ }
490
+ }
491
+
492
+ unsubscribeSwaps.forEach(swap => {
493
+ this.unsubscribePayment(swap);
494
+ });
495
+ }
496
+
497
+ /**
498
+ * Subscribes to and periodically checks txId used to send out funds for the swap for enough confirmations
499
+ *
500
+ * @param payment
501
+ */
502
+ protected subscribeToPayment(payment: ToBtcSwapAbs) {
503
+ this.swapLogger.info(payment, "subscribeToPayment(): subscribing to swap, txId: "+payment.txId+" address: "+payment.address);
504
+ this.activeSubscriptions[payment.txId] = payment;
505
+ }
506
+
507
+ protected unsubscribePayment(payment: ToBtcSwapAbs) {
508
+ if(payment.txId!=null) {
509
+ if(this.activeSubscriptions[payment.txId]!=null) {
510
+ this.swapLogger.info(payment, "unsubscribePayment(): unsubscribing swap, txId: "+payment.txId+" address: "+payment.address);
511
+ delete this.activeSubscriptions[payment.txId];
512
+ }
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Checks if expiry time on the swap leaves us enough room to send a transaction and for the transaction to confirm
518
+ *
519
+ * @param swap
520
+ * @private
521
+ * @throws DefinedRuntimeError will throw an error in case there isn't enough time for us to send a BTC payout tx
522
+ */
523
+ protected checkExpiresTooSoon(swap: ToBtcSwapAbs): void {
524
+ const currentTimestamp = new BN(Math.floor(Date.now()/1000));
525
+ const tsDelta = swap.data.getExpiry().sub(currentTimestamp);
526
+ const minRequiredCLTV = this.getExpiryFromCLTV(swap.preferedConfirmationTarget, swap.data.getConfirmations());
527
+ const hasRequiredCLTVDelta = tsDelta.gte(minRequiredCLTV);
528
+ if(!hasRequiredCLTVDelta) throw {
529
+ code: 90001,
530
+ msg: "TS delta too low",
531
+ data: {
532
+ required: minRequiredCLTV.toString(10),
533
+ actual: tsDelta.toString(10)
534
+ }
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Checks if the actual fee for the swap is no higher than the quoted estimate
540
+ *
541
+ * @param quotedSatsPerVbyte
542
+ * @param actualSatsPerVbyte
543
+ * @private
544
+ * @throws DefinedRuntimeError will throw an error in case the actual fee is higher than quoted fee
545
+ */
546
+ protected checkCalculatedTxFee(quotedSatsPerVbyte: BN, actualSatsPerVbyte: BN): void {
547
+ const swapPaysEnoughNetworkFee = quotedSatsPerVbyte.gte(actualSatsPerVbyte);
548
+ if(!swapPaysEnoughNetworkFee) throw {
549
+ code: 90003,
550
+ msg: "Fee changed too much!",
551
+ data: {
552
+ quotedFee: actualSatsPerVbyte.toString(10),
553
+ actualFee: quotedSatsPerVbyte.toString(10)
554
+ }
555
+ };
556
+ }
557
+
558
+ /**
559
+ * Runs sanity check on the calculated fee for the transaction
560
+ *
561
+ * @param psbt
562
+ * @param tx
563
+ * @param maxAllowedSatsPerVbyte
564
+ * @param actualSatsPerVbyte
565
+ * @private
566
+ * @throws {Error} Will throw an error if the fee sanity check doesn't pass
567
+ */
568
+ protected checkPsbtFee(
569
+ psbt: bitcoin.Psbt,
570
+ tx: bitcoin.Transaction,
571
+ maxAllowedSatsPerVbyte: BN,
572
+ actualSatsPerVbyte: BN
573
+ ): BN {
574
+ const txFee = new BN(psbt.getFee());
575
+
576
+ //Sanity check on sats/vB
577
+ const maxAllowedFee = new BN(tx.virtualSize())
578
+ //Considering the extra output was not added, because was detrminetal
579
+ .add(new BN(utils.outputBytes({type: this.LND_CHANGE_OUTPUT_TYPE})))
580
+ //Multiply by maximum allowed feerate
581
+ .mul(maxAllowedSatsPerVbyte)
582
+ //Possibility that extra output was not added due to it being lower than dust
583
+ .add(new BN(utils.dustThreshold({type: this.LND_CHANGE_OUTPUT_TYPE})));
584
+
585
+ if(txFee.gt(maxAllowedFee)) throw new Error("Generated tx fee too high: "+JSON.stringify({
586
+ maxAllowedFee: maxAllowedFee.toString(10),
587
+ actualFee: txFee.toString(10),
588
+ psbtHex: psbt.toHex(),
589
+ maxAllowedSatsPerVbyte: maxAllowedSatsPerVbyte.toString(10),
590
+ actualSatsPerVbyte: actualSatsPerVbyte.toString(10)
591
+ }));
592
+
593
+ return txFee;
594
+ }
595
+
596
+ /**
597
+ * Create PSBT for swap payout from coinselection result
598
+ *
599
+ * @param address
600
+ * @param amount
601
+ * @param escrowNonce
602
+ * @param coinselectResult
603
+ * @private
604
+ */
605
+ private async getPsbt(
606
+ address: string,
607
+ amount: BN,
608
+ escrowNonce: BN,
609
+ coinselectResult: {inputs: CoinselectTxInput[], outputs: CoinselectTxOutput[]}
610
+ ): Promise<bitcoin.Psbt> {
611
+ let psbt = new bitcoin.Psbt();
612
+
613
+ //Apply nonce
614
+ const nonceBuffer = Buffer.from(escrowNonce.toArray("be", 8));
615
+
616
+ const locktimeBN = new BN(nonceBuffer.slice(0, 5), "be");
617
+ let locktime = locktimeBN.toNumber() + 500000000;
618
+ psbt.setLocktime(locktime);
619
+
620
+ const sequenceBN = new BN(nonceBuffer.slice(5, 8), "be");
621
+ const sequence = 0xFE000000 + sequenceBN.toNumber();
622
+ psbt.addInputs(coinselectResult.inputs.map(input => {
623
+ return {
624
+ hash: input.txId,
625
+ index: input.vout,
626
+ witnessUtxo: {
627
+ script: input.outputScript,
628
+ value: input.value
629
+ },
630
+ sighashType: 0x01,
631
+ sequence
632
+ };
633
+ }));
634
+
635
+ psbt.addOutput({
636
+ script: bitcoin.address.toOutputScript(address, this.config.bitcoinNetwork),
637
+ value: amount.toNumber()
638
+ });
639
+
640
+ //Add change output
641
+ if(coinselectResult.outputs.length>1) psbt.addOutput({
642
+ script: bitcoin.address.toOutputScript(await this.getChangeAddress(), this.config.bitcoinNetwork),
643
+ value: coinselectResult.outputs[1].value
644
+ });
645
+
646
+ return psbt;
647
+ }
648
+
649
+ /**
650
+ * Signs provided PSBT and also returns a raw signed transaction
651
+ *
652
+ * @param psbt
653
+ * @private
654
+ */
655
+ protected async signPsbt(psbt: bitcoin.Psbt): Promise<{psbt: bitcoin.Psbt, rawTx: string}> {
656
+ const signedPsbt = await lncli.signPsbt({
657
+ lnd: this.LND,
658
+ psbt: psbt.toHex()
659
+ });
660
+ return {
661
+ psbt: bitcoin.Psbt.fromHex(signedPsbt.psbt),
662
+ rawTx: signedPsbt.transaction
663
+ };
664
+ }
665
+
666
+ /**
667
+ * Sends raw bitcoin transaction
668
+ *
669
+ * @param rawTx
670
+ * @private
671
+ */
672
+ protected async sendRawTransaction(rawTx: string): Promise<void> {
673
+ await lncli.broadcastChainTransaction({
674
+ lnd: this.LND,
675
+ transaction: rawTx
676
+ });
677
+ }
678
+
679
+ /**
680
+ * Sends a bitcoin transaction to payout BTC for a swap
681
+ *
682
+ * @param swap
683
+ * @private
684
+ * @throws DefinedRuntimeError will throw an error in case the payment cannot be initiated
685
+ */
686
+ private sendBitcoinPayment(swap: ToBtcSwapAbs) {
687
+ //Make sure that bitcoin payouts are processed sequentially to avoid race conditions between multiple payouts,
688
+ // e.g. that 2 payouts share the same input and would effectively double-spend each other
689
+ return this.sendBtcQueue.enqueue<void>(async () => {
690
+ //Run checks
691
+ this.checkExpiresTooSoon(swap);
692
+ if(swap.metadata!=null) swap.metadata.times.payCLTVChecked = Date.now();
693
+
694
+ const coinselectResult = await this.getChainFee(swap.address, swap.amount.toNumber());
695
+ if(coinselectResult==null) throw {
696
+ code: 90002,
697
+ msg: "Failed to run coinselect algorithm (not enough funds?)"
698
+ }
699
+ if(swap.metadata!=null) swap.metadata.times.payChainFee = Date.now();
700
+
701
+ this.checkCalculatedTxFee(swap.satsPerVbyte, coinselectResult.satsPerVbyte);
702
+
703
+ //Construct payment PSBT
704
+ let unsignedPsbt = await this.getPsbt(swap.address, swap.amount, swap.data.getEscrowNonce(), coinselectResult);
705
+ this.swapLogger.debug(swap, "sendBitcoinPayment(): generated psbt: "+unsignedPsbt.toHex());
706
+
707
+ //Sign the PSBT
708
+ const {psbt, rawTx} = await this.signPsbt(unsignedPsbt);
709
+ if(swap.metadata!=null) swap.metadata.times.paySignPSBT = Date.now();
710
+ this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: "+rawTx);
711
+
712
+ const tx = bitcoin.Transaction.fromHex(rawTx);
713
+ const txFee = this.checkPsbtFee(psbt, tx, swap.satsPerVbyte, coinselectResult.satsPerVbyte);
714
+
715
+ swap.txId = tx.getId();
716
+ swap.setRealNetworkFee(txFee);
717
+ await swap.setState(ToBtcSwapState.BTC_SENDING);
718
+ await this.storageManager.saveData(swap.getHash(), swap.getSequence(), swap);
719
+
720
+ await this.sendRawTransaction(rawTx);
721
+ if(swap.metadata!=null) swap.metadata.times.payTxSent = Date.now();
722
+ this.swapLogger.info(swap, "sendBitcoinPayment(): btc transaction generated, signed & broadcasted, txId: "+tx.getId()+" address: "+swap.address);
723
+ //Invalidate the UTXO cache
724
+ this.cachedUtxos = null;
725
+
726
+ await swap.setState(ToBtcSwapState.BTC_SENT);
727
+ await this.storageManager.saveData(swap.getHash(), swap.getSequence(), swap);
728
+ });
729
+ }
730
+
731
+ /**
732
+ * Called after swap was successfully committed, will check if bitcoin tx is already sent, if not tries to send it and subscribes to it
733
+ *
734
+ * @param swap
735
+ */
736
+ private async processInitialized(swap: ToBtcSwapAbs) {
737
+ if(swap.state===ToBtcSwapState.BTC_SENDING) {
738
+ //Bitcoin transaction was signed (maybe also sent)
739
+ const tx = await this.bitcoinRpc.getTransaction(swap.txId);
740
+
741
+ const isTxSent = tx!=null;
742
+ if(!isTxSent) {
743
+ //Reset the state to COMMITED
744
+ this.swapLogger.info(swap, "processInitialized(state=BTC_SENDING): btc transaction not found, resetting to COMMITED state, txId: "+swap.txId+" address: "+swap.address);
745
+ await swap.setState(ToBtcSwapState.COMMITED);
746
+ } else {
747
+ this.swapLogger.info(swap, "processInitialized(state=BTC_SENDING): btc transaction found, advancing to BTC_SENT state, txId: "+swap.txId+" address: "+swap.address);
748
+ await swap.setState(ToBtcSwapState.BTC_SENT);
749
+ await this.storageManager.saveData(swap.getHash(), swap.data.getSequence(), swap);
750
+ }
751
+ }
752
+
753
+ if(swap.state===ToBtcSwapState.SAVED) {
754
+ this.swapLogger.info(swap, "processInitialized(state=SAVED): advancing to COMMITED state, address: "+swap.address);
755
+ await swap.setState(ToBtcSwapState.COMMITED);
756
+ await this.storageManager.saveData(swap.getHash(), swap.data.getSequence(), swap);
757
+ }
758
+
759
+ if(swap.state===ToBtcSwapState.COMMITED) {
760
+ const unlock: () => boolean = swap.lock(60);
761
+ if(unlock==null) return;
762
+
763
+ this.swapLogger.debug(swap, "processInitialized(state=COMMITED): sending bitcoin transaction, address: "+swap.address);
764
+
765
+ try {
766
+ await this.sendBitcoinPayment(swap);
767
+ this.swapLogger.info(swap, "processInitialized(state=COMMITED): btc transaction sent, address: "+swap.address);
768
+ } catch (e) {
769
+ if(isDefinedRuntimeError(e)) {
770
+ this.swapLogger.error(swap, "processInitialized(state=COMMITED): setting state to NON_PAYABLE due to send bitcoin payment error", e);
771
+ if(swap.metadata!=null) swap.metadata.payError = e;
772
+ await swap.setState(ToBtcSwapState.NON_PAYABLE);
773
+ await this.storageManager.saveData(swap.getHash(), swap.data.getSequence(), swap);
774
+ } else {
775
+ this.swapLogger.error(swap, "processInitialized(state=COMMITED): send bitcoin payment error", e);
776
+ throw e;
777
+ }
778
+ }
779
+
780
+ unlock();
781
+ }
782
+
783
+ if(swap.state===ToBtcSwapState.NON_PAYABLE) return;
784
+
785
+ this.subscribeToPayment(swap);
786
+ }
787
+
788
+ protected async processInitializeEvent(chainIdentifier: string, event: InitializeEvent<SwapData>): Promise<void> {
789
+ if(event.swapType!==ChainSwapType.CHAIN_NONCED) return;
790
+
791
+ const paymentHash = event.paymentHash;
792
+
793
+ const swap = await this.storageManager.getData(paymentHash, event.sequence);
794
+ if(swap==null || swap.chainIdentifier!==chainIdentifier) return;
795
+
796
+ swap.txIds.init = (event as any).meta?.txId;
797
+ if(swap.metadata!=null) swap.metadata.times.txReceived = Date.now();
798
+
799
+ this.swapLogger.info(swap, "SC: InitializeEvent: swap initialized by the client, address: "+swap.address);
800
+
801
+ await this.processInitialized(swap);
802
+ }
803
+
804
+ protected async processClaimEvent(chainIdentifier: string, event: ClaimEvent<SwapData>): Promise<void> {
805
+ const paymentHash = event.paymentHash;
806
+
807
+ const swap = await this.storageManager.getData(paymentHash, event.sequence);
808
+ if(swap==null || swap.chainIdentifier!==chainIdentifier) return;
809
+
810
+ swap.txIds.claim = (event as any).meta?.txId;
811
+
812
+ this.swapLogger.info(swap, "SC: ClaimEvent: swap successfully claimed to us, address: "+swap.address);
813
+
814
+ //Also remove transaction from active subscriptions
815
+ this.unsubscribePayment(swap);
816
+ await this.removeSwapData(swap, ToBtcSwapState.CLAIMED);
817
+ }
818
+
819
+ protected async processRefundEvent(chainIdentifier: string, event: RefundEvent<SwapData>): Promise<void> {
820
+ const paymentHash = event.paymentHash;
821
+
822
+ const swap = await this.storageManager.getData(paymentHash, event.sequence);
823
+ if(swap==null || swap.chainIdentifier!==chainIdentifier) return;
824
+
825
+ swap.txIds.refund = (event as any).meta?.txId;
826
+
827
+ this.swapLogger.info(swap, "SC: RefundEvent: swap successfully refunded by the user, address: "+swap.address);
828
+
829
+ //Also remove transaction from active subscriptions
830
+ this.unsubscribePayment(swap);
831
+ await this.removeSwapData(swap, ToBtcSwapState.REFUNDED);
832
+ }
833
+
834
+ /**
835
+ * Returns required expiry delta for swap params
836
+ *
837
+ * @param confirmationTarget
838
+ * @param confirmations
839
+ */
840
+ protected getExpiryFromCLTV(confirmationTarget: number, confirmations: number): BN {
841
+ //Blocks = 10 + (confirmations + confirmationTarget)*2
842
+ //Time = 3600 + (600*blocks*2)
843
+ const cltv = this.config.minChainCltv.add(
844
+ new BN(confirmations).add(new BN(confirmationTarget)).mul(this.config.sendSafetyFactor)
845
+ );
846
+
847
+ return this.config.gracePeriod.add(this.config.bitcoinBlocktime.mul(cltv).mul(this.config.safetyFactor));
848
+ }
849
+
850
+ /**
851
+ * Checks if the requested nonce is valid
852
+ *
853
+ * @param nonce
854
+ * @throws {DefinedRuntimeError} will throw an error if the nonce is invalid
855
+ */
856
+ private checkNonceValid(nonce: BN): void {
857
+ if(nonce.isNeg() || nonce.gte(new BN(2).pow(new BN(64)))) throw {
858
+ code: 20021,
859
+ msg: "Invalid request body (nonce - cannot be parsed)"
860
+ };
861
+
862
+ const nonceBuffer = Buffer.from(nonce.toArray("be", 8));
863
+ const firstPart = new BN(nonceBuffer.slice(0, 5), "be");
864
+
865
+ const maxAllowedValue = new BN(Math.floor(Date.now()/1000)-600000000);
866
+ if(firstPart.gt(maxAllowedValue)) throw {
867
+ code: 20022,
868
+ msg: "Invalid request body (nonce - too high)"
869
+ };
870
+ }
871
+
872
+ /**
873
+ * Checks if confirmation target is within configured bounds
874
+ *
875
+ * @param confirmationTarget
876
+ * @throws {DefinedRuntimeError} will throw an error if the confirmationTarget is out of bounds
877
+ */
878
+ protected checkConfirmationTarget(confirmationTarget: number): void {
879
+ if(confirmationTarget>this.config.maxConfTarget) throw {
880
+ code: 20023,
881
+ msg: "Invalid request body (confirmationTarget - too high)"
882
+ };
883
+ if(confirmationTarget<this.config.minConfTarget) throw {
884
+ code: 20024,
885
+ msg: "Invalid request body (confirmationTarget - too low)"
886
+ };
887
+ }
888
+
889
+ /**
890
+ * Checks if the required confirmations are within configured bounds
891
+ *
892
+ * @param confirmations
893
+ * @throws {DefinedRuntimeError} will throw an error if the confirmations are out of bounds
894
+ */
895
+ protected checkRequiredConfirmations(confirmations: number): void {
896
+ if(confirmations>this.config.maxConfirmations) throw {
897
+ code: 20025,
898
+ msg: "Invalid request body (confirmations - too high)"
899
+ };
900
+ if(confirmations<this.config.minConfirmations) throw {
901
+ code: 20026,
902
+ msg: "Invalid request body (confirmations - too low)"
903
+ };
904
+ }
905
+
906
+ /**
907
+ * Checks the validity of the provided address, also checks if the resulting output script isn't too large
908
+ *
909
+ * @param address
910
+ * @throws {DefinedRuntimeError} will throw an error if the address is invalid
911
+ */
912
+ protected checkAddress(address: string): void {
913
+ let parsedOutputScript: Buffer;
914
+
915
+ try {
916
+ parsedOutputScript = bitcoin.address.toOutputScript(address, this.config.bitcoinNetwork);
917
+ } catch (e) {
918
+ throw {
919
+ code: 20031,
920
+ msg: "Invalid request body (address - cannot be parsed)"
921
+ };
922
+ }
923
+
924
+ if(parsedOutputScript.length > OUTPUT_SCRIPT_MAX_LENGTH) throw {
925
+ code: 20032,
926
+ msg: "Invalid request body (address's output script - too long)"
927
+ };
928
+ }
929
+
930
+ /**
931
+ * Checks if the swap is expired, taking into consideration on-chain time skew
932
+ *
933
+ * @param swap
934
+ * @throws {DefinedRuntimeError} will throw an error if the swap is expired
935
+ */
936
+ protected checkExpired(swap: ToBtcSwapAbs) {
937
+ const isExpired = swap.data.getExpiry().lt(new BN(Math.floor(Date.now()/1000)).sub(new BN(this.config.maxSkew)));
938
+ if(isExpired) throw {
939
+ _httpStatus: 200,
940
+ code: 20010,
941
+ msg: "Payment expired"
942
+ };
943
+ }
944
+
945
+ /**
946
+ * Checks & returns the network fee needed for a transaction
947
+ *
948
+ * @param address
949
+ * @param amount
950
+ * @throws {DefinedRuntimeError} will throw an error if there are not enough BTC funds
951
+ */
952
+ private async checkAndGetNetworkFee(address: string, amount: BN): Promise<{ networkFee: BN, satsPerVbyte: BN }> {
953
+ let chainFeeResp = await this.getChainFee(address, amount.toNumber(), true, this.config.networkFeeMultiplierPPM);
954
+
955
+ const hasEnoughFunds = chainFeeResp!=null;
956
+ if(!hasEnoughFunds) throw {
957
+ code: 20002,
958
+ msg: "Not enough liquidity"
959
+ };
960
+
961
+ return chainFeeResp;
962
+ }
963
+
964
+ startRestServer(restServer: Express) {
965
+ restServer.use(this.path+"/payInvoice", serverParamDecoder(10*1000));
966
+ restServer.post(this.path+"/payInvoice", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
967
+ const metadata: {
968
+ request: any,
969
+ times: {[key: string]: number}
970
+ } = {request: {}, times: {}};
971
+
972
+ const chainIdentifier = req.query.chain as string ?? this.chains.default;
973
+ const {swapContract, signer} = this.getChain(chainIdentifier);
974
+
975
+ metadata.times.requestReceived = Date.now();
976
+ /**
977
+ *Sent initially:
978
+ * address: string Bitcoin destination address
979
+ * amount: string Amount to send (in satoshis)
980
+ * confirmationTarget: number Desired confirmation target for the swap, how big of a fee should be assigned to TX
981
+ * confirmations: number Required number of confirmations for us to claim the swap
982
+ * nonce: string Nonce for the swap (used for replay protection)
983
+ * token: string Desired token to use
984
+ * offerer: string Address of the caller
985
+ * exactIn: boolean Whether the swap should be an exact in instead of exact out swap
986
+ *
987
+ *Sent later:
988
+ * feeRate: string Fee rate to use for the init signature
989
+ */
990
+ const parsedBody: ToBtcRequestType = await req.paramReader.getParams({
991
+ address: FieldTypeEnum.String,
992
+ amount: FieldTypeEnum.BN,
993
+ confirmationTarget: FieldTypeEnum.Number,
994
+ confirmations: FieldTypeEnum.Number,
995
+ nonce: FieldTypeEnum.BN,
996
+ token: (val: string) => val!=null &&
997
+ typeof(val)==="string" &&
998
+ this.isTokenSupported(chainIdentifier, val) ? val : null,
999
+ offerer: (val: string) => val!=null &&
1000
+ typeof(val)==="string" &&
1001
+ swapContract.isValidAddress(val) ? val : null,
1002
+ exactIn: FieldTypeEnum.BooleanOptional
1003
+ });
1004
+ if (parsedBody==null) throw {
1005
+ code: 20100,
1006
+ msg: "Invalid request body"
1007
+ };
1008
+ metadata.request = parsedBody;
1009
+
1010
+ const requestedAmount = {input: !!parsedBody.exactIn, amount: parsedBody.amount};
1011
+ const request = {
1012
+ chainIdentifier,
1013
+ raw: req,
1014
+ parsed: parsedBody,
1015
+ metadata
1016
+ };
1017
+ const useToken = parsedBody.token;
1018
+
1019
+ const responseStream = res.responseStream;
1020
+
1021
+ this.checkNonceValid(parsedBody.nonce);
1022
+ this.checkConfirmationTarget(parsedBody.confirmationTarget);
1023
+ this.checkRequiredConfirmations(parsedBody.confirmations);
1024
+ this.checkAddress(parsedBody.address);
1025
+ await this.checkVaultInitialized(chainIdentifier, parsedBody.token);
1026
+ const fees = await this.preCheckAmounts(request, requestedAmount, useToken);
1027
+
1028
+ metadata.times.requestChecked = Date.now();
1029
+
1030
+ //Initialize abort controller for the parallel async operations
1031
+ const abortController = this.getAbortController(responseStream);
1032
+
1033
+ const {pricePrefetchPromise, signDataPrefetchPromise} = this.getToBtcPrefetches(chainIdentifier, useToken, responseStream, abortController);
1034
+
1035
+ const {
1036
+ amountBD,
1037
+ networkFeeData,
1038
+ totalInToken,
1039
+ swapFee,
1040
+ swapFeeInToken,
1041
+ networkFeeInToken
1042
+ } = await this.checkToBtcAmount(request, requestedAmount, fees, useToken, async (amount: BN) => {
1043
+ metadata.times.amountsChecked = Date.now();
1044
+ const resp = await this.checkAndGetNetworkFee(parsedBody.address, amount);
1045
+ metadata.times.chainFeeCalculated = Date.now();
1046
+ return resp;
1047
+ }, abortController.signal, pricePrefetchPromise);
1048
+ metadata.times.priceCalculated = Date.now();
1049
+
1050
+ const paymentHash = this.getHash(chainIdentifier, parsedBody.address, parsedBody.nonce, amountBD, this.config.bitcoinNetwork).toString("hex");
1051
+
1052
+ //Add grace period another time, so the user has 1 hour to commit
1053
+ const expirySeconds = this.getExpiryFromCLTV(parsedBody.confirmationTarget, parsedBody.confirmations).add(new BN(this.config.gracePeriod));
1054
+ const currentTimestamp = new BN(Math.floor(Date.now()/1000));
1055
+ const minRequiredExpiry = currentTimestamp.add(expirySeconds);
1056
+
1057
+ const sequence = new BN(randomBytes(8));
1058
+ const payObject: SwapData = await swapContract.createSwapData(
1059
+ ChainSwapType.CHAIN_NONCED,
1060
+ parsedBody.offerer,
1061
+ signer.getAddress(),
1062
+ useToken,
1063
+ totalInToken,
1064
+ paymentHash,
1065
+ sequence,
1066
+ minRequiredExpiry,
1067
+ parsedBody.nonce,
1068
+ parsedBody.confirmations,
1069
+ true,
1070
+ false,
1071
+ new BN(0),
1072
+ new BN(0)
1073
+ );
1074
+ abortController.signal.throwIfAborted();
1075
+ metadata.times.swapCreated = Date.now();
1076
+
1077
+ const sigData = await this.getToBtcSignatureData(chainIdentifier, payObject, req, abortController.signal, signDataPrefetchPromise);
1078
+ metadata.times.swapSigned = Date.now();
1079
+
1080
+ const createdSwap = new ToBtcSwapAbs(
1081
+ chainIdentifier,
1082
+ parsedBody.address,
1083
+ amountBD,
1084
+ swapFee,
1085
+ swapFeeInToken,
1086
+ networkFeeData.networkFee,
1087
+ networkFeeInToken,
1088
+ networkFeeData.satsPerVbyte,
1089
+ parsedBody.nonce,
1090
+ parsedBody.confirmationTarget,
1091
+ new BN(sigData.timeout)
1092
+ );
1093
+ createdSwap.data = payObject;
1094
+ createdSwap.metadata = metadata;
1095
+
1096
+ await PluginManager.swapCreate(createdSwap);
1097
+ await this.storageManager.saveData(paymentHash, sequence, createdSwap);
1098
+
1099
+ this.swapLogger.info(createdSwap, "REST: /payInvoice: created swap address: "+createdSwap.address+" amount: "+amountBD.toString(10));
1100
+
1101
+ await responseStream.writeParamsAndEnd({
1102
+ code: 20000,
1103
+ msg: "Success",
1104
+ data: {
1105
+ amount: amountBD.toString(10),
1106
+ address: signer.getAddress(),
1107
+ satsPervByte: networkFeeData.satsPerVbyte.toString(10),
1108
+ networkFee: networkFeeInToken.toString(10),
1109
+ swapFee: swapFeeInToken.toString(10),
1110
+ totalFee: swapFeeInToken.add(networkFeeInToken).toString(10),
1111
+ total: totalInToken.toString(10),
1112
+ minRequiredExpiry: minRequiredExpiry.toString(10),
1113
+
1114
+ data: payObject.serialize(),
1115
+
1116
+ prefix: sigData.prefix,
1117
+ timeout: sigData.timeout,
1118
+ signature: sigData.signature
1119
+ }
1120
+ });
1121
+
1122
+ }));
1123
+
1124
+ const getRefundAuthorization = expressHandlerWrapper(async (req, res) => {
1125
+ /**
1126
+ * paymentHash: string Payment hash identifier of the swap
1127
+ * sequence: BN Sequence identifier of the swap
1128
+ */
1129
+ const parsedBody = verifySchema({...req.body, ...req.query}, {
1130
+ paymentHash: (val: string) => val!=null &&
1131
+ typeof(val)==="string" &&
1132
+ val.length===64 &&
1133
+ HEX_REGEX.test(val) ? val: null,
1134
+ sequence: FieldTypeEnum.BN
1135
+ });
1136
+ if (parsedBody==null) throw {
1137
+ code: 20100,
1138
+ msg: "Invalid request body/query (paymentHash/sequence)"
1139
+ };
1140
+
1141
+ this.checkSequence(parsedBody.sequence);
1142
+
1143
+ const payment = await this.storageManager.getData(parsedBody.paymentHash, parsedBody.sequence);
1144
+ if (payment == null || payment.state === ToBtcSwapState.SAVED) throw {
1145
+ _httpStatus: 200,
1146
+ code: 20007,
1147
+ msg: "Payment not found"
1148
+ };
1149
+
1150
+ const {swapContract, signer} = this.getChain(payment.chainIdentifier);
1151
+
1152
+ this.checkExpired(payment);
1153
+
1154
+ if (payment.state === ToBtcSwapState.COMMITED) throw {
1155
+ _httpStatus: 200,
1156
+ code: 20008,
1157
+ msg: "Payment processing"
1158
+ };
1159
+
1160
+ if (payment.state === ToBtcSwapState.BTC_SENT || payment.state===ToBtcSwapState.BTC_SENDING) throw {
1161
+ _httpStatus: 200,
1162
+ code: 20006,
1163
+ msg: "Already paid",
1164
+ data: {
1165
+ txId: payment.txId
1166
+ }
1167
+ };
1168
+
1169
+ if (payment.state === ToBtcSwapState.NON_PAYABLE) {
1170
+ const isCommited = await swapContract.isCommited(payment.data);
1171
+ if (!isCommited) throw {
1172
+ code: 20005,
1173
+ msg: "Not committed"
1174
+ };
1175
+
1176
+ const refundResponse = await swapContract.getRefundSignature(signer, payment.data, this.config.authorizationTimeout);
1177
+
1178
+ //Double check the state after promise result
1179
+ if (payment.state !== ToBtcSwapState.NON_PAYABLE) throw {
1180
+ code: 20005,
1181
+ msg: "Not committed"
1182
+ };
1183
+
1184
+ this.swapLogger.info(payment, "REST: /getRefundAuthorization: returning refund authorization, because swap is in NON_PAYABLE state, address: "+payment.address);
1185
+
1186
+ res.status(200).json({
1187
+ code: 20000,
1188
+ msg: "Success",
1189
+ data: {
1190
+ address: signer.getAddress(),
1191
+ prefix: refundResponse.prefix,
1192
+ timeout: refundResponse.timeout,
1193
+ signature: refundResponse.signature
1194
+ }
1195
+ });
1196
+ return;
1197
+ }
1198
+
1199
+ throw {
1200
+ _httpStatus: 500,
1201
+ code: 20009,
1202
+ msg: "Invalid payment status"
1203
+ };
1204
+ });
1205
+
1206
+ restServer.post(this.path+"/getRefundAuthorization", getRefundAuthorization);
1207
+ restServer.get(this.path+"/getRefundAuthorization", getRefundAuthorization);
1208
+
1209
+ this.logger.info("started at path: ", this.path);
1210
+ }
1211
+
1212
+ /**
1213
+ * Starts watchdog checking sent bitcoin transactions
1214
+ */
1215
+ protected async startTxTimer() {
1216
+ let rerun;
1217
+ rerun = async () => {
1218
+ await this.processBtcTxs().catch( e => this.logger.error("startTxTimer(): call to processBtcTxs() errored", e));
1219
+ setTimeout(rerun, this.config.txCheckInterval);
1220
+ };
1221
+ await rerun();
1222
+ }
1223
+
1224
+ async startWatchdog() {
1225
+ await super.startWatchdog();
1226
+ await this.startTxTimer();
1227
+ }
1228
+
1229
+ async init() {
1230
+ await this.storageManager.loadData(ToBtcSwapAbs);
1231
+ this.subscribeToEvents();
1232
+ await PluginManager.serviceInitialize(this);
1233
+ }
1234
+
1235
+ getInfoData(): any {
1236
+ return {
1237
+ minCltv: this.config.minChainCltv.toNumber(),
1238
+
1239
+ minConfirmations: this.config.minConfirmations,
1240
+ maxConfirmations: this.config.maxConfirmations,
1241
+
1242
+ minConfTarget: this.config.minConfTarget,
1243
+ maxConfTarget: this.config.maxConfTarget,
1244
+
1245
+ maxOutputScriptLen: OUTPUT_SCRIPT_MAX_LENGTH
1246
+ };
1247
+ }
1248
+
1249
+ }