-
Notifications
You must be signed in to change notification settings - Fork 498
/
RPCClientExtensions.cs
251 lines (224 loc) · 10.5 KB
/
RPCClientExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WalletWasabi.Helpers;
using WalletWasabi.Logging;
using WalletWasabi.Models;
namespace NBitcoin.RPC
{
public static class RPCClientExtensions
{
/// <summary>
/// Waits for a specific new block and returns useful info about it.
/// </summary>
/// <param name="timeout">(int, optional, default=0) Time in milliseconds to wait for a response. 0 indicates no timeout.</param>
/// <returns>Returns the current block on timeout or exit</returns>
public static async Task<(Height height, uint256 hash)> WaitForNewBlockAsync(this RPCClient rpc, long timeout = 0)
{
var resp = await rpc.SendCommandAsync("waitfornewblock", timeout);
return (int.Parse(resp.Result["height"].ToString()), uint256.Parse(resp.Result["hash"].ToString()));
}
/// <summary>
/// Waits for a specific new block and returns useful info about it.
/// </summary>
/// <param name="blockHash">Block hash to wait for</param>
/// <param name="timeout">(int, optional, default=0) Time in milliseconds to wait for a response. 0 indicates no timeout.</param>
/// <returns>Returns the current block on timeout or exit</returns>
public static async Task<(Height height, uint256 hash)> WaitForBlockAsync(this RPCClient rpc, uint256 blockHash, long timeout = 0)
{
var resp = await rpc.SendCommandAsync("waitforblock", blockHash, timeout);
return (int.Parse(resp.Result["height"].ToString()), uint256.Parse(resp.Result["hash"].ToString()));
}
public static async Task<EstimateSmartFeeResponse> EstimateSmartFeeAsync(this RPCClient rpc, int confirmationTarget, EstimateSmartFeeMode estimateMode = EstimateSmartFeeMode.Conservative, bool simulateIfRegTest = false, bool tryOtherFeeRates = true)
{
if (simulateIfRegTest && rpc.Network == Network.RegTest)
{
return SimulateRegTestFeeEstimation(confirmationTarget, estimateMode);
}
if (tryOtherFeeRates)
{
try
{
return await rpc.EstimateSmartFeeAsync(confirmationTarget, estimateMode);
}
catch (RPCException ex)
{
Logger.LogTrace<RPCClient>(ex);
// Hopefully Bitcoin Core brainfart: https://github.com/bitcoin/bitcoin/issues/14431
for (int i = 2; i <= Constants.SevenDaysConfirmationTarget; i++)
{
try
{
return await rpc.EstimateSmartFeeAsync(i, estimateMode);
}
catch (RPCException ex2)
{
Logger.LogTrace<RPCClient>(ex2);
}
}
}
// Let's try one more time, whatever.
}
return await rpc.EstimateSmartFeeAsync(confirmationTarget, estimateMode);
}
public static async Task<EstimateSmartFeeResponse> TryEstimateSmartFeeAsync(this RPCClient rpc, int confirmationTarget, EstimateSmartFeeMode estimateMode = EstimateSmartFeeMode.Conservative, bool simulateIfRegTest = false, bool tryOtherFeeRates = false)
{
if (simulateIfRegTest && rpc.Network == Network.RegTest)
{
return SimulateRegTestFeeEstimation(confirmationTarget, estimateMode);
}
if (tryOtherFeeRates)
{
EstimateSmartFeeResponse response = await rpc.TryEstimateSmartFeeAsync(confirmationTarget, estimateMode);
if (response != null)
{
return response;
}
else
{
// Hopefully Bitcoin Core brainfart: https://github.com/bitcoin/bitcoin/issues/14431
for (int i = 2; i <= Constants.SevenDaysConfirmationTarget; i++)
{
response = await rpc.TryEstimateSmartFeeAsync(i, estimateMode);
if (response != null)
{
return response;
}
}
}
// Let's try one more time, whatever.
}
return await rpc.TryEstimateSmartFeeAsync(confirmationTarget, estimateMode);
}
private static EstimateSmartFeeResponse SimulateRegTestFeeEstimation(int confirmationTarget, EstimateSmartFeeMode estimateMode)
{
int satoshiPerByte = estimateMode == EstimateSmartFeeMode.Conservative
? (Constants.SevenDaysConfirmationTarget + 1 + 6 - confirmationTarget) / 7
: (Constants.SevenDaysConfirmationTarget + 1 + 5 - confirmationTarget) / 7; // Economical
Money feePerK = Money.Satoshis(satoshiPerByte * 1000);
FeeRate feeRate = new FeeRate(feePerK);
var resp = new EstimateSmartFeeResponse { Blocks = confirmationTarget, FeeRate = feeRate };
return resp;
}
public static async Task<AllFeeEstimate> EstimateAllFeeAsync(this RPCClient rpc, EstimateSmartFeeMode estimateMode = EstimateSmartFeeMode.Conservative, bool simulateIfRegTest = false, bool tolerateBitcoinCoreBrainfuck = true)
{
var estimations = await rpc.EstimateHalfFeesAsync(new Dictionary<int, int>(), 2, 0, Constants.SevenDaysConfirmationTarget, 0, estimateMode, simulateIfRegTest, tolerateBitcoinCoreBrainfuck);
var allFeeEstimate = new AllFeeEstimate(estimateMode, estimations);
return allFeeEstimate;
}
private static async Task<Dictionary<int, int>> EstimateHalfFeesAsync(this RPCClient rpc, IDictionary<int, int> estimations, int smallTarget, int smallFee, int largeTarget, int largeFee, EstimateSmartFeeMode estimateMode = EstimateSmartFeeMode.Conservative, bool simulateIfRegTest = false, bool tolerateBitcoinCoreBrainfuck = true)
{
var newEstimations = new Dictionary<int, int>();
foreach (var est in estimations)
{
newEstimations.TryAdd(est.Key, est.Value);
}
if (Math.Abs(smallTarget - largeTarget) <= 1)
{
return newEstimations;
}
if (smallFee == 0)
{
var smallFeeResult = await rpc.EstimateSmartFeeAsync(smallTarget, estimateMode, simulateIfRegTest, tolerateBitcoinCoreBrainfuck);
smallFee = (int)Math.Ceiling(smallFeeResult.FeeRate.SatoshiPerByte);
newEstimations.TryAdd(smallTarget, smallFee);
}
if (largeFee == 0)
{
var largeFeeResult = await rpc.EstimateSmartFeeAsync(largeTarget, estimateMode, simulateIfRegTest, tolerateBitcoinCoreBrainfuck);
largeFee = (int)Math.Ceiling(largeFeeResult.FeeRate.SatoshiPerByte);
largeTarget = largeFeeResult.Blocks;
newEstimations.TryAdd(largeTarget, largeFee);
}
int halfTarget = (smallTarget + largeTarget) / 2;
var halfFeeResult = await rpc.EstimateSmartFeeAsync(halfTarget, estimateMode, simulateIfRegTest, tolerateBitcoinCoreBrainfuck);
int halfFee = (int)Math.Ceiling(halfFeeResult.FeeRate.SatoshiPerByte);
halfTarget = halfFeeResult.Blocks;
newEstimations.TryAdd(halfTarget, halfFee);
if (smallFee != halfFee)
{
var smallEstimations = await rpc.EstimateHalfFeesAsync(newEstimations, smallTarget, smallFee, halfTarget, halfFee, estimateMode, simulateIfRegTest, tolerateBitcoinCoreBrainfuck);
foreach (var est in smallEstimations)
{
newEstimations.TryAdd(est.Key, est.Value);
}
}
if (largeFee != halfFee)
{
var largeEstimations = await rpc.EstimateHalfFeesAsync(newEstimations, halfTarget, halfFee, largeTarget, largeFee, estimateMode, simulateIfRegTest, tolerateBitcoinCoreBrainfuck);
foreach (var est in largeEstimations)
{
newEstimations.TryAdd(est.Key, est.Value);
}
}
return newEstimations;
}
/// <returns>(allowed, reject-reason)</returns>
public static async Task<(bool accept, string rejectReason)> TestMempoolAcceptAsync(this RPCClient rpc, IEnumerable<Coin> coins)
{
// Check if mempool would accept a fake transaction created with the registered inputs.
// This will catch ascendant/descendant count and size limits for example.
var fakeTransaction = rpc.Network.CreateTransaction();
fakeTransaction.Inputs.AddRange(coins.Select(coin => new TxIn(coin.Outpoint)));
Money fakeOutputValue = NBitcoinHelpers.TakeAReasonableFee(coins.Sum(coin => coin.TxOut.Value));
fakeTransaction.Outputs.Add(fakeOutputValue, new Key());
MempoolAcceptResult testMempoolAcceptResult = await rpc.TestMempoolAcceptAsync(fakeTransaction, allowHighFees: true);
if (!testMempoolAcceptResult.IsAllowed)
{
string rejected = testMempoolAcceptResult.RejectReason;
if (!(rejected.Contains("mandatory-script-verify-flag-failed", StringComparison.OrdinalIgnoreCase)
|| rejected.Contains("non-mandatory-script-verify-flag", StringComparison.OrdinalIgnoreCase)))
{
return (false, rejected);
}
}
return (true, "");
}
/// <summary>
/// Gets the transactions that are unconfirmed using getrawmempool.
/// This is efficient when many transaction ids are provided.
/// </summary>
public static async Task<IEnumerable<uint256>> GetUnconfirmedAsync(this RPCClient rpc, IEnumerable<uint256> transactionHashes)
{
uint256[] unconfirmedTransactionHashes = await rpc.GetRawMempoolAsync();
// If there are common elements, then there's unconfirmed.
return transactionHashes.Intersect(unconfirmedTransactionHashes);
}
/// <summary>
/// Recursively gathers all the dependents of the mempool transactions provided.
/// </summary>
/// <param name="transactionHashes">Mempool transactions to gather their dependents.</param>
/// <param name="includingProvided">Should it include in the result the unconfirmed ones from the provided transactionHashes.</param>
/// <param name="likelyProvidedManyConfirmedOnes">If many provided transactionHashes are not confirmed then it optimizes by doing a check in the beginning of which ones are unconfirmed.</param>
/// <returns>All the dependents of the provided transactionHashes.</returns>
public static async Task<ISet<uint256>> GetAllDependentsAsync(this RPCClient rpc, IEnumerable<uint256> transactionHashes, bool includingProvided, bool likelyProvidedManyConfirmedOnes)
{
IEnumerable<uint256> workingTxHashes = likelyProvidedManyConfirmedOnes // If confirmed txIds are provided, then do a big check first.
? await rpc.GetUnconfirmedAsync(transactionHashes)
: transactionHashes;
var hashSet = new HashSet<uint256>();
foreach (var txId in workingTxHashes)
{
// Go through all the txIds provided and getmempoolentry to get the dependents and the confirmation status.
var entry = await rpc.GetMempoolEntryAsync(txId, throwIfNotFound: false);
if (entry != null)
{
// If we asked to include the provided transaction hashes into the result then check which ones are confirmed and do so.
if (includingProvided)
{
hashSet.Add(txId);
}
// Get all the dependents of all the dependents except the ones we already know of.
var except = entry.Depends.Except(hashSet);
var dependentsOfDependents = await rpc.GetAllDependentsAsync(except, includingProvided: true, likelyProvidedManyConfirmedOnes: false);
// Add them to the hashset.
hashSet.UnionWith(dependentsOfDependents);
}
}
return hashSet;
}
}
}