forked from nblockchain/geewallet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
EtherServer.fs
805 lines (699 loc) · 39.3 KB
/
EtherServer.fs
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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
namespace GWallet.Backend.Ether
open System
open System.IO
open System.Net
open System.Numerics
open System.Linq
open Nethereum.Util
open Nethereum.Hex.HexTypes
open Nethereum.Web3
open Nethereum.RPC.Eth.DTOs
open Nethereum.StandardTokenEIP20.ContractDefinition
open Fsdk
open GWallet.Backend
open GWallet.Backend.FSharpUtil.UwpHacks
type BalanceType =
| Unconfirmed
| Confirmed
type SomeWeb3 (connectionTimeOut, url: string) =
inherit Web3 (connectionTimeOut, url)
member val Url = url with get
type TransactionStatusDetails =
{
GasUsed: BigInteger
Status: BigInteger
}
module Web3ServerSeedList =
// -------------- SERVERS TO REVIEW ADDING TO THE REGISTRY: -----------------------------
//let private PUBLIC_WEB3_API_ETH_INFURA = "https://mainnet.infura.io:8545" ?
// not sure why the below one doesn't work, gives some JSON error
//let private ethWeb3EtherScan = SomeWeb3 "https://api.etherscan.io/api"
// TODO: add the one from https://etcchain.com/api/ too
// FIXME: the below one doesn't seem to work; we should include it anyway and make the algorithm discard it at runtime
//let private etcWeb3CommonWealthMantis = SomeWeb3("https://etc-mantis.callisto.network")
// these 2 only support simple balance requests
// (unconfirmed, because can't do getCurrentBlock requests)
// (non-tokens, because it only replies to balance queries):
// ETH: https://blockscout.com/eth/mainnet/api/eth_rpc
// ETC: https://blockscout.com/etc/mainnet/api/eth_rpc
// (more info: https://blockscout.com/etc/mainnet/eth_rpc_api_docs and https://blockscout.com/eth/mainnet/eth_rpc_api_docs)
// --------------------------------------------------------------------------------------
let private GetEtherServers (currency: Currency): List<ServerDetails> =
let baseCurrency =
if currency = Currency.ETC || currency = Currency.ETH then
currency
elif currency.IsEthToken() then
Currency.ETH
else
failwith <| SPrintF1 "Assertion failed: Ether currency %A not supported?" currency
Caching.Instance.GetServers baseCurrency |> List.ofSeq
let Randomize currency =
let serverList = GetEtherServers currency
Shuffler.Unsort serverList
module Server =
let private Web3Server (connectionTimeOut, serverDetails: ServerDetails) =
match serverDetails.ServerInfo.ConnectionType with
| { Protocol = Tcp _ ; Encrypted = _ } ->
failwith <| SPrintF1 "Ether server of TCP connection type?: %s" serverDetails.ServerInfo.NetworkPath
| { Protocol = Http ; Encrypted = encrypted } ->
let protocol =
if encrypted then
"https"
else
"http"
let uri = SPrintF2 "%s://%s" protocol serverDetails.ServerInfo.NetworkPath
SomeWeb3 (connectionTimeOut, uri)
let HttpRequestExceptionMatchesErrorCode (ex: Http.HttpRequestException) (errorCode: int): bool =
ex.Message.StartsWith(SPrintF1 "%i " errorCode) || ex.Message.Contains(SPrintF1 " %i " errorCode)
|| ex.Message.Contains(SPrintF1 " %i." errorCode)
let exMsg = "Could not communicate with EtherServer"
let PerformEtherRemoteCallWithTimeout<'T,'R> (job: Async<'R>): Async<'R> = async {
let! maybeResult = FSharpUtil.WithTimeout Config.DEFAULT_NETWORK_TIMEOUT job
match maybeResult with
| None ->
return raise <| ServerTimedOutException("Timeout when trying to communicate with Ether server")
| Some result ->
return result
}
let MaybeRethrowWebException (ex: Exception): unit =
let maybeWebEx = FSharpUtil.FindException<WebException> ex
match maybeWebEx with
| Some webEx ->
// TODO: send a warning in Sentry
if webEx.Status = WebExceptionStatus.UnknownError then
raise <| ServerUnreachableException(exMsg, webEx)
if webEx.Status = WebExceptionStatus.NameResolutionFailure then
raise <| ServerCannotBeResolvedException(exMsg, webEx)
if webEx.Status = WebExceptionStatus.ReceiveFailure then
raise <| ServerTimedOutException(exMsg, webEx)
if webEx.Status = WebExceptionStatus.ConnectFailure then
raise <| ServerUnreachableException(exMsg, webEx)
if webEx.Status = WebExceptionStatus.ProtocolError then
raise <| ServerUnreachableException(exMsg, webEx)
if webEx.Status = WebExceptionStatus.SendFailure then
raise <| ServerChannelNegotiationException(exMsg, webEx.Status, webEx)
if webEx.Status = WebExceptionStatus.SecureChannelFailure then
raise <| ServerChannelNegotiationException(exMsg, webEx.Status, webEx)
if webEx.Status = WebExceptionStatus.RequestCanceled then
raise <| ServerChannelNegotiationException(exMsg, webEx.Status, webEx)
if webEx.Status = WebExceptionStatus.TrustFailure then
raise <| ServerChannelNegotiationException(exMsg, webEx.Status, webEx)
raise <| UnhandledWebException(webEx.Status, webEx)
| None ->
()
let MaybeRethrowHttpRequestException (ex: Exception): unit =
let maybeHttpReqEx = FSharpUtil.FindException<Http.HttpRequestException> ex
match maybeHttpReqEx with
| Some httpReqEx ->
if HttpRequestExceptionMatchesErrorCode httpReqEx (int CloudFlareError.ConnectionTimeOut) then
raise <| ServerTimedOutException(exMsg, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int CloudFlareError.OriginUnreachable) then
raise <| ServerTimedOutException(exMsg, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int CloudFlareError.OriginSslHandshakeError) then
raise <| ServerChannelNegotiationException(exMsg, CloudFlareError.OriginSslHandshakeError, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int CloudFlareError.WebServerDown) then
raise <| ServerUnreachableException(exMsg, CloudFlareError.WebServerDown, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int CloudFlareError.OriginError) then
raise <| ServerUnreachableException(exMsg, CloudFlareError.OriginError, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCode.BadGateway) then
raise <| ServerUnreachableException(exMsg, HttpStatusCode.BadGateway, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCode.GatewayTimeout) then
raise <| ServerUnreachableException(exMsg, HttpStatusCode.GatewayTimeout, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCode.ServiceUnavailable) then
raise <| ServerUnavailableException(exMsg, httpReqEx)
// TODO: maybe in these cases below, blacklist the server somehow if it keeps giving this error:
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCode.Forbidden) then
raise <| ServerMisconfiguredException(exMsg, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCode.Unauthorized) then
raise <| ServerMisconfiguredException(exMsg, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCode.MethodNotAllowed) then
raise <| ServerMisconfiguredException(exMsg, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCode.InternalServerError) then
raise <| ServerUnavailableException(exMsg, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCode.NotFound) then
raise <| ServerUnavailableException(exMsg, httpReqEx)
// this happened once with ETC (www.ethercluster.com/etc) when trying to broadcast a transaction
// (not sure if it's correct to ignore it but I can't fathom how the tx could be a bad request:
// https://sentry.io/organizations/nblockchain/issues/1937781114/ )
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCode.BadRequest) then
raise <| ServerMisconfiguredException(exMsg, HttpStatusCode.BadRequest, httpReqEx)
if HttpRequestExceptionMatchesErrorCode
httpReqEx (int HttpStatusCodeNotPresentInTheBcl.TooManyRequests) then
raise <| ServerRestrictiveException(exMsg, httpReqEx)
if HttpRequestExceptionMatchesErrorCode httpReqEx (int HttpStatusCodeNotPresentInTheBcl.FrozenSite) then
raise <| ServerUnavailableException(exMsg, httpReqEx)
// TODO: report this one as a warning to sentry?
if httpReqEx.InnerException <> null &&
httpReqEx.InnerException :? System.Runtime.InteropServices.COMException then
// got this once, with the exception message
// "the text associated with this error code could not be found.
// The date in the certificate is invalid or has expired"
raise <| ServerMisconfiguredException(exMsg, httpReqEx)
// weird "IOException: The server returned an invalid or unrecognized response." since Mono 6.4.x (vs16.3)
if (FSharpUtil.FindException<IOException> httpReqEx).IsSome then
raise <| ServerMisconfiguredException(exMsg, httpReqEx)
| _ ->
()
let private err32kPossibleMessages =
[
"pruning=archive"
"header not found"
"error: no suitable peers available"
"missing trie node"
"getDeleteStateObject"
"execution aborted"
]
let MaybeRethrowRpcResponseException (ex: Exception): unit =
let maybeRpcResponseEx = FSharpUtil.FindException<JsonRpcSharp.Client.RpcResponseException> ex
match maybeRpcResponseEx with
| Some rpcResponseEx ->
if not (isNull rpcResponseEx.RpcError) then
match rpcResponseEx.RpcError.Code with
| a when a = int RpcErrorCode.JackOfAllTradesErrorCode ->
if not (err32kPossibleMessages.Any (fun msg -> rpcResponseEx.RpcError.Message.Contains msg)) then
let possibleErrMessages =
SPrintF1 "'%s'" (String.Join("' or '", err32kPossibleMessages))
raise <| Exception(
SPrintF3 "Expecting %s in message of a %d code, but got '%s'"
possibleErrMessages
(int RpcErrorCode.JackOfAllTradesErrorCode)
rpcResponseEx.RpcError.Message,
rpcResponseEx)
else
raise <| ServerMisconfiguredException(exMsg, rpcResponseEx)
| b when b = int RpcErrorCode.UnknownBlockNumber ->
raise <| ServerMisconfiguredException(exMsg, rpcResponseEx)
| c when c = int RpcErrorCode.GatewayTimeout ->
raise <| ServerMisconfiguredException(exMsg, rpcResponseEx)
| d when d = int RpcErrorCode.EmptyResponse ->
raise <| ServerMisconfiguredException(exMsg, rpcResponseEx)
| e when e = int RpcErrorCode.ProjectIdSettingsRejection ->
raise <| ServerRefusedException(exMsg, rpcResponseEx)
| f when f = int RpcErrorCode.DailyRequestCountExceededSoRequestRateLimited ->
raise <| ServerRefusedException(exMsg, rpcResponseEx)
| g when g = int RpcErrorCode.CannotFulfillRequest ->
raise <| ServerRefusedException(exMsg, rpcResponseEx)
| h when h = int RpcErrorCode.ResourceNotFound ->
raise <| ServerMisconfiguredException(exMsg, rpcResponseEx)
| i when i = int RpcErrorCode.InternalError ->
raise <| ServerFaultException(exMsg, rpcResponseEx)
| _ ->
raise
<| Exception (SPrintF3 "RpcResponseException with RpcError Code <%i> and Message '%s' (%s)"
rpcResponseEx.RpcError.Code
rpcResponseEx.RpcError.Message
rpcResponseEx.Message,
rpcResponseEx)
| None ->
()
let MaybeRethrowRpcClientTimeoutException (ex: Exception): unit =
let maybeRpcTimeoutException =
FSharpUtil.FindException<JsonRpcSharp.Client.RpcClientTimeoutException> ex
match maybeRpcTimeoutException with
| Some rpcTimeoutEx ->
raise <| ServerTimedOutException(exMsg, rpcTimeoutEx)
| None ->
()
let MaybeRethrowNetworkingException (ex: Exception): unit =
let maybeSocketRewrappedException = Networking.FindExceptionToRethrow ex exMsg
match maybeSocketRewrappedException with
| Some socketRewrappedException ->
raise socketRewrappedException
| None ->
()
// this could be a Xamarin.Android bug (see https://gitlab.com/nblockchain/geewallet/issues/119)
let MaybeRethrowObjectDisposedException (ex: Exception): unit =
let maybeRpcUnknownEx = FSharpUtil.FindException<JsonRpcSharp.Client.RpcClientUnknownException> ex
match maybeRpcUnknownEx with
| Some _ ->
let maybeObjectDisposedEx = FSharpUtil.FindException<ObjectDisposedException> ex
match maybeObjectDisposedEx with
| Some objectDisposedEx ->
if objectDisposedEx.Message.Contains "MobileAuthenticatedStream" then
raise <| ProtocolGlitchException(objectDisposedEx.Message, objectDisposedEx)
| None ->
()
| None ->
()
let MaybeRethrowInnerRpcException (ex: Exception): unit =
let maybeRpcUnknownEx = FSharpUtil.FindException<JsonRpcSharp.Client.RpcClientUnknownException> ex
match maybeRpcUnknownEx with
| Some rpcUnknownEx ->
let maybeDeSerializationEx =
FSharpUtil.FindException<JsonRpcSharp.Client.DeserializationException> rpcUnknownEx
match maybeDeSerializationEx with
| None ->
()
| Some deserEx ->
raise <| ServerMisconfiguredException(deserEx.Message, ex)
// this SSL exception could be a mono 6.0.x bug (see https://gitlab.com/nblockchain/geewallet/issues/121)
let maybeHttpReqEx = FSharpUtil.FindException<Http.HttpRequestException> ex
match maybeHttpReqEx with
| Some httpReqEx ->
(* NOTE: we disabled UWP tweaks for now, see other comment in Config.fs
// FIXME: report this UWP bug (see commit message for full stacktrace)
if Xamarin.Essentials.DeviceInfo.Platform.Equals Xamarin.Essentials.DevicePlatform.UWP &&
httpReqEx.InnerException <> null && httpReqEx.InnerException.GetType() = typeof<Exception> then
raise <| ServerCannotBeResolvedException(httpReqEx.InnerException.Message, ex)
*)
// this could be a mono 6.0.x bug (see https://gitlab.com/nblockchain/geewallet/issues/121)
if httpReqEx.Message.Contains "SSL" then
let maybeIOEx = FSharpUtil.FindException<IOException> ex
match maybeIOEx with
| Some ioEx ->
raise <| ProtocolGlitchException(ioEx.Message, ex)
| None ->
let maybeSecEx =
FSharpUtil.FindException<System.Security.Authentication.AuthenticationException> ex
match maybeSecEx with
| Some secEx ->
raise <| ProtocolGlitchException(secEx.Message, ex)
| None ->
()
| None ->
()
| None ->
()
let private ReworkException (ex: Exception): unit =
MaybeRethrowWebException ex
MaybeRethrowHttpRequestException ex
MaybeRethrowRpcResponseException ex
MaybeRethrowRpcClientTimeoutException ex
MaybeRethrowNetworkingException ex
MaybeRethrowObjectDisposedException ex
MaybeRethrowInnerRpcException ex
let private NumberOfParallelJobsForMode mode =
match mode with
| ServerSelectionMode.Fast -> 3u
| ServerSelectionMode.Analysis -> 2u
let etcEcosystemIsMomentarilyCentralized = false
let private FaultTolerantParallelClientInnerSettings (numberOfConsistentResponsesRequired: uint32)
(mode: ServerSelectionMode)
currency
maybeConsistencyConfig =
let consistencyConfig =
match maybeConsistencyConfig with
| None -> SpecificNumberOfConsistentResponsesRequired numberOfConsistentResponsesRequired
| Some specificConsistencyConfig -> specificConsistencyConfig
let retries =
match currency with
| Currency.ETC when etcEcosystemIsMomentarilyCentralized -> Config.NUMBER_OF_RETRIES_TO_SAME_SERVERS * 2u
| _ -> Config.NUMBER_OF_RETRIES_TO_SAME_SERVERS
{
NumberOfParallelJobsAllowed = NumberOfParallelJobsForMode mode
NumberOfRetries = retries
NumberOfRetriesForInconsistency = retries
ExceptionHandler = Some
(
fun ex ->
Infrastructure.ReportWarning ex
|> ignore<bool>
)
ResultSelectionMode =
Selective
{
ServerSelectionMode = mode
ConsistencyConfig = consistencyConfig
ReportUncanceledJobs = true
}
}
let private FaultTolerantParallelClientDefaultSettings (mode: ServerSelectionMode) (currency: Currency) =
let numberOfConsistentResponsesRequired =
if etcEcosystemIsMomentarilyCentralized && currency = Currency.ETC then
1u
else
2u
FaultTolerantParallelClientInnerSettings numberOfConsistentResponsesRequired
mode
currency
let private FaultTolerantParallelClientSettingsForBalanceCheck (mode: ServerSelectionMode)
(currency: Currency)
(cacheOrInitialBalanceMatchFunc: decimal->bool) =
let consistencyConfig =
if etcEcosystemIsMomentarilyCentralized && currency = Currency.ETC then
None
elif mode = ServerSelectionMode.Fast then
Some (OneServerConsistentWithCertainValueOrTwoServers cacheOrInitialBalanceMatchFunc)
else
None
FaultTolerantParallelClientDefaultSettings mode currency consistencyConfig
let private FaultTolerantParallelClientSettingsForBroadcast currency =
FaultTolerantParallelClientInnerSettings 1u ServerSelectionMode.Fast currency None
let private faultTolerantEtherClient =
FaultTolerantParallelClient<ServerDetails,ServerDiscardedException> Caching.Instance.SaveServerLastStat
let Web3ServerToRetrievalFunc (server: ServerDetails)
(web3ClientFunc: SomeWeb3->Async<'R>)
currency
: Async<'R> =
let HandlePossibleEtherFailures (job: Async<'R>): Async<'R> =
async {
try
let! result = PerformEtherRemoteCallWithTimeout job
return result
with
| ex ->
ReworkException ex
return raise <| FSharpUtil.ReRaise ex
}
let connectionTimeout =
match currency with
| Currency.ETC when etcEcosystemIsMomentarilyCentralized ->
Config.DEFAULT_NETWORK_TIMEOUT + Config.DEFAULT_NETWORK_TIMEOUT
| _ ->
Config.DEFAULT_NETWORK_TIMEOUT
async {
let web3Server = Web3Server (connectionTimeout, server)
try
return! HandlePossibleEtherFailures (web3ClientFunc web3Server)
// NOTE: try to make this 'with' block be in sync with the one in UtxoCoinAccount:GetRandomizedFuncs()
with
| :? CommunicationUnsuccessfulException as ex ->
let msg = SPrintF2 "%s: %s" (ex.GetType().FullName) ex.Message
return raise <| ServerDiscardedException(msg, ex)
| ex ->
return raise <| Exception(SPrintF1 "Some problem when connecting to '%s'"
server.ServerInfo.NetworkPath, ex)
}
// FIXME: seems there's some code duplication between this function and UtxoCoinAccount.fs's GetServerFuncs function
// and room for simplification to not pass a new ad-hoc delegate?
let GetServerFuncs<'R> (web3Func: SomeWeb3->Async<'R>)
(etherServers: seq<ServerDetails>)
(currency: Currency)
: seq<Server<ServerDetails,'R>> =
let Web3ServerToGenericServer (web3ClientFunc: SomeWeb3->Async<'R>)
(etherServer: ServerDetails)
: Server<ServerDetails,'R> =
{
Details = etherServer
Retrieval = Web3ServerToRetrievalFunc etherServer web3ClientFunc currency
}
let serverFuncs =
Seq.map (Web3ServerToGenericServer web3Func)
etherServers
serverFuncs
let private GetRandomizedFuncs<'R> (currency: Currency)
(web3Func: SomeWeb3->Async<'R>)
: List<Server<ServerDetails,'R>> =
let etherServers = Web3ServerSeedList.Randomize currency
GetServerFuncs web3Func etherServers currency
|> List.ofSeq
let GetTransactionCount (currency: Currency) (address: string)
: Async<HexBigInteger> =
async {
let web3Funcs =
let web3Func (web3: Web3): Async<HexBigInteger> =
async {
let! cancelToken = Async.CancellationToken
let task =
web3.Eth.Transactions.GetTransactionCount.SendRequestAsync(address, null, cancelToken)
let! txCount = Async.AwaitTask task
if isNull txCount then
raise <| AbnormalNullValueInJsonResponseException "Weird null response from tx count job"
return txCount
}
GetRandomizedFuncs currency web3Func
return! faultTolerantEtherClient.Query
(FaultTolerantParallelClientDefaultSettings ServerSelectionMode.Fast currency None)
web3Funcs
}
let private NUMBER_OF_CONFIRMATIONS_TO_CONSIDER_BALANCE_CONFIRMED = BigInteger(45)
let private GetBlockToCheckForConfirmedBalance(web3: Web3): Async<BlockParameter> =
async {
let! cancelToken = Async.CancellationToken
let! latestBlock =
web3.Eth.Blocks.GetBlockNumber.SendRequestAsync (null, cancelToken)
|> Async.AwaitTask
if isNull latestBlock then
failwith "latestBlock somehow is null"
let blockToCheck = BigInteger.Subtract(latestBlock.Value,
NUMBER_OF_CONFIRMATIONS_TO_CONSIDER_BALANCE_CONFIRMED)
if blockToCheck.Sign < 0 then
let errMsg = SPrintF2
"Looks like we received a wrong latestBlock(%s) because the substract was negative(%s)"
(latestBlock.Value.ToString())
(blockToCheck.ToString())
raise <| ServerMisconfiguredException errMsg
return BlockParameter(HexBigInteger(blockToCheck))
}
let private GetConfirmedEtherBalanceInternal (web3: Web3) (publicAddress: string): Async<HexBigInteger> =
async {
let! blockForConfirmationReference = GetBlockToCheckForConfirmedBalance web3
(*
if (Config.DebugLog) then
Infrastructure.LogError (SPrintF2 "Last block number and last confirmed block number: %s: %s"
(latestBlock.Value.ToString()) (blockForConfirmationReference.BlockNumber.Value.ToString()))
*)
let! cancelToken = Async.CancellationToken
cancelToken.ThrowIfCancellationRequested()
let! balance =
web3.Eth.GetBalance.SendRequestAsync (publicAddress,
blockForConfirmationReference,
null,
cancelToken)
|> Async.AwaitTask
return balance
}
let private BalanceMatchWithCacheOrInitialBalance address currency someRetrievedBalance =
if Caching.Instance.FirstRun then
someRetrievedBalance = 0m
else
match Caching.Instance.TryRetrieveLastCompoundBalance address currency with
| None -> false
| Some balance -> someRetrievedBalance = balance
let GetEtherBalance (currency: Currency)
(address: string)
(balType: BalanceType)
(mode: ServerSelectionMode)
(cancelSourceOption: Option<CustomCancelSource>)
: Async<decimal> =
async {
let web3Funcs =
let web3Func (web3: Web3): Async<decimal> = async {
let! balance =
match balType with
| BalanceType.Confirmed ->
GetConfirmedEtherBalanceInternal web3 address
| BalanceType.Unconfirmed ->
async {
let! cancelToken = Async.CancellationToken
let task = web3.Eth.GetBalance.SendRequestAsync (address, null, cancelToken)
return! Async.AwaitTask task
}
if Object.ReferenceEquals(balance, null) then
raise <|
AbnormalNullValueInJsonResponseException
AbnormalNullValueInJsonResponseException.BalanceJobErrorMessage
return UnitConversion.Convert.FromWei(balance.Value, UnitConversion.EthUnit.Ether)
}
GetRandomizedFuncs currency web3Func
let query =
match cancelSourceOption with
| None ->
faultTolerantEtherClient.Query
| Some cancelSource ->
faultTolerantEtherClient.QueryWithCancellation cancelSource
return! query
(FaultTolerantParallelClientSettingsForBalanceCheck
mode currency (BalanceMatchWithCacheOrInitialBalance address currency))
web3Funcs
}
let private GetConfirmedTokenBalanceInternal (web3: Web3) (publicAddress: string) (currency: Currency)
: Async<decimal> =
if isNull web3 then
invalidArg "web3" "web3 argument should not be null"
async {
let! blockForConfirmationReference = GetBlockToCheckForConfirmedBalance web3
let balanceOfFunctionMsg = BalanceOfFunction(Owner = publicAddress)
let contractAddress = TokenManager.GetTokenContractAddress currency
let contractHandler = web3.Eth.GetContractHandler contractAddress
if isNull contractHandler then
failwith "contractHandler somehow is null"
let! cancelToken = Async.CancellationToken
cancelToken.ThrowIfCancellationRequested()
let! balance = contractHandler.QueryAsync<BalanceOfFunction,BigInteger>
(balanceOfFunctionMsg,
blockForConfirmationReference,
cancelToken) |> Async.AwaitTask
return UnitConversion.Convert.FromWei(balance, UnitConversion.EthUnit.Ether)
}
let GetTokenBalance (currency: Currency)
(address: string)
(balType: BalanceType)
(mode: ServerSelectionMode)
(cancelSourceOption: Option<CustomCancelSource>)
: Async<decimal> =
async {
let web3Funcs =
let web3Func (web3: Web3): Async<decimal> =
match balType with
| BalanceType.Confirmed ->
GetConfirmedTokenBalanceInternal web3 address currency
| BalanceType.Unconfirmed ->
let tokenService = TokenManager.TokenServiceWrapper (web3, currency)
async {
let! cancelToken = Async.CancellationToken
let task = tokenService.BalanceOfQueryAsync (address, null, cancelToken)
let! balance = Async.AwaitTask task
return UnitConversion.Convert.FromWei(balance, UnitConversion.EthUnit.Ether)
}
GetRandomizedFuncs currency web3Func
let query =
match cancelSourceOption with
| None ->
faultTolerantEtherClient.Query
| Some cancelSource ->
faultTolerantEtherClient.QueryWithCancellation cancelSource
return! query
(FaultTolerantParallelClientSettingsForBalanceCheck
mode currency (BalanceMatchWithCacheOrInitialBalance address currency))
web3Funcs
}
let EstimateTokenTransferFee (account: IAccount) (amount: decimal) destination
: Async<HexBigInteger> =
async {
let web3Funcs =
let web3Func (web3: Web3): Async<HexBigInteger> =
let contractAddress = TokenManager.GetTokenContractAddress account.Currency
let contractHandler = web3.Eth.GetContractHandler contractAddress
let amountInWei = UnitConversion.Convert.ToWei(amount, UnitConversion.EthUnit.Ether)
let transferFunctionMsg = TransferFunction(FromAddress = account.PublicAddress,
To = destination,
Value = amountInWei)
async {
let! cancelToken = Async.CancellationToken
let task =
contractHandler.EstimateGasAsync<TransferFunction>(transferFunctionMsg, cancelToken)
let! fee = Async.AwaitTask task
if isNull fee then
raise <| AbnormalNullValueInJsonResponseException "Weird null response from transfer fee job"
return fee
}
GetRandomizedFuncs account.Currency web3Func
return! faultTolerantEtherClient.Query
(FaultTolerantParallelClientDefaultSettings ServerSelectionMode.Fast account.Currency None)
web3Funcs
}
let private AverageGasPrice (gasPricesFromDifferentServers: List<HexBigInteger>): HexBigInteger =
let sum = gasPricesFromDifferentServers.Select(fun hbi -> hbi.Value)
.Aggregate(fun bi1 bi2 -> BigInteger.Add(bi1, bi2))
let avg = BigInteger.Divide(sum, BigInteger(gasPricesFromDifferentServers.Length))
HexBigInteger(avg)
let GetGasPrice (currency: Currency)
: Async<HexBigInteger> =
async {
let web3Funcs =
let web3Func (web3: Web3): Async<HexBigInteger> =
async {
let! cancelToken = Async.CancellationToken
let task = web3.Eth.GasPrice.SendRequestAsync(null, cancelToken)
let! hexBigInteger = Async.AwaitTask task
if isNull hexBigInteger then
raise <| AbnormalNullValueInJsonResponseException "Weird null response from gas price job"
if hexBigInteger.Value = BigInteger 0 then
return failwith "Some server returned zero for gas price, which is invalid"
return hexBigInteger
}
GetRandomizedFuncs currency web3Func
let minResponsesRequired =
if etcEcosystemIsMomentarilyCentralized && currency = Currency.ETC then
1u
else
2u
return! faultTolerantEtherClient.Query
(FaultTolerantParallelClientDefaultSettings
ServerSelectionMode.Fast
currency
(Some (AverageBetweenResponses (minResponsesRequired, AverageGasPrice))))
web3Funcs
}
let BroadcastTransaction (currency: Currency) (transaction: string)
: Async<string> =
let insufficientFundsMsg = "Insufficient funds"
async {
let web3Funcs =
let web3Func (web3: Web3): Async<string> =
async {
let! cancelToken = Async.CancellationToken
let task =
web3.Eth.Transactions.SendRawTransaction.SendRequestAsync(transaction, null, cancelToken)
let! response = Async.AwaitTask task
if isNull response then
raise <|
AbnormalNullValueInJsonResponseException
"Weird null response from broadcast transaction job"
return response
}
GetRandomizedFuncs currency web3Func
try
return! faultTolerantEtherClient.Query
(FaultTolerantParallelClientSettingsForBroadcast currency)
web3Funcs
with
| ex ->
match FSharpUtil.FindException<JsonRpcSharp.Client.RpcResponseException> ex with
| None ->
return raise (FSharpUtil.ReRaise ex)
| Some rpcResponseException ->
// FIXME: this is fragile, ideally should respond with an error code
if rpcResponseException.Message.StartsWith(insufficientFundsMsg,
StringComparison.InvariantCultureIgnoreCase) then
return raise InsufficientFunds
else
return raise (FSharpUtil.ReRaise ex)
}
let private GetTransactionDetailsFromTransactionReceipt (currency: Currency) (txHash: string)
: Async<TransactionStatusDetails> =
async {
let web3Funcs =
let web3Func (web3: Web3): Async<TransactionStatusDetails> =
async {
let! cancelToken = Async.CancellationToken
let task =
web3.TransactionManager.TransactionReceiptService.PollForReceiptAsync(txHash, cancelToken)
let! transactionReceipt = Async.AwaitTask task
if isNull transactionReceipt || isNull transactionReceipt.GasUsed || isNull transactionReceipt.Status then
raise <|
AbnormalNullValueInJsonResponseException
(SPrintF1 "Weird null response when getting details from tx receipt (%A)" transactionReceipt)
return {
GasUsed = transactionReceipt.GasUsed.Value
Status = transactionReceipt.Status.Value
}
}
GetRandomizedFuncs currency web3Func
return! faultTolerantEtherClient.Query
(FaultTolerantParallelClientDefaultSettings ServerSelectionMode.Fast currency None)
web3Funcs
}
let IsOutOfGas (currency: Currency) (txHash: string) (spentGas: int64): Async<bool> =
async {
let! transactionStatusDetails = GetTransactionDetailsFromTransactionReceipt currency txHash
let failureStatus = BigInteger.Zero
return transactionStatusDetails.Status = failureStatus &&
transactionStatusDetails.GasUsed = BigInteger(spentGas)
}
let private GetContractCode (baseCurrency: Currency) (address: string)
: Async<string> =
async {
let web3Funcs =
let web3Func (web3: Web3): Async<string> =
async {
let! cancelToken = Async.CancellationToken
let task = web3.Eth.GetCode.SendRequestAsync(address, null, cancelToken)
return! Async.AwaitTask task
}
GetRandomizedFuncs baseCurrency web3Func
return! faultTolerantEtherClient.Query
(FaultTolerantParallelClientDefaultSettings ServerSelectionMode.Fast baseCurrency None)
web3Funcs
}
let CheckIfAddressIsAValidPaymentDestination (currency: Currency) (address: string): Async<unit> =
async {
let! contractCode = GetContractCode currency address
let emptyContract = "0x"
if not (contractCode.StartsWith emptyContract) then
failwith <| SPrintF2 "GetCode API should always return a string starting with %s, but got: %s"
emptyContract contractCode
elif contractCode <> emptyContract then
return raise <| InvalidDestinationAddress "Sending to contract addresses is not supported yet. Supply a normal address please."
}