@atomiqlabs/lp-lib 17.5.1 → 17.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +201 -201
  2. package/dist/fees/IBtcFeeEstimator.d.ts +3 -3
  3. package/dist/fees/IBtcFeeEstimator.js +2 -2
  4. package/dist/index.d.ts +43 -43
  5. package/dist/index.js +59 -59
  6. package/dist/info/InfoHandler.d.ts +17 -17
  7. package/dist/info/InfoHandler.js +61 -61
  8. package/dist/plugins/IPlugin.d.ts +158 -158
  9. package/dist/plugins/IPlugin.js +35 -35
  10. package/dist/plugins/PluginManager.d.ts +115 -115
  11. package/dist/plugins/PluginManager.js +304 -304
  12. package/dist/prices/BinanceSwapPrice.d.ts +29 -29
  13. package/dist/prices/BinanceSwapPrice.js +79 -79
  14. package/dist/prices/CoinGeckoSwapPrice.d.ts +33 -33
  15. package/dist/prices/CoinGeckoSwapPrice.js +51 -51
  16. package/dist/prices/ISwapPrice.d.ts +43 -43
  17. package/dist/prices/ISwapPrice.js +55 -55
  18. package/dist/prices/OKXSwapPrice.d.ts +29 -29
  19. package/dist/prices/OKXSwapPrice.js +79 -79
  20. package/dist/storage/IIntermediaryStorage.d.ts +18 -18
  21. package/dist/storage/IIntermediaryStorage.js +2 -2
  22. package/dist/storagemanager/IntermediaryStorageManager.d.ts +19 -19
  23. package/dist/storagemanager/IntermediaryStorageManager.js +111 -111
  24. package/dist/storagemanager/StorageManager.d.ts +13 -13
  25. package/dist/storagemanager/StorageManager.js +64 -64
  26. package/dist/swaps/SwapHandler.d.ts +178 -178
  27. package/dist/swaps/SwapHandler.js +225 -225
  28. package/dist/swaps/SwapHandlerSwap.d.ts +80 -80
  29. package/dist/swaps/SwapHandlerSwap.js +78 -78
  30. package/dist/swaps/assertions/AmountAssertions.d.ts +37 -37
  31. package/dist/swaps/assertions/AmountAssertions.js +83 -83
  32. package/dist/swaps/assertions/FromBtcAmountAssertions.d.ts +76 -76
  33. package/dist/swaps/assertions/FromBtcAmountAssertions.js +189 -189
  34. package/dist/swaps/assertions/LightningAssertions.d.ts +44 -44
  35. package/dist/swaps/assertions/LightningAssertions.js +86 -86
  36. package/dist/swaps/assertions/ToBtcAmountAssertions.d.ts +53 -53
  37. package/dist/swaps/assertions/ToBtcAmountAssertions.js +154 -154
  38. package/dist/swaps/escrow/EscrowHandler.d.ts +50 -50
  39. package/dist/swaps/escrow/EscrowHandler.js +151 -151
  40. package/dist/swaps/escrow/EscrowHandlerSwap.d.ts +35 -35
  41. package/dist/swaps/escrow/EscrowHandlerSwap.js +69 -69
  42. package/dist/swaps/escrow/FromBtcBaseSwap.d.ts +14 -14
  43. package/dist/swaps/escrow/FromBtcBaseSwap.js +32 -32
  44. package/dist/swaps/escrow/FromBtcBaseSwapHandler.d.ts +102 -102
  45. package/dist/swaps/escrow/FromBtcBaseSwapHandler.js +210 -210
  46. package/dist/swaps/escrow/ToBtcBaseSwap.d.ts +36 -36
  47. package/dist/swaps/escrow/ToBtcBaseSwap.js +67 -67
  48. package/dist/swaps/escrow/ToBtcBaseSwapHandler.d.ts +53 -53
  49. package/dist/swaps/escrow/ToBtcBaseSwapHandler.js +81 -81
  50. package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.d.ts +84 -84
  51. package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +329 -329
  52. package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.d.ts +22 -22
  53. package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.js +53 -53
  54. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.d.ts +115 -115
  55. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +719 -719
  56. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.d.ts +34 -34
  57. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.js +94 -94
  58. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.d.ts +119 -119
  59. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +730 -730
  60. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.d.ts +56 -56
  61. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.js +123 -123
  62. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +170 -170
  63. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +769 -769
  64. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +29 -29
  65. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +67 -67
  66. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +178 -178
  67. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +912 -912
  68. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.d.ts +25 -25
  69. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.js +61 -61
  70. package/dist/swaps/spv_vault_swap/SpvVault.d.ts +45 -45
  71. package/dist/swaps/spv_vault_swap/SpvVault.js +148 -148
  72. package/dist/swaps/spv_vault_swap/SpvVaultSwap.d.ts +71 -71
  73. package/dist/swaps/spv_vault_swap/SpvVaultSwap.js +165 -165
  74. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.d.ts +72 -72
  75. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +710 -710
  76. package/dist/swaps/spv_vault_swap/SpvVaults.d.ts +70 -70
  77. package/dist/swaps/spv_vault_swap/SpvVaults.js +554 -554
  78. package/dist/swaps/spv_vault_swap/StickyAddress.d.ts +6 -6
  79. package/dist/swaps/spv_vault_swap/StickyAddress.js +19 -19
  80. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.d.ts +52 -52
  81. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +677 -677
  82. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.d.ts +53 -53
  83. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.js +121 -121
  84. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.d.ts +77 -77
  85. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +518 -518
  86. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.d.ts +35 -35
  87. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.js +84 -84
  88. package/dist/utils/BitcoinUtils.d.ts +4 -4
  89. package/dist/utils/BitcoinUtils.js +61 -61
  90. package/dist/utils/Utils.d.ts +32 -32
  91. package/dist/utils/Utils.js +130 -130
  92. package/dist/utils/paramcoders/IParamReader.d.ts +5 -5
  93. package/dist/utils/paramcoders/IParamReader.js +2 -2
  94. package/dist/utils/paramcoders/IParamWriter.d.ts +4 -4
  95. package/dist/utils/paramcoders/IParamWriter.js +2 -2
  96. package/dist/utils/paramcoders/LegacyParamEncoder.d.ts +10 -10
  97. package/dist/utils/paramcoders/LegacyParamEncoder.js +22 -22
  98. package/dist/utils/paramcoders/ParamDecoder.d.ts +25 -25
  99. package/dist/utils/paramcoders/ParamDecoder.js +222 -222
  100. package/dist/utils/paramcoders/ParamEncoder.d.ts +9 -9
  101. package/dist/utils/paramcoders/ParamEncoder.js +22 -22
  102. package/dist/utils/paramcoders/SchemaVerifier.d.ts +21 -21
  103. package/dist/utils/paramcoders/SchemaVerifier.js +84 -84
  104. package/dist/utils/paramcoders/server/ServerParamDecoder.d.ts +8 -8
  105. package/dist/utils/paramcoders/server/ServerParamDecoder.js +107 -107
  106. package/dist/utils/paramcoders/server/ServerParamEncoder.d.ts +11 -11
  107. package/dist/utils/paramcoders/server/ServerParamEncoder.js +65 -65
  108. package/dist/wallets/IBitcoinWallet.d.ts +155 -155
  109. package/dist/wallets/IBitcoinWallet.js +100 -100
  110. package/dist/wallets/ILightningWallet.d.ts +137 -137
  111. package/dist/wallets/ILightningWallet.js +37 -37
  112. package/dist/wallets/ISpvVaultSigner.d.ts +7 -7
  113. package/dist/wallets/ISpvVaultSigner.js +2 -2
  114. package/package.json +37 -37
  115. package/src/fees/IBtcFeeEstimator.ts +6 -6
  116. package/src/index.ts +54 -54
  117. package/src/info/InfoHandler.ts +106 -106
  118. package/src/plugins/IPlugin.ts +196 -196
  119. package/src/plugins/PluginManager.ts +389 -389
  120. package/src/prices/BinanceSwapPrice.ts +101 -101
  121. package/src/prices/CoinGeckoSwapPrice.ts +75 -75
  122. package/src/prices/ISwapPrice.ts +88 -88
  123. package/src/prices/OKXSwapPrice.ts +101 -101
  124. package/src/storage/IIntermediaryStorage.ts +19 -19
  125. package/src/storagemanager/IntermediaryStorageManager.ts +118 -118
  126. package/src/storagemanager/StorageManager.ts +78 -78
  127. package/src/swaps/SwapHandler.ts +337 -337
  128. package/src/swaps/SwapHandlerSwap.ts +143 -143
  129. package/src/swaps/assertions/AmountAssertions.ts +97 -97
  130. package/src/swaps/assertions/FromBtcAmountAssertions.ts +257 -257
  131. package/src/swaps/assertions/LightningAssertions.ts +103 -103
  132. package/src/swaps/assertions/ToBtcAmountAssertions.ts +209 -209
  133. package/src/swaps/escrow/EscrowHandler.ts +172 -172
  134. package/src/swaps/escrow/EscrowHandlerSwap.ts +86 -86
  135. package/src/swaps/escrow/FromBtcBaseSwap.ts +38 -38
  136. package/src/swaps/escrow/FromBtcBaseSwapHandler.ts +286 -286
  137. package/src/swaps/escrow/ToBtcBaseSwap.ts +85 -85
  138. package/src/swaps/escrow/ToBtcBaseSwapHandler.ts +129 -129
  139. package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +468 -468
  140. package/src/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.ts +65 -65
  141. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +900 -900
  142. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.ts +145 -145
  143. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +892 -892
  144. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.ts +200 -200
  145. package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +947 -947
  146. package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +112 -112
  147. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +1167 -1167
  148. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.ts +84 -84
  149. package/src/swaps/spv_vault_swap/SpvVault.ts +182 -182
  150. package/src/swaps/spv_vault_swap/SpvVaultSwap.ts +239 -239
  151. package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +897 -897
  152. package/src/swaps/spv_vault_swap/SpvVaults.ts +640 -640
  153. package/src/swaps/spv_vault_swap/StickyAddress.ts +20 -20
  154. package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +782 -782
  155. package/src/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.ts +189 -189
  156. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +622 -622
  157. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.ts +125 -125
  158. package/src/utils/BitcoinUtils.ts +59 -59
  159. package/src/utils/Utils.ts +152 -152
  160. package/src/utils/paramcoders/IParamReader.ts +7 -7
  161. package/src/utils/paramcoders/IParamWriter.ts +8 -8
  162. package/src/utils/paramcoders/LegacyParamEncoder.ts +27 -27
  163. package/src/utils/paramcoders/ParamDecoder.ts +218 -218
  164. package/src/utils/paramcoders/ParamEncoder.ts +29 -29
  165. package/src/utils/paramcoders/SchemaVerifier.ts +96 -96
  166. package/src/utils/paramcoders/server/ServerParamDecoder.ts +118 -118
  167. package/src/utils/paramcoders/server/ServerParamEncoder.ts +75 -75
  168. package/src/wallets/IBitcoinWallet.ts +246 -246
  169. package/src/wallets/ILightningWallet.ts +201 -201
  170. package/src/wallets/ISpvVaultSigner.ts +10 -10
@@ -1,1167 +1,1167 @@
1
- import {Express, Request, Response} from "express";
2
- import {ToBtcLnSwapAbs, ToBtcLnSwapState} from "./ToBtcLnSwapAbs";
3
- import {MultichainData, SwapHandlerType} from "../../SwapHandler";
4
- import {ISwapPrice} from "../../../prices/ISwapPrice";
5
- import {
6
- BigIntBufferUtils,
7
- ChainSwapType,
8
- ClaimEvent,
9
- InitializeEvent,
10
- RefundEvent, SwapCommitStateType,
11
- SwapData
12
- } from "@atomiqlabs/base";
13
- import {expressHandlerWrapper, getAbortController, HEX_REGEX, isDefinedRuntimeError} from "../../../utils/Utils";
14
- import {PluginManager} from "../../../plugins/PluginManager";
15
- import {IIntermediaryStorage} from "../../../storage/IIntermediaryStorage";
16
- import {randomBytes} from "crypto";
17
- import {serverParamDecoder} from "../../../utils/paramcoders/server/ServerParamDecoder";
18
- import {IParamReader} from "../../../utils/paramcoders/IParamReader";
19
- import {FieldTypeEnum, verifySchema} from "../../../utils/paramcoders/SchemaVerifier";
20
- import {ServerParamEncoder} from "../../../utils/paramcoders/server/ServerParamEncoder";
21
- import {ToBtcBaseConfig, ToBtcBaseSwapHandler} from "../ToBtcBaseSwapHandler";
22
- import {
23
- ILightningWallet,
24
- OutgoingLightningNetworkPayment,
25
- ParsedPaymentRequest,
26
- ProbeAndRouteInit,
27
- ProbeAndRouteResponse,
28
- routesMatch
29
- } from "../../../wallets/ILightningWallet";
30
- import { LightningAssertions } from "../../assertions/LightningAssertions";
31
- import {isQuoteThrow} from "../../../plugins/IPlugin";
32
- import {ToBtcSwapState} from "../tobtc_abstract/ToBtcSwapAbs";
33
-
34
- export type ToBtcLnConfig = ToBtcBaseConfig & {
35
- routingFeeMultiplier: bigint,
36
-
37
- minSendCltv: bigint,
38
-
39
- allowProbeFailedSwaps: boolean,
40
- allowShortExpiry: boolean,
41
-
42
- minLnRoutingFeePPM?: bigint,
43
- minLnBaseFee?: bigint,
44
-
45
- exactInExpiry?: number,
46
-
47
- lnSendBitcoinBlockTimeSafetyFactorPPM?: bigint
48
- };
49
-
50
- type ExactInAuthorization = {
51
- chainIdentifier: string,
52
- reqId: string,
53
- expiry: number,
54
-
55
- amount: bigint,
56
- initialInvoice: ParsedPaymentRequest,
57
-
58
- quotedNetworkFeeInToken: bigint,
59
- swapFeeInToken: bigint,
60
- total: bigint,
61
- confidence: number,
62
- quotedNetworkFee: bigint,
63
- swapFee: bigint,
64
-
65
- token: string,
66
- swapExpiry: bigint,
67
- offerer: string,
68
-
69
- preFetchSignData: any,
70
- metadata: {
71
- request: any,
72
- probeRequest?: any,
73
- probeResponse?: any,
74
- routeResponse?: any,
75
- times: {[key: string]: number}
76
- }
77
- }
78
-
79
- export type ToBtcLnRequestType = {
80
- pr: string,
81
- maxFee: bigint,
82
- expiryTimestamp: bigint,
83
- token: string,
84
- offerer: string,
85
- exactIn?: boolean,
86
- amount?: bigint
87
- };
88
-
89
- /**
90
- * Swap handler handling to BTCLN swaps using submarine swaps
91
- */
92
- export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwapState> {
93
- readonly type = SwapHandlerType.TO_BTCLN;
94
- readonly swapType = ChainSwapType.HTLC;
95
- readonly inflightSwapStates = new Set([ToBtcLnSwapState.COMMITED, ToBtcLnSwapState.PAID]);
96
-
97
- activeSubscriptions: Set<string> = new Set<string>();
98
-
99
- readonly config: ToBtcLnConfig & {minTsSendCltv: bigint};
100
-
101
- readonly exactInAuths: {
102
- [reqId: string]: ExactInAuthorization
103
- } = {};
104
-
105
- readonly lightning: ILightningWallet;
106
- readonly LightningAssertions: LightningAssertions;
107
-
108
- constructor(
109
- storageDirectory: IIntermediaryStorage<ToBtcLnSwapAbs>,
110
- path: string,
111
- chainData: MultichainData,
112
- lightning: ILightningWallet,
113
- swapPricing: ISwapPrice,
114
- config: ToBtcLnConfig
115
- ) {
116
- super(storageDirectory, path, chainData, swapPricing, config);
117
- this.lightning = lightning;
118
- this.LightningAssertions = new LightningAssertions(this.logger, lightning);
119
- const anyConfig = config as any;
120
- anyConfig.minTsSendCltv = config.gracePeriod + (config.bitcoinBlocktime * config.minSendCltv * config.safetyFactor);
121
- this.config = anyConfig;
122
- this.config.minLnRoutingFeePPM = this.config.minLnRoutingFeePPM || 1000n;
123
- this.config.minLnBaseFee = this.config.minLnBaseFee || 5n;
124
- this.config.exactInExpiry = this.config.exactInExpiry || 10*1000;
125
- this.config.lnSendBitcoinBlockTimeSafetyFactorPPM = this.config.lnSendBitcoinBlockTimeSafetyFactorPPM ?? (this.config.safetyFactor * 1_000_000n);
126
- if(this.config.lnSendBitcoinBlockTimeSafetyFactorPPM <= 1_100_000n) {
127
- throw new Error("Lightning network send block safety factor set below 1.1, this is insecure!");
128
- }
129
- }
130
-
131
- /**
132
- * Cleans up exactIn authorization that are already past their expiry
133
- *
134
- * @protected
135
- */
136
- private cleanExpiredExactInAuthorizations() {
137
- for(let key in this.exactInAuths) {
138
- const obj = this.exactInAuths[key];
139
- if(obj.expiry<Date.now()) {
140
- this.logger.info("cleanExpiredExactInAuthorizations(): remove expired authorization, reqId: "+key);
141
- delete this.exactInAuths[key];
142
- }
143
- }
144
- }
145
-
146
- protected async processPastSwap(swap: ToBtcLnSwapAbs): Promise<void> {
147
- const {swapContract, signer} = this.getChain(swap.chainIdentifier);
148
-
149
- if (swap.state === ToBtcLnSwapState.SAVED) {
150
- //Cancel the swaps where signature is expired
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, invoice: "+swap.pr);
156
- await this.removeSwapData(swap, ToBtcLnSwapState.CANCELED);
157
- return;
158
- } else {
159
- this.swapLogger.info(swap, "processPastSwap(state=SAVED): swap committed (detected from processPastSwap), invoice: "+swap.pr);
160
- await swap.setState(ToBtcLnSwapState.COMMITED);
161
- await this.saveSwapData(swap);
162
- }
163
- }
164
- }
165
-
166
- if (swap.state === ToBtcLnSwapState.COMMITED || swap.state === ToBtcLnSwapState.PAID) {
167
- //Process swaps in commited & paid state
168
- await this.processInitialized(swap);
169
- }
170
-
171
- if (swap.state === ToBtcLnSwapState.NON_PAYABLE) {
172
- //Remove expired swaps (as these can already be unilaterally refunded by the client), so we don't need
173
- // to be able to cooperatively refund them
174
- if(await swapContract.isExpired(swap.data.getOfferer(), swap.data)) {
175
- this.swapLogger.info(swap, "processPastSwap(state=NON_PAYABLE): swap expired, removing swap data, invoice: "+swap.pr);
176
- await this.removeSwapData(swap);
177
- }
178
- }
179
- }
180
-
181
- /**
182
- * Checks past swaps, deletes ones that are already expired, and tries to process ones that are committed.
183
- */
184
- protected async processPastSwaps() {
185
- this.cleanExpiredExactInAuthorizations();
186
-
187
- const queriedData = await this.storageManager.query([
188
- {
189
- key: "state",
190
- value: [
191
- ToBtcLnSwapState.SAVED,
192
- ToBtcLnSwapState.COMMITED,
193
- ToBtcLnSwapState.PAID,
194
- ToBtcLnSwapState.NON_PAYABLE
195
- ]
196
- }
197
- ]);
198
-
199
- for(let {obj: swap} of queriedData) {
200
- await this.processPastSwap(swap);
201
- }
202
- }
203
-
204
- /**
205
- * Tries to claim the swap funds on the SC side, returns false if the swap is already locked (claim tx is already being sent)
206
- *
207
- * @param swap
208
- * @private
209
- * @returns Whether the transaction was successfully sent
210
- */
211
- private async tryClaimSwap(swap: ToBtcLnSwapAbs): Promise<boolean> {
212
- if(swap.secret==null) throw new Error("Invalid swap state, needs payment pre-image!");
213
-
214
- const {swapContract, signer} = this.getChain(swap.chainIdentifier);
215
-
216
- //Check if escrow state exists
217
- const isCommited = await swapContract.isCommited(swap.data);
218
- if(!isCommited) {
219
- const status = await swapContract.getCommitStatus(signer.getAddress(), swap.data);
220
- if(status?.type===SwapCommitStateType.PAID) {
221
- //This is alright, we got the money
222
- swap.txIds ??= {};
223
- swap.txIds.claim = await status.getClaimTxId();
224
- await this.removeSwapData(swap, ToBtcLnSwapState.CLAIMED);
225
- return true;
226
- } else if(status?.type===SwapCommitStateType.EXPIRED) {
227
- //This means the user was able to refund before we were able to claim, no good
228
- swap.txIds ??= {};
229
- swap.txIds.refund = status.getRefundTxId==null ? null : await status.getRefundTxId();
230
- await this.removeSwapData(swap, ToBtcLnSwapState.REFUNDED);
231
- }
232
- this.swapLogger.warn(swap, "processPaymentResult(): tried to claim but escrow doesn't exist anymore,"+
233
- " status: "+status+
234
- " invoice: "+swap.pr);
235
- return false;
236
- }
237
-
238
- //Set flag that we are sending the transaction already, so we don't end up with race condition
239
- const unlock: () => boolean = swap.lock(swapContract.claimWithSecretTimeout);
240
- if(unlock==null) return false;
241
-
242
- try {
243
- this.swapLogger.debug(swap, "tryClaimSwap(): initiate claim of swap, secret: "+swap.secret);
244
- const success = await swapContract.claimWithSecret(signer, swap.data, swap.secret, false, false, {
245
- waitForConfirmation: true
246
- });
247
- this.swapLogger.info(swap, "tryClaimSwap(): swap claimed successfully, secret: "+swap.secret+" invoice: "+swap.pr);
248
- if(swap.metadata!=null) swap.metadata.times.txClaimed = Date.now();
249
- unlock();
250
- return true;
251
- } catch (e) {
252
- this.swapLogger.error(swap, "tryClaimSwap(): error occurred claiming swap, secret: "+swap.secret+" invoice: "+swap.pr, e);
253
- return false;
254
- }
255
- }
256
-
257
- /**
258
- * Process the result of attempted lightning network payment
259
- *
260
- * @param swap
261
- * @param lnPaymentStatus
262
- */
263
- private async processPaymentResult(swap: ToBtcLnSwapAbs, lnPaymentStatus: OutgoingLightningNetworkPayment) {
264
- switch(lnPaymentStatus.status) {
265
- case "pending":
266
- return;
267
-
268
- case "failed":
269
- this.swapLogger.info(swap, "processPaymentResult(): invoice payment failed, cancelling swap, invoice: "+swap.pr);
270
- await swap.setState(ToBtcLnSwapState.NON_PAYABLE);
271
- await this.saveSwapData(swap);
272
- return;
273
-
274
- case "confirmed":
275
- swap.secret = lnPaymentStatus.secret;
276
- swap.setRealNetworkFee(lnPaymentStatus.feeMtokens / 1000n);
277
- this.swapLogger.info(swap, "processPaymentResult(): invoice paid, secret: "+swap.secret+" realRoutingFee: "+swap.realNetworkFee.toString(10)+" invoice: "+swap.pr);
278
- await swap.setState(ToBtcLnSwapState.PAID);
279
- await this.saveSwapData(swap);
280
-
281
- const success = await this.tryClaimSwap(swap);
282
- if(success) this.swapLogger.info(swap, "processPaymentResult(): swap claimed successfully, invoice: "+swap.pr);
283
- return;
284
-
285
- default:
286
- throw new Error("Invalid lnPaymentStatus");
287
- }
288
- }
289
-
290
- /**
291
- * Subscribe to a pending lightning network payment attempt
292
- *
293
- * @param invoiceData
294
- */
295
- private subscribeToPayment(invoiceData: ToBtcLnSwapAbs): boolean {
296
- const paymentHash = invoiceData.lnPaymentHash;
297
- if(this.activeSubscriptions.has(paymentHash)) return false;
298
-
299
- this.lightning.waitForPayment(paymentHash).then(result => {
300
- this.swapLogger.info(invoiceData, "subscribeToPayment(): result callback, outcome: "+result.status+" invoice: "+invoiceData.pr);
301
- this.processPaymentResult(invoiceData, result).catch(e => this.swapLogger.error(invoiceData, "subscribeToPayment(): process payment result", e));
302
- this.activeSubscriptions.delete(paymentHash);
303
- });
304
- this.swapLogger.info(invoiceData, "subscribeToPayment(): subscribe to payment outcome, invoice: "+invoiceData.pr);
305
-
306
- this.activeSubscriptions.add(paymentHash);
307
- return true;
308
- }
309
-
310
- private async sendLightningPayment(swap: ToBtcLnSwapAbs): Promise<boolean> {
311
- const decodedPR = await this.lightning.parsePaymentRequest(swap.pr);
312
- const expiryTimestamp: bigint = swap.data.getExpiry();
313
- const currentTimestamp: bigint = BigInt(Math.floor(Date.now()/1000));
314
-
315
- //Run checks
316
- const hasEnoughTimeToPay = (expiryTimestamp - currentTimestamp) >= this.config.minTsSendCltv;
317
- if(!hasEnoughTimeToPay) throw {
318
- code: 90005,
319
- msg: "Not enough time to reliably pay the invoice"
320
- }
321
-
322
- const isInvoiceExpired = decodedPR.expiryEpochMillis < Date.now();
323
- if (isInvoiceExpired) throw {
324
- code: 90006,
325
- msg: "Invoice already expired"
326
- };
327
-
328
- //Compute max cltv delta
329
- const maxFee = swap.quotedNetworkFee;
330
- const maxUsableCLTVdelta = (expiryTimestamp - currentTimestamp - this.config.gracePeriod)
331
- / (this.config.bitcoinBlocktime * this.config.lnSendBitcoinBlockTimeSafetyFactorPPM / 1_000_000n);
332
-
333
- //Initiate payment
334
- this.swapLogger.info(swap, "sendLightningPayment(): paying lightning network invoice,"+
335
- " cltvDelta: "+maxUsableCLTVdelta.toString(10)+
336
- " maxFee: "+maxFee.toString(10)+
337
- " invoice: "+swap.pr);
338
-
339
- const blockHeight = await this.lightning.getBlockheight();
340
-
341
- const pluginCheckResult = await PluginManager.onHandlePreToBtcExecute(
342
- SwapHandlerType.TO_BTCLN,
343
- swap
344
- );
345
- if(isQuoteThrow(pluginCheckResult)) {
346
- throw {
347
- code: 29999,
348
- msg: pluginCheckResult.message
349
- };
350
- }
351
-
352
- if(swap.state!==ToBtcLnSwapState.COMMITED) return false;
353
-
354
- swap.payInitiated = true;
355
- await this.saveSwapData(swap);
356
- try {
357
- await this.lightning.pay({
358
- request: swap.pr,
359
- maxFeeMtokens: maxFee * 1000n,
360
- maxTimeoutHeight: blockHeight+Number(maxUsableCLTVdelta)
361
- })
362
- } catch (e) {
363
- throw {
364
- code: 90007,
365
- msg: "Failed to initiate invoice payment",
366
- data: {
367
- error: JSON.stringify(e)
368
- }
369
- }
370
- }
371
- if(swap.metadata!=null) swap.metadata.times.payComplete = Date.now();
372
-
373
- return true;
374
- }
375
-
376
- /**
377
- * Begins a lightning network payment attempt, if not attempted already
378
- *
379
- * @param swap
380
- */
381
- private async processInitialized(swap: ToBtcLnSwapAbs) {
382
- //Check if payment was already made
383
- if(swap.state===ToBtcLnSwapState.PAID) {
384
- const success = await this.tryClaimSwap(swap);
385
- if(success) this.swapLogger.info(swap, "processInitialized(): swap claimed successfully, invoice: "+swap.pr);
386
- return;
387
- }
388
-
389
- if(swap.state===ToBtcLnSwapState.COMMITED) {
390
- if(swap.metadata!=null) swap.metadata.times.payPaymentChecked = Date.now();
391
- let lnPaymentStatus = await this.lightning.getPayment(swap.lnPaymentHash);
392
- if(lnPaymentStatus!=null) {
393
- if(lnPaymentStatus.status==="pending") {
394
- //Payment still ongoing, process the result
395
- this.subscribeToPayment(swap);
396
- return;
397
- } else {
398
- //Payment has already concluded, process the result
399
- await this.processPaymentResult(swap, lnPaymentStatus);
400
- return;
401
- }
402
- } else {
403
- //Payment not founds, try to process again
404
- await swap.setState(ToBtcLnSwapState.SAVED);
405
- }
406
- }
407
-
408
- if(swap.state===ToBtcLnSwapState.SAVED) {
409
- try {
410
- this.checkTooManyInflightSwaps();
411
- } catch (e) {
412
- this.swapLogger.error(swap, "processInitialized(): checking too many inflight swaps error: ", e);
413
- if(isDefinedRuntimeError(e)) {
414
- if(swap.metadata!=null) swap.metadata.payError = e;
415
- await swap.setState(ToBtcLnSwapState.NON_PAYABLE);
416
- await this.saveSwapData(swap);
417
- return;
418
- } else throw e;
419
- }
420
-
421
- await swap.setState(ToBtcLnSwapState.COMMITED);
422
- await this.saveSwapData(swap);
423
- try {
424
- if(!await this.sendLightningPayment(swap)) return;
425
- } catch (e) {
426
- this.swapLogger.error(swap, "processInitialized(): lightning payment error", e);
427
- if(isDefinedRuntimeError(e)) {
428
- if(swap.metadata!=null) swap.metadata.payError = e;
429
- await swap.setState(ToBtcLnSwapState.NON_PAYABLE);
430
- await this.saveSwapData(swap);
431
- return;
432
- } else throw e;
433
- }
434
- this.subscribeToPayment(swap);
435
- return;
436
- }
437
- }
438
-
439
- protected async processInitializeEvent(chainIdentifier: string, swap: ToBtcLnSwapAbs, event: InitializeEvent<SwapData>): Promise<void> {
440
- this.swapLogger.info(swap, "SC: InitializeEvent: swap initialized by the client, invoice: "+swap.pr);
441
-
442
- //Only process swaps in SAVED state
443
- if(swap.state!==ToBtcLnSwapState.SAVED) return;
444
- await this.processInitialized(swap);
445
- }
446
-
447
- protected async processClaimEvent(chainIdentifier: string, swap: ToBtcLnSwapAbs, event: ClaimEvent<SwapData>): Promise<void> {
448
- this.swapLogger.info(swap, "SC: ClaimEvent: swap claimed to us, secret: "+event.result+" invoice: "+swap.pr);
449
-
450
- await this.removeSwapData(swap, ToBtcLnSwapState.CLAIMED);
451
- }
452
-
453
- protected async processRefundEvent(chainIdentifier: string, swap: ToBtcLnSwapAbs, event: RefundEvent<SwapData>): Promise<void> {
454
- this.swapLogger.info(swap, "SC: RefundEvent: swap refunded back to the client, invoice: "+swap.pr);
455
-
456
- await this.removeSwapData(swap, ToBtcLnSwapState.REFUNDED);
457
- }
458
-
459
- /**
460
- * Checks if the amount was supplied in the exactIn request
461
- *
462
- * @param amount
463
- * @param exactIn
464
- * @throws {DefinedRuntimeError} will throw an error if the swap was exactIn, but amount not specified
465
- */
466
- private checkAmount(amount: bigint, exactIn: boolean): void {
467
- if(exactIn) {
468
- if(amount==null) {
469
- throw {
470
- code: 20040,
471
- msg: "Invalid request body (amount not specified)!"
472
- };
473
- }
474
- }
475
- }
476
-
477
- /**
478
- * Checks if the maxFee parameter is in valid range (>0)
479
- *
480
- * @param maxFee
481
- * @throws {DefinedRuntimeError} will throw an error if the maxFee is zero or negative
482
- */
483
- private checkMaxFee(maxFee: bigint): void {
484
- if(maxFee <= 0) {
485
- throw {
486
- code: 20030,
487
- msg: "Invalid request body (maxFee too low)!"
488
- };
489
- }
490
- }
491
-
492
- /**
493
- * Checks and parses a payment request (bolt11 invoice), additionally also checks expiration time of the invoice
494
- *
495
- * @param chainIdentifier
496
- * @param pr
497
- * @throws {DefinedRuntimeError} will throw an error if the pr is invalid, without amount or expired
498
- */
499
- private async checkPaymentRequest(chainIdentifier: string, pr: string): Promise<{
500
- parsedPR: ParsedPaymentRequest,
501
- halfConfidence: boolean
502
- }> {
503
- let parsedPR: ParsedPaymentRequest;
504
-
505
- try {
506
- parsedPR = await this.lightning.parsePaymentRequest(pr);
507
- } catch (e) {
508
- throw {
509
- code: 20021,
510
- msg: "Invalid request body (pr - cannot be parsed)"
511
- };
512
- }
513
-
514
- if(parsedPR.mtokens==null) throw {
515
- code: 20022,
516
- msg: "Invalid request body (pr - needs to have amount)"
517
- };
518
-
519
- let halfConfidence = false;
520
- if(parsedPR.expiryEpochMillis < Date.now()+((this.getInitAuthorizationTimeout(chainIdentifier)+(2*60))*1000) ) {
521
- if(!this.config.allowShortExpiry) {
522
- throw {
523
- code: 20020,
524
- msg: "Invalid request body (pr - expired)"
525
- };
526
- } else if(parsedPR.expiryEpochMillis < Date.now()) {
527
- throw {
528
- code: 20020,
529
- msg: "Invalid request body (pr - expired)"
530
- };
531
- }
532
- halfConfidence = true;
533
- }
534
-
535
- return {parsedPR, halfConfidence};
536
- }
537
-
538
- /**
539
- * Checks if the request specified too short of an expiry
540
- *
541
- * @param expiryTimestamp
542
- * @param currentTimestamp
543
- * @throws {DefinedRuntimeError} will throw an error if the expiry time is too short
544
- */
545
- private checkExpiry(expiryTimestamp: bigint, currentTimestamp: bigint): void {
546
- const expiresTooSoon = (expiryTimestamp - currentTimestamp) < this.config.minTsSendCltv;
547
- if(expiresTooSoon) {
548
- throw {
549
- code: 20001,
550
- msg: "Expiry time too low!"
551
- };
552
- }
553
- }
554
-
555
- /**
556
- * Estimates the routing fee & confidence by either probing or routing (if probing fails), the fee is also adjusted
557
- * according to routing fee multiplier, and subject to minimums set in config
558
- *
559
- * @param amountBD
560
- * @param maxFee
561
- * @param expiryTimestamp
562
- * @param currentTimestamp
563
- * @param pr
564
- * @param metadata
565
- * @param abortSignal
566
- * @throws {DefinedRuntimeError} will throw an error if the destination is unreachable
567
- */
568
- private async checkAndGetNetworkFee(amountBD: bigint, maxFee: bigint, expiryTimestamp: bigint, currentTimestamp: bigint, pr: string, metadata: any, abortSignal: AbortSignal): Promise<{
569
- confidence: number,
570
- networkFee: bigint
571
- }> {
572
- const maxUsableCLTV: bigint = (expiryTimestamp - currentTimestamp - this.config.gracePeriod)
573
- / (this.config.bitcoinBlocktime * this.config.lnSendBitcoinBlockTimeSafetyFactorPPM / 1_000_000n);
574
-
575
- const blockHeight = await this.lightning.getBlockheight();
576
- abortSignal.throwIfAborted();
577
- metadata.times.blockheightFetched = Date.now();
578
- metadata.probeAndRouteTimeoutDelta = maxUsableCLTV;
579
-
580
- const maxTimeoutBlockheight = BigInt(blockHeight) + maxUsableCLTV;
581
-
582
- const req: ProbeAndRouteInit = {
583
- request: pr,
584
- amountMtokens: amountBD * 1000n,
585
- maxFeeMtokens: maxFee * 1000n,
586
- maxTimeoutHeight: Number(maxTimeoutBlockheight)
587
- };
588
-
589
- this.logger.debug(`checkAndGetNetworkFee(): Attempting to probe/route the payment amount: ${amountBD.toString(10)}`+
590
- `, maxFee: ${maxFee.toString(10)}, expiryDelta: ${maxUsableCLTV.toString(10)}, expiryHeight: ${maxTimeoutBlockheight.toString(10)}, pr: ${pr}`);
591
-
592
- let probeOrRouteResp: ProbeAndRouteResponse = await this.lightning.probe(req);
593
- metadata.times.probeResult = Date.now();
594
- metadata.probeResponse = {...probeOrRouteResp};
595
- abortSignal.throwIfAborted();
596
-
597
- if(probeOrRouteResp==null) {
598
- if(!this.config.allowProbeFailedSwaps) throw {
599
- code: 20002,
600
- msg: "Cannot route the payment!"
601
- };
602
-
603
- const routeResp = await this.lightning.route(req);
604
- metadata.times.routingResult = Date.now();
605
- metadata.routeResponse = {...routeResp};
606
- abortSignal.throwIfAborted();
607
-
608
- if(routeResp==null) throw {
609
- code: 20002,
610
- msg: "Cannot route the payment!"
611
- };
612
-
613
- this.logger.info("checkAndGetNetworkFee(): routing result,"+
614
- " destination: "+routeResp.destination+
615
- " confidence: "+routeResp.confidence+
616
- " fee mtokens: "+routeResp.feeMtokens.toString(10));
617
-
618
- probeOrRouteResp = routeResp;
619
- } else {
620
- this.logger.info("checkAndGetNetworkFee(): route probed,"+
621
- " destination: "+probeOrRouteResp.destination+
622
- " confidence: "+probeOrRouteResp.confidence+
623
- " fee mtokens: "+probeOrRouteResp.feeMtokens.toString(10));
624
- }
625
-
626
- const safeFeeTokens = (probeOrRouteResp.feeMtokens + 999n) / 1000n;
627
-
628
- let actualRoutingFee: bigint = safeFeeTokens * this.config.routingFeeMultiplier;
629
-
630
- const minRoutingFee: bigint = (amountBD * this.config.minLnRoutingFeePPM / 1000000n) + this.config.minLnBaseFee;
631
- if(actualRoutingFee < minRoutingFee) {
632
- actualRoutingFee = minRoutingFee;
633
- if(actualRoutingFee > maxFee) {
634
- probeOrRouteResp.confidence = 0;
635
- }
636
- }
637
-
638
- if(actualRoutingFee > maxFee) {
639
- actualRoutingFee = maxFee;
640
- }
641
-
642
- this.logger.debug("checkAndGetNetworkFee(): network fee calculated, amount: "+amountBD.toString(10)+" fee: "+actualRoutingFee.toString(10));
643
-
644
- return {
645
- networkFee: actualRoutingFee,
646
- confidence: probeOrRouteResp.confidence
647
- };
648
- }
649
-
650
- /**
651
- * Checks and consumes (deletes & returns) exactIn authorizaton with a specific reqId
652
- *
653
- * @param reqId
654
- * @throws {DefinedRuntimeError} will throw an error if the authorization doesn't exist
655
- */
656
- private checkExactInAuthorization(reqId: string): ExactInAuthorization {
657
- const parsedAuth = this.exactInAuths[reqId];
658
- if (parsedAuth==null) {
659
- throw {
660
- code: 20070,
661
- msg: "Invalid reqId"
662
- };
663
- }
664
- delete this.exactInAuths[reqId];
665
- if(parsedAuth.expiry<Date.now()) {
666
- throw {
667
- code: 20200,
668
- msg: "Authorization already expired!"
669
- };
670
- }
671
- return parsedAuth;
672
- }
673
-
674
- /**
675
- * Checks if the newly submitted PR has the same parameters (destination, cltv_delta, routes) as the initial dummy
676
- * invoice sent for exactIn swap quote
677
- *
678
- * @param parsedRequest
679
- * @param parsedAuth
680
- */
681
- private isPaymentRequestMatchingInitial(parsedRequest: ParsedPaymentRequest, parsedAuth: ExactInAuthorization): boolean {
682
- return parsedRequest.destination===parsedAuth.initialInvoice.destination &&
683
- parsedRequest.cltvDelta===parsedAuth.initialInvoice.cltvDelta &&
684
- routesMatch(parsedRequest.routes, parsedAuth.initialInvoice.routes);
685
- }
686
-
687
- startRestServer(restServer: Express) {
688
-
689
- restServer.use(this.path+"/payInvoiceExactIn", serverParamDecoder(10*1000));
690
- restServer.post(this.path+"/payInvoiceExactIn", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
691
- /**
692
- * pr: string bolt11 lightning invoice
693
- * reqId: string Identifier of the swap
694
- * feeRate: string Fee rate to use for the init tx
695
- */
696
- const parsedBody = await req.paramReader.getParams({
697
- pr: FieldTypeEnum.String,
698
- reqId: FieldTypeEnum.String,
699
- feeRate: FieldTypeEnum.String
700
- });
701
- if (parsedBody==null) {
702
- throw {
703
- code: 20100,
704
- msg: "Invalid request body"
705
- };
706
- }
707
-
708
- this.checkTooManyInflightSwaps();
709
- const parsedAuth = this.checkExactInAuthorization(parsedBody.reqId);
710
-
711
- const responseStream = res.responseStream;
712
- const abortSignal = responseStream.getAbortSignal();
713
-
714
- //Check request params
715
- const {parsedPR, halfConfidence} = await this.checkPaymentRequest(parsedAuth.chainIdentifier, parsedBody.pr);
716
- if(parsedPR.mtokens!==parsedAuth.amount*1000n) throw {
717
- code: 20102,
718
- msg: "Provided PR doesn't match requested (amount)!"
719
- };
720
- if(!this.isPaymentRequestMatchingInitial(parsedPR, parsedAuth)) {
721
- //The provided payment request doesn't match the parameters from the initial one, try to probe/route again
722
- // with the same max fee parameters
723
- const currentTimestamp: bigint = BigInt(Math.floor(Date.now()/1000));
724
- const {networkFee, confidence} = await this.checkAndGetNetworkFee(
725
- parsedAuth.amount, parsedAuth.quotedNetworkFee, parsedAuth.swapExpiry,
726
- currentTimestamp, parsedBody.pr, parsedAuth.metadata, abortSignal
727
- );
728
- this.logger.info("REST: /payInvoiceExactIn: re-checked network fee for exact-in swap,"+
729
- " reqId: "+parsedBody.reqId+
730
- " initialNetworkFee: "+parsedAuth.quotedNetworkFee.toString(10)+
731
- " newNetworkFee: "+networkFee.toString(10)+
732
- " oldConfidence: "+parsedAuth.confidence.toString(10)+
733
- " newConfidence: "+confidence.toString(10)+
734
- " invoice: "+parsedBody.pr);
735
- parsedAuth.confidence = confidence;
736
- }
737
-
738
- const metadata = parsedAuth.metadata;
739
-
740
- const sequence = BigIntBufferUtils.fromBuffer(randomBytes(8));
741
-
742
- const {swapContract, signer} = this.getChain(parsedAuth.chainIdentifier);
743
- const claimHash = swapContract.getHashForHtlc(Buffer.from(parsedPR.id, "hex"))
744
-
745
- //Create swap data
746
- const payObject: SwapData = await swapContract.createSwapData(
747
- ChainSwapType.HTLC,
748
- parsedAuth.offerer,
749
- signer.getAddress(),
750
- parsedAuth.token,
751
- parsedAuth.total,
752
- claimHash.toString("hex"),
753
- sequence,
754
- parsedAuth.swapExpiry,
755
- true,
756
- false,
757
- 0n,
758
- 0n
759
- );
760
- metadata.times.swapCreated = Date.now();
761
-
762
- //Sign swap data
763
- const prefetchedSignData = parsedAuth.preFetchSignData;
764
- const sigData = await this.getToBtcSignatureData(parsedAuth.chainIdentifier, payObject, req, abortSignal, prefetchedSignData);
765
- metadata.times.swapSigned = Date.now();
766
-
767
- //Create swap
768
- const createdSwap = new ToBtcLnSwapAbs(
769
- parsedAuth.chainIdentifier,
770
- parsedPR.id,
771
- parsedBody.pr,
772
- parsedPR.mtokens,
773
- parsedAuth.swapFee,
774
- parsedAuth.swapFeeInToken,
775
- parsedAuth.quotedNetworkFee,
776
- parsedAuth.quotedNetworkFeeInToken
777
- );
778
- createdSwap.data = payObject;
779
- createdSwap.metadata = metadata;
780
- createdSwap.prefix = sigData.prefix;
781
- createdSwap.timeout = sigData.timeout;
782
- createdSwap.signature = sigData.signature
783
- createdSwap.feeRate = sigData.feeRate;
784
-
785
- await PluginManager.swapCreate(createdSwap);
786
- await this.saveSwapData(createdSwap);
787
-
788
- this.swapLogger.info(createdSwap, "REST: /payInvoiceExactIn: created exact in swap,"+
789
- " reqId: "+parsedBody.reqId+
790
- " mtokens: "+parsedPR.mtokens.toString(10)+
791
- " invoice: "+createdSwap.pr);
792
-
793
- await responseStream.writeParamsAndEnd({
794
- code: 20000,
795
- msg: "Success",
796
- data: {
797
- maxFee: parsedAuth.quotedNetworkFeeInToken.toString(10),
798
- swapFee: parsedAuth.swapFeeInToken.toString(10),
799
- total: parsedAuth.total.toString(10),
800
- confidence: halfConfidence ? parsedAuth.confidence/2000000 : parsedAuth.confidence/1000000,
801
- address: signer.getAddress(),
802
-
803
- routingFeeSats: parsedAuth.quotedNetworkFee.toString(10),
804
-
805
- data: payObject.serialize(),
806
-
807
- prefix: sigData.prefix,
808
- timeout: sigData.timeout,
809
- signature: sigData.signature
810
- }
811
- });
812
-
813
- }));
814
-
815
- restServer.use(this.path+"/payInvoice", serverParamDecoder(10*1000));
816
- restServer.post(this.path+"/payInvoice", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
817
- const metadata: {
818
- request: any,
819
- probeRequest?: any,
820
- probeResponse?: any,
821
- routeResponse?: any,
822
- times: {[key: string]: number}
823
- } = {request: {}, times: {}};
824
-
825
- const chainIdentifier = req.query.chain as string;
826
- const {swapContract, signer, chainInterface} = this.getChain(chainIdentifier);
827
-
828
- metadata.times.requestReceived = Date.now();
829
- /**
830
- *Sent initially:
831
- * pr: string bolt11 lightning invoice
832
- * maxFee: string maximum routing fee
833
- * expiryTimestamp: string expiry timestamp of the to be created HTLC, determines how many LN paths can be considered
834
- * token: string Desired token to use
835
- * offerer: string Address of the caller
836
- * exactIn: boolean Whether to do an exact in swap instead of exact out
837
- * amount: string Input amount for exactIn swaps
838
- *
839
- *Sent later:
840
- * feeRate: string Fee rate to use for the init signature
841
- */
842
- const parsedBody: ToBtcLnRequestType = await req.paramReader.getParams({
843
- pr: FieldTypeEnum.String,
844
- maxFee: FieldTypeEnum.BigInt,
845
- expiryTimestamp: FieldTypeEnum.BigInt,
846
- token: (val: string) => val!=null &&
847
- typeof(val)==="string" &&
848
- this.isTokenSupported(chainIdentifier, val) ? val : null,
849
- offerer: (val: string) => val!=null &&
850
- typeof(val)==="string" &&
851
- chainInterface.isValidAddress(val, true) ? val : null,
852
- exactIn: FieldTypeEnum.BooleanOptional,
853
- amount: FieldTypeEnum.BigIntOptional
854
- });
855
- if (parsedBody==null) {
856
- throw {
857
- code: 20100,
858
- msg: "Invalid request body"
859
- };
860
- }
861
- metadata.request = parsedBody;
862
-
863
- const request = {
864
- chainIdentifier,
865
- raw: req,
866
- parsed: parsedBody,
867
- metadata
868
- };
869
- const useToken = parsedBody.token;
870
-
871
- const responseStream = res.responseStream;
872
-
873
- const currentTimestamp: bigint = BigInt(Math.floor(Date.now()/1000));
874
-
875
- //Check request params
876
- this.checkTooManyInflightSwaps();
877
- this.checkAmount(parsedBody.amount, parsedBody.exactIn);
878
- this.checkMaxFee(parsedBody.maxFee);
879
- this.checkExpiry(parsedBody.expiryTimestamp, currentTimestamp);
880
- await this.checkVaultInitialized(chainIdentifier, parsedBody.token);
881
- const {parsedPR, halfConfidence} = await this.checkPaymentRequest(chainIdentifier, parsedBody.pr);
882
- const requestedAmount = {
883
- input: !!parsedBody.exactIn,
884
- amount: !!parsedBody.exactIn ? parsedBody.amount : (parsedPR.mtokens + 999n) / 1000n,
885
- token: useToken
886
- };
887
- const fees = await this.AmountAssertions.preCheckToBtcAmounts(this.type, request, requestedAmount);
888
- metadata.times.requestChecked = Date.now();
889
-
890
- //Create abort controller for parallel pre-fetches
891
- const abortController = getAbortController(responseStream);
892
-
893
- //Pre-fetch
894
- const {pricePrefetchPromise, signDataPrefetchPromise} = this.getToBtcPrefetches(chainIdentifier, useToken, responseStream, abortController);
895
- const nativeBalancePrefetch = this.prefetchNativeBalanceIfNeeded(chainIdentifier, abortController);
896
-
897
- //Check if prior payment has been made
898
- await this.LightningAssertions.checkPriorPayment(parsedPR.id, abortController.signal);
899
- metadata.times.priorPaymentChecked = Date.now();
900
-
901
- //Check if we still have enough native balance
902
- await this.checkNativeBalance(chainIdentifier, nativeBalancePrefetch, abortController.signal);
903
-
904
- //Check amounts
905
- const {
906
- amountBD,
907
- networkFeeData,
908
- totalInToken,
909
- swapFee,
910
- swapFeeInToken,
911
- networkFeeInToken
912
- } = await this.AmountAssertions.checkToBtcAmount(this.type, request, {...requestedAmount, pricePrefetch: pricePrefetchPromise}, fees, async (amountBD: bigint) => {
913
- //Check if we have enough liquidity to process the swap
914
- await this.LightningAssertions.checkLiquidity(amountBD, abortController.signal, true);
915
- metadata.times.liquidityChecked = Date.now();
916
-
917
- const maxFee = parsedBody.exactIn ?
918
- await this.swapPricing.getToBtcSwapAmount(parsedBody.maxFee, useToken, chainIdentifier, null, pricePrefetchPromise) :
919
- parsedBody.maxFee;
920
-
921
- return await this.checkAndGetNetworkFee(amountBD, maxFee, parsedBody.expiryTimestamp, currentTimestamp, parsedBody.pr, metadata, abortController.signal);
922
- }, abortController.signal);
923
- metadata.times.priceCalculated = Date.now();
924
-
925
- //For exactIn swap, just save and wait for the actual invoice to be submitted
926
- if(parsedBody.exactIn) {
927
- const reqId = randomBytes(32).toString("hex");
928
- this.exactInAuths[reqId] = {
929
- chainIdentifier,
930
- reqId,
931
- expiry: Date.now() + this.config.exactInExpiry,
932
-
933
- amount: amountBD,
934
- initialInvoice: parsedPR,
935
-
936
- quotedNetworkFeeInToken: networkFeeInToken,
937
- swapFeeInToken,
938
- total: totalInToken,
939
- confidence: networkFeeData.confidence,
940
- quotedNetworkFee: networkFeeData.networkFee,
941
- swapFee,
942
-
943
- token: useToken,
944
- swapExpiry: parsedBody.expiryTimestamp,
945
- offerer: parsedBody.offerer,
946
-
947
- preFetchSignData: signDataPrefetchPromise != null ? await signDataPrefetchPromise : null,
948
- metadata
949
- };
950
-
951
- this.logger.info("REST: /payInvoice: created exact in swap,"+
952
- " reqId: "+reqId+
953
- " amount: "+amountBD.toString(10)+
954
- " destination: "+parsedPR.destination);
955
-
956
- await responseStream.writeParamsAndEnd({
957
- code: 20000,
958
- msg: "Success",
959
- data: {
960
- amount: amountBD.toString(10),
961
- reqId
962
- }
963
- });
964
- return;
965
- }
966
-
967
- const sequence = BigIntBufferUtils.fromBuffer(randomBytes(8));
968
- const claimHash = swapContract.getHashForHtlc(Buffer.from(parsedPR.id, "hex"));
969
-
970
- //Create swap data
971
- const payObject: SwapData = await swapContract.createSwapData(
972
- ChainSwapType.HTLC,
973
- parsedBody.offerer,
974
- signer.getAddress(),
975
- useToken,
976
- totalInToken,
977
- claimHash.toString("hex"),
978
- sequence,
979
- parsedBody.expiryTimestamp,
980
- true,
981
- false,
982
- 0n,
983
- 0n
984
- );
985
- abortController.signal.throwIfAborted();
986
- metadata.times.swapCreated = Date.now();
987
-
988
- //Sign swap data
989
- const sigData = await this.getToBtcSignatureData(chainIdentifier, payObject, req, abortController.signal, signDataPrefetchPromise);
990
- metadata.times.swapSigned = Date.now();
991
-
992
- //Create swap
993
- const createdSwap = new ToBtcLnSwapAbs(
994
- chainIdentifier,
995
- parsedPR.id,
996
- parsedBody.pr,
997
- parsedPR.mtokens,
998
- swapFee,
999
- swapFeeInToken,
1000
- networkFeeData.networkFee,
1001
- networkFeeInToken
1002
- );
1003
- createdSwap.data = payObject;
1004
- createdSwap.metadata = metadata;
1005
- createdSwap.prefix = sigData.prefix;
1006
- createdSwap.timeout = sigData.timeout;
1007
- createdSwap.signature = sigData.signature
1008
- createdSwap.feeRate = sigData.feeRate;
1009
-
1010
- await PluginManager.swapCreate(createdSwap);
1011
- await this.saveSwapData(createdSwap);
1012
-
1013
- this.swapLogger.info(createdSwap, "REST: /payInvoice: created swap,"+
1014
- " amount: "+amountBD.toString(10)+
1015
- " invoice: "+createdSwap.pr);
1016
-
1017
- await responseStream.writeParamsAndEnd({
1018
- code: 20000,
1019
- msg: "Success",
1020
- data: {
1021
- maxFee: networkFeeInToken.toString(10),
1022
- swapFee: swapFeeInToken.toString(10),
1023
- total: totalInToken.toString(10),
1024
- confidence: halfConfidence ? networkFeeData.confidence/2000000 : networkFeeData.confidence/1000000,
1025
- address: signer.getAddress(),
1026
-
1027
- routingFeeSats: networkFeeData.networkFee.toString(10),
1028
-
1029
- data: payObject.serialize(),
1030
-
1031
- prefix: sigData.prefix,
1032
- timeout: sigData.timeout,
1033
- signature: sigData.signature
1034
- }
1035
- });
1036
- }));
1037
-
1038
- const getRefundAuthorization = expressHandlerWrapper(async (req, res) => {
1039
- /**
1040
- * paymentHash: string Identifier of the swap
1041
- * sequence: BN Sequence identifier of the swap
1042
- */
1043
- const parsedBody = verifySchema({...req.body, ...req.query}, {
1044
- paymentHash: (val: string) => val!=null &&
1045
- typeof(val)==="string" &&
1046
- val.length===64 &&
1047
- HEX_REGEX.test(val) ? val: null,
1048
- sequence: FieldTypeEnum.BigInt
1049
- });
1050
- if (parsedBody==null) throw {
1051
- code: 20100,
1052
- msg: "Invalid request body/query (paymentHash/sequence)"
1053
- };
1054
-
1055
- this.checkSequence(parsedBody.sequence);
1056
-
1057
- let data = await this.storageManager.getData(parsedBody.paymentHash, parsedBody.sequence);
1058
- if(data==null) {
1059
- for(let chainId in this.chains.chains) {
1060
- const _data = this.getSwapByEscrowHash(chainId, parsedBody.paymentHash);
1061
- if(_data!=null && _data.getSequence()===parsedBody.sequence) {
1062
- data = _data;
1063
- break;
1064
- }
1065
- }
1066
- }
1067
-
1068
- const isSwapFound = data!=null;
1069
- if(isSwapFound) {
1070
- const {signer, swapContract} = this.getChain(data.chainIdentifier);
1071
-
1072
- if(await swapContract.isExpired(signer.getAddress(), data.data)) throw {
1073
- _httpStatus: 200,
1074
- code: 20010,
1075
- msg: "Payment expired"
1076
- };
1077
-
1078
- if(data.state===ToBtcLnSwapState.NON_PAYABLE) {
1079
- const refundSigData = await swapContract.getRefundSignature(signer, data.data, this.config.refundAuthorizationTimeout);
1080
-
1081
- //Double check the state after promise result
1082
- if (data.state !== ToBtcLnSwapState.NON_PAYABLE) throw {
1083
- code: 20005,
1084
- msg: "Not committed"
1085
- };
1086
-
1087
- this.swapLogger.info(data, "REST: /getRefundAuthorization: returning refund authorization, because invoice in NON_PAYABLE state, invoice: "+data.pr);
1088
-
1089
- res.status(200).json({
1090
- code: 20000,
1091
- msg: "Success",
1092
- data: {
1093
- address: signer.getAddress(),
1094
- prefix: refundSigData.prefix,
1095
- timeout: refundSigData.timeout,
1096
- signature: refundSigData.signature
1097
- }
1098
- });
1099
- return;
1100
- }
1101
- }
1102
-
1103
- const payment = await this.lightning.getPayment(parsedBody.paymentHash);
1104
-
1105
- if(payment==null) throw {
1106
- _httpStatus: 200,
1107
- code: 20007,
1108
- msg: "Payment not found"
1109
- };
1110
-
1111
- if(payment.status==="pending") {
1112
- res.status(200).json({
1113
- code: 20008,
1114
- msg: "Payment in-flight"
1115
- });
1116
- return;
1117
- }
1118
-
1119
- if(payment.status==="confirmed") {
1120
- res.status(200).json({
1121
- code: 20006,
1122
- msg: "Already paid",
1123
- data: {
1124
- secret: payment.secret
1125
- }
1126
- });
1127
- return;
1128
- }
1129
-
1130
- if(payment.status==="failed") throw {
1131
- _httpStatus: 200,
1132
- code: 20010,
1133
- msg: "Payment expired",
1134
- data: {
1135
- reason: payment.failedReason
1136
- }
1137
- };
1138
- });
1139
-
1140
- restServer.post(this.path+'/getRefundAuthorization', getRefundAuthorization);
1141
- restServer.get(this.path+'/getRefundAuthorization', getRefundAuthorization);
1142
-
1143
- this.logger.info("started at path: ", this.path);
1144
- }
1145
-
1146
- async init() {
1147
- await this.loadData(ToBtcLnSwapAbs);
1148
- //Check if all swaps contain a valid amount
1149
- for(let {obj: swap} of await this.storageManager.query([])) {
1150
- if(swap.amount==null || swap.lnPaymentHash==null) {
1151
- const parsedPR = await this.lightning.parsePaymentRequest(swap.pr);
1152
- swap.amount = (parsedPR.mtokens + 999n) / 1000n;
1153
- swap.lnPaymentHash = parsedPR.id;
1154
- }
1155
- }
1156
- this.subscribeToEvents();
1157
- await PluginManager.serviceInitialize(this);
1158
- }
1159
-
1160
- getInfoData(): any {
1161
- return {
1162
- minCltv: Number(this.config.minSendCltv),
1163
- minTimestampCltv: Number(this.config.minTsSendCltv)
1164
- };
1165
- }
1166
-
1167
- }
1
+ import {Express, Request, Response} from "express";
2
+ import {ToBtcLnSwapAbs, ToBtcLnSwapState} from "./ToBtcLnSwapAbs";
3
+ import {MultichainData, SwapHandlerType} from "../../SwapHandler";
4
+ import {ISwapPrice} from "../../../prices/ISwapPrice";
5
+ import {
6
+ BigIntBufferUtils,
7
+ ChainSwapType,
8
+ ClaimEvent,
9
+ InitializeEvent,
10
+ RefundEvent, SwapCommitStateType,
11
+ SwapData
12
+ } from "@atomiqlabs/base";
13
+ import {expressHandlerWrapper, getAbortController, HEX_REGEX, isDefinedRuntimeError} from "../../../utils/Utils";
14
+ import {PluginManager} from "../../../plugins/PluginManager";
15
+ import {IIntermediaryStorage} from "../../../storage/IIntermediaryStorage";
16
+ import {randomBytes} from "crypto";
17
+ import {serverParamDecoder} from "../../../utils/paramcoders/server/ServerParamDecoder";
18
+ import {IParamReader} from "../../../utils/paramcoders/IParamReader";
19
+ import {FieldTypeEnum, verifySchema} from "../../../utils/paramcoders/SchemaVerifier";
20
+ import {ServerParamEncoder} from "../../../utils/paramcoders/server/ServerParamEncoder";
21
+ import {ToBtcBaseConfig, ToBtcBaseSwapHandler} from "../ToBtcBaseSwapHandler";
22
+ import {
23
+ ILightningWallet,
24
+ OutgoingLightningNetworkPayment,
25
+ ParsedPaymentRequest,
26
+ ProbeAndRouteInit,
27
+ ProbeAndRouteResponse,
28
+ routesMatch
29
+ } from "../../../wallets/ILightningWallet";
30
+ import { LightningAssertions } from "../../assertions/LightningAssertions";
31
+ import {isQuoteThrow} from "../../../plugins/IPlugin";
32
+ import {ToBtcSwapState} from "../tobtc_abstract/ToBtcSwapAbs";
33
+
34
+ export type ToBtcLnConfig = ToBtcBaseConfig & {
35
+ routingFeeMultiplier: bigint,
36
+
37
+ minSendCltv: bigint,
38
+
39
+ allowProbeFailedSwaps: boolean,
40
+ allowShortExpiry: boolean,
41
+
42
+ minLnRoutingFeePPM?: bigint,
43
+ minLnBaseFee?: bigint,
44
+
45
+ exactInExpiry?: number,
46
+
47
+ lnSendBitcoinBlockTimeSafetyFactorPPM?: bigint
48
+ };
49
+
50
+ type ExactInAuthorization = {
51
+ chainIdentifier: string,
52
+ reqId: string,
53
+ expiry: number,
54
+
55
+ amount: bigint,
56
+ initialInvoice: ParsedPaymentRequest,
57
+
58
+ quotedNetworkFeeInToken: bigint,
59
+ swapFeeInToken: bigint,
60
+ total: bigint,
61
+ confidence: number,
62
+ quotedNetworkFee: bigint,
63
+ swapFee: bigint,
64
+
65
+ token: string,
66
+ swapExpiry: bigint,
67
+ offerer: string,
68
+
69
+ preFetchSignData: any,
70
+ metadata: {
71
+ request: any,
72
+ probeRequest?: any,
73
+ probeResponse?: any,
74
+ routeResponse?: any,
75
+ times: {[key: string]: number}
76
+ }
77
+ }
78
+
79
+ export type ToBtcLnRequestType = {
80
+ pr: string,
81
+ maxFee: bigint,
82
+ expiryTimestamp: bigint,
83
+ token: string,
84
+ offerer: string,
85
+ exactIn?: boolean,
86
+ amount?: bigint
87
+ };
88
+
89
+ /**
90
+ * Swap handler handling to BTCLN swaps using submarine swaps
91
+ */
92
+ export class ToBtcLnAbs extends ToBtcBaseSwapHandler<ToBtcLnSwapAbs, ToBtcLnSwapState> {
93
+ readonly type = SwapHandlerType.TO_BTCLN;
94
+ readonly swapType = ChainSwapType.HTLC;
95
+ readonly inflightSwapStates = new Set([ToBtcLnSwapState.COMMITED, ToBtcLnSwapState.PAID]);
96
+
97
+ activeSubscriptions: Set<string> = new Set<string>();
98
+
99
+ readonly config: ToBtcLnConfig & {minTsSendCltv: bigint};
100
+
101
+ readonly exactInAuths: {
102
+ [reqId: string]: ExactInAuthorization
103
+ } = {};
104
+
105
+ readonly lightning: ILightningWallet;
106
+ readonly LightningAssertions: LightningAssertions;
107
+
108
+ constructor(
109
+ storageDirectory: IIntermediaryStorage<ToBtcLnSwapAbs>,
110
+ path: string,
111
+ chainData: MultichainData,
112
+ lightning: ILightningWallet,
113
+ swapPricing: ISwapPrice,
114
+ config: ToBtcLnConfig
115
+ ) {
116
+ super(storageDirectory, path, chainData, swapPricing, config);
117
+ this.lightning = lightning;
118
+ this.LightningAssertions = new LightningAssertions(this.logger, lightning);
119
+ const anyConfig = config as any;
120
+ anyConfig.minTsSendCltv = config.gracePeriod + (config.bitcoinBlocktime * config.minSendCltv * config.safetyFactor);
121
+ this.config = anyConfig;
122
+ this.config.minLnRoutingFeePPM = this.config.minLnRoutingFeePPM || 1000n;
123
+ this.config.minLnBaseFee = this.config.minLnBaseFee || 5n;
124
+ this.config.exactInExpiry = this.config.exactInExpiry || 10*1000;
125
+ this.config.lnSendBitcoinBlockTimeSafetyFactorPPM = this.config.lnSendBitcoinBlockTimeSafetyFactorPPM ?? (this.config.safetyFactor * 1_000_000n);
126
+ if(this.config.lnSendBitcoinBlockTimeSafetyFactorPPM <= 1_100_000n) {
127
+ throw new Error("Lightning network send block safety factor set below 1.1, this is insecure!");
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Cleans up exactIn authorization that are already past their expiry
133
+ *
134
+ * @protected
135
+ */
136
+ private cleanExpiredExactInAuthorizations() {
137
+ for(let key in this.exactInAuths) {
138
+ const obj = this.exactInAuths[key];
139
+ if(obj.expiry<Date.now()) {
140
+ this.logger.info("cleanExpiredExactInAuthorizations(): remove expired authorization, reqId: "+key);
141
+ delete this.exactInAuths[key];
142
+ }
143
+ }
144
+ }
145
+
146
+ protected async processPastSwap(swap: ToBtcLnSwapAbs): Promise<void> {
147
+ const {swapContract, signer} = this.getChain(swap.chainIdentifier);
148
+
149
+ if (swap.state === ToBtcLnSwapState.SAVED) {
150
+ //Cancel the swaps where signature is expired
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, invoice: "+swap.pr);
156
+ await this.removeSwapData(swap, ToBtcLnSwapState.CANCELED);
157
+ return;
158
+ } else {
159
+ this.swapLogger.info(swap, "processPastSwap(state=SAVED): swap committed (detected from processPastSwap), invoice: "+swap.pr);
160
+ await swap.setState(ToBtcLnSwapState.COMMITED);
161
+ await this.saveSwapData(swap);
162
+ }
163
+ }
164
+ }
165
+
166
+ if (swap.state === ToBtcLnSwapState.COMMITED || swap.state === ToBtcLnSwapState.PAID) {
167
+ //Process swaps in commited & paid state
168
+ await this.processInitialized(swap);
169
+ }
170
+
171
+ if (swap.state === ToBtcLnSwapState.NON_PAYABLE) {
172
+ //Remove expired swaps (as these can already be unilaterally refunded by the client), so we don't need
173
+ // to be able to cooperatively refund them
174
+ if(await swapContract.isExpired(swap.data.getOfferer(), swap.data)) {
175
+ this.swapLogger.info(swap, "processPastSwap(state=NON_PAYABLE): swap expired, removing swap data, invoice: "+swap.pr);
176
+ await this.removeSwapData(swap);
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Checks past swaps, deletes ones that are already expired, and tries to process ones that are committed.
183
+ */
184
+ protected async processPastSwaps() {
185
+ this.cleanExpiredExactInAuthorizations();
186
+
187
+ const queriedData = await this.storageManager.query([
188
+ {
189
+ key: "state",
190
+ value: [
191
+ ToBtcLnSwapState.SAVED,
192
+ ToBtcLnSwapState.COMMITED,
193
+ ToBtcLnSwapState.PAID,
194
+ ToBtcLnSwapState.NON_PAYABLE
195
+ ]
196
+ }
197
+ ]);
198
+
199
+ for(let {obj: swap} of queriedData) {
200
+ await this.processPastSwap(swap);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Tries to claim the swap funds on the SC side, returns false if the swap is already locked (claim tx is already being sent)
206
+ *
207
+ * @param swap
208
+ * @private
209
+ * @returns Whether the transaction was successfully sent
210
+ */
211
+ private async tryClaimSwap(swap: ToBtcLnSwapAbs): Promise<boolean> {
212
+ if(swap.secret==null) throw new Error("Invalid swap state, needs payment pre-image!");
213
+
214
+ const {swapContract, signer} = this.getChain(swap.chainIdentifier);
215
+
216
+ //Check if escrow state exists
217
+ const isCommited = await swapContract.isCommited(swap.data);
218
+ if(!isCommited) {
219
+ const status = await swapContract.getCommitStatus(signer.getAddress(), swap.data);
220
+ if(status?.type===SwapCommitStateType.PAID) {
221
+ //This is alright, we got the money
222
+ swap.txIds ??= {};
223
+ swap.txIds.claim = await status.getClaimTxId();
224
+ await this.removeSwapData(swap, ToBtcLnSwapState.CLAIMED);
225
+ return true;
226
+ } else if(status?.type===SwapCommitStateType.EXPIRED) {
227
+ //This means the user was able to refund before we were able to claim, no good
228
+ swap.txIds ??= {};
229
+ swap.txIds.refund = status.getRefundTxId==null ? null : await status.getRefundTxId();
230
+ await this.removeSwapData(swap, ToBtcLnSwapState.REFUNDED);
231
+ }
232
+ this.swapLogger.warn(swap, "processPaymentResult(): tried to claim but escrow doesn't exist anymore,"+
233
+ " status: "+status+
234
+ " invoice: "+swap.pr);
235
+ return false;
236
+ }
237
+
238
+ //Set flag that we are sending the transaction already, so we don't end up with race condition
239
+ const unlock: () => boolean = swap.lock(swapContract.claimWithSecretTimeout);
240
+ if(unlock==null) return false;
241
+
242
+ try {
243
+ this.swapLogger.debug(swap, "tryClaimSwap(): initiate claim of swap, secret: "+swap.secret);
244
+ const success = await swapContract.claimWithSecret(signer, swap.data, swap.secret, false, false, {
245
+ waitForConfirmation: true
246
+ });
247
+ this.swapLogger.info(swap, "tryClaimSwap(): swap claimed successfully, secret: "+swap.secret+" invoice: "+swap.pr);
248
+ if(swap.metadata!=null) swap.metadata.times.txClaimed = Date.now();
249
+ unlock();
250
+ return true;
251
+ } catch (e) {
252
+ this.swapLogger.error(swap, "tryClaimSwap(): error occurred claiming swap, secret: "+swap.secret+" invoice: "+swap.pr, e);
253
+ return false;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Process the result of attempted lightning network payment
259
+ *
260
+ * @param swap
261
+ * @param lnPaymentStatus
262
+ */
263
+ private async processPaymentResult(swap: ToBtcLnSwapAbs, lnPaymentStatus: OutgoingLightningNetworkPayment) {
264
+ switch(lnPaymentStatus.status) {
265
+ case "pending":
266
+ return;
267
+
268
+ case "failed":
269
+ this.swapLogger.info(swap, "processPaymentResult(): invoice payment failed, cancelling swap, invoice: "+swap.pr);
270
+ await swap.setState(ToBtcLnSwapState.NON_PAYABLE);
271
+ await this.saveSwapData(swap);
272
+ return;
273
+
274
+ case "confirmed":
275
+ swap.secret = lnPaymentStatus.secret;
276
+ swap.setRealNetworkFee(lnPaymentStatus.feeMtokens / 1000n);
277
+ this.swapLogger.info(swap, "processPaymentResult(): invoice paid, secret: "+swap.secret+" realRoutingFee: "+swap.realNetworkFee.toString(10)+" invoice: "+swap.pr);
278
+ await swap.setState(ToBtcLnSwapState.PAID);
279
+ await this.saveSwapData(swap);
280
+
281
+ const success = await this.tryClaimSwap(swap);
282
+ if(success) this.swapLogger.info(swap, "processPaymentResult(): swap claimed successfully, invoice: "+swap.pr);
283
+ return;
284
+
285
+ default:
286
+ throw new Error("Invalid lnPaymentStatus");
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Subscribe to a pending lightning network payment attempt
292
+ *
293
+ * @param invoiceData
294
+ */
295
+ private subscribeToPayment(invoiceData: ToBtcLnSwapAbs): boolean {
296
+ const paymentHash = invoiceData.lnPaymentHash;
297
+ if(this.activeSubscriptions.has(paymentHash)) return false;
298
+
299
+ this.lightning.waitForPayment(paymentHash).then(result => {
300
+ this.swapLogger.info(invoiceData, "subscribeToPayment(): result callback, outcome: "+result.status+" invoice: "+invoiceData.pr);
301
+ this.processPaymentResult(invoiceData, result).catch(e => this.swapLogger.error(invoiceData, "subscribeToPayment(): process payment result", e));
302
+ this.activeSubscriptions.delete(paymentHash);
303
+ });
304
+ this.swapLogger.info(invoiceData, "subscribeToPayment(): subscribe to payment outcome, invoice: "+invoiceData.pr);
305
+
306
+ this.activeSubscriptions.add(paymentHash);
307
+ return true;
308
+ }
309
+
310
+ private async sendLightningPayment(swap: ToBtcLnSwapAbs): Promise<boolean> {
311
+ const decodedPR = await this.lightning.parsePaymentRequest(swap.pr);
312
+ const expiryTimestamp: bigint = swap.data.getExpiry();
313
+ const currentTimestamp: bigint = BigInt(Math.floor(Date.now()/1000));
314
+
315
+ //Run checks
316
+ const hasEnoughTimeToPay = (expiryTimestamp - currentTimestamp) >= this.config.minTsSendCltv;
317
+ if(!hasEnoughTimeToPay) throw {
318
+ code: 90005,
319
+ msg: "Not enough time to reliably pay the invoice"
320
+ }
321
+
322
+ const isInvoiceExpired = decodedPR.expiryEpochMillis < Date.now();
323
+ if (isInvoiceExpired) throw {
324
+ code: 90006,
325
+ msg: "Invoice already expired"
326
+ };
327
+
328
+ //Compute max cltv delta
329
+ const maxFee = swap.quotedNetworkFee;
330
+ const maxUsableCLTVdelta = (expiryTimestamp - currentTimestamp - this.config.gracePeriod)
331
+ / (this.config.bitcoinBlocktime * this.config.lnSendBitcoinBlockTimeSafetyFactorPPM / 1_000_000n);
332
+
333
+ //Initiate payment
334
+ this.swapLogger.info(swap, "sendLightningPayment(): paying lightning network invoice,"+
335
+ " cltvDelta: "+maxUsableCLTVdelta.toString(10)+
336
+ " maxFee: "+maxFee.toString(10)+
337
+ " invoice: "+swap.pr);
338
+
339
+ const blockHeight = await this.lightning.getBlockheight();
340
+
341
+ const pluginCheckResult = await PluginManager.onHandlePreToBtcExecute(
342
+ SwapHandlerType.TO_BTCLN,
343
+ swap
344
+ );
345
+ if(isQuoteThrow(pluginCheckResult)) {
346
+ throw {
347
+ code: 29999,
348
+ msg: pluginCheckResult.message
349
+ };
350
+ }
351
+
352
+ if(swap.state!==ToBtcLnSwapState.COMMITED) return false;
353
+
354
+ swap.payInitiated = true;
355
+ await this.saveSwapData(swap);
356
+ try {
357
+ await this.lightning.pay({
358
+ request: swap.pr,
359
+ maxFeeMtokens: maxFee * 1000n,
360
+ maxTimeoutHeight: blockHeight+Number(maxUsableCLTVdelta)
361
+ })
362
+ } catch (e) {
363
+ throw {
364
+ code: 90007,
365
+ msg: "Failed to initiate invoice payment",
366
+ data: {
367
+ error: JSON.stringify(e)
368
+ }
369
+ }
370
+ }
371
+ if(swap.metadata!=null) swap.metadata.times.payComplete = Date.now();
372
+
373
+ return true;
374
+ }
375
+
376
+ /**
377
+ * Begins a lightning network payment attempt, if not attempted already
378
+ *
379
+ * @param swap
380
+ */
381
+ private async processInitialized(swap: ToBtcLnSwapAbs) {
382
+ //Check if payment was already made
383
+ if(swap.state===ToBtcLnSwapState.PAID) {
384
+ const success = await this.tryClaimSwap(swap);
385
+ if(success) this.swapLogger.info(swap, "processInitialized(): swap claimed successfully, invoice: "+swap.pr);
386
+ return;
387
+ }
388
+
389
+ if(swap.state===ToBtcLnSwapState.COMMITED) {
390
+ if(swap.metadata!=null) swap.metadata.times.payPaymentChecked = Date.now();
391
+ let lnPaymentStatus = await this.lightning.getPayment(swap.lnPaymentHash);
392
+ if(lnPaymentStatus!=null) {
393
+ if(lnPaymentStatus.status==="pending") {
394
+ //Payment still ongoing, process the result
395
+ this.subscribeToPayment(swap);
396
+ return;
397
+ } else {
398
+ //Payment has already concluded, process the result
399
+ await this.processPaymentResult(swap, lnPaymentStatus);
400
+ return;
401
+ }
402
+ } else {
403
+ //Payment not founds, try to process again
404
+ await swap.setState(ToBtcLnSwapState.SAVED);
405
+ }
406
+ }
407
+
408
+ if(swap.state===ToBtcLnSwapState.SAVED) {
409
+ try {
410
+ this.checkTooManyInflightSwaps();
411
+ } catch (e) {
412
+ this.swapLogger.error(swap, "processInitialized(): checking too many inflight swaps error: ", e);
413
+ if(isDefinedRuntimeError(e)) {
414
+ if(swap.metadata!=null) swap.metadata.payError = e;
415
+ await swap.setState(ToBtcLnSwapState.NON_PAYABLE);
416
+ await this.saveSwapData(swap);
417
+ return;
418
+ } else throw e;
419
+ }
420
+
421
+ await swap.setState(ToBtcLnSwapState.COMMITED);
422
+ await this.saveSwapData(swap);
423
+ try {
424
+ if(!await this.sendLightningPayment(swap)) return;
425
+ } catch (e) {
426
+ this.swapLogger.error(swap, "processInitialized(): lightning payment error", e);
427
+ if(isDefinedRuntimeError(e)) {
428
+ if(swap.metadata!=null) swap.metadata.payError = e;
429
+ await swap.setState(ToBtcLnSwapState.NON_PAYABLE);
430
+ await this.saveSwapData(swap);
431
+ return;
432
+ } else throw e;
433
+ }
434
+ this.subscribeToPayment(swap);
435
+ return;
436
+ }
437
+ }
438
+
439
+ protected async processInitializeEvent(chainIdentifier: string, swap: ToBtcLnSwapAbs, event: InitializeEvent<SwapData>): Promise<void> {
440
+ this.swapLogger.info(swap, "SC: InitializeEvent: swap initialized by the client, invoice: "+swap.pr);
441
+
442
+ //Only process swaps in SAVED state
443
+ if(swap.state!==ToBtcLnSwapState.SAVED) return;
444
+ await this.processInitialized(swap);
445
+ }
446
+
447
+ protected async processClaimEvent(chainIdentifier: string, swap: ToBtcLnSwapAbs, event: ClaimEvent<SwapData>): Promise<void> {
448
+ this.swapLogger.info(swap, "SC: ClaimEvent: swap claimed to us, secret: "+event.result+" invoice: "+swap.pr);
449
+
450
+ await this.removeSwapData(swap, ToBtcLnSwapState.CLAIMED);
451
+ }
452
+
453
+ protected async processRefundEvent(chainIdentifier: string, swap: ToBtcLnSwapAbs, event: RefundEvent<SwapData>): Promise<void> {
454
+ this.swapLogger.info(swap, "SC: RefundEvent: swap refunded back to the client, invoice: "+swap.pr);
455
+
456
+ await this.removeSwapData(swap, ToBtcLnSwapState.REFUNDED);
457
+ }
458
+
459
+ /**
460
+ * Checks if the amount was supplied in the exactIn request
461
+ *
462
+ * @param amount
463
+ * @param exactIn
464
+ * @throws {DefinedRuntimeError} will throw an error if the swap was exactIn, but amount not specified
465
+ */
466
+ private checkAmount(amount: bigint, exactIn: boolean): void {
467
+ if(exactIn) {
468
+ if(amount==null) {
469
+ throw {
470
+ code: 20040,
471
+ msg: "Invalid request body (amount not specified)!"
472
+ };
473
+ }
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Checks if the maxFee parameter is in valid range (>0)
479
+ *
480
+ * @param maxFee
481
+ * @throws {DefinedRuntimeError} will throw an error if the maxFee is zero or negative
482
+ */
483
+ private checkMaxFee(maxFee: bigint): void {
484
+ if(maxFee <= 0) {
485
+ throw {
486
+ code: 20030,
487
+ msg: "Invalid request body (maxFee too low)!"
488
+ };
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Checks and parses a payment request (bolt11 invoice), additionally also checks expiration time of the invoice
494
+ *
495
+ * @param chainIdentifier
496
+ * @param pr
497
+ * @throws {DefinedRuntimeError} will throw an error if the pr is invalid, without amount or expired
498
+ */
499
+ private async checkPaymentRequest(chainIdentifier: string, pr: string): Promise<{
500
+ parsedPR: ParsedPaymentRequest,
501
+ halfConfidence: boolean
502
+ }> {
503
+ let parsedPR: ParsedPaymentRequest;
504
+
505
+ try {
506
+ parsedPR = await this.lightning.parsePaymentRequest(pr);
507
+ } catch (e) {
508
+ throw {
509
+ code: 20021,
510
+ msg: "Invalid request body (pr - cannot be parsed)"
511
+ };
512
+ }
513
+
514
+ if(parsedPR.mtokens==null) throw {
515
+ code: 20022,
516
+ msg: "Invalid request body (pr - needs to have amount)"
517
+ };
518
+
519
+ let halfConfidence = false;
520
+ if(parsedPR.expiryEpochMillis < Date.now()+((this.getInitAuthorizationTimeout(chainIdentifier)+(2*60))*1000) ) {
521
+ if(!this.config.allowShortExpiry) {
522
+ throw {
523
+ code: 20020,
524
+ msg: "Invalid request body (pr - expired)"
525
+ };
526
+ } else if(parsedPR.expiryEpochMillis < Date.now()) {
527
+ throw {
528
+ code: 20020,
529
+ msg: "Invalid request body (pr - expired)"
530
+ };
531
+ }
532
+ halfConfidence = true;
533
+ }
534
+
535
+ return {parsedPR, halfConfidence};
536
+ }
537
+
538
+ /**
539
+ * Checks if the request specified too short of an expiry
540
+ *
541
+ * @param expiryTimestamp
542
+ * @param currentTimestamp
543
+ * @throws {DefinedRuntimeError} will throw an error if the expiry time is too short
544
+ */
545
+ private checkExpiry(expiryTimestamp: bigint, currentTimestamp: bigint): void {
546
+ const expiresTooSoon = (expiryTimestamp - currentTimestamp) < this.config.minTsSendCltv;
547
+ if(expiresTooSoon) {
548
+ throw {
549
+ code: 20001,
550
+ msg: "Expiry time too low!"
551
+ };
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Estimates the routing fee & confidence by either probing or routing (if probing fails), the fee is also adjusted
557
+ * according to routing fee multiplier, and subject to minimums set in config
558
+ *
559
+ * @param amountBD
560
+ * @param maxFee
561
+ * @param expiryTimestamp
562
+ * @param currentTimestamp
563
+ * @param pr
564
+ * @param metadata
565
+ * @param abortSignal
566
+ * @throws {DefinedRuntimeError} will throw an error if the destination is unreachable
567
+ */
568
+ private async checkAndGetNetworkFee(amountBD: bigint, maxFee: bigint, expiryTimestamp: bigint, currentTimestamp: bigint, pr: string, metadata: any, abortSignal: AbortSignal): Promise<{
569
+ confidence: number,
570
+ networkFee: bigint
571
+ }> {
572
+ const maxUsableCLTV: bigint = (expiryTimestamp - currentTimestamp - this.config.gracePeriod)
573
+ / (this.config.bitcoinBlocktime * this.config.lnSendBitcoinBlockTimeSafetyFactorPPM / 1_000_000n);
574
+
575
+ const blockHeight = await this.lightning.getBlockheight();
576
+ abortSignal.throwIfAborted();
577
+ metadata.times.blockheightFetched = Date.now();
578
+ metadata.probeAndRouteTimeoutDelta = maxUsableCLTV;
579
+
580
+ const maxTimeoutBlockheight = BigInt(blockHeight) + maxUsableCLTV;
581
+
582
+ const req: ProbeAndRouteInit = {
583
+ request: pr,
584
+ amountMtokens: amountBD * 1000n,
585
+ maxFeeMtokens: maxFee * 1000n,
586
+ maxTimeoutHeight: Number(maxTimeoutBlockheight)
587
+ };
588
+
589
+ this.logger.debug(`checkAndGetNetworkFee(): Attempting to probe/route the payment amount: ${amountBD.toString(10)}`+
590
+ `, maxFee: ${maxFee.toString(10)}, expiryDelta: ${maxUsableCLTV.toString(10)}, expiryHeight: ${maxTimeoutBlockheight.toString(10)}, pr: ${pr}`);
591
+
592
+ let probeOrRouteResp: ProbeAndRouteResponse = await this.lightning.probe(req);
593
+ metadata.times.probeResult = Date.now();
594
+ metadata.probeResponse = {...probeOrRouteResp};
595
+ abortSignal.throwIfAborted();
596
+
597
+ if(probeOrRouteResp==null) {
598
+ if(!this.config.allowProbeFailedSwaps) throw {
599
+ code: 20002,
600
+ msg: "Cannot route the payment!"
601
+ };
602
+
603
+ const routeResp = await this.lightning.route(req);
604
+ metadata.times.routingResult = Date.now();
605
+ metadata.routeResponse = {...routeResp};
606
+ abortSignal.throwIfAborted();
607
+
608
+ if(routeResp==null) throw {
609
+ code: 20002,
610
+ msg: "Cannot route the payment!"
611
+ };
612
+
613
+ this.logger.info("checkAndGetNetworkFee(): routing result,"+
614
+ " destination: "+routeResp.destination+
615
+ " confidence: "+routeResp.confidence+
616
+ " fee mtokens: "+routeResp.feeMtokens.toString(10));
617
+
618
+ probeOrRouteResp = routeResp;
619
+ } else {
620
+ this.logger.info("checkAndGetNetworkFee(): route probed,"+
621
+ " destination: "+probeOrRouteResp.destination+
622
+ " confidence: "+probeOrRouteResp.confidence+
623
+ " fee mtokens: "+probeOrRouteResp.feeMtokens.toString(10));
624
+ }
625
+
626
+ const safeFeeTokens = (probeOrRouteResp.feeMtokens + 999n) / 1000n;
627
+
628
+ let actualRoutingFee: bigint = safeFeeTokens * this.config.routingFeeMultiplier;
629
+
630
+ const minRoutingFee: bigint = (amountBD * this.config.minLnRoutingFeePPM / 1000000n) + this.config.minLnBaseFee;
631
+ if(actualRoutingFee < minRoutingFee) {
632
+ actualRoutingFee = minRoutingFee;
633
+ if(actualRoutingFee > maxFee) {
634
+ probeOrRouteResp.confidence = 0;
635
+ }
636
+ }
637
+
638
+ if(actualRoutingFee > maxFee) {
639
+ actualRoutingFee = maxFee;
640
+ }
641
+
642
+ this.logger.debug("checkAndGetNetworkFee(): network fee calculated, amount: "+amountBD.toString(10)+" fee: "+actualRoutingFee.toString(10));
643
+
644
+ return {
645
+ networkFee: actualRoutingFee,
646
+ confidence: probeOrRouteResp.confidence
647
+ };
648
+ }
649
+
650
+ /**
651
+ * Checks and consumes (deletes & returns) exactIn authorizaton with a specific reqId
652
+ *
653
+ * @param reqId
654
+ * @throws {DefinedRuntimeError} will throw an error if the authorization doesn't exist
655
+ */
656
+ private checkExactInAuthorization(reqId: string): ExactInAuthorization {
657
+ const parsedAuth = this.exactInAuths[reqId];
658
+ if (parsedAuth==null) {
659
+ throw {
660
+ code: 20070,
661
+ msg: "Invalid reqId"
662
+ };
663
+ }
664
+ delete this.exactInAuths[reqId];
665
+ if(parsedAuth.expiry<Date.now()) {
666
+ throw {
667
+ code: 20200,
668
+ msg: "Authorization already expired!"
669
+ };
670
+ }
671
+ return parsedAuth;
672
+ }
673
+
674
+ /**
675
+ * Checks if the newly submitted PR has the same parameters (destination, cltv_delta, routes) as the initial dummy
676
+ * invoice sent for exactIn swap quote
677
+ *
678
+ * @param parsedRequest
679
+ * @param parsedAuth
680
+ */
681
+ private isPaymentRequestMatchingInitial(parsedRequest: ParsedPaymentRequest, parsedAuth: ExactInAuthorization): boolean {
682
+ return parsedRequest.destination===parsedAuth.initialInvoice.destination &&
683
+ parsedRequest.cltvDelta===parsedAuth.initialInvoice.cltvDelta &&
684
+ routesMatch(parsedRequest.routes, parsedAuth.initialInvoice.routes);
685
+ }
686
+
687
+ startRestServer(restServer: Express) {
688
+
689
+ restServer.use(this.path+"/payInvoiceExactIn", serverParamDecoder(10*1000));
690
+ restServer.post(this.path+"/payInvoiceExactIn", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
691
+ /**
692
+ * pr: string bolt11 lightning invoice
693
+ * reqId: string Identifier of the swap
694
+ * feeRate: string Fee rate to use for the init tx
695
+ */
696
+ const parsedBody = await req.paramReader.getParams({
697
+ pr: FieldTypeEnum.String,
698
+ reqId: FieldTypeEnum.String,
699
+ feeRate: FieldTypeEnum.String
700
+ });
701
+ if (parsedBody==null) {
702
+ throw {
703
+ code: 20100,
704
+ msg: "Invalid request body"
705
+ };
706
+ }
707
+
708
+ this.checkTooManyInflightSwaps();
709
+ const parsedAuth = this.checkExactInAuthorization(parsedBody.reqId);
710
+
711
+ const responseStream = res.responseStream;
712
+ const abortSignal = responseStream.getAbortSignal();
713
+
714
+ //Check request params
715
+ const {parsedPR, halfConfidence} = await this.checkPaymentRequest(parsedAuth.chainIdentifier, parsedBody.pr);
716
+ if(parsedPR.mtokens!==parsedAuth.amount*1000n) throw {
717
+ code: 20102,
718
+ msg: "Provided PR doesn't match requested (amount)!"
719
+ };
720
+ if(!this.isPaymentRequestMatchingInitial(parsedPR, parsedAuth)) {
721
+ //The provided payment request doesn't match the parameters from the initial one, try to probe/route again
722
+ // with the same max fee parameters
723
+ const currentTimestamp: bigint = BigInt(Math.floor(Date.now()/1000));
724
+ const {networkFee, confidence} = await this.checkAndGetNetworkFee(
725
+ parsedAuth.amount, parsedAuth.quotedNetworkFee, parsedAuth.swapExpiry,
726
+ currentTimestamp, parsedBody.pr, parsedAuth.metadata, abortSignal
727
+ );
728
+ this.logger.info("REST: /payInvoiceExactIn: re-checked network fee for exact-in swap,"+
729
+ " reqId: "+parsedBody.reqId+
730
+ " initialNetworkFee: "+parsedAuth.quotedNetworkFee.toString(10)+
731
+ " newNetworkFee: "+networkFee.toString(10)+
732
+ " oldConfidence: "+parsedAuth.confidence.toString(10)+
733
+ " newConfidence: "+confidence.toString(10)+
734
+ " invoice: "+parsedBody.pr);
735
+ parsedAuth.confidence = confidence;
736
+ }
737
+
738
+ const metadata = parsedAuth.metadata;
739
+
740
+ const sequence = BigIntBufferUtils.fromBuffer(randomBytes(8));
741
+
742
+ const {swapContract, signer} = this.getChain(parsedAuth.chainIdentifier);
743
+ const claimHash = swapContract.getHashForHtlc(Buffer.from(parsedPR.id, "hex"))
744
+
745
+ //Create swap data
746
+ const payObject: SwapData = await swapContract.createSwapData(
747
+ ChainSwapType.HTLC,
748
+ parsedAuth.offerer,
749
+ signer.getAddress(),
750
+ parsedAuth.token,
751
+ parsedAuth.total,
752
+ claimHash.toString("hex"),
753
+ sequence,
754
+ parsedAuth.swapExpiry,
755
+ true,
756
+ false,
757
+ 0n,
758
+ 0n
759
+ );
760
+ metadata.times.swapCreated = Date.now();
761
+
762
+ //Sign swap data
763
+ const prefetchedSignData = parsedAuth.preFetchSignData;
764
+ const sigData = await this.getToBtcSignatureData(parsedAuth.chainIdentifier, payObject, req, abortSignal, prefetchedSignData);
765
+ metadata.times.swapSigned = Date.now();
766
+
767
+ //Create swap
768
+ const createdSwap = new ToBtcLnSwapAbs(
769
+ parsedAuth.chainIdentifier,
770
+ parsedPR.id,
771
+ parsedBody.pr,
772
+ parsedPR.mtokens,
773
+ parsedAuth.swapFee,
774
+ parsedAuth.swapFeeInToken,
775
+ parsedAuth.quotedNetworkFee,
776
+ parsedAuth.quotedNetworkFeeInToken
777
+ );
778
+ createdSwap.data = payObject;
779
+ createdSwap.metadata = metadata;
780
+ createdSwap.prefix = sigData.prefix;
781
+ createdSwap.timeout = sigData.timeout;
782
+ createdSwap.signature = sigData.signature
783
+ createdSwap.feeRate = sigData.feeRate;
784
+
785
+ await PluginManager.swapCreate(createdSwap);
786
+ await this.saveSwapData(createdSwap);
787
+
788
+ this.swapLogger.info(createdSwap, "REST: /payInvoiceExactIn: created exact in swap,"+
789
+ " reqId: "+parsedBody.reqId+
790
+ " mtokens: "+parsedPR.mtokens.toString(10)+
791
+ " invoice: "+createdSwap.pr);
792
+
793
+ await responseStream.writeParamsAndEnd({
794
+ code: 20000,
795
+ msg: "Success",
796
+ data: {
797
+ maxFee: parsedAuth.quotedNetworkFeeInToken.toString(10),
798
+ swapFee: parsedAuth.swapFeeInToken.toString(10),
799
+ total: parsedAuth.total.toString(10),
800
+ confidence: halfConfidence ? parsedAuth.confidence/2000000 : parsedAuth.confidence/1000000,
801
+ address: signer.getAddress(),
802
+
803
+ routingFeeSats: parsedAuth.quotedNetworkFee.toString(10),
804
+
805
+ data: payObject.serialize(),
806
+
807
+ prefix: sigData.prefix,
808
+ timeout: sigData.timeout,
809
+ signature: sigData.signature
810
+ }
811
+ });
812
+
813
+ }));
814
+
815
+ restServer.use(this.path+"/payInvoice", serverParamDecoder(10*1000));
816
+ restServer.post(this.path+"/payInvoice", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
817
+ const metadata: {
818
+ request: any,
819
+ probeRequest?: any,
820
+ probeResponse?: any,
821
+ routeResponse?: any,
822
+ times: {[key: string]: number}
823
+ } = {request: {}, times: {}};
824
+
825
+ const chainIdentifier = req.query.chain as string;
826
+ const {swapContract, signer, chainInterface} = this.getChain(chainIdentifier);
827
+
828
+ metadata.times.requestReceived = Date.now();
829
+ /**
830
+ *Sent initially:
831
+ * pr: string bolt11 lightning invoice
832
+ * maxFee: string maximum routing fee
833
+ * expiryTimestamp: string expiry timestamp of the to be created HTLC, determines how many LN paths can be considered
834
+ * token: string Desired token to use
835
+ * offerer: string Address of the caller
836
+ * exactIn: boolean Whether to do an exact in swap instead of exact out
837
+ * amount: string Input amount for exactIn swaps
838
+ *
839
+ *Sent later:
840
+ * feeRate: string Fee rate to use for the init signature
841
+ */
842
+ const parsedBody: ToBtcLnRequestType = await req.paramReader.getParams({
843
+ pr: FieldTypeEnum.String,
844
+ maxFee: FieldTypeEnum.BigInt,
845
+ expiryTimestamp: FieldTypeEnum.BigInt,
846
+ token: (val: string) => val!=null &&
847
+ typeof(val)==="string" &&
848
+ this.isTokenSupported(chainIdentifier, val) ? val : null,
849
+ offerer: (val: string) => val!=null &&
850
+ typeof(val)==="string" &&
851
+ chainInterface.isValidAddress(val, true) ? val : null,
852
+ exactIn: FieldTypeEnum.BooleanOptional,
853
+ amount: FieldTypeEnum.BigIntOptional
854
+ });
855
+ if (parsedBody==null) {
856
+ throw {
857
+ code: 20100,
858
+ msg: "Invalid request body"
859
+ };
860
+ }
861
+ metadata.request = parsedBody;
862
+
863
+ const request = {
864
+ chainIdentifier,
865
+ raw: req,
866
+ parsed: parsedBody,
867
+ metadata
868
+ };
869
+ const useToken = parsedBody.token;
870
+
871
+ const responseStream = res.responseStream;
872
+
873
+ const currentTimestamp: bigint = BigInt(Math.floor(Date.now()/1000));
874
+
875
+ //Check request params
876
+ this.checkTooManyInflightSwaps();
877
+ this.checkAmount(parsedBody.amount, parsedBody.exactIn);
878
+ this.checkMaxFee(parsedBody.maxFee);
879
+ this.checkExpiry(parsedBody.expiryTimestamp, currentTimestamp);
880
+ await this.checkVaultInitialized(chainIdentifier, parsedBody.token);
881
+ const {parsedPR, halfConfidence} = await this.checkPaymentRequest(chainIdentifier, parsedBody.pr);
882
+ const requestedAmount = {
883
+ input: !!parsedBody.exactIn,
884
+ amount: !!parsedBody.exactIn ? parsedBody.amount : (parsedPR.mtokens + 999n) / 1000n,
885
+ token: useToken
886
+ };
887
+ const fees = await this.AmountAssertions.preCheckToBtcAmounts(this.type, request, requestedAmount);
888
+ metadata.times.requestChecked = Date.now();
889
+
890
+ //Create abort controller for parallel pre-fetches
891
+ const abortController = getAbortController(responseStream);
892
+
893
+ //Pre-fetch
894
+ const {pricePrefetchPromise, signDataPrefetchPromise} = this.getToBtcPrefetches(chainIdentifier, useToken, responseStream, abortController);
895
+ const nativeBalancePrefetch = this.prefetchNativeBalanceIfNeeded(chainIdentifier, abortController);
896
+
897
+ //Check if prior payment has been made
898
+ await this.LightningAssertions.checkPriorPayment(parsedPR.id, abortController.signal);
899
+ metadata.times.priorPaymentChecked = Date.now();
900
+
901
+ //Check if we still have enough native balance
902
+ await this.checkNativeBalance(chainIdentifier, nativeBalancePrefetch, abortController.signal);
903
+
904
+ //Check amounts
905
+ const {
906
+ amountBD,
907
+ networkFeeData,
908
+ totalInToken,
909
+ swapFee,
910
+ swapFeeInToken,
911
+ networkFeeInToken
912
+ } = await this.AmountAssertions.checkToBtcAmount(this.type, request, {...requestedAmount, pricePrefetch: pricePrefetchPromise}, fees, async (amountBD: bigint) => {
913
+ //Check if we have enough liquidity to process the swap
914
+ await this.LightningAssertions.checkLiquidity(amountBD, abortController.signal, true);
915
+ metadata.times.liquidityChecked = Date.now();
916
+
917
+ const maxFee = parsedBody.exactIn ?
918
+ await this.swapPricing.getToBtcSwapAmount(parsedBody.maxFee, useToken, chainIdentifier, null, pricePrefetchPromise) :
919
+ parsedBody.maxFee;
920
+
921
+ return await this.checkAndGetNetworkFee(amountBD, maxFee, parsedBody.expiryTimestamp, currentTimestamp, parsedBody.pr, metadata, abortController.signal);
922
+ }, abortController.signal);
923
+ metadata.times.priceCalculated = Date.now();
924
+
925
+ //For exactIn swap, just save and wait for the actual invoice to be submitted
926
+ if(parsedBody.exactIn) {
927
+ const reqId = randomBytes(32).toString("hex");
928
+ this.exactInAuths[reqId] = {
929
+ chainIdentifier,
930
+ reqId,
931
+ expiry: Date.now() + this.config.exactInExpiry,
932
+
933
+ amount: amountBD,
934
+ initialInvoice: parsedPR,
935
+
936
+ quotedNetworkFeeInToken: networkFeeInToken,
937
+ swapFeeInToken,
938
+ total: totalInToken,
939
+ confidence: networkFeeData.confidence,
940
+ quotedNetworkFee: networkFeeData.networkFee,
941
+ swapFee,
942
+
943
+ token: useToken,
944
+ swapExpiry: parsedBody.expiryTimestamp,
945
+ offerer: parsedBody.offerer,
946
+
947
+ preFetchSignData: signDataPrefetchPromise != null ? await signDataPrefetchPromise : null,
948
+ metadata
949
+ };
950
+
951
+ this.logger.info("REST: /payInvoice: created exact in swap,"+
952
+ " reqId: "+reqId+
953
+ " amount: "+amountBD.toString(10)+
954
+ " destination: "+parsedPR.destination);
955
+
956
+ await responseStream.writeParamsAndEnd({
957
+ code: 20000,
958
+ msg: "Success",
959
+ data: {
960
+ amount: amountBD.toString(10),
961
+ reqId
962
+ }
963
+ });
964
+ return;
965
+ }
966
+
967
+ const sequence = BigIntBufferUtils.fromBuffer(randomBytes(8));
968
+ const claimHash = swapContract.getHashForHtlc(Buffer.from(parsedPR.id, "hex"));
969
+
970
+ //Create swap data
971
+ const payObject: SwapData = await swapContract.createSwapData(
972
+ ChainSwapType.HTLC,
973
+ parsedBody.offerer,
974
+ signer.getAddress(),
975
+ useToken,
976
+ totalInToken,
977
+ claimHash.toString("hex"),
978
+ sequence,
979
+ parsedBody.expiryTimestamp,
980
+ true,
981
+ false,
982
+ 0n,
983
+ 0n
984
+ );
985
+ abortController.signal.throwIfAborted();
986
+ metadata.times.swapCreated = Date.now();
987
+
988
+ //Sign swap data
989
+ const sigData = await this.getToBtcSignatureData(chainIdentifier, payObject, req, abortController.signal, signDataPrefetchPromise);
990
+ metadata.times.swapSigned = Date.now();
991
+
992
+ //Create swap
993
+ const createdSwap = new ToBtcLnSwapAbs(
994
+ chainIdentifier,
995
+ parsedPR.id,
996
+ parsedBody.pr,
997
+ parsedPR.mtokens,
998
+ swapFee,
999
+ swapFeeInToken,
1000
+ networkFeeData.networkFee,
1001
+ networkFeeInToken
1002
+ );
1003
+ createdSwap.data = payObject;
1004
+ createdSwap.metadata = metadata;
1005
+ createdSwap.prefix = sigData.prefix;
1006
+ createdSwap.timeout = sigData.timeout;
1007
+ createdSwap.signature = sigData.signature
1008
+ createdSwap.feeRate = sigData.feeRate;
1009
+
1010
+ await PluginManager.swapCreate(createdSwap);
1011
+ await this.saveSwapData(createdSwap);
1012
+
1013
+ this.swapLogger.info(createdSwap, "REST: /payInvoice: created swap,"+
1014
+ " amount: "+amountBD.toString(10)+
1015
+ " invoice: "+createdSwap.pr);
1016
+
1017
+ await responseStream.writeParamsAndEnd({
1018
+ code: 20000,
1019
+ msg: "Success",
1020
+ data: {
1021
+ maxFee: networkFeeInToken.toString(10),
1022
+ swapFee: swapFeeInToken.toString(10),
1023
+ total: totalInToken.toString(10),
1024
+ confidence: halfConfidence ? networkFeeData.confidence/2000000 : networkFeeData.confidence/1000000,
1025
+ address: signer.getAddress(),
1026
+
1027
+ routingFeeSats: networkFeeData.networkFee.toString(10),
1028
+
1029
+ data: payObject.serialize(),
1030
+
1031
+ prefix: sigData.prefix,
1032
+ timeout: sigData.timeout,
1033
+ signature: sigData.signature
1034
+ }
1035
+ });
1036
+ }));
1037
+
1038
+ const getRefundAuthorization = expressHandlerWrapper(async (req, res) => {
1039
+ /**
1040
+ * paymentHash: string Identifier of the swap
1041
+ * sequence: BN Sequence identifier of the swap
1042
+ */
1043
+ const parsedBody = verifySchema({...req.body, ...req.query}, {
1044
+ paymentHash: (val: string) => val!=null &&
1045
+ typeof(val)==="string" &&
1046
+ val.length===64 &&
1047
+ HEX_REGEX.test(val) ? val: null,
1048
+ sequence: FieldTypeEnum.BigInt
1049
+ });
1050
+ if (parsedBody==null) throw {
1051
+ code: 20100,
1052
+ msg: "Invalid request body/query (paymentHash/sequence)"
1053
+ };
1054
+
1055
+ this.checkSequence(parsedBody.sequence);
1056
+
1057
+ let data = await this.storageManager.getData(parsedBody.paymentHash, parsedBody.sequence);
1058
+ if(data==null) {
1059
+ for(let chainId in this.chains.chains) {
1060
+ const _data = this.getSwapByEscrowHash(chainId, parsedBody.paymentHash);
1061
+ if(_data!=null && _data.getSequence()===parsedBody.sequence) {
1062
+ data = _data;
1063
+ break;
1064
+ }
1065
+ }
1066
+ }
1067
+
1068
+ const isSwapFound = data!=null;
1069
+ if(isSwapFound) {
1070
+ const {signer, swapContract} = this.getChain(data.chainIdentifier);
1071
+
1072
+ if(await swapContract.isExpired(signer.getAddress(), data.data)) throw {
1073
+ _httpStatus: 200,
1074
+ code: 20010,
1075
+ msg: "Payment expired"
1076
+ };
1077
+
1078
+ if(data.state===ToBtcLnSwapState.NON_PAYABLE) {
1079
+ const refundSigData = await swapContract.getRefundSignature(signer, data.data, this.config.refundAuthorizationTimeout);
1080
+
1081
+ //Double check the state after promise result
1082
+ if (data.state !== ToBtcLnSwapState.NON_PAYABLE) throw {
1083
+ code: 20005,
1084
+ msg: "Not committed"
1085
+ };
1086
+
1087
+ this.swapLogger.info(data, "REST: /getRefundAuthorization: returning refund authorization, because invoice in NON_PAYABLE state, invoice: "+data.pr);
1088
+
1089
+ res.status(200).json({
1090
+ code: 20000,
1091
+ msg: "Success",
1092
+ data: {
1093
+ address: signer.getAddress(),
1094
+ prefix: refundSigData.prefix,
1095
+ timeout: refundSigData.timeout,
1096
+ signature: refundSigData.signature
1097
+ }
1098
+ });
1099
+ return;
1100
+ }
1101
+ }
1102
+
1103
+ const payment = await this.lightning.getPayment(parsedBody.paymentHash);
1104
+
1105
+ if(payment==null) throw {
1106
+ _httpStatus: 200,
1107
+ code: 20007,
1108
+ msg: "Payment not found"
1109
+ };
1110
+
1111
+ if(payment.status==="pending") {
1112
+ res.status(200).json({
1113
+ code: 20008,
1114
+ msg: "Payment in-flight"
1115
+ });
1116
+ return;
1117
+ }
1118
+
1119
+ if(payment.status==="confirmed") {
1120
+ res.status(200).json({
1121
+ code: 20006,
1122
+ msg: "Already paid",
1123
+ data: {
1124
+ secret: payment.secret
1125
+ }
1126
+ });
1127
+ return;
1128
+ }
1129
+
1130
+ if(payment.status==="failed") throw {
1131
+ _httpStatus: 200,
1132
+ code: 20010,
1133
+ msg: "Payment expired",
1134
+ data: {
1135
+ reason: payment.failedReason
1136
+ }
1137
+ };
1138
+ });
1139
+
1140
+ restServer.post(this.path+'/getRefundAuthorization', getRefundAuthorization);
1141
+ restServer.get(this.path+'/getRefundAuthorization', getRefundAuthorization);
1142
+
1143
+ this.logger.info("started at path: ", this.path);
1144
+ }
1145
+
1146
+ async init() {
1147
+ await this.loadData(ToBtcLnSwapAbs);
1148
+ //Check if all swaps contain a valid amount
1149
+ for(let {obj: swap} of await this.storageManager.query([])) {
1150
+ if(swap.amount==null || swap.lnPaymentHash==null) {
1151
+ const parsedPR = await this.lightning.parsePaymentRequest(swap.pr);
1152
+ swap.amount = (parsedPR.mtokens + 999n) / 1000n;
1153
+ swap.lnPaymentHash = parsedPR.id;
1154
+ }
1155
+ }
1156
+ this.subscribeToEvents();
1157
+ await PluginManager.serviceInitialize(this);
1158
+ }
1159
+
1160
+ getInfoData(): any {
1161
+ return {
1162
+ minCltv: Number(this.config.minSendCltv),
1163
+ minTimestampCltv: Number(this.config.minTsSendCltv)
1164
+ };
1165
+ }
1166
+
1167
+ }