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