@atomiqlabs/lp-lib 15.0.5 → 15.0.6

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 (163) 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 +40 -40
  5. package/dist/index.js +56 -56
  6. package/dist/info/InfoHandler.d.ts +17 -17
  7. package/dist/info/InfoHandler.js +61 -61
  8. package/dist/plugins/IPlugin.d.ts +143 -143
  9. package/dist/plugins/IPlugin.js +34 -34
  10. package/dist/plugins/PluginManager.d.ts +112 -112
  11. package/dist/plugins/PluginManager.js +259 -259
  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 +18 -18
  23. package/dist/storagemanager/IntermediaryStorageManager.js +104 -104
  24. package/dist/storagemanager/StorageManager.d.ts +12 -12
  25. package/dist/storagemanager/StorageManager.js +57 -57
  26. package/dist/swaps/SwapHandler.d.ts +156 -156
  27. package/dist/swaps/SwapHandler.js +163 -163
  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 +72 -72
  32. package/dist/swaps/assertions/FromBtcAmountAssertions.d.ts +76 -76
  33. package/dist/swaps/assertions/FromBtcAmountAssertions.js +172 -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 +101 -101
  45. package/dist/swaps/escrow/FromBtcBaseSwapHandler.js +207 -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 +675 -675
  56. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.d.ts +32 -32
  57. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.js +88 -88
  58. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +171 -171
  59. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +718 -718
  60. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +28 -28
  61. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +64 -64
  62. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +177 -177
  63. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +863 -863
  64. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.d.ts +24 -24
  65. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.js +58 -58
  66. package/dist/swaps/spv_vault_swap/SpvVault.d.ts +43 -43
  67. package/dist/swaps/spv_vault_swap/SpvVault.js +111 -111
  68. package/dist/swaps/spv_vault_swap/SpvVaultSwap.d.ts +68 -68
  69. package/dist/swaps/spv_vault_swap/SpvVaultSwap.js +158 -158
  70. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.d.ts +68 -68
  71. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +517 -517
  72. package/dist/swaps/spv_vault_swap/SpvVaults.d.ts +61 -61
  73. package/dist/swaps/spv_vault_swap/SpvVaults.js +372 -372
  74. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.d.ts +51 -51
  75. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +650 -650
  76. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.d.ts +52 -52
  77. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.js +118 -118
  78. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.d.ts +76 -76
  79. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +495 -495
  80. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.d.ts +34 -34
  81. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.js +81 -81
  82. package/dist/utils/BitcoinUtils.d.ts +6 -6
  83. package/dist/utils/BitcoinUtils.js +75 -75
  84. package/dist/utils/Utils.d.ts +29 -29
  85. package/dist/utils/Utils.js +88 -88
  86. package/dist/utils/paramcoders/IParamReader.d.ts +5 -5
  87. package/dist/utils/paramcoders/IParamReader.js +2 -2
  88. package/dist/utils/paramcoders/IParamWriter.d.ts +4 -4
  89. package/dist/utils/paramcoders/IParamWriter.js +2 -2
  90. package/dist/utils/paramcoders/LegacyParamEncoder.d.ts +10 -10
  91. package/dist/utils/paramcoders/LegacyParamEncoder.js +22 -22
  92. package/dist/utils/paramcoders/ParamDecoder.d.ts +25 -25
  93. package/dist/utils/paramcoders/ParamDecoder.js +222 -222
  94. package/dist/utils/paramcoders/ParamEncoder.d.ts +9 -9
  95. package/dist/utils/paramcoders/ParamEncoder.js +22 -22
  96. package/dist/utils/paramcoders/SchemaVerifier.d.ts +21 -21
  97. package/dist/utils/paramcoders/SchemaVerifier.js +84 -84
  98. package/dist/utils/paramcoders/server/ServerParamDecoder.d.ts +8 -8
  99. package/dist/utils/paramcoders/server/ServerParamDecoder.js +105 -105
  100. package/dist/utils/paramcoders/server/ServerParamEncoder.d.ts +11 -11
  101. package/dist/utils/paramcoders/server/ServerParamEncoder.js +65 -65
  102. package/dist/wallets/IBitcoinWallet.d.ts +67 -67
  103. package/dist/wallets/IBitcoinWallet.js +2 -2
  104. package/dist/wallets/ILightningWallet.d.ts +117 -117
  105. package/dist/wallets/ILightningWallet.js +37 -37
  106. package/dist/wallets/ISpvVaultSigner.d.ts +7 -7
  107. package/dist/wallets/ISpvVaultSigner.js +2 -2
  108. package/dist/wallets/ISpvVaultWallet.d.ts +42 -42
  109. package/dist/wallets/ISpvVaultWallet.js +2 -2
  110. package/package.json +36 -36
  111. package/src/fees/IBtcFeeEstimator.ts +6 -6
  112. package/src/index.ts +51 -51
  113. package/src/info/InfoHandler.ts +106 -106
  114. package/src/plugins/IPlugin.ts +168 -168
  115. package/src/plugins/PluginManager.ts +336 -336
  116. package/src/prices/BinanceSwapPrice.ts +113 -113
  117. package/src/prices/CoinGeckoSwapPrice.ts +87 -87
  118. package/src/prices/ISwapPrice.ts +88 -88
  119. package/src/prices/OKXSwapPrice.ts +113 -113
  120. package/src/storage/IIntermediaryStorage.ts +19 -19
  121. package/src/storagemanager/IntermediaryStorageManager.ts +109 -109
  122. package/src/storagemanager/StorageManager.ts +68 -68
  123. package/src/swaps/SwapHandler.ts +280 -280
  124. package/src/swaps/SwapHandlerSwap.ts +141 -141
  125. package/src/swaps/assertions/AmountAssertions.ts +76 -76
  126. package/src/swaps/assertions/FromBtcAmountAssertions.ts +238 -238
  127. package/src/swaps/assertions/LightningAssertions.ts +103 -103
  128. package/src/swaps/assertions/ToBtcAmountAssertions.ts +203 -203
  129. package/src/swaps/escrow/EscrowHandler.ts +179 -179
  130. package/src/swaps/escrow/EscrowHandlerSwap.ts +86 -86
  131. package/src/swaps/escrow/FromBtcBaseSwap.ts +38 -38
  132. package/src/swaps/escrow/FromBtcBaseSwapHandler.ts +283 -283
  133. package/src/swaps/escrow/ToBtcBaseSwap.ts +85 -85
  134. package/src/swaps/escrow/ToBtcBaseSwapHandler.ts +129 -129
  135. package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +452 -452
  136. package/src/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.ts +61 -61
  137. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +856 -856
  138. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.ts +137 -137
  139. package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +890 -890
  140. package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +108 -108
  141. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +1112 -1112
  142. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.ts +80 -80
  143. package/src/swaps/spv_vault_swap/SpvVault.ts +143 -143
  144. package/src/swaps/spv_vault_swap/SpvVaultSwap.ts +228 -228
  145. package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +654 -654
  146. package/src/swaps/spv_vault_swap/SpvVaults.ts +444 -444
  147. package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +747 -747
  148. package/src/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.ts +185 -185
  149. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +592 -592
  150. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.ts +121 -121
  151. package/src/utils/BitcoinUtils.ts +71 -71
  152. package/src/utils/Utils.ts +102 -102
  153. package/src/utils/paramcoders/IParamReader.ts +7 -7
  154. package/src/utils/paramcoders/IParamWriter.ts +8 -8
  155. package/src/utils/paramcoders/LegacyParamEncoder.ts +27 -27
  156. package/src/utils/paramcoders/ParamDecoder.ts +218 -218
  157. package/src/utils/paramcoders/ParamEncoder.ts +29 -29
  158. package/src/utils/paramcoders/SchemaVerifier.ts +96 -96
  159. package/src/utils/paramcoders/server/ServerParamDecoder.ts +115 -115
  160. package/src/utils/paramcoders/server/ServerParamEncoder.ts +75 -75
  161. package/src/wallets/IBitcoinWallet.ts +68 -68
  162. package/src/wallets/ILightningWallet.ts +178 -178
  163. package/src/wallets/ISpvVaultSigner.ts +10 -10
@@ -1,748 +1,748 @@
1
- import {FromBtcTrustedSwap, FromBtcTrustedSwapState} from "./FromBtcTrustedSwap";
2
- import {BitcoinRpc, BtcBlock, BtcTx, BtcVout} from "@atomiqlabs/base";
3
- import {Express, Request, Response} from "express";
4
- import {MultichainData, SwapBaseConfig, SwapHandler, SwapHandlerType} from "../../SwapHandler";
5
- import {IIntermediaryStorage} from "../../../storage/IIntermediaryStorage";
6
- import {ISwapPrice} from "../../../prices/ISwapPrice";
7
- import {PluginManager} from "../../../plugins/PluginManager";
8
- import {expressHandlerWrapper, getAbortController, HEX_REGEX} from "../../../utils/Utils";
9
- import {IParamReader} from "../../../utils/paramcoders/IParamReader";
10
- import {ServerParamEncoder} from "../../../utils/paramcoders/server/ServerParamEncoder";
11
- import {FieldTypeEnum, verifySchema} from "../../../utils/paramcoders/SchemaVerifier";
12
- import {IBitcoinWallet} from "../../../wallets/IBitcoinWallet";
13
- import {FromBtcAmountAssertions} from "../../assertions/FromBtcAmountAssertions";
14
-
15
- export type FromBtcTrustedConfig = SwapBaseConfig & {
16
- doubleSpendCheckInterval: number,
17
- swapAddressExpiry: number,
18
- recommendFeeMultiplier?: number
19
- }
20
-
21
- export type FromBtcTrustedRequestType = {
22
- address: string,
23
- amount: bigint,
24
- exactIn?: boolean,
25
- refundAddress?: string,
26
- token?: string
27
- };
28
-
29
- export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrustedSwapState> {
30
- readonly type = SwapHandlerType.FROM_BTC_TRUSTED;
31
-
32
- readonly config: FromBtcTrustedConfig;
33
- readonly bitcoin: IBitcoinWallet;
34
- readonly bitcoinRpc: BitcoinRpc<BtcBlock>;
35
-
36
- readonly subscriptions: Map<string, FromBtcTrustedSwap> = new Map<string, FromBtcTrustedSwap>();
37
- readonly doubleSpendWatchdogSwaps: Set<FromBtcTrustedSwap> = new Set<FromBtcTrustedSwap>();
38
-
39
- readonly refundedSwaps: Map<string, string> = new Map();
40
- readonly doubleSpentSwaps: Map<string, string> = new Map();
41
- readonly processedTxIds: Map<string, { scTxId: string, txId: string, adjustedAmount: bigint, adjustedTotal: bigint }> = new Map();
42
-
43
- readonly AmountAssertions: FromBtcAmountAssertions;
44
-
45
- constructor(
46
- storageDirectory: IIntermediaryStorage<FromBtcTrustedSwap>,
47
- path: string,
48
- chains: MultichainData,
49
- bitcoin: IBitcoinWallet,
50
- swapPricing: ISwapPrice,
51
- bitcoinRpc: BitcoinRpc<BtcBlock>,
52
- config: FromBtcTrustedConfig
53
- ) {
54
- super(storageDirectory, path, chains, swapPricing);
55
- this.AmountAssertions = new FromBtcAmountAssertions(config, swapPricing);
56
- this.config = config;
57
- this.config.recommendFeeMultiplier ??= 1.25;
58
- this.bitcoin = bitcoin;
59
- this.bitcoinRpc = bitcoinRpc;
60
- }
61
-
62
- private getAllAncestors(tx: BtcTx): Promise<{tx: BtcTx, vout: number}[]> {
63
- return Promise.all(tx.ins.map(input => this.bitcoinRpc.getTransaction(input.txid).then(tx => {
64
- return {tx, vout: input.vout}
65
- })));
66
- }
67
-
68
- private async refundSwap(swap: FromBtcTrustedSwap) {
69
- if(swap.refundAddress==null) {
70
- if(swap.state!==FromBtcTrustedSwapState.REFUNDABLE) {
71
- await swap.setState(FromBtcTrustedSwapState.REFUNDABLE);
72
- await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
73
- }
74
- return;
75
- }
76
-
77
- let unlock = swap.lock(30*1000);
78
- if(unlock==null) return;
79
-
80
- const feeRate = await this.bitcoin.getFeeRate();
81
-
82
- const ourOutput = swap.btcTx.outs[swap.vout];
83
-
84
- const resp = await this.bitcoin.drainAll(swap.refundAddress, [{
85
- type: this.bitcoin.getAddressType(),
86
- confirmations: swap.btcTx.confirmations,
87
- outputScript: Buffer.from(ourOutput.scriptPubKey.hex, "hex"),
88
- value: ourOutput.value,
89
- txId: swap.btcTx.txid,
90
- vout: swap.vout
91
- }], feeRate);
92
-
93
- if(resp==null) {
94
- this.swapLogger.error(swap, "refundSwap(): cannot refund swap because of dust limit, txId: "+swap.txId);
95
- unlock();
96
- return;
97
- }
98
-
99
- if(swap.metadata!=null) swap.metadata.times.refundSignPSBT = Date.now();
100
- this.swapLogger.debug(swap, "refundSwap(): signed raw transaction: "+resp.raw);
101
-
102
- const refundTxId = resp.txId;
103
- swap.refundTxId = refundTxId;
104
-
105
- //Send the refund TX
106
- await this.bitcoin.sendRawTransaction(resp.raw);
107
- this.swapLogger.debug(swap, "refundSwap(): sent refund transaction: "+refundTxId);
108
-
109
- this.refundedSwaps.set(swap.getIdentifierHash(), refundTxId);
110
- await this.removeSwapData(swap, FromBtcTrustedSwapState.REFUNDED);
111
- unlock();
112
- }
113
-
114
- private async burn(swap: FromBtcTrustedSwap) {
115
- const ourOutput = swap.btcTx.outs[swap.vout];
116
-
117
- //Check if we can even increase the feeRate by burning
118
- const txSize = 110;
119
- const burnTxFeeRate = Math.floor(ourOutput.value/txSize);
120
- const initialTxFeeRate = Math.ceil(swap.txFee/swap.txSize);
121
-
122
- if(burnTxFeeRate<initialTxFeeRate) {
123
- this.swapLogger.warn(swap, "burn(): cannot send burn transaction, pays too little fee, " +
124
- "initialTxId: "+swap.txId+" initialTxFeeRate: "+initialTxFeeRate+" burnTxFeeRate: "+burnTxFeeRate);
125
- this.doubleSpentSwaps.set(swap.getIdentifierHash(), null);
126
- await this.removeSwapData(swap, FromBtcTrustedSwapState.DOUBLE_SPENT);
127
- return;
128
- }
129
-
130
- //Construct PSBT
131
- const resp = await this.bitcoin.burnAll([{
132
- type: this.bitcoin.getAddressType(),
133
- confirmations: swap.btcTx.confirmations,
134
- outputScript: Buffer.from(ourOutput.scriptPubKey.hex, "hex"),
135
- value: ourOutput.value,
136
- txId: swap.btcTx.txid,
137
- vout: swap.vout
138
- }]);
139
- if(swap.metadata!=null) swap.metadata.times.burnSignPSBT = Date.now();
140
- this.swapLogger.debug(swap, "burn(): signed raw transaction: "+resp.raw);
141
-
142
- const burnTxId = resp.txId;
143
- swap.burnTxId = burnTxId;
144
-
145
- //Send the original TX + our burn TX as a package
146
- const sendTxns = [swap.btcTx.raw, resp.raw];
147
- //TODO: We should handle this in a better way
148
- try {
149
- await this.bitcoinRpc.sendRawPackage(sendTxns);
150
- this.swapLogger.debug(swap, "burn(): sent burn transaction: "+burnTxId);
151
- } catch (e) {
152
- this.swapLogger.error(swap, "burn(): error sending burn package: ", e);
153
- }
154
-
155
- this.doubleSpentSwaps.set(swap.getIdentifierHash(), burnTxId);
156
- await this.removeSwapData(swap, FromBtcTrustedSwapState.DOUBLE_SPENT);
157
- }
158
-
159
- protected async processPastSwap(swap: FromBtcTrustedSwap, tx: BtcTx | null, vout: number | null): Promise<void> {
160
- const foundVout: BtcVout = tx.outs[vout];
161
-
162
- const {chainInterface, signer} = this.getChain(swap.chainIdentifier);
163
-
164
- const outputScript = this.bitcoin.toOutputScript(swap.btcAddress).toString("hex");
165
-
166
- if(swap.state===FromBtcTrustedSwapState.CREATED) {
167
- this.subscriptions.set(outputScript, swap);
168
- if(foundVout==null) {
169
- //Check expiry
170
- if(swap.expiresAt<Date.now()) {
171
- this.subscriptions.delete(outputScript);
172
- await this.bitcoin.addUnusedAddress(swap.btcAddress);
173
- await this.removeSwapData(swap, FromBtcTrustedSwapState.EXPIRED);
174
- return;
175
- }
176
- return;
177
- }
178
- const sentSats = BigInt(foundVout.value);
179
- if(sentSats === swap.amount) {
180
- swap.adjustedInput = swap.amount;
181
- swap.adjustedOutput = swap.outputTokens;
182
- } else {
183
- //If lower than minimum then ignore
184
- if(sentSats < this.config.min) return;
185
- if(sentSats > this.config.max) {
186
- swap.adjustedInput = sentSats;
187
- swap.btcTx = tx;
188
- swap.txId = tx.txid;
189
- swap.vout = vout;
190
- this.subscriptions.delete(outputScript);
191
- await this.refundSwap(swap);
192
- return;
193
- }
194
- //Adjust the amount
195
- swap.adjustedInput = sentSats;
196
- swap.adjustedOutput = swap.outputTokens * sentSats / swap.amount;
197
- }
198
- swap.btcTx = tx;
199
- swap.txId = tx.txid;
200
- swap.vout = vout;
201
- this.subscriptions.delete(outputScript);
202
- await swap.setState(FromBtcTrustedSwapState.RECEIVED);
203
- await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
204
- }
205
-
206
- if(swap.state===FromBtcTrustedSwapState.RECEIVED) {
207
- //Check if transaction still exists
208
- if(tx==null || foundVout==null || tx.txid!==swap.txId) {
209
- await swap.setState(FromBtcTrustedSwapState.CREATED);
210
- await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
211
- return;
212
- }
213
- //Check if it is confirmed
214
- if(tx.confirmations>0) {
215
- await swap.setState(FromBtcTrustedSwapState.BTC_CONFIRMED);
216
- await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
217
- } else {
218
- //Check if it pays high enough fee AND has confirmed ancestors
219
- const ancestors = await this.getAllAncestors(tx);
220
- const allAncestorsConfirmed = ancestors.reduce((prev, curr) => prev && curr.tx.confirmations>0, true);
221
- const totalInput = ancestors.reduce((prev, curr) => prev + curr.tx.outs[curr.vout].value, 0);
222
- const totalOutput = tx.outs.reduce((prev, curr) => prev + curr.value, 0);
223
- const fee = totalInput-totalOutput;
224
- const feePerVbyte = Math.ceil(fee/tx.vsize);
225
- if(
226
- allAncestorsConfirmed &&
227
- (feePerVbyte>=swap.recommendedFee || feePerVbyte>=await this.bitcoin.getFeeRate())
228
- ) {
229
- if(swap.state!==FromBtcTrustedSwapState.RECEIVED) return;
230
- swap.txSize = tx.vsize;
231
- swap.txFee = fee;
232
- await swap.setState(FromBtcTrustedSwapState.BTC_CONFIRMED);
233
- await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
234
- } else {
235
- return;
236
- }
237
- }
238
- }
239
-
240
- if(swap.state===FromBtcTrustedSwapState.REFUNDABLE) {
241
- if(swap.refundAddress!=null) {
242
- await this.refundSwap(swap);
243
- return;
244
- }
245
- }
246
-
247
- if(swap.doubleSpent || tx==null || foundVout==null || tx.txid!==swap.txId) {
248
- if(swap.state===FromBtcTrustedSwapState.REFUNDABLE) {
249
- await swap.setState(FromBtcTrustedSwapState.CREATED);
250
- return;
251
- }
252
- if(!swap.doubleSpent) {
253
- swap.doubleSpent = true;
254
- try {
255
- await this.burn(swap);
256
- this.doubleSpendWatchdogSwaps.delete(swap);
257
- } catch (e) {
258
- this.swapLogger.error(swap, "processPastSwap(): Error burning swap: ", e);
259
- swap.doubleSpent = false;
260
- }
261
- }
262
- return;
263
- } else {
264
- if(tx.confirmations<=0 && !this.doubleSpendWatchdogSwaps.has(swap)) {
265
- this.swapLogger.debug(swap, "processPastSwap(): Adding swap transaction to double spend watchdog list: ", swap.txId);
266
- this.doubleSpendWatchdogSwaps.add(swap);
267
- }
268
- }
269
- if(tx.confirmations>0 && this.doubleSpendWatchdogSwaps.delete(swap)) {
270
- this.swapLogger.debug(swap, "processPastSwap(): Removing confirmed swap transaction from double spend watchdog list: ", swap.txId);
271
- }
272
-
273
- if(swap.state===FromBtcTrustedSwapState.BTC_CONFIRMED) {
274
- //Send gas token
275
- const balance: Promise<bigint> = chainInterface.getBalance(signer.getAddress(), swap.token);
276
- try {
277
- await this.checkBalance(swap.adjustedOutput, balance, null);
278
- if(swap.metadata!=null) swap.metadata.times.receivedBalanceChecked = Date.now();
279
- } catch (e) {
280
- this.swapLogger.error(swap, "processPastSwap(): Error not enough balance: ", e);
281
- await this.refundSwap(swap);
282
- return;
283
- }
284
-
285
- if(swap.state!==FromBtcTrustedSwapState.BTC_CONFIRMED) return;
286
-
287
- let unlock = swap.lock(30*1000);
288
- if(unlock==null) return;
289
-
290
- const txns = await chainInterface.txsTransfer(signer.getAddress(), swap.token, swap.adjustedOutput, swap.dstAddress);
291
- await chainInterface.sendAndConfirm(signer, txns, true, null, false, async (txId: string, rawTx: string) => {
292
- swap.txIds = {init: txId};
293
- swap.scRawTx = rawTx;
294
- if(swap.state===FromBtcTrustedSwapState.BTC_CONFIRMED) {
295
- await swap.setState(FromBtcTrustedSwapState.SENT);
296
- await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
297
- }
298
- if(unlock!=null) unlock();
299
- unlock = null;
300
- });
301
- }
302
-
303
- if(swap.state===FromBtcTrustedSwapState.SENT) {
304
- const txStatus = await chainInterface.getTxStatus(swap.scRawTx);
305
- switch(txStatus) {
306
- case "not_found":
307
- //Retry
308
- swap.txIds = {init: null};
309
- swap.scRawTx = null;
310
- await swap.setState(FromBtcTrustedSwapState.RECEIVED);
311
- await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
312
- break;
313
- case "reverted":
314
- //Cancel invoice
315
- await this.refundSwap(swap);
316
- this.swapLogger.info(swap, "processPastSwap(): transaction reverted, refunding btc on-chain: ", swap.btcAddress);
317
- break;
318
- case "success":
319
- await swap.setState(FromBtcTrustedSwapState.CONFIRMED);
320
- await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
321
- break;
322
- }
323
- }
324
-
325
- if(swap.state===FromBtcTrustedSwapState.CONFIRMED) {
326
- this.processedTxIds.set(swap.getIdentifierHash(), {
327
- txId: swap.txId,
328
- scTxId: swap.txIds.init,
329
- adjustedAmount: swap.adjustedInput,
330
- adjustedTotal: swap.adjustedOutput
331
- });
332
- if(tx.confirmations>0) await this.removeSwapData(swap, FromBtcTrustedSwapState.FINISHED);
333
- }
334
- }
335
-
336
- protected async processPastSwaps(): Promise<void> {
337
- const queriedData = await this.storageManager.query([
338
- {
339
- key: "state",
340
- value: [
341
- FromBtcTrustedSwapState.REFUNDABLE,
342
- FromBtcTrustedSwapState.CREATED,
343
- FromBtcTrustedSwapState.RECEIVED,
344
- FromBtcTrustedSwapState.BTC_CONFIRMED,
345
- FromBtcTrustedSwapState.SENT,
346
- FromBtcTrustedSwapState.CONFIRMED
347
- ]
348
- }
349
- ]);
350
-
351
- const startingBlockheight = queriedData.reduce((prev, {obj: swap}) => Math.min(prev, swap.createdHeight), Infinity);
352
- if(startingBlockheight===Infinity) return;
353
- const transactions = await this.bitcoin.getWalletTransactions(startingBlockheight);
354
-
355
- const map = new Map<string, {tx: BtcTx, vout: number}[]>();
356
- transactions.forEach(tx => {
357
- tx.outs.forEach((out, vout) => {
358
- const existing = map.get(out.scriptPubKey.hex);
359
- if(existing==null) {
360
- map.set(out.scriptPubKey.hex, [{tx, vout}]);
361
- } else {
362
- existing.push({tx, vout});
363
- }
364
- })
365
- });
366
-
367
- for(let {obj: swap} of queriedData) {
368
- const outputScript = this.bitcoin.toOutputScript(swap.btcAddress).toString("hex");
369
- const txs = map.get(outputScript) ?? [];
370
- try {
371
- await this.processPastSwap(swap, txs[0]?.tx, txs[0]?.vout);
372
- } catch (e) {
373
- this.swapLogger.error(swap, "processPastSwaps(): Error ocurred while processing swap: ", e);
374
- }
375
- }
376
- }
377
-
378
- private isValidBitcoinAddress(address: string) {
379
- try {
380
- this.bitcoin.toOutputScript(address);
381
- return true;
382
- } catch (e) {}
383
- return false;
384
- }
385
-
386
- startRestServer(restServer: Express): void {
387
-
388
- const getAddress = expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
389
- const metadata: {
390
- request: any,
391
- invoiceRequest?: any,
392
- invoiceResponse?: any,
393
- times: {[key: string]: number}
394
- } = {request: {}, times: {}};
395
-
396
- const chainIdentifier = req.query.chain as string ?? this.chains.default;
397
- const {chainInterface, signer} = this.getChain(chainIdentifier);
398
-
399
- metadata.times.requestReceived = Date.now();
400
- /**
401
- * address: string solana address of the recipient
402
- * refundAddress?: string bitcoin address to use in case of refund
403
- * amount: string amount (in lamports/smart chain base units) of the invoice
404
- * exactOut: boolean whether to create and exact output swap
405
- */
406
- req.query.token ??= chainInterface.getNativeCurrencyAddress();
407
- const parsedBody: FromBtcTrustedRequestType = verifySchema(req.query,{
408
- address: (val: string) => val!=null &&
409
- typeof(val)==="string" &&
410
- chainInterface.isValidAddress(val) ? val : null,
411
- refundAddress: (val: string) => val==null ? "" :
412
- typeof(val)==="string" &&
413
- this.isValidBitcoinAddress(val) ? val : null,
414
- token: (val: string) => val!=null &&
415
- typeof(val)==="string" &&
416
- this.isTokenSupported(chainIdentifier, val) ? val : null,
417
- amount: FieldTypeEnum.BigInt,
418
- exactIn: (val: string) => val==="true" ? true :
419
- (val==="false" || val===undefined) ? false : null
420
- });
421
- if(parsedBody==null) throw {
422
- code: 20100,
423
- msg: "Invalid request body"
424
- };
425
- metadata.request = parsedBody;
426
-
427
- const refundAddress = parsedBody.refundAddress==="" ? null : parsedBody.refundAddress;
428
-
429
- const requestedAmount = {input: parsedBody.exactIn, amount: parsedBody.amount, token: parsedBody.token};
430
- const request = {
431
- chainIdentifier,
432
- raw: req,
433
- parsed: parsedBody,
434
- metadata
435
- };
436
- const useToken = parsedBody.token;
437
-
438
- //Check request params
439
- const fees = await this.AmountAssertions.preCheckFromBtcAmounts(this.type, request, requestedAmount);
440
- metadata.times.requestChecked = Date.now();
441
-
442
- //Create abortController for parallel prefetches
443
- const responseStream = res.responseStream;
444
- const abortController = getAbortController(responseStream);
445
-
446
- //Pre-fetch data
447
- const pricePrefetchPromise = this.swapPricing.preFetchPrice(useToken, chainIdentifier).catch(e => {
448
- this.logger.error("pricePrefetchPromise(): pricePrefetch error: ", e);
449
- abortController.abort(e);
450
- return null;
451
- });
452
- const balancePrefetch = chainInterface.getBalance(signer.getAddress(), useToken).catch(e => {
453
- this.logger.error("getBalancePrefetch(): balancePrefetch error: ", e);
454
- abortController.abort(e);
455
- return null;
456
- });
457
-
458
- //Check valid amount specified (min/max)
459
- const {
460
- amountBD,
461
- swapFee,
462
- swapFeeInToken,
463
- totalInToken
464
- } = await this.AmountAssertions.checkFromBtcAmount(this.type, request, {...requestedAmount, pricePrefetch: pricePrefetchPromise}, fees, abortController.signal);
465
- metadata.times.priceCalculated = Date.now();
466
-
467
- //Make sure we have MORE THAN ENOUGH to honor the swap request
468
- await this.checkBalance(totalInToken * 4n, balancePrefetch, abortController.signal)
469
- metadata.times.balanceChecked = Date.now();
470
-
471
- const blockHeight = await this.bitcoin.getBlockheight();
472
- const feeRate = await this.bitcoin.getFeeRate();
473
- const recommendedFee = Math.ceil(feeRate*this.config.recommendFeeMultiplier);
474
- if(recommendedFee===0) throw {
475
- _httpStatus: 500,
476
- code: 21100,
477
- msg: "Cannot estimate bitcoin fee!"
478
- };
479
- metadata.times.feeEstimated = Date.now();
480
-
481
- const receiveAddress = await this.bitcoin.getAddress();
482
- const outputScript = this.bitcoin.toOutputScript(receiveAddress).toString("hex");
483
- abortController.signal.throwIfAborted();
484
- metadata.times.addressCreated = Date.now();
485
-
486
- const createdSwap = new FromBtcTrustedSwap(
487
- chainIdentifier,
488
- swapFee,
489
- swapFeeInToken,
490
- receiveAddress,
491
- amountBD,
492
- parsedBody.address,
493
- totalInToken,
494
- blockHeight,
495
- Date.now()+(this.config.swapAddressExpiry*1000),
496
- recommendedFee,
497
- refundAddress,
498
- useToken
499
- );
500
- metadata.times.swapCreated = Date.now();
501
- createdSwap.metadata = metadata;
502
-
503
- await PluginManager.swapCreate(createdSwap);
504
- await this.storageManager.saveData(createdSwap.getIdentifierHash(), createdSwap.getSequence(), createdSwap);
505
- this.subscriptions.set(outputScript, createdSwap);
506
-
507
- this.swapLogger.info(createdSwap, "REST: /getAddress: Created swap address: "+createdSwap.btcAddress+" amount: "+amountBD.toString(10));
508
-
509
- res.status(200).json({
510
- code: 10000,
511
- msg: "Success",
512
- data: {
513
- paymentHash: createdSwap.getIdentifierHash(),
514
- sequence: createdSwap.getSequence().toString(10),
515
- btcAddress: receiveAddress,
516
- amountSats: amountBD.toString(10),
517
- swapFeeSats: swapFee.toString(10),
518
- swapFee: swapFeeInToken.toString(10),
519
- total: totalInToken.toString(10),
520
- intermediaryKey: signer.getAddress(),
521
- recommendedFee,
522
- expiresAt: createdSwap.expiresAt
523
- }
524
- });
525
- });
526
- restServer.get(this.path+"/getAddress", getAddress);
527
-
528
- const getInvoiceStatus = expressHandlerWrapper(async (req, res) => {
529
- /**
530
- * paymentHash: string payment hash of the invoice
531
- * sequence: BN secret sequence for the swap,
532
- */
533
- const parsedBody = verifySchema(req.query, {
534
- paymentHash: (val: string) => val!=null &&
535
- typeof(val)==="string" &&
536
- val.length===64 &&
537
- HEX_REGEX.test(val) ? val: null,
538
- sequence: FieldTypeEnum.BigInt,
539
- });
540
- if(parsedBody==null) throw {
541
- code: 20100,
542
- msg: "Invalid request"
543
- };
544
-
545
- const processedTxData = this.processedTxIds.get(parsedBody.paymentHash);
546
- if(processedTxData!=null) throw {
547
- _httpStatus: 200,
548
- code: 10000,
549
- msg: "Success, tx confirmed",
550
- data: {
551
- adjustedAmount: processedTxData.adjustedAmount.toString(10),
552
- adjustedTotal: processedTxData.adjustedTotal.toString(10),
553
- txId: processedTxData.txId,
554
- scTxId: processedTxData.scTxId
555
- }
556
- };
557
-
558
- const refundTxId = this.refundedSwaps.get(parsedBody.paymentHash);
559
- if(refundTxId!=null) throw {
560
- _httpStatus: 200,
561
- code: 10014,
562
- msg: "Refunded",
563
- data: {
564
- txId: refundTxId
565
- }
566
- };
567
-
568
- const doubleSpendTxId = this.doubleSpentSwaps.get(parsedBody.paymentHash);
569
- if(doubleSpendTxId!=null) throw {
570
- _httpStatus: 200,
571
- code: 10015,
572
- msg: "Double spend detected, deposit burned",
573
- data: {
574
- txId: doubleSpendTxId
575
- }
576
- };
577
-
578
- const invoiceData: FromBtcTrustedSwap = await this.storageManager.getData(parsedBody.paymentHash, parsedBody.sequence);
579
- if (invoiceData==null) throw {
580
- _httpStatus: 200,
581
- code: 10001,
582
- msg: "Swap expired/canceled"
583
- };
584
-
585
- if (invoiceData.state === FromBtcTrustedSwapState.CREATED) throw {
586
- _httpStatus: 200,
587
- code: 10010,
588
- msg: "Bitcoin yet unpaid"
589
- };
590
-
591
- if (invoiceData.state === FromBtcTrustedSwapState.RECEIVED) throw {
592
- _httpStatus: 200,
593
- code: 10011,
594
- msg: "Bitcoin received, payment processing",
595
- data: {
596
- adjustedAmount: invoiceData.adjustedInput.toString(10),
597
- adjustedTotal: invoiceData.adjustedOutput.toString(10),
598
- txId: invoiceData.txId
599
- }
600
- };
601
-
602
- if (invoiceData.state === FromBtcTrustedSwapState.BTC_CONFIRMED) throw {
603
- _httpStatus: 200,
604
- code: 10013,
605
- msg: "Bitcoin accepted, payment processing",
606
- data: {
607
- adjustedAmount: invoiceData.adjustedInput.toString(10),
608
- adjustedTotal: invoiceData.adjustedOutput.toString(10),
609
- txId: invoiceData.txId
610
- }
611
- };
612
-
613
- if (invoiceData.state === FromBtcTrustedSwapState.SENT) throw {
614
- _httpStatus: 200,
615
- code: 10012,
616
- msg: "Tx sent",
617
- data: {
618
- adjustedAmount: invoiceData.adjustedInput.toString(10),
619
- adjustedTotal: invoiceData.adjustedOutput.toString(10),
620
- txId: invoiceData.txId,
621
- scTxId: invoiceData.txIds.init
622
- }
623
- };
624
-
625
- if (invoiceData.state === FromBtcTrustedSwapState.CONFIRMED || invoiceData.state === FromBtcTrustedSwapState.FINISHED) throw {
626
- _httpStatus: 200,
627
- code: 10000,
628
- msg: "Success, tx confirmed",
629
- data: {
630
- adjustedAmount: invoiceData.adjustedInput.toString(10),
631
- adjustedTotal: invoiceData.adjustedOutput.toString(10),
632
- txId: invoiceData.txId,
633
- scTxId: invoiceData.txIds.init
634
- }
635
- };
636
-
637
- if (invoiceData.state === FromBtcTrustedSwapState.REFUNDABLE) throw {
638
- _httpStatus: 200,
639
- code: 10016,
640
- msg: "Refundable",
641
- data: {
642
- adjustedAmount: invoiceData.adjustedInput.toString(10)
643
- }
644
- };
645
- });
646
- restServer.get(this.path+"/getAddressStatus", getInvoiceStatus);
647
-
648
- const setRefundAddress = expressHandlerWrapper(async (req, res) => {
649
- /**
650
- * paymentHash: string payment hash of the invoice
651
- * sequence: BN secret sequence for the swap,
652
- * refundAddress: string valid bitcoin address to be used for refunds
653
- */
654
- const parsedBody = verifySchema({...req.body, ...req.query}, {
655
- paymentHash: (val: string) => val!=null &&
656
- typeof(val)==="string" &&
657
- val.length===64 &&
658
- HEX_REGEX.test(val) ? val: null,
659
- sequence: FieldTypeEnum.BigInt,
660
- refundAddress: (val: string) => val!=null &&
661
- typeof(val)==="string" &&
662
- this.isValidBitcoinAddress(val) ? val : null
663
- });
664
- if(parsedBody==null) throw {
665
- code: 20100,
666
- msg: "Invalid request"
667
- };
668
-
669
- const invoiceData: FromBtcTrustedSwap = await this.storageManager.getData(parsedBody.paymentHash, null);
670
- if (invoiceData==null || invoiceData.getSequence()!==parsedBody.sequence) throw {
671
- code: 10001,
672
- msg: "Swap not found"
673
- };
674
-
675
- if(invoiceData.refundAddress!=null) throw {
676
- code: 10080,
677
- msg: "Refund address already set!",
678
- data: {
679
- refundAddress: invoiceData.refundAddress
680
- }
681
- };
682
-
683
- invoiceData.refundAddress = parsedBody.refundAddress;
684
-
685
- if (invoiceData.state === FromBtcTrustedSwapState.REFUNDABLE) {
686
- this.refundSwap(invoiceData).catch(e => {
687
- this.swapLogger.error(invoiceData, "/setRefundAddress: Failed to refund!");
688
- });
689
- }
690
-
691
- throw {
692
- _httpStatus: 200,
693
- code: 10000,
694
- msg: "Refund address set"
695
- };
696
- });
697
- restServer.get(this.path+"/setRefundAddress", setRefundAddress);
698
- restServer.post(this.path+"/setRefundAddress", setRefundAddress);
699
-
700
- this.logger.info("started at path: ", this.path);
701
- }
702
-
703
- private async checkDoubleSpends(): Promise<void> {
704
- for(let swap of this.doubleSpendWatchdogSwaps.keys()) {
705
- const tx = await this.bitcoin.getWalletTransaction(swap.txId);
706
- if(tx==null) {
707
- this.swapLogger.debug(swap, "checkDoubleSpends(): Swap was double spent, burning... - original txId: "+swap.txId);
708
- this.processPastSwap(swap, null, null);
709
- }
710
- }
711
- }
712
-
713
- private async startDoubleSpendWatchdog() {
714
- let rerun: () => Promise<void>;
715
- rerun = async () => {
716
- await this.checkDoubleSpends().catch( e => console.error(e));
717
- setTimeout(rerun, this.config.doubleSpendCheckInterval);
718
- };
719
- await rerun();
720
- }
721
-
722
- private listenToTxns() {
723
- this.bitcoin.subscribeToWalletTransactions((btcTx: BtcTx) => {
724
- for(let out of btcTx.outs) {
725
- const savedSwap = this.subscriptions.get(out.scriptPubKey.hex);
726
- if(savedSwap==null) continue;
727
- this.processPastSwap(savedSwap, btcTx, out.n);
728
- return;
729
- }
730
- });
731
- }
732
-
733
- async startWatchdog() {
734
- await super.startWatchdog();
735
- await this.startDoubleSpendWatchdog();
736
- }
737
-
738
- async init(): Promise<void> {
739
- await this.storageManager.loadData(FromBtcTrustedSwap);
740
- this.listenToTxns();
741
- await PluginManager.serviceInitialize(this);
742
- }
743
-
744
- getInfoData(): any {
745
- return {};
746
- }
747
-
1
+ import {FromBtcTrustedSwap, FromBtcTrustedSwapState} from "./FromBtcTrustedSwap";
2
+ import {BitcoinRpc, BtcBlock, BtcTx, BtcVout} from "@atomiqlabs/base";
3
+ import {Express, Request, Response} from "express";
4
+ import {MultichainData, SwapBaseConfig, SwapHandler, SwapHandlerType} from "../../SwapHandler";
5
+ import {IIntermediaryStorage} from "../../../storage/IIntermediaryStorage";
6
+ import {ISwapPrice} from "../../../prices/ISwapPrice";
7
+ import {PluginManager} from "../../../plugins/PluginManager";
8
+ import {expressHandlerWrapper, getAbortController, HEX_REGEX} from "../../../utils/Utils";
9
+ import {IParamReader} from "../../../utils/paramcoders/IParamReader";
10
+ import {ServerParamEncoder} from "../../../utils/paramcoders/server/ServerParamEncoder";
11
+ import {FieldTypeEnum, verifySchema} from "../../../utils/paramcoders/SchemaVerifier";
12
+ import {IBitcoinWallet} from "../../../wallets/IBitcoinWallet";
13
+ import {FromBtcAmountAssertions} from "../../assertions/FromBtcAmountAssertions";
14
+
15
+ export type FromBtcTrustedConfig = SwapBaseConfig & {
16
+ doubleSpendCheckInterval: number,
17
+ swapAddressExpiry: number,
18
+ recommendFeeMultiplier?: number
19
+ }
20
+
21
+ export type FromBtcTrustedRequestType = {
22
+ address: string,
23
+ amount: bigint,
24
+ exactIn?: boolean,
25
+ refundAddress?: string,
26
+ token?: string
27
+ };
28
+
29
+ export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrustedSwapState> {
30
+ readonly type = SwapHandlerType.FROM_BTC_TRUSTED;
31
+
32
+ readonly config: FromBtcTrustedConfig;
33
+ readonly bitcoin: IBitcoinWallet;
34
+ readonly bitcoinRpc: BitcoinRpc<BtcBlock>;
35
+
36
+ readonly subscriptions: Map<string, FromBtcTrustedSwap> = new Map<string, FromBtcTrustedSwap>();
37
+ readonly doubleSpendWatchdogSwaps: Set<FromBtcTrustedSwap> = new Set<FromBtcTrustedSwap>();
38
+
39
+ readonly refundedSwaps: Map<string, string> = new Map();
40
+ readonly doubleSpentSwaps: Map<string, string> = new Map();
41
+ readonly processedTxIds: Map<string, { scTxId: string, txId: string, adjustedAmount: bigint, adjustedTotal: bigint }> = new Map();
42
+
43
+ readonly AmountAssertions: FromBtcAmountAssertions;
44
+
45
+ constructor(
46
+ storageDirectory: IIntermediaryStorage<FromBtcTrustedSwap>,
47
+ path: string,
48
+ chains: MultichainData,
49
+ bitcoin: IBitcoinWallet,
50
+ swapPricing: ISwapPrice,
51
+ bitcoinRpc: BitcoinRpc<BtcBlock>,
52
+ config: FromBtcTrustedConfig
53
+ ) {
54
+ super(storageDirectory, path, chains, swapPricing);
55
+ this.AmountAssertions = new FromBtcAmountAssertions(config, swapPricing);
56
+ this.config = config;
57
+ this.config.recommendFeeMultiplier ??= 1.25;
58
+ this.bitcoin = bitcoin;
59
+ this.bitcoinRpc = bitcoinRpc;
60
+ }
61
+
62
+ private getAllAncestors(tx: BtcTx): Promise<{tx: BtcTx, vout: number}[]> {
63
+ return Promise.all(tx.ins.map(input => this.bitcoinRpc.getTransaction(input.txid).then(tx => {
64
+ return {tx, vout: input.vout}
65
+ })));
66
+ }
67
+
68
+ private async refundSwap(swap: FromBtcTrustedSwap) {
69
+ if(swap.refundAddress==null) {
70
+ if(swap.state!==FromBtcTrustedSwapState.REFUNDABLE) {
71
+ await swap.setState(FromBtcTrustedSwapState.REFUNDABLE);
72
+ await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
73
+ }
74
+ return;
75
+ }
76
+
77
+ let unlock = swap.lock(30*1000);
78
+ if(unlock==null) return;
79
+
80
+ const feeRate = await this.bitcoin.getFeeRate();
81
+
82
+ const ourOutput = swap.btcTx.outs[swap.vout];
83
+
84
+ const resp = await this.bitcoin.drainAll(swap.refundAddress, [{
85
+ type: this.bitcoin.getAddressType(),
86
+ confirmations: swap.btcTx.confirmations,
87
+ outputScript: Buffer.from(ourOutput.scriptPubKey.hex, "hex"),
88
+ value: ourOutput.value,
89
+ txId: swap.btcTx.txid,
90
+ vout: swap.vout
91
+ }], feeRate);
92
+
93
+ if(resp==null) {
94
+ this.swapLogger.error(swap, "refundSwap(): cannot refund swap because of dust limit, txId: "+swap.txId);
95
+ unlock();
96
+ return;
97
+ }
98
+
99
+ if(swap.metadata!=null) swap.metadata.times.refundSignPSBT = Date.now();
100
+ this.swapLogger.debug(swap, "refundSwap(): signed raw transaction: "+resp.raw);
101
+
102
+ const refundTxId = resp.txId;
103
+ swap.refundTxId = refundTxId;
104
+
105
+ //Send the refund TX
106
+ await this.bitcoin.sendRawTransaction(resp.raw);
107
+ this.swapLogger.debug(swap, "refundSwap(): sent refund transaction: "+refundTxId);
108
+
109
+ this.refundedSwaps.set(swap.getIdentifierHash(), refundTxId);
110
+ await this.removeSwapData(swap, FromBtcTrustedSwapState.REFUNDED);
111
+ unlock();
112
+ }
113
+
114
+ private async burn(swap: FromBtcTrustedSwap) {
115
+ const ourOutput = swap.btcTx.outs[swap.vout];
116
+
117
+ //Check if we can even increase the feeRate by burning
118
+ const txSize = 110;
119
+ const burnTxFeeRate = Math.floor(ourOutput.value/txSize);
120
+ const initialTxFeeRate = Math.ceil(swap.txFee/swap.txSize);
121
+
122
+ if(burnTxFeeRate<initialTxFeeRate) {
123
+ this.swapLogger.warn(swap, "burn(): cannot send burn transaction, pays too little fee, " +
124
+ "initialTxId: "+swap.txId+" initialTxFeeRate: "+initialTxFeeRate+" burnTxFeeRate: "+burnTxFeeRate);
125
+ this.doubleSpentSwaps.set(swap.getIdentifierHash(), null);
126
+ await this.removeSwapData(swap, FromBtcTrustedSwapState.DOUBLE_SPENT);
127
+ return;
128
+ }
129
+
130
+ //Construct PSBT
131
+ const resp = await this.bitcoin.burnAll([{
132
+ type: this.bitcoin.getAddressType(),
133
+ confirmations: swap.btcTx.confirmations,
134
+ outputScript: Buffer.from(ourOutput.scriptPubKey.hex, "hex"),
135
+ value: ourOutput.value,
136
+ txId: swap.btcTx.txid,
137
+ vout: swap.vout
138
+ }]);
139
+ if(swap.metadata!=null) swap.metadata.times.burnSignPSBT = Date.now();
140
+ this.swapLogger.debug(swap, "burn(): signed raw transaction: "+resp.raw);
141
+
142
+ const burnTxId = resp.txId;
143
+ swap.burnTxId = burnTxId;
144
+
145
+ //Send the original TX + our burn TX as a package
146
+ const sendTxns = [swap.btcTx.raw, resp.raw];
147
+ //TODO: We should handle this in a better way
148
+ try {
149
+ await this.bitcoinRpc.sendRawPackage(sendTxns);
150
+ this.swapLogger.debug(swap, "burn(): sent burn transaction: "+burnTxId);
151
+ } catch (e) {
152
+ this.swapLogger.error(swap, "burn(): error sending burn package: ", e);
153
+ }
154
+
155
+ this.doubleSpentSwaps.set(swap.getIdentifierHash(), burnTxId);
156
+ await this.removeSwapData(swap, FromBtcTrustedSwapState.DOUBLE_SPENT);
157
+ }
158
+
159
+ protected async processPastSwap(swap: FromBtcTrustedSwap, tx: BtcTx | null, vout: number | null): Promise<void> {
160
+ const foundVout: BtcVout = tx.outs[vout];
161
+
162
+ const {chainInterface, signer} = this.getChain(swap.chainIdentifier);
163
+
164
+ const outputScript = this.bitcoin.toOutputScript(swap.btcAddress).toString("hex");
165
+
166
+ if(swap.state===FromBtcTrustedSwapState.CREATED) {
167
+ this.subscriptions.set(outputScript, swap);
168
+ if(foundVout==null) {
169
+ //Check expiry
170
+ if(swap.expiresAt<Date.now()) {
171
+ this.subscriptions.delete(outputScript);
172
+ await this.bitcoin.addUnusedAddress(swap.btcAddress);
173
+ await this.removeSwapData(swap, FromBtcTrustedSwapState.EXPIRED);
174
+ return;
175
+ }
176
+ return;
177
+ }
178
+ const sentSats = BigInt(foundVout.value);
179
+ if(sentSats === swap.amount) {
180
+ swap.adjustedInput = swap.amount;
181
+ swap.adjustedOutput = swap.outputTokens;
182
+ } else {
183
+ //If lower than minimum then ignore
184
+ if(sentSats < this.config.min) return;
185
+ if(sentSats > this.config.max) {
186
+ swap.adjustedInput = sentSats;
187
+ swap.btcTx = tx;
188
+ swap.txId = tx.txid;
189
+ swap.vout = vout;
190
+ this.subscriptions.delete(outputScript);
191
+ await this.refundSwap(swap);
192
+ return;
193
+ }
194
+ //Adjust the amount
195
+ swap.adjustedInput = sentSats;
196
+ swap.adjustedOutput = swap.outputTokens * sentSats / swap.amount;
197
+ }
198
+ swap.btcTx = tx;
199
+ swap.txId = tx.txid;
200
+ swap.vout = vout;
201
+ this.subscriptions.delete(outputScript);
202
+ await swap.setState(FromBtcTrustedSwapState.RECEIVED);
203
+ await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
204
+ }
205
+
206
+ if(swap.state===FromBtcTrustedSwapState.RECEIVED) {
207
+ //Check if transaction still exists
208
+ if(tx==null || foundVout==null || tx.txid!==swap.txId) {
209
+ await swap.setState(FromBtcTrustedSwapState.CREATED);
210
+ await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
211
+ return;
212
+ }
213
+ //Check if it is confirmed
214
+ if(tx.confirmations>0) {
215
+ await swap.setState(FromBtcTrustedSwapState.BTC_CONFIRMED);
216
+ await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
217
+ } else {
218
+ //Check if it pays high enough fee AND has confirmed ancestors
219
+ const ancestors = await this.getAllAncestors(tx);
220
+ const allAncestorsConfirmed = ancestors.reduce((prev, curr) => prev && curr.tx.confirmations>0, true);
221
+ const totalInput = ancestors.reduce((prev, curr) => prev + curr.tx.outs[curr.vout].value, 0);
222
+ const totalOutput = tx.outs.reduce((prev, curr) => prev + curr.value, 0);
223
+ const fee = totalInput-totalOutput;
224
+ const feePerVbyte = Math.ceil(fee/tx.vsize);
225
+ if(
226
+ allAncestorsConfirmed &&
227
+ (feePerVbyte>=swap.recommendedFee || feePerVbyte>=await this.bitcoin.getFeeRate())
228
+ ) {
229
+ if(swap.state!==FromBtcTrustedSwapState.RECEIVED) return;
230
+ swap.txSize = tx.vsize;
231
+ swap.txFee = fee;
232
+ await swap.setState(FromBtcTrustedSwapState.BTC_CONFIRMED);
233
+ await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
234
+ } else {
235
+ return;
236
+ }
237
+ }
238
+ }
239
+
240
+ if(swap.state===FromBtcTrustedSwapState.REFUNDABLE) {
241
+ if(swap.refundAddress!=null) {
242
+ await this.refundSwap(swap);
243
+ return;
244
+ }
245
+ }
246
+
247
+ if(swap.doubleSpent || tx==null || foundVout==null || tx.txid!==swap.txId) {
248
+ if(swap.state===FromBtcTrustedSwapState.REFUNDABLE) {
249
+ await swap.setState(FromBtcTrustedSwapState.CREATED);
250
+ return;
251
+ }
252
+ if(!swap.doubleSpent) {
253
+ swap.doubleSpent = true;
254
+ try {
255
+ await this.burn(swap);
256
+ this.doubleSpendWatchdogSwaps.delete(swap);
257
+ } catch (e) {
258
+ this.swapLogger.error(swap, "processPastSwap(): Error burning swap: ", e);
259
+ swap.doubleSpent = false;
260
+ }
261
+ }
262
+ return;
263
+ } else {
264
+ if(tx.confirmations<=0 && !this.doubleSpendWatchdogSwaps.has(swap)) {
265
+ this.swapLogger.debug(swap, "processPastSwap(): Adding swap transaction to double spend watchdog list: ", swap.txId);
266
+ this.doubleSpendWatchdogSwaps.add(swap);
267
+ }
268
+ }
269
+ if(tx.confirmations>0 && this.doubleSpendWatchdogSwaps.delete(swap)) {
270
+ this.swapLogger.debug(swap, "processPastSwap(): Removing confirmed swap transaction from double spend watchdog list: ", swap.txId);
271
+ }
272
+
273
+ if(swap.state===FromBtcTrustedSwapState.BTC_CONFIRMED) {
274
+ //Send gas token
275
+ const balance: Promise<bigint> = chainInterface.getBalance(signer.getAddress(), swap.token);
276
+ try {
277
+ await this.checkBalance(swap.adjustedOutput, balance, null);
278
+ if(swap.metadata!=null) swap.metadata.times.receivedBalanceChecked = Date.now();
279
+ } catch (e) {
280
+ this.swapLogger.error(swap, "processPastSwap(): Error not enough balance: ", e);
281
+ await this.refundSwap(swap);
282
+ return;
283
+ }
284
+
285
+ if(swap.state!==FromBtcTrustedSwapState.BTC_CONFIRMED) return;
286
+
287
+ let unlock = swap.lock(30*1000);
288
+ if(unlock==null) return;
289
+
290
+ const txns = await chainInterface.txsTransfer(signer.getAddress(), swap.token, swap.adjustedOutput, swap.dstAddress);
291
+ await chainInterface.sendAndConfirm(signer, txns, true, null, false, async (txId: string, rawTx: string) => {
292
+ swap.txIds = {init: txId};
293
+ swap.scRawTx = rawTx;
294
+ if(swap.state===FromBtcTrustedSwapState.BTC_CONFIRMED) {
295
+ await swap.setState(FromBtcTrustedSwapState.SENT);
296
+ await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
297
+ }
298
+ if(unlock!=null) unlock();
299
+ unlock = null;
300
+ });
301
+ }
302
+
303
+ if(swap.state===FromBtcTrustedSwapState.SENT) {
304
+ const txStatus = await chainInterface.getTxStatus(swap.scRawTx);
305
+ switch(txStatus) {
306
+ case "not_found":
307
+ //Retry
308
+ swap.txIds = {init: null};
309
+ swap.scRawTx = null;
310
+ await swap.setState(FromBtcTrustedSwapState.RECEIVED);
311
+ await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
312
+ break;
313
+ case "reverted":
314
+ //Cancel invoice
315
+ await this.refundSwap(swap);
316
+ this.swapLogger.info(swap, "processPastSwap(): transaction reverted, refunding btc on-chain: ", swap.btcAddress);
317
+ break;
318
+ case "success":
319
+ await swap.setState(FromBtcTrustedSwapState.CONFIRMED);
320
+ await this.storageManager.saveData(swap.getIdentifierHash(), swap.getSequence(), swap);
321
+ break;
322
+ }
323
+ }
324
+
325
+ if(swap.state===FromBtcTrustedSwapState.CONFIRMED) {
326
+ this.processedTxIds.set(swap.getIdentifierHash(), {
327
+ txId: swap.txId,
328
+ scTxId: swap.txIds.init,
329
+ adjustedAmount: swap.adjustedInput,
330
+ adjustedTotal: swap.adjustedOutput
331
+ });
332
+ if(tx.confirmations>0) await this.removeSwapData(swap, FromBtcTrustedSwapState.FINISHED);
333
+ }
334
+ }
335
+
336
+ protected async processPastSwaps(): Promise<void> {
337
+ const queriedData = await this.storageManager.query([
338
+ {
339
+ key: "state",
340
+ value: [
341
+ FromBtcTrustedSwapState.REFUNDABLE,
342
+ FromBtcTrustedSwapState.CREATED,
343
+ FromBtcTrustedSwapState.RECEIVED,
344
+ FromBtcTrustedSwapState.BTC_CONFIRMED,
345
+ FromBtcTrustedSwapState.SENT,
346
+ FromBtcTrustedSwapState.CONFIRMED
347
+ ]
348
+ }
349
+ ]);
350
+
351
+ const startingBlockheight = queriedData.reduce((prev, {obj: swap}) => Math.min(prev, swap.createdHeight), Infinity);
352
+ if(startingBlockheight===Infinity) return;
353
+ const transactions = await this.bitcoin.getWalletTransactions(startingBlockheight);
354
+
355
+ const map = new Map<string, {tx: BtcTx, vout: number}[]>();
356
+ transactions.forEach(tx => {
357
+ tx.outs.forEach((out, vout) => {
358
+ const existing = map.get(out.scriptPubKey.hex);
359
+ if(existing==null) {
360
+ map.set(out.scriptPubKey.hex, [{tx, vout}]);
361
+ } else {
362
+ existing.push({tx, vout});
363
+ }
364
+ })
365
+ });
366
+
367
+ for(let {obj: swap} of queriedData) {
368
+ const outputScript = this.bitcoin.toOutputScript(swap.btcAddress).toString("hex");
369
+ const txs = map.get(outputScript) ?? [];
370
+ try {
371
+ await this.processPastSwap(swap, txs[0]?.tx, txs[0]?.vout);
372
+ } catch (e) {
373
+ this.swapLogger.error(swap, "processPastSwaps(): Error ocurred while processing swap: ", e);
374
+ }
375
+ }
376
+ }
377
+
378
+ private isValidBitcoinAddress(address: string) {
379
+ try {
380
+ this.bitcoin.toOutputScript(address);
381
+ return true;
382
+ } catch (e) {}
383
+ return false;
384
+ }
385
+
386
+ startRestServer(restServer: Express): void {
387
+
388
+ const getAddress = expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
389
+ const metadata: {
390
+ request: any,
391
+ invoiceRequest?: any,
392
+ invoiceResponse?: any,
393
+ times: {[key: string]: number}
394
+ } = {request: {}, times: {}};
395
+
396
+ const chainIdentifier = req.query.chain as string ?? this.chains.default;
397
+ const {chainInterface, signer} = this.getChain(chainIdentifier);
398
+
399
+ metadata.times.requestReceived = Date.now();
400
+ /**
401
+ * address: string solana address of the recipient
402
+ * refundAddress?: string bitcoin address to use in case of refund
403
+ * amount: string amount (in lamports/smart chain base units) of the invoice
404
+ * exactOut: boolean whether to create and exact output swap
405
+ */
406
+ req.query.token ??= chainInterface.getNativeCurrencyAddress();
407
+ const parsedBody: FromBtcTrustedRequestType = verifySchema(req.query,{
408
+ address: (val: string) => val!=null &&
409
+ typeof(val)==="string" &&
410
+ chainInterface.isValidAddress(val) ? val : null,
411
+ refundAddress: (val: string) => val==null ? "" :
412
+ typeof(val)==="string" &&
413
+ this.isValidBitcoinAddress(val) ? val : null,
414
+ token: (val: string) => val!=null &&
415
+ typeof(val)==="string" &&
416
+ this.isTokenSupported(chainIdentifier, val) ? val : null,
417
+ amount: FieldTypeEnum.BigInt,
418
+ exactIn: (val: string) => val==="true" ? true :
419
+ (val==="false" || val===undefined) ? false : null
420
+ });
421
+ if(parsedBody==null) throw {
422
+ code: 20100,
423
+ msg: "Invalid request body"
424
+ };
425
+ metadata.request = parsedBody;
426
+
427
+ const refundAddress = parsedBody.refundAddress==="" ? null : parsedBody.refundAddress;
428
+
429
+ const requestedAmount = {input: parsedBody.exactIn, amount: parsedBody.amount, token: parsedBody.token};
430
+ const request = {
431
+ chainIdentifier,
432
+ raw: req,
433
+ parsed: parsedBody,
434
+ metadata
435
+ };
436
+ const useToken = parsedBody.token;
437
+
438
+ //Check request params
439
+ const fees = await this.AmountAssertions.preCheckFromBtcAmounts(this.type, request, requestedAmount);
440
+ metadata.times.requestChecked = Date.now();
441
+
442
+ //Create abortController for parallel prefetches
443
+ const responseStream = res.responseStream;
444
+ const abortController = getAbortController(responseStream);
445
+
446
+ //Pre-fetch data
447
+ const pricePrefetchPromise = this.swapPricing.preFetchPrice(useToken, chainIdentifier).catch(e => {
448
+ this.logger.error("pricePrefetchPromise(): pricePrefetch error: ", e);
449
+ abortController.abort(e);
450
+ return null;
451
+ });
452
+ const balancePrefetch = chainInterface.getBalance(signer.getAddress(), useToken).catch(e => {
453
+ this.logger.error("getBalancePrefetch(): balancePrefetch error: ", e);
454
+ abortController.abort(e);
455
+ return null;
456
+ });
457
+
458
+ //Check valid amount specified (min/max)
459
+ const {
460
+ amountBD,
461
+ swapFee,
462
+ swapFeeInToken,
463
+ totalInToken
464
+ } = await this.AmountAssertions.checkFromBtcAmount(this.type, request, {...requestedAmount, pricePrefetch: pricePrefetchPromise}, fees, abortController.signal);
465
+ metadata.times.priceCalculated = Date.now();
466
+
467
+ //Make sure we have MORE THAN ENOUGH to honor the swap request
468
+ await this.checkBalance(totalInToken * 4n, balancePrefetch, abortController.signal)
469
+ metadata.times.balanceChecked = Date.now();
470
+
471
+ const blockHeight = await this.bitcoin.getBlockheight();
472
+ const feeRate = await this.bitcoin.getFeeRate();
473
+ const recommendedFee = Math.ceil(feeRate*this.config.recommendFeeMultiplier);
474
+ if(recommendedFee===0) throw {
475
+ _httpStatus: 500,
476
+ code: 21100,
477
+ msg: "Cannot estimate bitcoin fee!"
478
+ };
479
+ metadata.times.feeEstimated = Date.now();
480
+
481
+ const receiveAddress = await this.bitcoin.getAddress();
482
+ const outputScript = this.bitcoin.toOutputScript(receiveAddress).toString("hex");
483
+ abortController.signal.throwIfAborted();
484
+ metadata.times.addressCreated = Date.now();
485
+
486
+ const createdSwap = new FromBtcTrustedSwap(
487
+ chainIdentifier,
488
+ swapFee,
489
+ swapFeeInToken,
490
+ receiveAddress,
491
+ amountBD,
492
+ parsedBody.address,
493
+ totalInToken,
494
+ blockHeight,
495
+ Date.now()+(this.config.swapAddressExpiry*1000),
496
+ recommendedFee,
497
+ refundAddress,
498
+ useToken
499
+ );
500
+ metadata.times.swapCreated = Date.now();
501
+ createdSwap.metadata = metadata;
502
+
503
+ await PluginManager.swapCreate(createdSwap);
504
+ await this.storageManager.saveData(createdSwap.getIdentifierHash(), createdSwap.getSequence(), createdSwap);
505
+ this.subscriptions.set(outputScript, createdSwap);
506
+
507
+ this.swapLogger.info(createdSwap, "REST: /getAddress: Created swap address: "+createdSwap.btcAddress+" amount: "+amountBD.toString(10));
508
+
509
+ res.status(200).json({
510
+ code: 10000,
511
+ msg: "Success",
512
+ data: {
513
+ paymentHash: createdSwap.getIdentifierHash(),
514
+ sequence: createdSwap.getSequence().toString(10),
515
+ btcAddress: receiveAddress,
516
+ amountSats: amountBD.toString(10),
517
+ swapFeeSats: swapFee.toString(10),
518
+ swapFee: swapFeeInToken.toString(10),
519
+ total: totalInToken.toString(10),
520
+ intermediaryKey: signer.getAddress(),
521
+ recommendedFee,
522
+ expiresAt: createdSwap.expiresAt
523
+ }
524
+ });
525
+ });
526
+ restServer.get(this.path+"/getAddress", getAddress);
527
+
528
+ const getInvoiceStatus = expressHandlerWrapper(async (req, res) => {
529
+ /**
530
+ * paymentHash: string payment hash of the invoice
531
+ * sequence: BN secret sequence for the swap,
532
+ */
533
+ const parsedBody = verifySchema(req.query, {
534
+ paymentHash: (val: string) => val!=null &&
535
+ typeof(val)==="string" &&
536
+ val.length===64 &&
537
+ HEX_REGEX.test(val) ? val: null,
538
+ sequence: FieldTypeEnum.BigInt,
539
+ });
540
+ if(parsedBody==null) throw {
541
+ code: 20100,
542
+ msg: "Invalid request"
543
+ };
544
+
545
+ const processedTxData = this.processedTxIds.get(parsedBody.paymentHash);
546
+ if(processedTxData!=null) throw {
547
+ _httpStatus: 200,
548
+ code: 10000,
549
+ msg: "Success, tx confirmed",
550
+ data: {
551
+ adjustedAmount: processedTxData.adjustedAmount.toString(10),
552
+ adjustedTotal: processedTxData.adjustedTotal.toString(10),
553
+ txId: processedTxData.txId,
554
+ scTxId: processedTxData.scTxId
555
+ }
556
+ };
557
+
558
+ const refundTxId = this.refundedSwaps.get(parsedBody.paymentHash);
559
+ if(refundTxId!=null) throw {
560
+ _httpStatus: 200,
561
+ code: 10014,
562
+ msg: "Refunded",
563
+ data: {
564
+ txId: refundTxId
565
+ }
566
+ };
567
+
568
+ const doubleSpendTxId = this.doubleSpentSwaps.get(parsedBody.paymentHash);
569
+ if(doubleSpendTxId!=null) throw {
570
+ _httpStatus: 200,
571
+ code: 10015,
572
+ msg: "Double spend detected, deposit burned",
573
+ data: {
574
+ txId: doubleSpendTxId
575
+ }
576
+ };
577
+
578
+ const invoiceData: FromBtcTrustedSwap = await this.storageManager.getData(parsedBody.paymentHash, parsedBody.sequence);
579
+ if (invoiceData==null) throw {
580
+ _httpStatus: 200,
581
+ code: 10001,
582
+ msg: "Swap expired/canceled"
583
+ };
584
+
585
+ if (invoiceData.state === FromBtcTrustedSwapState.CREATED) throw {
586
+ _httpStatus: 200,
587
+ code: 10010,
588
+ msg: "Bitcoin yet unpaid"
589
+ };
590
+
591
+ if (invoiceData.state === FromBtcTrustedSwapState.RECEIVED) throw {
592
+ _httpStatus: 200,
593
+ code: 10011,
594
+ msg: "Bitcoin received, payment processing",
595
+ data: {
596
+ adjustedAmount: invoiceData.adjustedInput.toString(10),
597
+ adjustedTotal: invoiceData.adjustedOutput.toString(10),
598
+ txId: invoiceData.txId
599
+ }
600
+ };
601
+
602
+ if (invoiceData.state === FromBtcTrustedSwapState.BTC_CONFIRMED) throw {
603
+ _httpStatus: 200,
604
+ code: 10013,
605
+ msg: "Bitcoin accepted, payment processing",
606
+ data: {
607
+ adjustedAmount: invoiceData.adjustedInput.toString(10),
608
+ adjustedTotal: invoiceData.adjustedOutput.toString(10),
609
+ txId: invoiceData.txId
610
+ }
611
+ };
612
+
613
+ if (invoiceData.state === FromBtcTrustedSwapState.SENT) throw {
614
+ _httpStatus: 200,
615
+ code: 10012,
616
+ msg: "Tx sent",
617
+ data: {
618
+ adjustedAmount: invoiceData.adjustedInput.toString(10),
619
+ adjustedTotal: invoiceData.adjustedOutput.toString(10),
620
+ txId: invoiceData.txId,
621
+ scTxId: invoiceData.txIds.init
622
+ }
623
+ };
624
+
625
+ if (invoiceData.state === FromBtcTrustedSwapState.CONFIRMED || invoiceData.state === FromBtcTrustedSwapState.FINISHED) throw {
626
+ _httpStatus: 200,
627
+ code: 10000,
628
+ msg: "Success, tx confirmed",
629
+ data: {
630
+ adjustedAmount: invoiceData.adjustedInput.toString(10),
631
+ adjustedTotal: invoiceData.adjustedOutput.toString(10),
632
+ txId: invoiceData.txId,
633
+ scTxId: invoiceData.txIds.init
634
+ }
635
+ };
636
+
637
+ if (invoiceData.state === FromBtcTrustedSwapState.REFUNDABLE) throw {
638
+ _httpStatus: 200,
639
+ code: 10016,
640
+ msg: "Refundable",
641
+ data: {
642
+ adjustedAmount: invoiceData.adjustedInput.toString(10)
643
+ }
644
+ };
645
+ });
646
+ restServer.get(this.path+"/getAddressStatus", getInvoiceStatus);
647
+
648
+ const setRefundAddress = expressHandlerWrapper(async (req, res) => {
649
+ /**
650
+ * paymentHash: string payment hash of the invoice
651
+ * sequence: BN secret sequence for the swap,
652
+ * refundAddress: string valid bitcoin address to be used for refunds
653
+ */
654
+ const parsedBody = verifySchema({...req.body, ...req.query}, {
655
+ paymentHash: (val: string) => val!=null &&
656
+ typeof(val)==="string" &&
657
+ val.length===64 &&
658
+ HEX_REGEX.test(val) ? val: null,
659
+ sequence: FieldTypeEnum.BigInt,
660
+ refundAddress: (val: string) => val!=null &&
661
+ typeof(val)==="string" &&
662
+ this.isValidBitcoinAddress(val) ? val : null
663
+ });
664
+ if(parsedBody==null) throw {
665
+ code: 20100,
666
+ msg: "Invalid request"
667
+ };
668
+
669
+ const invoiceData: FromBtcTrustedSwap = await this.storageManager.getData(parsedBody.paymentHash, null);
670
+ if (invoiceData==null || invoiceData.getSequence()!==parsedBody.sequence) throw {
671
+ code: 10001,
672
+ msg: "Swap not found"
673
+ };
674
+
675
+ if(invoiceData.refundAddress!=null) throw {
676
+ code: 10080,
677
+ msg: "Refund address already set!",
678
+ data: {
679
+ refundAddress: invoiceData.refundAddress
680
+ }
681
+ };
682
+
683
+ invoiceData.refundAddress = parsedBody.refundAddress;
684
+
685
+ if (invoiceData.state === FromBtcTrustedSwapState.REFUNDABLE) {
686
+ this.refundSwap(invoiceData).catch(e => {
687
+ this.swapLogger.error(invoiceData, "/setRefundAddress: Failed to refund!");
688
+ });
689
+ }
690
+
691
+ throw {
692
+ _httpStatus: 200,
693
+ code: 10000,
694
+ msg: "Refund address set"
695
+ };
696
+ });
697
+ restServer.get(this.path+"/setRefundAddress", setRefundAddress);
698
+ restServer.post(this.path+"/setRefundAddress", setRefundAddress);
699
+
700
+ this.logger.info("started at path: ", this.path);
701
+ }
702
+
703
+ private async checkDoubleSpends(): Promise<void> {
704
+ for(let swap of this.doubleSpendWatchdogSwaps.keys()) {
705
+ const tx = await this.bitcoin.getWalletTransaction(swap.txId);
706
+ if(tx==null) {
707
+ this.swapLogger.debug(swap, "checkDoubleSpends(): Swap was double spent, burning... - original txId: "+swap.txId);
708
+ this.processPastSwap(swap, null, null);
709
+ }
710
+ }
711
+ }
712
+
713
+ private async startDoubleSpendWatchdog() {
714
+ let rerun: () => Promise<void>;
715
+ rerun = async () => {
716
+ await this.checkDoubleSpends().catch( e => console.error(e));
717
+ setTimeout(rerun, this.config.doubleSpendCheckInterval);
718
+ };
719
+ await rerun();
720
+ }
721
+
722
+ private listenToTxns() {
723
+ this.bitcoin.subscribeToWalletTransactions((btcTx: BtcTx) => {
724
+ for(let out of btcTx.outs) {
725
+ const savedSwap = this.subscriptions.get(out.scriptPubKey.hex);
726
+ if(savedSwap==null) continue;
727
+ this.processPastSwap(savedSwap, btcTx, out.n);
728
+ return;
729
+ }
730
+ });
731
+ }
732
+
733
+ async startWatchdog() {
734
+ await super.startWatchdog();
735
+ await this.startDoubleSpendWatchdog();
736
+ }
737
+
738
+ async init(): Promise<void> {
739
+ await this.storageManager.loadData(FromBtcTrustedSwap);
740
+ this.listenToTxns();
741
+ await PluginManager.serviceInitialize(this);
742
+ }
743
+
744
+ getInfoData(): any {
745
+ return {};
746
+ }
747
+
748
748
  }