@atomiqlabs/lp-lib 17.5.1 → 17.5.2

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 (170) hide show
  1. package/LICENSE +201 -201
  2. package/dist/fees/IBtcFeeEstimator.d.ts +3 -3
  3. package/dist/fees/IBtcFeeEstimator.js +2 -2
  4. package/dist/index.d.ts +43 -43
  5. package/dist/index.js +59 -59
  6. package/dist/info/InfoHandler.d.ts +17 -17
  7. package/dist/info/InfoHandler.js +61 -61
  8. package/dist/plugins/IPlugin.d.ts +158 -158
  9. package/dist/plugins/IPlugin.js +35 -35
  10. package/dist/plugins/PluginManager.d.ts +115 -115
  11. package/dist/plugins/PluginManager.js +304 -304
  12. package/dist/prices/BinanceSwapPrice.d.ts +29 -29
  13. package/dist/prices/BinanceSwapPrice.js +79 -79
  14. package/dist/prices/CoinGeckoSwapPrice.d.ts +33 -33
  15. package/dist/prices/CoinGeckoSwapPrice.js +51 -51
  16. package/dist/prices/ISwapPrice.d.ts +43 -43
  17. package/dist/prices/ISwapPrice.js +55 -55
  18. package/dist/prices/OKXSwapPrice.d.ts +29 -29
  19. package/dist/prices/OKXSwapPrice.js +79 -79
  20. package/dist/storage/IIntermediaryStorage.d.ts +18 -18
  21. package/dist/storage/IIntermediaryStorage.js +2 -2
  22. package/dist/storagemanager/IntermediaryStorageManager.d.ts +19 -19
  23. package/dist/storagemanager/IntermediaryStorageManager.js +111 -111
  24. package/dist/storagemanager/StorageManager.d.ts +13 -13
  25. package/dist/storagemanager/StorageManager.js +64 -64
  26. package/dist/swaps/SwapHandler.d.ts +178 -178
  27. package/dist/swaps/SwapHandler.js +225 -225
  28. package/dist/swaps/SwapHandlerSwap.d.ts +80 -80
  29. package/dist/swaps/SwapHandlerSwap.js +78 -78
  30. package/dist/swaps/assertions/AmountAssertions.d.ts +37 -37
  31. package/dist/swaps/assertions/AmountAssertions.js +83 -83
  32. package/dist/swaps/assertions/FromBtcAmountAssertions.d.ts +76 -76
  33. package/dist/swaps/assertions/FromBtcAmountAssertions.js +189 -189
  34. package/dist/swaps/assertions/LightningAssertions.d.ts +44 -44
  35. package/dist/swaps/assertions/LightningAssertions.js +86 -86
  36. package/dist/swaps/assertions/ToBtcAmountAssertions.d.ts +53 -53
  37. package/dist/swaps/assertions/ToBtcAmountAssertions.js +154 -154
  38. package/dist/swaps/escrow/EscrowHandler.d.ts +50 -50
  39. package/dist/swaps/escrow/EscrowHandler.js +151 -151
  40. package/dist/swaps/escrow/EscrowHandlerSwap.d.ts +35 -35
  41. package/dist/swaps/escrow/EscrowHandlerSwap.js +69 -69
  42. package/dist/swaps/escrow/FromBtcBaseSwap.d.ts +14 -14
  43. package/dist/swaps/escrow/FromBtcBaseSwap.js +32 -32
  44. package/dist/swaps/escrow/FromBtcBaseSwapHandler.d.ts +102 -102
  45. package/dist/swaps/escrow/FromBtcBaseSwapHandler.js +210 -210
  46. package/dist/swaps/escrow/ToBtcBaseSwap.d.ts +36 -36
  47. package/dist/swaps/escrow/ToBtcBaseSwap.js +67 -67
  48. package/dist/swaps/escrow/ToBtcBaseSwapHandler.d.ts +53 -53
  49. package/dist/swaps/escrow/ToBtcBaseSwapHandler.js +81 -81
  50. package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.d.ts +84 -84
  51. package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +329 -329
  52. package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.d.ts +22 -22
  53. package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.js +53 -53
  54. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.d.ts +115 -115
  55. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +719 -719
  56. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.d.ts +34 -34
  57. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.js +94 -94
  58. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.d.ts +119 -119
  59. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +730 -730
  60. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.d.ts +56 -56
  61. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.js +123 -123
  62. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +170 -170
  63. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +769 -769
  64. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +29 -29
  65. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +67 -67
  66. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +178 -178
  67. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +912 -912
  68. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.d.ts +25 -25
  69. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.js +61 -61
  70. package/dist/swaps/spv_vault_swap/SpvVault.d.ts +45 -45
  71. package/dist/swaps/spv_vault_swap/SpvVault.js +148 -148
  72. package/dist/swaps/spv_vault_swap/SpvVaultSwap.d.ts +71 -71
  73. package/dist/swaps/spv_vault_swap/SpvVaultSwap.js +165 -165
  74. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.d.ts +72 -72
  75. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +710 -710
  76. package/dist/swaps/spv_vault_swap/SpvVaults.d.ts +70 -70
  77. package/dist/swaps/spv_vault_swap/SpvVaults.js +554 -554
  78. package/dist/swaps/spv_vault_swap/StickyAddress.d.ts +6 -6
  79. package/dist/swaps/spv_vault_swap/StickyAddress.js +19 -19
  80. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.d.ts +52 -52
  81. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +677 -677
  82. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.d.ts +53 -53
  83. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.js +121 -121
  84. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.d.ts +77 -77
  85. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +518 -518
  86. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.d.ts +35 -35
  87. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.js +84 -84
  88. package/dist/utils/BitcoinUtils.d.ts +4 -4
  89. package/dist/utils/BitcoinUtils.js +61 -61
  90. package/dist/utils/Utils.d.ts +32 -32
  91. package/dist/utils/Utils.js +130 -130
  92. package/dist/utils/paramcoders/IParamReader.d.ts +5 -5
  93. package/dist/utils/paramcoders/IParamReader.js +2 -2
  94. package/dist/utils/paramcoders/IParamWriter.d.ts +4 -4
  95. package/dist/utils/paramcoders/IParamWriter.js +2 -2
  96. package/dist/utils/paramcoders/LegacyParamEncoder.d.ts +10 -10
  97. package/dist/utils/paramcoders/LegacyParamEncoder.js +22 -22
  98. package/dist/utils/paramcoders/ParamDecoder.d.ts +25 -25
  99. package/dist/utils/paramcoders/ParamDecoder.js +222 -222
  100. package/dist/utils/paramcoders/ParamEncoder.d.ts +9 -9
  101. package/dist/utils/paramcoders/ParamEncoder.js +22 -22
  102. package/dist/utils/paramcoders/SchemaVerifier.d.ts +21 -21
  103. package/dist/utils/paramcoders/SchemaVerifier.js +84 -84
  104. package/dist/utils/paramcoders/server/ServerParamDecoder.d.ts +8 -8
  105. package/dist/utils/paramcoders/server/ServerParamDecoder.js +107 -107
  106. package/dist/utils/paramcoders/server/ServerParamEncoder.d.ts +11 -11
  107. package/dist/utils/paramcoders/server/ServerParamEncoder.js +65 -65
  108. package/dist/wallets/IBitcoinWallet.d.ts +155 -155
  109. package/dist/wallets/IBitcoinWallet.js +100 -100
  110. package/dist/wallets/ILightningWallet.d.ts +137 -137
  111. package/dist/wallets/ILightningWallet.js +37 -37
  112. package/dist/wallets/ISpvVaultSigner.d.ts +7 -7
  113. package/dist/wallets/ISpvVaultSigner.js +2 -2
  114. package/package.json +37 -37
  115. package/src/fees/IBtcFeeEstimator.ts +6 -6
  116. package/src/index.ts +54 -54
  117. package/src/info/InfoHandler.ts +106 -106
  118. package/src/plugins/IPlugin.ts +196 -196
  119. package/src/plugins/PluginManager.ts +389 -389
  120. package/src/prices/BinanceSwapPrice.ts +101 -101
  121. package/src/prices/CoinGeckoSwapPrice.ts +75 -75
  122. package/src/prices/ISwapPrice.ts +88 -88
  123. package/src/prices/OKXSwapPrice.ts +101 -101
  124. package/src/storage/IIntermediaryStorage.ts +19 -19
  125. package/src/storagemanager/IntermediaryStorageManager.ts +118 -118
  126. package/src/storagemanager/StorageManager.ts +78 -78
  127. package/src/swaps/SwapHandler.ts +337 -337
  128. package/src/swaps/SwapHandlerSwap.ts +143 -143
  129. package/src/swaps/assertions/AmountAssertions.ts +97 -97
  130. package/src/swaps/assertions/FromBtcAmountAssertions.ts +257 -257
  131. package/src/swaps/assertions/LightningAssertions.ts +103 -103
  132. package/src/swaps/assertions/ToBtcAmountAssertions.ts +209 -209
  133. package/src/swaps/escrow/EscrowHandler.ts +172 -172
  134. package/src/swaps/escrow/EscrowHandlerSwap.ts +86 -86
  135. package/src/swaps/escrow/FromBtcBaseSwap.ts +38 -38
  136. package/src/swaps/escrow/FromBtcBaseSwapHandler.ts +286 -286
  137. package/src/swaps/escrow/ToBtcBaseSwap.ts +85 -85
  138. package/src/swaps/escrow/ToBtcBaseSwapHandler.ts +129 -129
  139. package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +468 -468
  140. package/src/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.ts +65 -65
  141. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +900 -900
  142. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.ts +145 -145
  143. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +892 -892
  144. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.ts +200 -200
  145. package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +947 -947
  146. package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +112 -112
  147. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +1167 -1167
  148. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.ts +84 -84
  149. package/src/swaps/spv_vault_swap/SpvVault.ts +182 -182
  150. package/src/swaps/spv_vault_swap/SpvVaultSwap.ts +239 -239
  151. package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +897 -897
  152. package/src/swaps/spv_vault_swap/SpvVaults.ts +640 -640
  153. package/src/swaps/spv_vault_swap/StickyAddress.ts +20 -20
  154. package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +782 -782
  155. package/src/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.ts +189 -189
  156. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +622 -622
  157. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.ts +125 -125
  158. package/src/utils/BitcoinUtils.ts +59 -59
  159. package/src/utils/Utils.ts +152 -152
  160. package/src/utils/paramcoders/IParamReader.ts +7 -7
  161. package/src/utils/paramcoders/IParamWriter.ts +8 -8
  162. package/src/utils/paramcoders/LegacyParamEncoder.ts +27 -27
  163. package/src/utils/paramcoders/ParamDecoder.ts +218 -218
  164. package/src/utils/paramcoders/ParamEncoder.ts +29 -29
  165. package/src/utils/paramcoders/SchemaVerifier.ts +96 -96
  166. package/src/utils/paramcoders/server/ServerParamDecoder.ts +118 -118
  167. package/src/utils/paramcoders/server/ServerParamEncoder.ts +75 -75
  168. package/src/wallets/IBitcoinWallet.ts +246 -246
  169. package/src/wallets/ILightningWallet.ts +201 -201
  170. package/src/wallets/ISpvVaultSigner.ts +10 -10
@@ -1,947 +1,947 @@
1
- import {Express, Request, Response} from "express";
2
- import {ToBtcSwapAbs, ToBtcSwapState} from "./ToBtcSwapAbs";
3
- import {MultichainData, SwapHandlerType} from "../../SwapHandler";
4
- import {ISwapPrice} from "../../../prices/ISwapPrice";
5
- import {
6
- BtcTx,
7
- ChainSwapType,
8
- ClaimEvent,
9
- InitializeEvent,
10
- RefundEvent,
11
- SwapData,
12
- BitcoinRpc,
13
- BtcBlock, BigIntBufferUtils, SwapCommitStateType
14
- } from "@atomiqlabs/base";
15
- import {expressHandlerWrapper, getAbortController, HEX_REGEX, isDefinedRuntimeError} from "../../../utils/Utils";
16
- import {PluginManager} from "../../../plugins/PluginManager";
17
- import {IIntermediaryStorage} from "../../../storage/IIntermediaryStorage";
18
- import {randomBytes} from "crypto";
19
- import {FieldTypeEnum, verifySchema} from "../../../utils/paramcoders/SchemaVerifier";
20
- import {serverParamDecoder} from "../../../utils/paramcoders/server/ServerParamDecoder";
21
- import {IParamReader} from "../../../utils/paramcoders/IParamReader";
22
- import {ServerParamEncoder} from "../../../utils/paramcoders/server/ServerParamEncoder";
23
- import {ToBtcBaseConfig, ToBtcBaseSwapHandler} from "../ToBtcBaseSwapHandler";
24
- import {IBitcoinWallet} from "../../../wallets/IBitcoinWallet";
25
- import {checkTransactionReplaced} from "../../../utils/BitcoinUtils";
26
- import {isQuoteThrow} from "../../../plugins/IPlugin";
27
- import {FromBtcLnAutoSwapState} from "../frombtcln_autoinit/FromBtcLnAutoSwap";
28
-
29
- const OUTPUT_SCRIPT_MAX_LENGTH = 200;
30
-
31
- export type ToBtcConfig = ToBtcBaseConfig & {
32
- sendSafetyFactor: bigint,
33
-
34
- minChainCltv: bigint,
35
-
36
- networkFeeMultiplier: number,
37
- minConfirmations: number,
38
- maxConfirmations: number,
39
- maxConfTarget: number,
40
- minConfTarget: number,
41
-
42
- txCheckInterval: number
43
- };
44
-
45
- export type ToBtcRequestType = {
46
- address: string,
47
- amount: bigint,
48
- confirmationTarget: number,
49
- confirmations: number,
50
- nonce: bigint,
51
- token: string,
52
- offerer: string,
53
- exactIn?: boolean
54
- };
55
-
56
- const MAX_PARALLEL_TX_PROCESSED = 10;
57
-
58
- /**
59
- * Handler for to BTC swaps, utilizing PTLCs (proof-time locked contracts) using btc relay (on-chain bitcoin SPV)
60
- */
61
- export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState> {
62
- readonly type = SwapHandlerType.TO_BTC;
63
- readonly swapType = ChainSwapType.CHAIN_NONCED;
64
- readonly inflightSwapStates = new Set([ToBtcSwapState.BTC_SENDING, ToBtcSwapState.BTC_SENT]);
65
-
66
- activeSubscriptions: {[txId: string]: ToBtcSwapAbs} = {};
67
- bitcoinRpc: BitcoinRpc<BtcBlock>;
68
- bitcoin: IBitcoinWallet;
69
-
70
- readonly config: ToBtcConfig;
71
-
72
- constructor(
73
- storageDirectory: IIntermediaryStorage<ToBtcSwapAbs>,
74
- path: string,
75
- chainData: MultichainData,
76
- bitcoin: IBitcoinWallet,
77
- swapPricing: ISwapPrice,
78
- bitcoinRpc: BitcoinRpc<BtcBlock>,
79
- config: ToBtcConfig
80
- ) {
81
- super(storageDirectory, path, chainData, swapPricing, config);
82
- this.bitcoinRpc = bitcoinRpc;
83
- this.bitcoin = bitcoin;
84
- this.config = config;
85
- }
86
-
87
- /**
88
- * Returns the payment hash of the swap, takes swap nonce into account. Payment hash is chain-specific.
89
- *
90
- * @param chainIdentifier
91
- * @param address
92
- * @param confirmations
93
- * @param nonce
94
- * @param amount
95
- */
96
- private getHash(chainIdentifier: string, address: string, confirmations: number, nonce: bigint, amount: bigint): Buffer {
97
- const parsedOutputScript = this.bitcoin.toOutputScript(address);
98
- const {swapContract} = this.getChain(chainIdentifier);
99
- return swapContract.getHashForOnchain(parsedOutputScript, amount, confirmations, nonce);
100
- }
101
-
102
- /**
103
- * Tries to claim the swap after our transaction was confirmed
104
- *
105
- * @param tx
106
- * @param swap
107
- * @param vout
108
- */
109
- private async tryClaimSwap(tx: {blockhash: string, confirmations: number, txid: string, hex: string}, swap: ToBtcSwapAbs, vout: number): Promise<boolean> {
110
- const {chainInterface, swapContract, signer} = this.getChain(swap.chainIdentifier);
111
-
112
- const blockHeader = await this.bitcoinRpc.getBlockHeader(tx.blockhash);
113
-
114
- //Set flag that we are sending the transaction already, so we don't end up with race condition
115
- if(swap.isLocked()) return false;
116
-
117
- let txns: any[];
118
- try {
119
- txns = await swapContract.txsClaimWithTxData(
120
- signer,
121
- swap.data,
122
- {...tx, height: blockHeader.getHeight()},
123
- swap.requiredConfirmations,
124
- vout,
125
- null,
126
- null,
127
- false
128
- );
129
- } catch (e) {
130
- this.swapLogger.error(swap, "tryClaimSwap(): error occurred creating swap claim transactions, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout+" address: "+swap.address, e);
131
- return false
132
- }
133
-
134
- const unlock: () => boolean = swap.lock(swapContract.claimWithTxDataTimeout);
135
- if(unlock==null) return false;
136
-
137
- try {
138
- this.swapLogger.debug(swap, "tryClaimSwap(): initiate claim of swap, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout);
139
- await chainInterface.sendAndConfirm(signer, txns, true);
140
- this.swapLogger.info(swap, "tryClaimSwap(): swap claimed successfully, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout+" address: "+swap.address);
141
- if(swap.metadata!=null) swap.metadata.times.txClaimed = Date.now();
142
- unlock();
143
- return true;
144
- } catch (e) {
145
- this.swapLogger.error(swap, "tryClaimSwap(): error occurred claiming swap, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout+" address: "+swap.address, e);
146
- return false
147
- }
148
- }
149
-
150
- protected async processPastSwap(swap: ToBtcSwapAbs) {
151
- const {swapContract, signer} = this.getChain(swap.chainIdentifier);
152
-
153
- if(swap.state===ToBtcSwapState.SAVED) {
154
- const isSignatureExpired = await swapContract.isInitAuthorizationExpired(swap.data, swap);
155
- if(isSignatureExpired) {
156
- const isCommitted = await swapContract.isCommited(swap.data);
157
- if(!isCommitted) {
158
- this.swapLogger.info(swap, "processPastSwap(state=SAVED): authorization expired & swap not committed, cancelling swap, address: "+swap.address);
159
- await this.removeSwapData(swap, ToBtcSwapState.CANCELED);
160
- } else {
161
- this.swapLogger.info(swap, "processPastSwap(state=SAVED): swap committed (detected from processPastSwap), address: "+swap.address);
162
- await swap.setState(ToBtcSwapState.COMMITED);
163
- await this.saveSwapData(swap);
164
- }
165
- return;
166
- }
167
- }
168
-
169
- if(swap.state===ToBtcSwapState.NON_PAYABLE || swap.state===ToBtcSwapState.SAVED) {
170
- if(await swapContract.isExpired(signer.getAddress(), swap.data)) {
171
- this.swapLogger.info(swap, "processPastSwap(state=NON_PAYABLE|SAVED): swap expired, cancelling, address: "+swap.address);
172
- await this.removeSwapData(swap, ToBtcSwapState.CANCELED);
173
- return;
174
- }
175
- }
176
-
177
- //Sanity check for sent swaps
178
- if(swap.state===ToBtcSwapState.BTC_SENT) {
179
- const isCommited = await swapContract.isCommited(swap.data);
180
- if(!isCommited) {
181
- const status = await swapContract.getCommitStatus(signer.getAddress(), swap.data);
182
- if(status.type===SwapCommitStateType.PAID) {
183
- this.swapLogger.info(swap, "processPastSwap(state=BTC_SENT): swap claimed (detected from processPastSwap), address: "+swap.address);
184
- this.unsubscribePayment(swap);
185
- swap.txIds ??= {};
186
- swap.txIds.claim = await status.getClaimTxId();
187
- await this.removeSwapData(swap, ToBtcSwapState.CLAIMED);
188
- } else if(status.type===SwapCommitStateType.EXPIRED) {
189
- this.swapLogger.warn(swap, "processPastSwap(state=BTC_SENT): swap expired, but bitcoin was probably already sent, txId: "+swap.txId+" address: "+swap.address);
190
- this.unsubscribePayment(swap);
191
- swap.txIds ??= {};
192
- swap.txIds.refund = status.getRefundTxId==null ? null : await status.getRefundTxId();
193
- await this.removeSwapData(swap, ToBtcSwapState.REFUNDED);
194
- }
195
- return;
196
- }
197
- }
198
-
199
- if(swap.state===ToBtcSwapState.COMMITED || swap.state===ToBtcSwapState.BTC_SENDING || swap.state===ToBtcSwapState.BTC_SENT) {
200
- await this.processInitialized(swap);
201
- return;
202
- }
203
- }
204
-
205
- /**
206
- * Checks past swaps, deletes ones that are already expired.
207
- */
208
- protected async processPastSwaps() {
209
- const queriedData = await this.storageManager.query([
210
- {
211
- key: "state",
212
- values: [
213
- ToBtcSwapState.SAVED,
214
- ToBtcSwapState.NON_PAYABLE,
215
- ToBtcSwapState.COMMITED,
216
- ToBtcSwapState.BTC_SENDING,
217
- ToBtcSwapState.BTC_SENT,
218
- ]
219
- }
220
- ]);
221
-
222
- for(let {obj: swap} of queriedData) {
223
- await this.processPastSwap(swap);
224
- }
225
- }
226
-
227
- protected async processBtcTx(swap: ToBtcSwapAbs, tx: BtcTx): Promise<boolean> {
228
- tx.confirmations = tx.confirmations ?? 0;
229
-
230
- //Check transaction has enough confirmations
231
- const hasEnoughConfirmations = tx.confirmations>=swap.requiredConfirmations;
232
- if(!hasEnoughConfirmations || tx.blockhash==null) {
233
- return false;
234
- }
235
- const _tx = tx as BtcTx & {blockhash: string, confirmations: number};
236
-
237
- this.swapLogger.debug(swap, "processBtcTx(): address: "+swap.address+" amount: "+swap.amount.toString(10)+" btcTx: "+tx);
238
-
239
- //Search for required transaction output (vout)
240
- const outputScript = this.bitcoin.toOutputScript(swap.address);
241
- const vout = tx.outs.find(e => BigInt(e.value)===swap.amount && Buffer.from(e.scriptPubKey.hex, "hex").equals(outputScript));
242
- if(vout==null) {
243
- this.swapLogger.warn(swap, "processBtcTx(): cannot find correct vout,"+
244
- " required output script: "+outputScript.toString("hex")+
245
- " required amount: "+swap.amount.toString(10)+
246
- " vouts: ", tx.outs);
247
- return false;
248
- }
249
-
250
- if(swap.metadata!=null) swap.metadata.times.payTxConfirmed = Date.now();
251
-
252
- const success = await this.tryClaimSwap(_tx, swap, vout.n);
253
-
254
- return success;
255
- }
256
-
257
- /**
258
- * Checks active sent out bitcoin transactions
259
- */
260
- private async processBtcTxs() {
261
- const unsubscribeSwaps: ToBtcSwapAbs[] = [];
262
-
263
- let promises: Promise<void>[] = [];
264
- for(let txId in this.activeSubscriptions) {
265
- const swap: ToBtcSwapAbs = this.activeSubscriptions[txId];
266
- //TODO: RBF the transaction if it's already taking too long to confirm
267
- promises.push((async () => {
268
- try {
269
- let tx: BtcTx = await this.bitcoin.getWalletTransaction(txId);
270
- if(tx==null) return;
271
-
272
- if(await this.processBtcTx(swap, tx)) {
273
- this.swapLogger.info(swap, "processBtcTxs(): swap claimed successfully, txId: "+tx.txid+" address: "+swap.address);
274
- unsubscribeSwaps.push(swap);
275
- }
276
- } catch (e) {
277
- this.swapLogger.error(swap, "processBtcTxs(): error processing btc transaction", e);
278
- }
279
- })());
280
- if(promises.length >= MAX_PARALLEL_TX_PROCESSED) {
281
- await Promise.all(promises);
282
- promises = [];
283
- }
284
- }
285
- await Promise.all(promises);
286
-
287
- unsubscribeSwaps.forEach(swap => {
288
- this.unsubscribePayment(swap);
289
- });
290
- }
291
-
292
- /**
293
- * Subscribes to and periodically checks txId used to send out funds for the swap for enough confirmations
294
- *
295
- * @param payment
296
- */
297
- protected subscribeToPayment(payment: ToBtcSwapAbs) {
298
- this.swapLogger.info(payment, "subscribeToPayment(): subscribing to swap, txId: "+payment.txId+" address: "+payment.address);
299
- this.activeSubscriptions[payment.txId] = payment;
300
- }
301
-
302
- protected unsubscribePayment(payment: ToBtcSwapAbs) {
303
- if(payment.txId!=null) {
304
- if(this.activeSubscriptions[payment.txId]!=null) {
305
- this.swapLogger.info(payment, "unsubscribePayment(): unsubscribing swap, txId: "+payment.txId+" address: "+payment.address);
306
- delete this.activeSubscriptions[payment.txId];
307
- }
308
- }
309
- }
310
-
311
- /**
312
- * Checks if expiry time on the swap leaves us enough room to send a transaction and for the transaction to confirm
313
- *
314
- * @param swap
315
- * @private
316
- * @throws DefinedRuntimeError will throw an error in case there isn't enough time for us to send a BTC payout tx
317
- */
318
- protected checkExpiresTooSoon(swap: ToBtcSwapAbs): void {
319
- const currentTimestamp = BigInt(Math.floor(Date.now()/1000));
320
- const tsDelta = swap.data.getExpiry() - currentTimestamp;
321
- const minRequiredCLTV = this.getExpiryFromCLTV(swap.preferedConfirmationTarget, swap.requiredConfirmations);
322
- const hasRequiredCLTVDelta = tsDelta >= minRequiredCLTV;
323
- if(!hasRequiredCLTVDelta) throw {
324
- code: 90001,
325
- msg: "TS delta too low",
326
- data: {
327
- required: minRequiredCLTV.toString(10),
328
- actual: tsDelta.toString(10)
329
- }
330
- }
331
- }
332
-
333
- /**
334
- * Checks if the actual fee for the swap is no higher than the quoted estimate
335
- *
336
- * @param quotedSatsPerVbyte
337
- * @param actualSatsPerVbyte
338
- * @private
339
- * @throws DefinedRuntimeError will throw an error in case the actual fee is higher than quoted fee
340
- */
341
- protected checkCalculatedTxFee(quotedSatsPerVbyte: bigint, actualSatsPerVbyte: bigint): void {
342
- const swapPaysEnoughNetworkFee = quotedSatsPerVbyte >= actualSatsPerVbyte;
343
- if(!swapPaysEnoughNetworkFee) throw {
344
- code: 90003,
345
- msg: "Fee changed too much!",
346
- data: {
347
- quotedFee: quotedSatsPerVbyte.toString(10),
348
- actualFee: actualSatsPerVbyte.toString(10)
349
- }
350
- };
351
- }
352
-
353
- /**
354
- * Sends a bitcoin transaction to payout BTC for a swap
355
- *
356
- * @param swap
357
- * @private
358
- * @throws DefinedRuntimeError will throw an error in case the payment cannot be initiated
359
- */
360
- private sendBitcoinPayment(swap: ToBtcSwapAbs) {
361
- //Make sure that bitcoin payouts are processed sequentially to avoid race conditions between multiple payouts,
362
- // e.g. that 2 payouts share the same input and would effectively double-spend each other
363
- return this.bitcoin.execute(async () => {
364
- //Run checks
365
- this.checkTooManyInflightSwaps();
366
- this.checkExpiresTooSoon(swap);
367
- if(swap.metadata!=null) swap.metadata.times.payCLTVChecked = Date.now();
368
-
369
- const satsPerVbyte = await this.bitcoin.getFeeRate();
370
- this.checkCalculatedTxFee(swap.satsPerVbyte, BigInt(satsPerVbyte));
371
- if(swap.metadata!=null) swap.metadata.times.payChainFee = Date.now();
372
-
373
- const signResult = await this.bitcoin.getSignedTransaction(
374
- swap.address,
375
- Number(swap.amount),
376
- satsPerVbyte,
377
- swap.nonce,
378
- Number(swap.satsPerVbyte)
379
- );
380
- if(signResult==null) throw {
381
- code: 90002,
382
- msg: "Failed to create signed transaction (not enough funds?)"
383
- }
384
- if(swap.metadata!=null) swap.metadata.times.paySignPSBT = Date.now();
385
-
386
- try {
387
- this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: "+signResult.raw);
388
- swap.txId = signResult.tx.id;
389
- swap.btcRawTx = signResult.raw;
390
- swap.setRealNetworkFee(BigInt(signResult.networkFee));
391
- swap.sending = true;
392
- await swap.setState(ToBtcSwapState.BTC_SENDING);
393
- await this.saveSwapData(swap);
394
-
395
- await this.bitcoin.sendRawTransaction(signResult.raw);
396
- swap.sending = false;
397
- } catch (e) {
398
- swap.sending = false;
399
- throw e;
400
- }
401
-
402
- if(swap.metadata!=null) swap.metadata.times.payTxSent = Date.now();
403
- this.swapLogger.info(swap, "sendBitcoinPayment(): btc transaction generated, signed & broadcasted, txId: "+swap.txId+" address: "+swap.address);
404
-
405
- await swap.setState(ToBtcSwapState.BTC_SENT);
406
- await this.saveSwapData(swap);
407
- });
408
- }
409
-
410
- /**
411
- * Called after swap was successfully committed, will check if bitcoin tx is already sent, if not tries to send it and subscribes to it
412
- *
413
- * @param swap
414
- */
415
- private async processInitialized(swap: ToBtcSwapAbs) {
416
- if(swap.state===ToBtcSwapState.BTC_SENDING) {
417
- if(swap.sending) return;
418
- //Bitcoin transaction was signed (maybe also sent)
419
- const tx = await checkTransactionReplaced(swap.txId, swap.btcRawTx, this.bitcoinRpc);
420
-
421
- const isTxSent = tx!=null;
422
- if(!isTxSent) {
423
- //Reset the state to COMMITED
424
- this.swapLogger.info(swap, "processInitialized(state=BTC_SENDING): btc transaction not found, resetting to COMMITED state, txId: "+swap.txId+" address: "+swap.address);
425
- await swap.setState(ToBtcSwapState.COMMITED);
426
- } else {
427
- this.swapLogger.info(swap, "processInitialized(state=BTC_SENDING): btc transaction found, advancing to BTC_SENT state, txId: "+swap.txId+" address: "+swap.address);
428
- await swap.setState(ToBtcSwapState.BTC_SENT);
429
- await this.saveSwapData(swap);
430
- }
431
- }
432
-
433
- if(swap.state===ToBtcSwapState.SAVED) {
434
- this.swapLogger.info(swap, "processInitialized(state=SAVED): advancing to COMMITED state, address: "+swap.address);
435
- await swap.setState(ToBtcSwapState.COMMITED);
436
- await this.saveSwapData(swap);
437
- }
438
-
439
- if(swap.state===ToBtcSwapState.COMMITED) {
440
- const unlock: () => boolean = swap.lock(Infinity);
441
- if(unlock==null) return;
442
-
443
- const pluginCheckResult = await PluginManager.onHandlePreToBtcExecute(
444
- SwapHandlerType.TO_BTC,
445
- swap
446
- );
447
- if(isQuoteThrow(pluginCheckResult)) {
448
- const error = {
449
- code: 29999,
450
- msg: pluginCheckResult.message
451
- };
452
- this.swapLogger.error(swap, "processInitialized(state=COMMITED): setting state to NON_PAYABLE due to send bitcoin payment error", error);
453
- if(swap.metadata!=null) swap.metadata.payError = error;
454
- unlock();
455
- if(swap.state===ToBtcSwapState.COMMITED) {
456
- await swap.setState(ToBtcSwapState.NON_PAYABLE);
457
- await this.saveSwapData(swap);
458
- }
459
- return;
460
- }
461
-
462
- if(swap.state!==ToBtcSwapState.COMMITED) {
463
- unlock();
464
- return;
465
- }
466
-
467
- this.swapLogger.debug(swap, "processInitialized(state=COMMITED): sending bitcoin transaction, address: "+swap.address);
468
-
469
- try {
470
- await this.sendBitcoinPayment(swap);
471
- this.swapLogger.info(swap, "processInitialized(state=COMMITED): btc transaction sent, address: "+swap.address);
472
- } catch (e) {
473
- if(isDefinedRuntimeError(e)) {
474
- this.swapLogger.error(swap, "processInitialized(state=COMMITED): setting state to NON_PAYABLE due to send bitcoin payment error", e);
475
- if(swap.metadata!=null) swap.metadata.payError = e;
476
- await swap.setState(ToBtcSwapState.NON_PAYABLE);
477
- await this.saveSwapData(swap);
478
- } else {
479
- this.swapLogger.error(swap, "processInitialized(state=COMMITED): send bitcoin payment error", e);
480
- throw e;
481
- }
482
- } finally {
483
- unlock();
484
- }
485
- }
486
-
487
- if(swap.state===ToBtcSwapState.NON_PAYABLE) return;
488
-
489
- this.subscribeToPayment(swap);
490
- }
491
-
492
- protected async processInitializeEvent(chainIdentifier: string, swap: ToBtcSwapAbs, event: InitializeEvent<SwapData>): Promise<void> {
493
- this.swapLogger.info(swap, "SC: InitializeEvent: swap initialized by the client, address: "+swap.address);
494
-
495
- await this.processInitialized(swap);
496
- }
497
-
498
- protected async processClaimEvent(chainIdentifier: string, swap: ToBtcSwapAbs, event: ClaimEvent<SwapData>): Promise<void> {
499
- this.swapLogger.info(swap, "SC: ClaimEvent: swap successfully claimed to us, address: "+swap.address);
500
-
501
- //Also remove transaction from active subscriptions
502
- this.unsubscribePayment(swap);
503
- await this.removeSwapData(swap, ToBtcSwapState.CLAIMED);
504
- }
505
-
506
- protected async processRefundEvent(chainIdentifier: string, swap: ToBtcSwapAbs, event: RefundEvent<SwapData>): Promise<void> {
507
- this.swapLogger.info(swap, "SC: RefundEvent: swap successfully refunded by the user, address: "+swap.address);
508
-
509
- //Also remove transaction from active subscriptions
510
- this.unsubscribePayment(swap);
511
- await this.removeSwapData(swap, ToBtcSwapState.REFUNDED);
512
- }
513
-
514
- /**
515
- * Returns required expiry delta for swap params
516
- *
517
- * @param confirmationTarget
518
- * @param confirmations
519
- */
520
- protected getExpiryFromCLTV(confirmationTarget: number, confirmations: number): bigint {
521
- //Blocks = 10 + (confirmations + confirmationTarget)*2
522
- //Time = 3600 + (600*blocks*2)
523
- const cltv = this.config.minChainCltv + (
524
- BigInt(confirmations + confirmationTarget) * this.config.sendSafetyFactor
525
- );
526
-
527
- return this.config.gracePeriod + (this.config.bitcoinBlocktime * cltv * this.config.safetyFactor);
528
- }
529
-
530
- /**
531
- * Checks if the requested nonce is valid
532
- *
533
- * @param nonce
534
- * @throws {DefinedRuntimeError} will throw an error if the nonce is invalid
535
- */
536
- private checkNonceValid(nonce: bigint): void {
537
- if(nonce < 0 || nonce >= (2n ** 64n)) throw {
538
- code: 20021,
539
- msg: "Invalid request body (nonce - cannot be parsed)"
540
- };
541
-
542
- const firstPart = nonce >> 24n;
543
-
544
- const maxAllowedValue = BigInt(Math.floor(Date.now()/1000)-600000000);
545
- if(firstPart > maxAllowedValue) throw {
546
- code: 20022,
547
- msg: "Invalid request body (nonce - too high)"
548
- };
549
- }
550
-
551
- /**
552
- * Checks if confirmation target is within configured bounds
553
- *
554
- * @param confirmationTarget
555
- * @throws {DefinedRuntimeError} will throw an error if the confirmationTarget is out of bounds
556
- */
557
- protected checkConfirmationTarget(confirmationTarget: number): void {
558
- if(confirmationTarget>this.config.maxConfTarget) throw {
559
- code: 20023,
560
- msg: "Invalid request body (confirmationTarget - too high)"
561
- };
562
- if(confirmationTarget<this.config.minConfTarget) throw {
563
- code: 20024,
564
- msg: "Invalid request body (confirmationTarget - too low)"
565
- };
566
- }
567
-
568
- /**
569
- * Checks if the required confirmations are within configured bounds
570
- *
571
- * @param confirmations
572
- * @throws {DefinedRuntimeError} will throw an error if the confirmations are out of bounds
573
- */
574
- protected checkRequiredConfirmations(confirmations: number): void {
575
- if(confirmations>this.config.maxConfirmations) throw {
576
- code: 20025,
577
- msg: "Invalid request body (confirmations - too high)"
578
- };
579
- if(confirmations<this.config.minConfirmations) throw {
580
- code: 20026,
581
- msg: "Invalid request body (confirmations - too low)"
582
- };
583
- }
584
-
585
- /**
586
- * Checks the validity of the provided address, also checks if the resulting output script isn't too large
587
- *
588
- * @param address
589
- * @throws {DefinedRuntimeError} will throw an error if the address is invalid
590
- */
591
- protected checkAddress(address: string): void {
592
- let parsedOutputScript: Buffer;
593
-
594
- try {
595
- parsedOutputScript = this.bitcoin.toOutputScript(address);
596
- } catch (e) {
597
- throw {
598
- code: 20031,
599
- msg: "Invalid request body (address - cannot be parsed)"
600
- };
601
- }
602
-
603
- if(parsedOutputScript.length > OUTPUT_SCRIPT_MAX_LENGTH) throw {
604
- code: 20032,
605
- msg: "Invalid request body (address's output script - too long)"
606
- };
607
- }
608
-
609
- /**
610
- * Checks if the swap is expired, taking into consideration on-chain time skew
611
- *
612
- * @param swap
613
- * @throws {DefinedRuntimeError} will throw an error if the swap is expired
614
- */
615
- protected async checkExpired(swap: ToBtcSwapAbs) {
616
- const {swapContract, signer} = this.getChain(swap.chainIdentifier);
617
- const isExpired = await swapContract.isExpired(signer.getAddress(), swap.data);
618
- if(isExpired) throw {
619
- _httpStatus: 200,
620
- code: 20010,
621
- msg: "Payment expired"
622
- };
623
- }
624
-
625
- /**
626
- * Checks & returns the network fee needed for a transaction
627
- *
628
- * @param address
629
- * @param amount
630
- * @throws {DefinedRuntimeError} will throw an error if there are not enough BTC funds
631
- */
632
- private async checkAndGetNetworkFee(address: string, amount: bigint): Promise<{ networkFee: bigint, satsPerVbyte: bigint }> {
633
- let chainFeeResp = await this.bitcoin.estimateFee(address, Number(amount), null, this.config.networkFeeMultiplier);
634
-
635
- const hasEnoughFunds = chainFeeResp!=null;
636
- if(!hasEnoughFunds) throw {
637
- code: 20002,
638
- msg: "Not enough liquidity"
639
- };
640
-
641
- return {
642
- networkFee: BigInt(chainFeeResp.networkFee),
643
- satsPerVbyte: BigInt(chainFeeResp.satsPerVbyte)
644
- };
645
- }
646
-
647
- startRestServer(restServer: Express) {
648
- restServer.use(this.path+"/payInvoice", serverParamDecoder(10*1000));
649
- restServer.post(this.path+"/payInvoice", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
650
- const metadata: {
651
- request: any,
652
- times: {[key: string]: number}
653
- } = {request: {}, times: {}};
654
-
655
- const chainIdentifier = req.query.chain as string;
656
- const {swapContract, signer, chainInterface} = this.getChain(chainIdentifier);
657
-
658
- metadata.times.requestReceived = Date.now();
659
- /**
660
- *Sent initially:
661
- * address: string Bitcoin destination address
662
- * amount: string Amount to send (in satoshis)
663
- * confirmationTarget: number Desired confirmation target for the swap, how big of a fee should be assigned to TX
664
- * confirmations: number Required number of confirmations for us to claim the swap
665
- * nonce: string Nonce for the swap (used for replay protection)
666
- * token: string Desired token to use
667
- * offerer: string Address of the caller
668
- * exactIn: boolean Whether the swap should be an exact in instead of exact out swap
669
- *
670
- *Sent later:
671
- * feeRate: string Fee rate to use for the init signature
672
- */
673
- const parsedBody: ToBtcRequestType = await req.paramReader.getParams({
674
- address: FieldTypeEnum.String,
675
- amount: FieldTypeEnum.BigInt,
676
- confirmationTarget: FieldTypeEnum.Number,
677
- confirmations: FieldTypeEnum.Number,
678
- nonce: FieldTypeEnum.BigInt,
679
- token: (val: string) => val!=null &&
680
- typeof(val)==="string" &&
681
- this.isTokenSupported(chainIdentifier, val) ? val : null,
682
- offerer: (val: string) => val!=null &&
683
- typeof(val)==="string" &&
684
- chainInterface.isValidAddress(val, true) ? val : null,
685
- exactIn: FieldTypeEnum.BooleanOptional
686
- });
687
- if (parsedBody==null) throw {
688
- code: 20100,
689
- msg: "Invalid request body"
690
- };
691
- metadata.request = parsedBody;
692
-
693
- const requestedAmount = {input: !!parsedBody.exactIn, amount: parsedBody.amount, token: parsedBody.token};
694
- const request = {
695
- chainIdentifier,
696
- raw: req,
697
- parsed: parsedBody,
698
- metadata
699
- };
700
- const useToken = parsedBody.token;
701
-
702
- const responseStream = res.responseStream;
703
-
704
- this.checkTooManyInflightSwaps();
705
- this.checkNonceValid(parsedBody.nonce);
706
- this.checkConfirmationTarget(parsedBody.confirmationTarget);
707
- this.checkRequiredConfirmations(parsedBody.confirmations);
708
- this.checkAddress(parsedBody.address);
709
- await this.checkVaultInitialized(chainIdentifier, parsedBody.token);
710
- const fees = await this.AmountAssertions.preCheckToBtcAmounts(this.type, request, requestedAmount);
711
-
712
- metadata.times.requestChecked = Date.now();
713
-
714
- //Initialize abort controller for the parallel async operations
715
- const abortController = getAbortController(responseStream);
716
-
717
- const {pricePrefetchPromise, signDataPrefetchPromise} = this.getToBtcPrefetches(chainIdentifier, useToken, responseStream, abortController);
718
- const nativeBalancePrefetch = this.prefetchNativeBalanceIfNeeded(chainIdentifier, abortController);
719
-
720
- await this.checkNativeBalance(chainIdentifier, nativeBalancePrefetch, abortController.signal);
721
-
722
- const {
723
- amountBD,
724
- networkFeeData,
725
- totalInToken,
726
- swapFee,
727
- swapFeeInToken,
728
- networkFeeInToken
729
- } = await this.AmountAssertions.checkToBtcAmount(this.type, request, {...requestedAmount, pricePrefetch: pricePrefetchPromise}, fees, async (amount: bigint) => {
730
- metadata.times.amountsChecked = Date.now();
731
- const resp = await this.checkAndGetNetworkFee(parsedBody.address, amount);
732
- this.logger.debug("checkToBtcAmount(): network fee calculated, amount: "+amount.toString(10)+" fee: "+resp.networkFee.toString(10));
733
- metadata.times.chainFeeCalculated = Date.now();
734
- return resp;
735
- }, abortController.signal);
736
- metadata.times.priceCalculated = Date.now();
737
-
738
- if(amountBD < 600n) throw {
739
- code: "20019",
740
- msg: "Swap output amount is below bitcoin dust (600 sats)!"
741
- };
742
-
743
- const claimHash = this.getHash(chainIdentifier, parsedBody.address, parsedBody.confirmations, parsedBody.nonce, amountBD).toString("hex");
744
-
745
- //Add grace period another time, so the user has 1 hour to commit
746
- const expirySeconds = this.getExpiryFromCLTV(parsedBody.confirmationTarget, parsedBody.confirmations) + this.config.gracePeriod;
747
- const currentTimestamp = BigInt(Math.floor(Date.now()/1000));
748
- const minRequiredExpiry = currentTimestamp + expirySeconds;
749
-
750
- const sequence = BigIntBufferUtils.fromBuffer(randomBytes(8));
751
- const payObject: SwapData = await swapContract.createSwapData(
752
- ChainSwapType.CHAIN_NONCED,
753
- parsedBody.offerer,
754
- signer.getAddress(),
755
- useToken,
756
- totalInToken,
757
- claimHash,
758
- sequence,
759
- minRequiredExpiry,
760
- true,
761
- false,
762
- 0n,
763
- 0n
764
- );
765
- abortController.signal.throwIfAborted();
766
- metadata.times.swapCreated = Date.now();
767
-
768
- const sigData = await this.getToBtcSignatureData(chainIdentifier, payObject, req, abortController.signal, signDataPrefetchPromise);
769
- metadata.times.swapSigned = Date.now();
770
-
771
- const createdSwap = new ToBtcSwapAbs(
772
- chainIdentifier,
773
- parsedBody.address,
774
- amountBD,
775
- swapFee,
776
- swapFeeInToken,
777
- networkFeeData.networkFee,
778
- networkFeeInToken,
779
- networkFeeData.satsPerVbyte,
780
- parsedBody.nonce,
781
- parsedBody.confirmations,
782
- parsedBody.confirmationTarget
783
- );
784
- createdSwap.data = payObject;
785
- createdSwap.metadata = metadata;
786
- createdSwap.prefix = sigData.prefix;
787
- createdSwap.timeout = sigData.timeout;
788
- createdSwap.signature = sigData.signature
789
- createdSwap.feeRate = sigData.feeRate;
790
-
791
- await PluginManager.swapCreate(createdSwap);
792
- await this.saveSwapData(createdSwap);
793
-
794
- this.swapLogger.info(createdSwap, "REST: /payInvoice: created swap address: "+createdSwap.address+" amount: "+amountBD.toString(10));
795
-
796
- await responseStream.writeParamsAndEnd({
797
- code: 20000,
798
- msg: "Success",
799
- data: {
800
- amount: amountBD.toString(10),
801
- address: signer.getAddress(),
802
- satsPervByte: networkFeeData.satsPerVbyte.toString(10),
803
- networkFee: networkFeeInToken.toString(10),
804
- swapFee: swapFeeInToken.toString(10),
805
- totalFee: (swapFeeInToken + networkFeeInToken).toString(10),
806
- total: totalInToken.toString(10),
807
- minRequiredExpiry: minRequiredExpiry.toString(10),
808
-
809
- data: payObject.serialize(),
810
-
811
- prefix: sigData.prefix,
812
- timeout: sigData.timeout,
813
- signature: sigData.signature
814
- }
815
- });
816
-
817
- }));
818
-
819
- const getRefundAuthorization = expressHandlerWrapper(async (req, res) => {
820
- /**
821
- * paymentHash: string Payment hash identifier of the swap
822
- * sequence: BN Sequence identifier of the swap
823
- */
824
- const parsedBody = verifySchema({...req.body, ...req.query}, {
825
- paymentHash: (val: string) => val!=null &&
826
- typeof(val)==="string" &&
827
- HEX_REGEX.test(val) ? val: null,
828
- sequence: FieldTypeEnum.BigInt
829
- });
830
- if (parsedBody==null) throw {
831
- code: 20100,
832
- msg: "Invalid request body/query (paymentHash/sequence)"
833
- };
834
-
835
- this.checkSequence(parsedBody.sequence);
836
-
837
- const payment = await this.storageManager.getData(parsedBody.paymentHash, parsedBody.sequence);
838
- if (payment == null || payment.state === ToBtcSwapState.SAVED) throw {
839
- _httpStatus: 200,
840
- code: 20007,
841
- msg: "Payment not found"
842
- };
843
-
844
- await this.checkExpired(payment);
845
-
846
- if (payment.state === ToBtcSwapState.COMMITED) {
847
- res.status(200).json({
848
- code: 20008,
849
- msg: "Payment processing"
850
- });
851
- return;
852
- }
853
-
854
- if (payment.state === ToBtcSwapState.BTC_SENT || payment.state===ToBtcSwapState.BTC_SENDING) {
855
- res.status(200).json({
856
- code: 20006,
857
- msg: "Already paid",
858
- data: {
859
- txId: payment.txId
860
- }
861
- });
862
- return;
863
- }
864
-
865
- const {swapContract, signer} = this.getChain(payment.chainIdentifier);
866
-
867
- if (payment.state === ToBtcSwapState.NON_PAYABLE) {
868
- const isCommited = await swapContract.isCommited(payment.data);
869
- if (!isCommited) throw {
870
- code: 20005,
871
- msg: "Not committed"
872
- };
873
-
874
- const refundResponse = await swapContract.getRefundSignature(signer, payment.data, this.config.refundAuthorizationTimeout);
875
-
876
- //Double check the state after promise result
877
- if (payment.state !== ToBtcSwapState.NON_PAYABLE) throw {
878
- code: 20005,
879
- msg: "Not committed"
880
- };
881
-
882
- this.swapLogger.info(payment, "REST: /getRefundAuthorization: returning refund authorization, because swap is in NON_PAYABLE state, address: "+payment.address);
883
-
884
- res.status(200).json({
885
- code: 20000,
886
- msg: "Success",
887
- data: {
888
- address: signer.getAddress(),
889
- prefix: refundResponse.prefix,
890
- timeout: refundResponse.timeout,
891
- signature: refundResponse.signature
892
- }
893
- });
894
- return;
895
- }
896
-
897
- throw {
898
- _httpStatus: 500,
899
- code: 20009,
900
- msg: "Invalid payment status"
901
- };
902
- });
903
-
904
- restServer.post(this.path+"/getRefundAuthorization", getRefundAuthorization);
905
- restServer.get(this.path+"/getRefundAuthorization", getRefundAuthorization);
906
-
907
- this.logger.info("started at path: ", this.path);
908
- }
909
-
910
- /**
911
- * Starts watchdog checking sent bitcoin transactions
912
- */
913
- protected async startTxTimer() {
914
- let rerun;
915
- rerun = async () => {
916
- await this.processBtcTxs().catch( e => this.logger.error("startTxTimer(): call to processBtcTxs() errored", e));
917
- setTimeout(rerun, this.config.txCheckInterval);
918
- };
919
- await rerun();
920
- }
921
-
922
- async startWatchdog() {
923
- await super.startWatchdog();
924
- await this.startTxTimer();
925
- }
926
-
927
- async init() {
928
- await this.loadData(ToBtcSwapAbs);
929
- this.subscribeToEvents();
930
- await PluginManager.serviceInitialize(this);
931
- }
932
-
933
- getInfoData(): any {
934
- return {
935
- minCltv: Number(this.config.minChainCltv),
936
-
937
- minConfirmations: this.config.minConfirmations,
938
- maxConfirmations: this.config.maxConfirmations,
939
-
940
- minConfTarget: this.config.minConfTarget,
941
- maxConfTarget: this.config.maxConfTarget,
942
-
943
- maxOutputScriptLen: OUTPUT_SCRIPT_MAX_LENGTH
944
- };
945
- }
946
-
947
- }
1
+ import {Express, Request, Response} from "express";
2
+ import {ToBtcSwapAbs, ToBtcSwapState} from "./ToBtcSwapAbs";
3
+ import {MultichainData, SwapHandlerType} from "../../SwapHandler";
4
+ import {ISwapPrice} from "../../../prices/ISwapPrice";
5
+ import {
6
+ BtcTx,
7
+ ChainSwapType,
8
+ ClaimEvent,
9
+ InitializeEvent,
10
+ RefundEvent,
11
+ SwapData,
12
+ BitcoinRpc,
13
+ BtcBlock, BigIntBufferUtils, SwapCommitStateType
14
+ } from "@atomiqlabs/base";
15
+ import {expressHandlerWrapper, getAbortController, HEX_REGEX, isDefinedRuntimeError} from "../../../utils/Utils";
16
+ import {PluginManager} from "../../../plugins/PluginManager";
17
+ import {IIntermediaryStorage} from "../../../storage/IIntermediaryStorage";
18
+ import {randomBytes} from "crypto";
19
+ import {FieldTypeEnum, verifySchema} from "../../../utils/paramcoders/SchemaVerifier";
20
+ import {serverParamDecoder} from "../../../utils/paramcoders/server/ServerParamDecoder";
21
+ import {IParamReader} from "../../../utils/paramcoders/IParamReader";
22
+ import {ServerParamEncoder} from "../../../utils/paramcoders/server/ServerParamEncoder";
23
+ import {ToBtcBaseConfig, ToBtcBaseSwapHandler} from "../ToBtcBaseSwapHandler";
24
+ import {IBitcoinWallet} from "../../../wallets/IBitcoinWallet";
25
+ import {checkTransactionReplaced} from "../../../utils/BitcoinUtils";
26
+ import {isQuoteThrow} from "../../../plugins/IPlugin";
27
+ import {FromBtcLnAutoSwapState} from "../frombtcln_autoinit/FromBtcLnAutoSwap";
28
+
29
+ const OUTPUT_SCRIPT_MAX_LENGTH = 200;
30
+
31
+ export type ToBtcConfig = ToBtcBaseConfig & {
32
+ sendSafetyFactor: bigint,
33
+
34
+ minChainCltv: bigint,
35
+
36
+ networkFeeMultiplier: number,
37
+ minConfirmations: number,
38
+ maxConfirmations: number,
39
+ maxConfTarget: number,
40
+ minConfTarget: number,
41
+
42
+ txCheckInterval: number
43
+ };
44
+
45
+ export type ToBtcRequestType = {
46
+ address: string,
47
+ amount: bigint,
48
+ confirmationTarget: number,
49
+ confirmations: number,
50
+ nonce: bigint,
51
+ token: string,
52
+ offerer: string,
53
+ exactIn?: boolean
54
+ };
55
+
56
+ const MAX_PARALLEL_TX_PROCESSED = 10;
57
+
58
+ /**
59
+ * Handler for to BTC swaps, utilizing PTLCs (proof-time locked contracts) using btc relay (on-chain bitcoin SPV)
60
+ */
61
+ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState> {
62
+ readonly type = SwapHandlerType.TO_BTC;
63
+ readonly swapType = ChainSwapType.CHAIN_NONCED;
64
+ readonly inflightSwapStates = new Set([ToBtcSwapState.BTC_SENDING, ToBtcSwapState.BTC_SENT]);
65
+
66
+ activeSubscriptions: {[txId: string]: ToBtcSwapAbs} = {};
67
+ bitcoinRpc: BitcoinRpc<BtcBlock>;
68
+ bitcoin: IBitcoinWallet;
69
+
70
+ readonly config: ToBtcConfig;
71
+
72
+ constructor(
73
+ storageDirectory: IIntermediaryStorage<ToBtcSwapAbs>,
74
+ path: string,
75
+ chainData: MultichainData,
76
+ bitcoin: IBitcoinWallet,
77
+ swapPricing: ISwapPrice,
78
+ bitcoinRpc: BitcoinRpc<BtcBlock>,
79
+ config: ToBtcConfig
80
+ ) {
81
+ super(storageDirectory, path, chainData, swapPricing, config);
82
+ this.bitcoinRpc = bitcoinRpc;
83
+ this.bitcoin = bitcoin;
84
+ this.config = config;
85
+ }
86
+
87
+ /**
88
+ * Returns the payment hash of the swap, takes swap nonce into account. Payment hash is chain-specific.
89
+ *
90
+ * @param chainIdentifier
91
+ * @param address
92
+ * @param confirmations
93
+ * @param nonce
94
+ * @param amount
95
+ */
96
+ private getHash(chainIdentifier: string, address: string, confirmations: number, nonce: bigint, amount: bigint): Buffer {
97
+ const parsedOutputScript = this.bitcoin.toOutputScript(address);
98
+ const {swapContract} = this.getChain(chainIdentifier);
99
+ return swapContract.getHashForOnchain(parsedOutputScript, amount, confirmations, nonce);
100
+ }
101
+
102
+ /**
103
+ * Tries to claim the swap after our transaction was confirmed
104
+ *
105
+ * @param tx
106
+ * @param swap
107
+ * @param vout
108
+ */
109
+ private async tryClaimSwap(tx: {blockhash: string, confirmations: number, txid: string, hex: string}, swap: ToBtcSwapAbs, vout: number): Promise<boolean> {
110
+ const {chainInterface, swapContract, signer} = this.getChain(swap.chainIdentifier);
111
+
112
+ const blockHeader = await this.bitcoinRpc.getBlockHeader(tx.blockhash);
113
+
114
+ //Set flag that we are sending the transaction already, so we don't end up with race condition
115
+ if(swap.isLocked()) return false;
116
+
117
+ let txns: any[];
118
+ try {
119
+ txns = await swapContract.txsClaimWithTxData(
120
+ signer,
121
+ swap.data,
122
+ {...tx, height: blockHeader.getHeight()},
123
+ swap.requiredConfirmations,
124
+ vout,
125
+ null,
126
+ null,
127
+ false
128
+ );
129
+ } catch (e) {
130
+ this.swapLogger.error(swap, "tryClaimSwap(): error occurred creating swap claim transactions, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout+" address: "+swap.address, e);
131
+ return false
132
+ }
133
+
134
+ const unlock: () => boolean = swap.lock(swapContract.claimWithTxDataTimeout);
135
+ if(unlock==null) return false;
136
+
137
+ try {
138
+ this.swapLogger.debug(swap, "tryClaimSwap(): initiate claim of swap, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout);
139
+ await chainInterface.sendAndConfirm(signer, txns, true);
140
+ this.swapLogger.info(swap, "tryClaimSwap(): swap claimed successfully, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout+" address: "+swap.address);
141
+ if(swap.metadata!=null) swap.metadata.times.txClaimed = Date.now();
142
+ unlock();
143
+ return true;
144
+ } catch (e) {
145
+ this.swapLogger.error(swap, "tryClaimSwap(): error occurred claiming swap, height: "+blockHeader.getHeight()+" utxo: "+tx.txid+":"+vout+" address: "+swap.address, e);
146
+ return false
147
+ }
148
+ }
149
+
150
+ protected async processPastSwap(swap: ToBtcSwapAbs) {
151
+ const {swapContract, signer} = this.getChain(swap.chainIdentifier);
152
+
153
+ if(swap.state===ToBtcSwapState.SAVED) {
154
+ const isSignatureExpired = await swapContract.isInitAuthorizationExpired(swap.data, swap);
155
+ if(isSignatureExpired) {
156
+ const isCommitted = await swapContract.isCommited(swap.data);
157
+ if(!isCommitted) {
158
+ this.swapLogger.info(swap, "processPastSwap(state=SAVED): authorization expired & swap not committed, cancelling swap, address: "+swap.address);
159
+ await this.removeSwapData(swap, ToBtcSwapState.CANCELED);
160
+ } else {
161
+ this.swapLogger.info(swap, "processPastSwap(state=SAVED): swap committed (detected from processPastSwap), address: "+swap.address);
162
+ await swap.setState(ToBtcSwapState.COMMITED);
163
+ await this.saveSwapData(swap);
164
+ }
165
+ return;
166
+ }
167
+ }
168
+
169
+ if(swap.state===ToBtcSwapState.NON_PAYABLE || swap.state===ToBtcSwapState.SAVED) {
170
+ if(await swapContract.isExpired(signer.getAddress(), swap.data)) {
171
+ this.swapLogger.info(swap, "processPastSwap(state=NON_PAYABLE|SAVED): swap expired, cancelling, address: "+swap.address);
172
+ await this.removeSwapData(swap, ToBtcSwapState.CANCELED);
173
+ return;
174
+ }
175
+ }
176
+
177
+ //Sanity check for sent swaps
178
+ if(swap.state===ToBtcSwapState.BTC_SENT) {
179
+ const isCommited = await swapContract.isCommited(swap.data);
180
+ if(!isCommited) {
181
+ const status = await swapContract.getCommitStatus(signer.getAddress(), swap.data);
182
+ if(status.type===SwapCommitStateType.PAID) {
183
+ this.swapLogger.info(swap, "processPastSwap(state=BTC_SENT): swap claimed (detected from processPastSwap), address: "+swap.address);
184
+ this.unsubscribePayment(swap);
185
+ swap.txIds ??= {};
186
+ swap.txIds.claim = await status.getClaimTxId();
187
+ await this.removeSwapData(swap, ToBtcSwapState.CLAIMED);
188
+ } else if(status.type===SwapCommitStateType.EXPIRED) {
189
+ this.swapLogger.warn(swap, "processPastSwap(state=BTC_SENT): swap expired, but bitcoin was probably already sent, txId: "+swap.txId+" address: "+swap.address);
190
+ this.unsubscribePayment(swap);
191
+ swap.txIds ??= {};
192
+ swap.txIds.refund = status.getRefundTxId==null ? null : await status.getRefundTxId();
193
+ await this.removeSwapData(swap, ToBtcSwapState.REFUNDED);
194
+ }
195
+ return;
196
+ }
197
+ }
198
+
199
+ if(swap.state===ToBtcSwapState.COMMITED || swap.state===ToBtcSwapState.BTC_SENDING || swap.state===ToBtcSwapState.BTC_SENT) {
200
+ await this.processInitialized(swap);
201
+ return;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Checks past swaps, deletes ones that are already expired.
207
+ */
208
+ protected async processPastSwaps() {
209
+ const queriedData = await this.storageManager.query([
210
+ {
211
+ key: "state",
212
+ values: [
213
+ ToBtcSwapState.SAVED,
214
+ ToBtcSwapState.NON_PAYABLE,
215
+ ToBtcSwapState.COMMITED,
216
+ ToBtcSwapState.BTC_SENDING,
217
+ ToBtcSwapState.BTC_SENT,
218
+ ]
219
+ }
220
+ ]);
221
+
222
+ for(let {obj: swap} of queriedData) {
223
+ await this.processPastSwap(swap);
224
+ }
225
+ }
226
+
227
+ protected async processBtcTx(swap: ToBtcSwapAbs, tx: BtcTx): Promise<boolean> {
228
+ tx.confirmations = tx.confirmations ?? 0;
229
+
230
+ //Check transaction has enough confirmations
231
+ const hasEnoughConfirmations = tx.confirmations>=swap.requiredConfirmations;
232
+ if(!hasEnoughConfirmations || tx.blockhash==null) {
233
+ return false;
234
+ }
235
+ const _tx = tx as BtcTx & {blockhash: string, confirmations: number};
236
+
237
+ this.swapLogger.debug(swap, "processBtcTx(): address: "+swap.address+" amount: "+swap.amount.toString(10)+" btcTx: "+tx);
238
+
239
+ //Search for required transaction output (vout)
240
+ const outputScript = this.bitcoin.toOutputScript(swap.address);
241
+ const vout = tx.outs.find(e => BigInt(e.value)===swap.amount && Buffer.from(e.scriptPubKey.hex, "hex").equals(outputScript));
242
+ if(vout==null) {
243
+ this.swapLogger.warn(swap, "processBtcTx(): cannot find correct vout,"+
244
+ " required output script: "+outputScript.toString("hex")+
245
+ " required amount: "+swap.amount.toString(10)+
246
+ " vouts: ", tx.outs);
247
+ return false;
248
+ }
249
+
250
+ if(swap.metadata!=null) swap.metadata.times.payTxConfirmed = Date.now();
251
+
252
+ const success = await this.tryClaimSwap(_tx, swap, vout.n);
253
+
254
+ return success;
255
+ }
256
+
257
+ /**
258
+ * Checks active sent out bitcoin transactions
259
+ */
260
+ private async processBtcTxs() {
261
+ const unsubscribeSwaps: ToBtcSwapAbs[] = [];
262
+
263
+ let promises: Promise<void>[] = [];
264
+ for(let txId in this.activeSubscriptions) {
265
+ const swap: ToBtcSwapAbs = this.activeSubscriptions[txId];
266
+ //TODO: RBF the transaction if it's already taking too long to confirm
267
+ promises.push((async () => {
268
+ try {
269
+ let tx: BtcTx = await this.bitcoin.getWalletTransaction(txId);
270
+ if(tx==null) return;
271
+
272
+ if(await this.processBtcTx(swap, tx)) {
273
+ this.swapLogger.info(swap, "processBtcTxs(): swap claimed successfully, txId: "+tx.txid+" address: "+swap.address);
274
+ unsubscribeSwaps.push(swap);
275
+ }
276
+ } catch (e) {
277
+ this.swapLogger.error(swap, "processBtcTxs(): error processing btc transaction", e);
278
+ }
279
+ })());
280
+ if(promises.length >= MAX_PARALLEL_TX_PROCESSED) {
281
+ await Promise.all(promises);
282
+ promises = [];
283
+ }
284
+ }
285
+ await Promise.all(promises);
286
+
287
+ unsubscribeSwaps.forEach(swap => {
288
+ this.unsubscribePayment(swap);
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Subscribes to and periodically checks txId used to send out funds for the swap for enough confirmations
294
+ *
295
+ * @param payment
296
+ */
297
+ protected subscribeToPayment(payment: ToBtcSwapAbs) {
298
+ this.swapLogger.info(payment, "subscribeToPayment(): subscribing to swap, txId: "+payment.txId+" address: "+payment.address);
299
+ this.activeSubscriptions[payment.txId] = payment;
300
+ }
301
+
302
+ protected unsubscribePayment(payment: ToBtcSwapAbs) {
303
+ if(payment.txId!=null) {
304
+ if(this.activeSubscriptions[payment.txId]!=null) {
305
+ this.swapLogger.info(payment, "unsubscribePayment(): unsubscribing swap, txId: "+payment.txId+" address: "+payment.address);
306
+ delete this.activeSubscriptions[payment.txId];
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Checks if expiry time on the swap leaves us enough room to send a transaction and for the transaction to confirm
313
+ *
314
+ * @param swap
315
+ * @private
316
+ * @throws DefinedRuntimeError will throw an error in case there isn't enough time for us to send a BTC payout tx
317
+ */
318
+ protected checkExpiresTooSoon(swap: ToBtcSwapAbs): void {
319
+ const currentTimestamp = BigInt(Math.floor(Date.now()/1000));
320
+ const tsDelta = swap.data.getExpiry() - currentTimestamp;
321
+ const minRequiredCLTV = this.getExpiryFromCLTV(swap.preferedConfirmationTarget, swap.requiredConfirmations);
322
+ const hasRequiredCLTVDelta = tsDelta >= minRequiredCLTV;
323
+ if(!hasRequiredCLTVDelta) throw {
324
+ code: 90001,
325
+ msg: "TS delta too low",
326
+ data: {
327
+ required: minRequiredCLTV.toString(10),
328
+ actual: tsDelta.toString(10)
329
+ }
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Checks if the actual fee for the swap is no higher than the quoted estimate
335
+ *
336
+ * @param quotedSatsPerVbyte
337
+ * @param actualSatsPerVbyte
338
+ * @private
339
+ * @throws DefinedRuntimeError will throw an error in case the actual fee is higher than quoted fee
340
+ */
341
+ protected checkCalculatedTxFee(quotedSatsPerVbyte: bigint, actualSatsPerVbyte: bigint): void {
342
+ const swapPaysEnoughNetworkFee = quotedSatsPerVbyte >= actualSatsPerVbyte;
343
+ if(!swapPaysEnoughNetworkFee) throw {
344
+ code: 90003,
345
+ msg: "Fee changed too much!",
346
+ data: {
347
+ quotedFee: quotedSatsPerVbyte.toString(10),
348
+ actualFee: actualSatsPerVbyte.toString(10)
349
+ }
350
+ };
351
+ }
352
+
353
+ /**
354
+ * Sends a bitcoin transaction to payout BTC for a swap
355
+ *
356
+ * @param swap
357
+ * @private
358
+ * @throws DefinedRuntimeError will throw an error in case the payment cannot be initiated
359
+ */
360
+ private sendBitcoinPayment(swap: ToBtcSwapAbs) {
361
+ //Make sure that bitcoin payouts are processed sequentially to avoid race conditions between multiple payouts,
362
+ // e.g. that 2 payouts share the same input and would effectively double-spend each other
363
+ return this.bitcoin.execute(async () => {
364
+ //Run checks
365
+ this.checkTooManyInflightSwaps();
366
+ this.checkExpiresTooSoon(swap);
367
+ if(swap.metadata!=null) swap.metadata.times.payCLTVChecked = Date.now();
368
+
369
+ const satsPerVbyte = await this.bitcoin.getFeeRate();
370
+ this.checkCalculatedTxFee(swap.satsPerVbyte, BigInt(satsPerVbyte));
371
+ if(swap.metadata!=null) swap.metadata.times.payChainFee = Date.now();
372
+
373
+ const signResult = await this.bitcoin.getSignedTransaction(
374
+ swap.address,
375
+ Number(swap.amount),
376
+ satsPerVbyte,
377
+ swap.nonce,
378
+ Number(swap.satsPerVbyte)
379
+ );
380
+ if(signResult==null) throw {
381
+ code: 90002,
382
+ msg: "Failed to create signed transaction (not enough funds?)"
383
+ }
384
+ if(swap.metadata!=null) swap.metadata.times.paySignPSBT = Date.now();
385
+
386
+ try {
387
+ this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: "+signResult.raw);
388
+ swap.txId = signResult.tx.id;
389
+ swap.btcRawTx = signResult.raw;
390
+ swap.setRealNetworkFee(BigInt(signResult.networkFee));
391
+ swap.sending = true;
392
+ await swap.setState(ToBtcSwapState.BTC_SENDING);
393
+ await this.saveSwapData(swap);
394
+
395
+ await this.bitcoin.sendRawTransaction(signResult.raw);
396
+ swap.sending = false;
397
+ } catch (e) {
398
+ swap.sending = false;
399
+ throw e;
400
+ }
401
+
402
+ if(swap.metadata!=null) swap.metadata.times.payTxSent = Date.now();
403
+ this.swapLogger.info(swap, "sendBitcoinPayment(): btc transaction generated, signed & broadcasted, txId: "+swap.txId+" address: "+swap.address);
404
+
405
+ await swap.setState(ToBtcSwapState.BTC_SENT);
406
+ await this.saveSwapData(swap);
407
+ });
408
+ }
409
+
410
+ /**
411
+ * Called after swap was successfully committed, will check if bitcoin tx is already sent, if not tries to send it and subscribes to it
412
+ *
413
+ * @param swap
414
+ */
415
+ private async processInitialized(swap: ToBtcSwapAbs) {
416
+ if(swap.state===ToBtcSwapState.BTC_SENDING) {
417
+ if(swap.sending) return;
418
+ //Bitcoin transaction was signed (maybe also sent)
419
+ const tx = await checkTransactionReplaced(swap.txId, swap.btcRawTx, this.bitcoinRpc);
420
+
421
+ const isTxSent = tx!=null;
422
+ if(!isTxSent) {
423
+ //Reset the state to COMMITED
424
+ this.swapLogger.info(swap, "processInitialized(state=BTC_SENDING): btc transaction not found, resetting to COMMITED state, txId: "+swap.txId+" address: "+swap.address);
425
+ await swap.setState(ToBtcSwapState.COMMITED);
426
+ } else {
427
+ this.swapLogger.info(swap, "processInitialized(state=BTC_SENDING): btc transaction found, advancing to BTC_SENT state, txId: "+swap.txId+" address: "+swap.address);
428
+ await swap.setState(ToBtcSwapState.BTC_SENT);
429
+ await this.saveSwapData(swap);
430
+ }
431
+ }
432
+
433
+ if(swap.state===ToBtcSwapState.SAVED) {
434
+ this.swapLogger.info(swap, "processInitialized(state=SAVED): advancing to COMMITED state, address: "+swap.address);
435
+ await swap.setState(ToBtcSwapState.COMMITED);
436
+ await this.saveSwapData(swap);
437
+ }
438
+
439
+ if(swap.state===ToBtcSwapState.COMMITED) {
440
+ const unlock: () => boolean = swap.lock(Infinity);
441
+ if(unlock==null) return;
442
+
443
+ const pluginCheckResult = await PluginManager.onHandlePreToBtcExecute(
444
+ SwapHandlerType.TO_BTC,
445
+ swap
446
+ );
447
+ if(isQuoteThrow(pluginCheckResult)) {
448
+ const error = {
449
+ code: 29999,
450
+ msg: pluginCheckResult.message
451
+ };
452
+ this.swapLogger.error(swap, "processInitialized(state=COMMITED): setting state to NON_PAYABLE due to send bitcoin payment error", error);
453
+ if(swap.metadata!=null) swap.metadata.payError = error;
454
+ unlock();
455
+ if(swap.state===ToBtcSwapState.COMMITED) {
456
+ await swap.setState(ToBtcSwapState.NON_PAYABLE);
457
+ await this.saveSwapData(swap);
458
+ }
459
+ return;
460
+ }
461
+
462
+ if(swap.state!==ToBtcSwapState.COMMITED) {
463
+ unlock();
464
+ return;
465
+ }
466
+
467
+ this.swapLogger.debug(swap, "processInitialized(state=COMMITED): sending bitcoin transaction, address: "+swap.address);
468
+
469
+ try {
470
+ await this.sendBitcoinPayment(swap);
471
+ this.swapLogger.info(swap, "processInitialized(state=COMMITED): btc transaction sent, address: "+swap.address);
472
+ } catch (e) {
473
+ if(isDefinedRuntimeError(e)) {
474
+ this.swapLogger.error(swap, "processInitialized(state=COMMITED): setting state to NON_PAYABLE due to send bitcoin payment error", e);
475
+ if(swap.metadata!=null) swap.metadata.payError = e;
476
+ await swap.setState(ToBtcSwapState.NON_PAYABLE);
477
+ await this.saveSwapData(swap);
478
+ } else {
479
+ this.swapLogger.error(swap, "processInitialized(state=COMMITED): send bitcoin payment error", e);
480
+ throw e;
481
+ }
482
+ } finally {
483
+ unlock();
484
+ }
485
+ }
486
+
487
+ if(swap.state===ToBtcSwapState.NON_PAYABLE) return;
488
+
489
+ this.subscribeToPayment(swap);
490
+ }
491
+
492
+ protected async processInitializeEvent(chainIdentifier: string, swap: ToBtcSwapAbs, event: InitializeEvent<SwapData>): Promise<void> {
493
+ this.swapLogger.info(swap, "SC: InitializeEvent: swap initialized by the client, address: "+swap.address);
494
+
495
+ await this.processInitialized(swap);
496
+ }
497
+
498
+ protected async processClaimEvent(chainIdentifier: string, swap: ToBtcSwapAbs, event: ClaimEvent<SwapData>): Promise<void> {
499
+ this.swapLogger.info(swap, "SC: ClaimEvent: swap successfully claimed to us, address: "+swap.address);
500
+
501
+ //Also remove transaction from active subscriptions
502
+ this.unsubscribePayment(swap);
503
+ await this.removeSwapData(swap, ToBtcSwapState.CLAIMED);
504
+ }
505
+
506
+ protected async processRefundEvent(chainIdentifier: string, swap: ToBtcSwapAbs, event: RefundEvent<SwapData>): Promise<void> {
507
+ this.swapLogger.info(swap, "SC: RefundEvent: swap successfully refunded by the user, address: "+swap.address);
508
+
509
+ //Also remove transaction from active subscriptions
510
+ this.unsubscribePayment(swap);
511
+ await this.removeSwapData(swap, ToBtcSwapState.REFUNDED);
512
+ }
513
+
514
+ /**
515
+ * Returns required expiry delta for swap params
516
+ *
517
+ * @param confirmationTarget
518
+ * @param confirmations
519
+ */
520
+ protected getExpiryFromCLTV(confirmationTarget: number, confirmations: number): bigint {
521
+ //Blocks = 10 + (confirmations + confirmationTarget)*2
522
+ //Time = 3600 + (600*blocks*2)
523
+ const cltv = this.config.minChainCltv + (
524
+ BigInt(confirmations + confirmationTarget) * this.config.sendSafetyFactor
525
+ );
526
+
527
+ return this.config.gracePeriod + (this.config.bitcoinBlocktime * cltv * this.config.safetyFactor);
528
+ }
529
+
530
+ /**
531
+ * Checks if the requested nonce is valid
532
+ *
533
+ * @param nonce
534
+ * @throws {DefinedRuntimeError} will throw an error if the nonce is invalid
535
+ */
536
+ private checkNonceValid(nonce: bigint): void {
537
+ if(nonce < 0 || nonce >= (2n ** 64n)) throw {
538
+ code: 20021,
539
+ msg: "Invalid request body (nonce - cannot be parsed)"
540
+ };
541
+
542
+ const firstPart = nonce >> 24n;
543
+
544
+ const maxAllowedValue = BigInt(Math.floor(Date.now()/1000)-600000000);
545
+ if(firstPart > maxAllowedValue) throw {
546
+ code: 20022,
547
+ msg: "Invalid request body (nonce - too high)"
548
+ };
549
+ }
550
+
551
+ /**
552
+ * Checks if confirmation target is within configured bounds
553
+ *
554
+ * @param confirmationTarget
555
+ * @throws {DefinedRuntimeError} will throw an error if the confirmationTarget is out of bounds
556
+ */
557
+ protected checkConfirmationTarget(confirmationTarget: number): void {
558
+ if(confirmationTarget>this.config.maxConfTarget) throw {
559
+ code: 20023,
560
+ msg: "Invalid request body (confirmationTarget - too high)"
561
+ };
562
+ if(confirmationTarget<this.config.minConfTarget) throw {
563
+ code: 20024,
564
+ msg: "Invalid request body (confirmationTarget - too low)"
565
+ };
566
+ }
567
+
568
+ /**
569
+ * Checks if the required confirmations are within configured bounds
570
+ *
571
+ * @param confirmations
572
+ * @throws {DefinedRuntimeError} will throw an error if the confirmations are out of bounds
573
+ */
574
+ protected checkRequiredConfirmations(confirmations: number): void {
575
+ if(confirmations>this.config.maxConfirmations) throw {
576
+ code: 20025,
577
+ msg: "Invalid request body (confirmations - too high)"
578
+ };
579
+ if(confirmations<this.config.minConfirmations) throw {
580
+ code: 20026,
581
+ msg: "Invalid request body (confirmations - too low)"
582
+ };
583
+ }
584
+
585
+ /**
586
+ * Checks the validity of the provided address, also checks if the resulting output script isn't too large
587
+ *
588
+ * @param address
589
+ * @throws {DefinedRuntimeError} will throw an error if the address is invalid
590
+ */
591
+ protected checkAddress(address: string): void {
592
+ let parsedOutputScript: Buffer;
593
+
594
+ try {
595
+ parsedOutputScript = this.bitcoin.toOutputScript(address);
596
+ } catch (e) {
597
+ throw {
598
+ code: 20031,
599
+ msg: "Invalid request body (address - cannot be parsed)"
600
+ };
601
+ }
602
+
603
+ if(parsedOutputScript.length > OUTPUT_SCRIPT_MAX_LENGTH) throw {
604
+ code: 20032,
605
+ msg: "Invalid request body (address's output script - too long)"
606
+ };
607
+ }
608
+
609
+ /**
610
+ * Checks if the swap is expired, taking into consideration on-chain time skew
611
+ *
612
+ * @param swap
613
+ * @throws {DefinedRuntimeError} will throw an error if the swap is expired
614
+ */
615
+ protected async checkExpired(swap: ToBtcSwapAbs) {
616
+ const {swapContract, signer} = this.getChain(swap.chainIdentifier);
617
+ const isExpired = await swapContract.isExpired(signer.getAddress(), swap.data);
618
+ if(isExpired) throw {
619
+ _httpStatus: 200,
620
+ code: 20010,
621
+ msg: "Payment expired"
622
+ };
623
+ }
624
+
625
+ /**
626
+ * Checks & returns the network fee needed for a transaction
627
+ *
628
+ * @param address
629
+ * @param amount
630
+ * @throws {DefinedRuntimeError} will throw an error if there are not enough BTC funds
631
+ */
632
+ private async checkAndGetNetworkFee(address: string, amount: bigint): Promise<{ networkFee: bigint, satsPerVbyte: bigint }> {
633
+ let chainFeeResp = await this.bitcoin.estimateFee(address, Number(amount), null, this.config.networkFeeMultiplier);
634
+
635
+ const hasEnoughFunds = chainFeeResp!=null;
636
+ if(!hasEnoughFunds) throw {
637
+ code: 20002,
638
+ msg: "Not enough liquidity"
639
+ };
640
+
641
+ return {
642
+ networkFee: BigInt(chainFeeResp.networkFee),
643
+ satsPerVbyte: BigInt(chainFeeResp.satsPerVbyte)
644
+ };
645
+ }
646
+
647
+ startRestServer(restServer: Express) {
648
+ restServer.use(this.path+"/payInvoice", serverParamDecoder(10*1000));
649
+ restServer.post(this.path+"/payInvoice", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
650
+ const metadata: {
651
+ request: any,
652
+ times: {[key: string]: number}
653
+ } = {request: {}, times: {}};
654
+
655
+ const chainIdentifier = req.query.chain as string;
656
+ const {swapContract, signer, chainInterface} = this.getChain(chainIdentifier);
657
+
658
+ metadata.times.requestReceived = Date.now();
659
+ /**
660
+ *Sent initially:
661
+ * address: string Bitcoin destination address
662
+ * amount: string Amount to send (in satoshis)
663
+ * confirmationTarget: number Desired confirmation target for the swap, how big of a fee should be assigned to TX
664
+ * confirmations: number Required number of confirmations for us to claim the swap
665
+ * nonce: string Nonce for the swap (used for replay protection)
666
+ * token: string Desired token to use
667
+ * offerer: string Address of the caller
668
+ * exactIn: boolean Whether the swap should be an exact in instead of exact out swap
669
+ *
670
+ *Sent later:
671
+ * feeRate: string Fee rate to use for the init signature
672
+ */
673
+ const parsedBody: ToBtcRequestType = await req.paramReader.getParams({
674
+ address: FieldTypeEnum.String,
675
+ amount: FieldTypeEnum.BigInt,
676
+ confirmationTarget: FieldTypeEnum.Number,
677
+ confirmations: FieldTypeEnum.Number,
678
+ nonce: FieldTypeEnum.BigInt,
679
+ token: (val: string) => val!=null &&
680
+ typeof(val)==="string" &&
681
+ this.isTokenSupported(chainIdentifier, val) ? val : null,
682
+ offerer: (val: string) => val!=null &&
683
+ typeof(val)==="string" &&
684
+ chainInterface.isValidAddress(val, true) ? val : null,
685
+ exactIn: FieldTypeEnum.BooleanOptional
686
+ });
687
+ if (parsedBody==null) throw {
688
+ code: 20100,
689
+ msg: "Invalid request body"
690
+ };
691
+ metadata.request = parsedBody;
692
+
693
+ const requestedAmount = {input: !!parsedBody.exactIn, amount: parsedBody.amount, token: parsedBody.token};
694
+ const request = {
695
+ chainIdentifier,
696
+ raw: req,
697
+ parsed: parsedBody,
698
+ metadata
699
+ };
700
+ const useToken = parsedBody.token;
701
+
702
+ const responseStream = res.responseStream;
703
+
704
+ this.checkTooManyInflightSwaps();
705
+ this.checkNonceValid(parsedBody.nonce);
706
+ this.checkConfirmationTarget(parsedBody.confirmationTarget);
707
+ this.checkRequiredConfirmations(parsedBody.confirmations);
708
+ this.checkAddress(parsedBody.address);
709
+ await this.checkVaultInitialized(chainIdentifier, parsedBody.token);
710
+ const fees = await this.AmountAssertions.preCheckToBtcAmounts(this.type, request, requestedAmount);
711
+
712
+ metadata.times.requestChecked = Date.now();
713
+
714
+ //Initialize abort controller for the parallel async operations
715
+ const abortController = getAbortController(responseStream);
716
+
717
+ const {pricePrefetchPromise, signDataPrefetchPromise} = this.getToBtcPrefetches(chainIdentifier, useToken, responseStream, abortController);
718
+ const nativeBalancePrefetch = this.prefetchNativeBalanceIfNeeded(chainIdentifier, abortController);
719
+
720
+ await this.checkNativeBalance(chainIdentifier, nativeBalancePrefetch, abortController.signal);
721
+
722
+ const {
723
+ amountBD,
724
+ networkFeeData,
725
+ totalInToken,
726
+ swapFee,
727
+ swapFeeInToken,
728
+ networkFeeInToken
729
+ } = await this.AmountAssertions.checkToBtcAmount(this.type, request, {...requestedAmount, pricePrefetch: pricePrefetchPromise}, fees, async (amount: bigint) => {
730
+ metadata.times.amountsChecked = Date.now();
731
+ const resp = await this.checkAndGetNetworkFee(parsedBody.address, amount);
732
+ this.logger.debug("checkToBtcAmount(): network fee calculated, amount: "+amount.toString(10)+" fee: "+resp.networkFee.toString(10));
733
+ metadata.times.chainFeeCalculated = Date.now();
734
+ return resp;
735
+ }, abortController.signal);
736
+ metadata.times.priceCalculated = Date.now();
737
+
738
+ if(amountBD < 600n) throw {
739
+ code: "20019",
740
+ msg: "Swap output amount is below bitcoin dust (600 sats)!"
741
+ };
742
+
743
+ const claimHash = this.getHash(chainIdentifier, parsedBody.address, parsedBody.confirmations, parsedBody.nonce, amountBD).toString("hex");
744
+
745
+ //Add grace period another time, so the user has 1 hour to commit
746
+ const expirySeconds = this.getExpiryFromCLTV(parsedBody.confirmationTarget, parsedBody.confirmations) + this.config.gracePeriod;
747
+ const currentTimestamp = BigInt(Math.floor(Date.now()/1000));
748
+ const minRequiredExpiry = currentTimestamp + expirySeconds;
749
+
750
+ const sequence = BigIntBufferUtils.fromBuffer(randomBytes(8));
751
+ const payObject: SwapData = await swapContract.createSwapData(
752
+ ChainSwapType.CHAIN_NONCED,
753
+ parsedBody.offerer,
754
+ signer.getAddress(),
755
+ useToken,
756
+ totalInToken,
757
+ claimHash,
758
+ sequence,
759
+ minRequiredExpiry,
760
+ true,
761
+ false,
762
+ 0n,
763
+ 0n
764
+ );
765
+ abortController.signal.throwIfAborted();
766
+ metadata.times.swapCreated = Date.now();
767
+
768
+ const sigData = await this.getToBtcSignatureData(chainIdentifier, payObject, req, abortController.signal, signDataPrefetchPromise);
769
+ metadata.times.swapSigned = Date.now();
770
+
771
+ const createdSwap = new ToBtcSwapAbs(
772
+ chainIdentifier,
773
+ parsedBody.address,
774
+ amountBD,
775
+ swapFee,
776
+ swapFeeInToken,
777
+ networkFeeData.networkFee,
778
+ networkFeeInToken,
779
+ networkFeeData.satsPerVbyte,
780
+ parsedBody.nonce,
781
+ parsedBody.confirmations,
782
+ parsedBody.confirmationTarget
783
+ );
784
+ createdSwap.data = payObject;
785
+ createdSwap.metadata = metadata;
786
+ createdSwap.prefix = sigData.prefix;
787
+ createdSwap.timeout = sigData.timeout;
788
+ createdSwap.signature = sigData.signature
789
+ createdSwap.feeRate = sigData.feeRate;
790
+
791
+ await PluginManager.swapCreate(createdSwap);
792
+ await this.saveSwapData(createdSwap);
793
+
794
+ this.swapLogger.info(createdSwap, "REST: /payInvoice: created swap address: "+createdSwap.address+" amount: "+amountBD.toString(10));
795
+
796
+ await responseStream.writeParamsAndEnd({
797
+ code: 20000,
798
+ msg: "Success",
799
+ data: {
800
+ amount: amountBD.toString(10),
801
+ address: signer.getAddress(),
802
+ satsPervByte: networkFeeData.satsPerVbyte.toString(10),
803
+ networkFee: networkFeeInToken.toString(10),
804
+ swapFee: swapFeeInToken.toString(10),
805
+ totalFee: (swapFeeInToken + networkFeeInToken).toString(10),
806
+ total: totalInToken.toString(10),
807
+ minRequiredExpiry: minRequiredExpiry.toString(10),
808
+
809
+ data: payObject.serialize(),
810
+
811
+ prefix: sigData.prefix,
812
+ timeout: sigData.timeout,
813
+ signature: sigData.signature
814
+ }
815
+ });
816
+
817
+ }));
818
+
819
+ const getRefundAuthorization = expressHandlerWrapper(async (req, res) => {
820
+ /**
821
+ * paymentHash: string Payment hash identifier of the swap
822
+ * sequence: BN Sequence identifier of the swap
823
+ */
824
+ const parsedBody = verifySchema({...req.body, ...req.query}, {
825
+ paymentHash: (val: string) => val!=null &&
826
+ typeof(val)==="string" &&
827
+ HEX_REGEX.test(val) ? val: null,
828
+ sequence: FieldTypeEnum.BigInt
829
+ });
830
+ if (parsedBody==null) throw {
831
+ code: 20100,
832
+ msg: "Invalid request body/query (paymentHash/sequence)"
833
+ };
834
+
835
+ this.checkSequence(parsedBody.sequence);
836
+
837
+ const payment = await this.storageManager.getData(parsedBody.paymentHash, parsedBody.sequence);
838
+ if (payment == null || payment.state === ToBtcSwapState.SAVED) throw {
839
+ _httpStatus: 200,
840
+ code: 20007,
841
+ msg: "Payment not found"
842
+ };
843
+
844
+ await this.checkExpired(payment);
845
+
846
+ if (payment.state === ToBtcSwapState.COMMITED) {
847
+ res.status(200).json({
848
+ code: 20008,
849
+ msg: "Payment processing"
850
+ });
851
+ return;
852
+ }
853
+
854
+ if (payment.state === ToBtcSwapState.BTC_SENT || payment.state===ToBtcSwapState.BTC_SENDING) {
855
+ res.status(200).json({
856
+ code: 20006,
857
+ msg: "Already paid",
858
+ data: {
859
+ txId: payment.txId
860
+ }
861
+ });
862
+ return;
863
+ }
864
+
865
+ const {swapContract, signer} = this.getChain(payment.chainIdentifier);
866
+
867
+ if (payment.state === ToBtcSwapState.NON_PAYABLE) {
868
+ const isCommited = await swapContract.isCommited(payment.data);
869
+ if (!isCommited) throw {
870
+ code: 20005,
871
+ msg: "Not committed"
872
+ };
873
+
874
+ const refundResponse = await swapContract.getRefundSignature(signer, payment.data, this.config.refundAuthorizationTimeout);
875
+
876
+ //Double check the state after promise result
877
+ if (payment.state !== ToBtcSwapState.NON_PAYABLE) throw {
878
+ code: 20005,
879
+ msg: "Not committed"
880
+ };
881
+
882
+ this.swapLogger.info(payment, "REST: /getRefundAuthorization: returning refund authorization, because swap is in NON_PAYABLE state, address: "+payment.address);
883
+
884
+ res.status(200).json({
885
+ code: 20000,
886
+ msg: "Success",
887
+ data: {
888
+ address: signer.getAddress(),
889
+ prefix: refundResponse.prefix,
890
+ timeout: refundResponse.timeout,
891
+ signature: refundResponse.signature
892
+ }
893
+ });
894
+ return;
895
+ }
896
+
897
+ throw {
898
+ _httpStatus: 500,
899
+ code: 20009,
900
+ msg: "Invalid payment status"
901
+ };
902
+ });
903
+
904
+ restServer.post(this.path+"/getRefundAuthorization", getRefundAuthorization);
905
+ restServer.get(this.path+"/getRefundAuthorization", getRefundAuthorization);
906
+
907
+ this.logger.info("started at path: ", this.path);
908
+ }
909
+
910
+ /**
911
+ * Starts watchdog checking sent bitcoin transactions
912
+ */
913
+ protected async startTxTimer() {
914
+ let rerun;
915
+ rerun = async () => {
916
+ await this.processBtcTxs().catch( e => this.logger.error("startTxTimer(): call to processBtcTxs() errored", e));
917
+ setTimeout(rerun, this.config.txCheckInterval);
918
+ };
919
+ await rerun();
920
+ }
921
+
922
+ async startWatchdog() {
923
+ await super.startWatchdog();
924
+ await this.startTxTimer();
925
+ }
926
+
927
+ async init() {
928
+ await this.loadData(ToBtcSwapAbs);
929
+ this.subscribeToEvents();
930
+ await PluginManager.serviceInitialize(this);
931
+ }
932
+
933
+ getInfoData(): any {
934
+ return {
935
+ minCltv: Number(this.config.minChainCltv),
936
+
937
+ minConfirmations: this.config.minConfirmations,
938
+ maxConfirmations: this.config.maxConfirmations,
939
+
940
+ minConfTarget: this.config.minConfTarget,
941
+ maxConfTarget: this.config.maxConfTarget,
942
+
943
+ maxOutputScriptLen: OUTPUT_SCRIPT_MAX_LENGTH
944
+ };
945
+ }
946
+
947
+ }