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