@atomiqlabs/lp-lib 16.2.0 → 17.0.0

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