@atomiqlabs/lp-lib 16.2.0 → 16.2.1

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