protected PayoutHandlerBase(IConnectionFactory cf, IMapper mapper, IShareRepository shareRepo, IBlockRepository blockRepo, IBalanceRepository balanceRepo, IPaymentRepository paymentRepo, IMasterClock clock, NotificationService notificationService) { Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(mapper, nameof(mapper)); Contract.RequiresNonNull(shareRepo, nameof(shareRepo)); Contract.RequiresNonNull(blockRepo, nameof(blockRepo)); Contract.RequiresNonNull(balanceRepo, nameof(balanceRepo)); Contract.RequiresNonNull(paymentRepo, nameof(paymentRepo)); Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(notificationService, nameof(notificationService)); this.cf = cf; this.mapper = mapper; this.clock = clock; this.shareRepo = shareRepo; this.blockRepo = blockRepo; this.balanceRepo = balanceRepo; this.paymentRepo = paymentRepo; this.notificationService = notificationService; BuildFaultHandlingPolicy(); }
public ApiServer( IMapper mapper, IConnectionFactory cf, IBlockRepository blocksRepo, IPaymentRepository paymentsRepo, IStatsRepository statsRepo, IMasterClock clock) { Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(statsRepo, nameof(statsRepo)); Contract.RequiresNonNull(blocksRepo, nameof(blocksRepo)); Contract.RequiresNonNull(paymentsRepo, nameof(paymentsRepo)); Contract.RequiresNonNull(mapper, nameof(mapper)); Contract.RequiresNonNull(clock, nameof(clock)); this.cf = cf; this.statsRepo = statsRepo; this.blocksRepo = blocksRepo; this.paymentsRepo = paymentsRepo; this.mapper = mapper; this.clock = clock; requestMap = new Dictionary <Regex, Func <HttpContext, Match, Task> > { { new Regex("^/api/pools$", RegexOptions.Compiled), HandleGetPoolsAsync }, { new Regex("^/api/pool/(?<poolId>[^/]+)/stats/hourly$", RegexOptions.Compiled), HandleGetPoolStatsAsync }, { new Regex("^/api/pool/(?<poolId>[^/]+)/blocks$", RegexOptions.Compiled), HandleGetBlocksPagedAsync }, { new Regex("^/api/pool/(?<poolId>[^/]+)/payments$", RegexOptions.Compiled), HandleGetPaymentsPagedAsync }, { new Regex("^/api/pool/(?<poolId>[^/]+)/miner/(?<address>[^/]+)/stats$", RegexOptions.Compiled), HandleGetMinerStatsAsync }, }; }
public void RespondError(object id, int code, string message) { Contract.RequiresNonNull(id, nameof(id)); Contract.Requires <ArgumentException>(!string.IsNullOrEmpty(message), $"{nameof(message)} must not be empty"); Respond(new JsonRpcResponse(new JsonRpcException(code, message, null), id)); }
public override void Configure(PoolConfig poolConfig, ClusterConfig clusterConfig) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); Contract.RequiresNonNull(clusterConfig, nameof(clusterConfig)); logger = LogUtil.GetPoolScopedLogger(typeof(JobManagerBase <MoneroJob>), poolConfig); this.poolConfig = poolConfig; this.clusterConfig = clusterConfig; // extract standard daemon endpoints daemonEndpoints = poolConfig.Daemons .Where(x => string.IsNullOrEmpty(x.Category)) .ToArray(); // extract wallet daemon endpoints walletDaemonEndpoints = poolConfig.Daemons .Where(x => x.Category?.ToLower() == MoneroConstants.WalletDaemonCategory) .ToArray(); if (walletDaemonEndpoints.Length == 0) { logger.ThrowLogPoolStartupException("Wallet-RPC daemon is not configured (Daemon configuration for monero-pools require an additional entry of category \'wallet' pointing to the wallet daemon)", LogCat); } ConfigureDaemons(); }
public virtual async Task StartAsync() { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); logger.Info(() => $"[{LogCat}] Launching ..."); try { SetupBanning(clusterConfig); await SetupJobManager(); var ipEndpoints = poolConfig.Ports.Keys .Select(port => PoolEndpoint2IPEndpoint(port, poolConfig.Ports[port])) .ToArray(); StartListeners(ipEndpoints); SetupStats(); await UpdateBlockChainStatsAsync(); logger.Info(() => $"[{LogCat}] Online"); OutputPoolInfo(); } catch (PoolStartupAbortException) { // just forward these throw; } catch (Exception ex) { logger.Error(ex); throw; } }
public void Configure(DaemonEndpointConfig[] endPoints, string digestAuthRealm = null) { Contract.RequiresNonNull(endPoints, nameof(endPoints)); Contract.Requires <ArgumentException>(endPoints.Length > 0, $"{nameof(endPoints)} must not be empty"); this.endPoints = endPoints; // create one HttpClient instance per endpoint that carries the associated credentials httpClients = endPoints.ToDictionary(endpoint => endpoint, endpoint => { var handler = new SocketsHttpHandler { Credentials = new NetworkCredential(endpoint.User, endpoint.Password), PreAuthenticate = true, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, }; if (endpoint.Ssl && !endpoint.ValidateCert) { handler.SslOptions = new SslClientAuthenticationOptions { RemoteCertificateValidationCallback = ((sender, certificate, chain, errors) => true), }; } return(new HttpClient(handler)); }); }
public void Send <T>(T payload) { Contract.RequiresNonNull(payload, nameof(payload)); if (isAlive) { using (var stream = (RecyclableMemoryStream)streamManager.GetStream()) { using (var writer = new StreamWriter(stream, Encoding, 0x400, true)) { Serializer.Serialize(writer, payload); } // append newline stream.WriteByte(0xa); // log it logger.Trace(() => $"[{ConnectionId}] Sending: {Encoding.GetString(stream.GetBuffer(), 0, (int)stream.Length)}"); // copy buffer and queue up var buf = ByteArrayPool.Rent((int)stream.Length); Array.Copy(stream.GetBuffer(), buf, stream.Length); SendInternal(new PooledArraySegment <byte>(buf, 0, (int)stream.Length)); } } }
public void Configure(ClusterConfig clusterConfig, PoolConfig poolConfig) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); this.poolConfig = poolConfig; this.clusterConfig = clusterConfig; logger = LogUtil.GetPoolScopedLogger(typeof(MoneroPayoutHandler), poolConfig); // configure standard daemon var jsonSerializerSettings = ctx.Resolve <JsonSerializerSettings>(); var daemonEndpoints = poolConfig.Daemons .Where(x => string.IsNullOrEmpty(x.Category)) .ToArray(); daemon = new DaemonClient(jsonSerializerSettings); daemon.Configure(daemonEndpoints, MoneroConstants.DaemonRpcLocation); // configure wallet daemon var walletDaemonEndpoints = poolConfig.Daemons .Where(x => x.Category?.ToLower() == MoneroConstants.WalletDaemonCategory) .ToArray(); walletDaemon = new DaemonClient(jsonSerializerSettings); walletDaemon.Configure(walletDaemonEndpoints, MoneroConstants.DaemonRpcLocation); }
public async Task PayoutAsync(Balance[] balances) { Contract.RequiresNonNull(balances, nameof(balances)); // simple balances first var simpleBalances = balances .Where(x => !x.Address.Contains(PayoutConstants.PayoutInfoSeperator)) .ToArray(); if (simpleBalances.Length > 0) { await PayoutBatch(simpleBalances); } // balances with paymentIds var extraConfig = poolConfig.PaymentProcessing.Extra.SafeExtensionDataAs <MoneroPoolPaymentProcessingConfigExtra>(); var minimumPaymentToPaymentId = extraConfig?.MinimumPaymentToPaymentId ?? poolConfig.PaymentProcessing.MinimumPayment; var paymentIdBalances = balances.Except(simpleBalances) .Where(x => x.Amount >= minimumPaymentToPaymentId) .ToArray(); foreach (var balance in paymentIdBalances) { await PayoutToPaymentId(balance); } }
public void Respond <T>(T payload, object id) { Contract.RequiresNonNull(payload, nameof(payload)); Contract.RequiresNonNull(id, nameof(id)); Respond(new JsonRpcResponse <T>(payload, id)); }
public void Ban(IPAddress address, TimeSpan duration) { Contract.RequiresNonNull(address, nameof(address)); Contract.Requires <ArgumentException>(duration.TotalMilliseconds > 0, $"{nameof(duration)} must not be empty"); cache.Set(address.ToString(), string.Empty, duration); }
public void Configure(DaemonEndpointConfig[] endPoints, string digestAuthRealm = null) { Contract.RequiresNonNull(endPoints, nameof(endPoints)); Contract.Requires <ArgumentException>(endPoints.Length > 0, $"{nameof(endPoints)} must not be empty"); this.endPoints = endPoints; // create one HttpClient instance per endpoint that carries the associated credentials httpClients = endPoints.ToDictionary(endpoint => endpoint, endpoint => { var handler = new HttpClientHandler { Credentials = new NetworkCredential(endpoint.User, endpoint.Password), PreAuthenticate = true, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }; if (endpoint.Ssl && !endpoint.ValidateCert) { handler.ClientCertificateOptions = ClientCertificateOption.Manual; handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true; } HttpClient httpClient = new HttpClient(handler); httpClient.Timeout = TimeSpan.FromSeconds(10); return(httpClient); }); }
protected PoolBase(IComponentContext ctx, JsonSerializerSettings serializerSettings, IConnectionFactory cf, IStatsRepository statsRepo, IMapper mapper, IEnumerable <Meta <INotificationSender, NotificationSenderMetadataAttribute> > notificationSenders) : base(ctx) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(serializerSettings, nameof(serializerSettings)); Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(statsRepo, nameof(statsRepo)); Contract.RequiresNonNull(mapper, nameof(mapper)); Contract.RequiresNonNull(notificationSenders, nameof(notificationSenders)); this.serializerSettings = serializerSettings; this.cf = cf; this.statsRepo = statsRepo; this.mapper = mapper; this.notificationSenders = notificationSenders; Shares = shareSubject .AsObservable() .Synchronize(); validShares = validSharesSubject .AsObservable() .Synchronize(); invalidShares = invalidSharesSubject .AsObservable() .Synchronize(); }
protected PayoutHandlerBase(IConnectionFactory cf, IMapper mapper, IShareRepository shareRepo, IBlockRepository blockRepo, IBalanceRepository balanceRepo, IPaymentRepository paymentRepo, IEnumerable <Meta <INotificationSender, NotificationSenderMetadataAttribute> > notificationSenders) { Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(mapper, nameof(mapper)); Contract.RequiresNonNull(shareRepo, nameof(shareRepo)); Contract.RequiresNonNull(blockRepo, nameof(blockRepo)); Contract.RequiresNonNull(balanceRepo, nameof(balanceRepo)); Contract.RequiresNonNull(paymentRepo, nameof(paymentRepo)); Contract.RequiresNonNull(notificationSenders, nameof(notificationSenders)); this.cf = cf; this.mapper = mapper; this.shareRepo = shareRepo; this.blockRepo = blockRepo; this.balanceRepo = balanceRepo; this.paymentRepo = paymentRepo; this.notificationSenders = notificationSenders; BuildFaultHandlingPolicy(); }
public async Task ConfigureAsync(ClusterConfig clusterConfig, PoolConfig poolConfig) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); this.poolConfig = poolConfig; this.clusterConfig = clusterConfig; extraConfig = poolConfig.PaymentProcessing.Extra.SafeExtensionDataAs <MoneroPoolPaymentProcessingConfigExtra>(); logger = LogUtil.GetPoolScopedLogger(typeof(MoneroPayoutHandler), poolConfig); // configure standard daemon var jsonSerializerSettings = ctx.Resolve <JsonSerializerSettings>(); var daemonEndpoints = poolConfig.Daemons .Where(x => string.IsNullOrEmpty(x.Category)) .ToArray(); daemon = new DaemonClient(jsonSerializerSettings); daemon.Configure(daemonEndpoints, MoneroConstants.DaemonRpcLocation); // configure wallet daemon var walletDaemonEndpoints = poolConfig.Daemons .Where(x => x.Category?.ToLower() == MoneroConstants.WalletDaemonCategory) .ToArray(); walletDaemon = new DaemonClient(jsonSerializerSettings); walletDaemon.Configure(walletDaemonEndpoints, MoneroConstants.DaemonRpcLocation); // detect transfer_split support var response = await walletDaemon.ExecuteCmdSingleAsync <TransferResponse>(MWC.TransferSplit); walletSupportsTransferSplit = response.Error.Code != MoneroConstants.MoneroRpcMethodNotFound; }
protected PoolBase(IComponentContext ctx, JsonSerializerSettings serializerSettings, IConnectionFactory cf, IStatsRepository statsRepo, IMapper mapper, IMasterClock clock, NotificationService notificationService) : base(ctx) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(serializerSettings, nameof(serializerSettings)); Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(statsRepo, nameof(statsRepo)); Contract.RequiresNonNull(mapper, nameof(mapper)); Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(notificationService, nameof(notificationService)); this.serializerSettings = serializerSettings; this.cf = cf; this.statsRepo = statsRepo; this.mapper = mapper; this.clock = clock; this.notificationService = notificationService; Shares = shareSubject .Synchronize(); }
public ShareRecorder(IConnectionFactory cf, IMapper mapper, JsonSerializerSettings jsonSerializerSettings, IShareRepository shareRepo, IBlockRepository blockRepo, IMasterClock clock, IMessageBus messageBus) { Contract.RequiresNonNull(cf, nameof(cf)); Contract.RequiresNonNull(mapper, nameof(mapper)); Contract.RequiresNonNull(shareRepo, nameof(shareRepo)); Contract.RequiresNonNull(blockRepo, nameof(blockRepo)); Contract.RequiresNonNull(jsonSerializerSettings, nameof(jsonSerializerSettings)); Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(messageBus, nameof(messageBus)); this.cf = cf; this.mapper = mapper; this.jsonSerializerSettings = jsonSerializerSettings; this.clock = clock; this.messageBus = messageBus; this.shareRepo = shareRepo; this.blockRepo = blockRepo; BuildFaultHandlingPolicy(); }
public void StartListeners(string id, params IPEndPoint[] stratumPorts) { Contract.RequiresNonNull(stratumPorts, nameof(stratumPorts)); // every port gets serviced by a dedicated loop thread foreach (var endpoint in stratumPorts) { var thread = new Thread(_ => { var loop = new Loop(); try { var listener = loop .CreateTcp() .NoDelay(true) .SimultaneousAccepts(false) .Listen(endpoint, (con, ex) => { if (ex == null) { OnClientConnected(con, endpoint, loop); } else { logger.Error(() => $"[{LogCat}] Connection error state: {ex.Message}"); } }); lock (ports) { ports[endpoint.Port] = listener; } } catch (Exception ex) { logger.Error(ex, $"[{LogCat}] {ex}"); throw; } logger.Info(() => $"[{LogCat}] Stratum port {endpoint.Address}:{endpoint.Port} online"); try { loop.RunDefault(); } catch (Exception ex) { logger.Error(ex, $"[{LogCat}] {ex}"); } }) { Name = $"UvLoopThread {id}:{endpoint.Port}" }; thread.Start(); } }
public MoneroJob(GetBlockTemplateResponse blockTemplate, byte[] instanceId, string jobId, PoolConfig poolConfig, ClusterConfig clusterConfig) { Contract.RequiresNonNull(blockTemplate, nameof(blockTemplate)); Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); Contract.RequiresNonNull(clusterConfig, nameof(clusterConfig)); Contract.RequiresNonNull(instanceId, nameof(instanceId)); Contract.Requires <ArgumentException>(!string.IsNullOrEmpty(jobId), $"{nameof(jobId)} must not be empty"); switch (poolConfig.Coin.Type) { case CoinType.AEON: hashSlow = LibCryptonote.CryptonightHashSlowLite; break; case CoinType.XMR: hashSlow = buf => { // PoW variant var variant = buf[0] >= 7 ? buf[0] - 6 : 0; return(LibCryptonote.CryptonightHashSlow(buf, variant)); }; break; default: hashSlow = buf => LibCryptonote.CryptonightHashSlow(buf, 0); break; } BlockTemplate = blockTemplate; PrepareBlobTemplate(instanceId); }
public bool IsBanned(IPAddress address) { Contract.RequiresNonNull(address, nameof(address)); var result = cache.Get(address.ToString()); return(result != null); }
protected JobManagerBase(IComponentContext ctx, IMessageBus messageBus) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(messageBus, nameof(messageBus)); this.ctx = ctx; this.messageBus = messageBus; }
protected StratumServer(IComponentContext ctx, IMasterClock clock) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(clock, nameof(clock)); this.ctx = ctx; this.clock = clock; }
public override async Task PayoutAsync(Balance[] balances) { Contract.RequiresNonNull(balances, nameof(balances)); // build args var amounts = balances .Where(x => x.Amount > 0) .ToDictionary(x => x.Address, x => Math.Round(x.Amount, 8)); if (amounts.Count == 0) { return; } logger.Info(() => $"[{LogCategory}] Paying out {FormatAmount(balances.Sum(x => x.Amount))} to {balances.Length} addresses"); var smr = new SendManyRequest(); smr.FromAccount = String.Empty; smr.Amounts = amounts; smr.FloData = "MiningCore payout"; if (extraPoolPaymentProcessingConfig?.MinersPayTxFees == true) { // distribute transaction fee equally over all recipients var subtractFeesFrom = amounts.Keys.ToArray(); smr.SubtractFeeFrom = subtractFeesFrom; } // send command var result = await daemon.ExecuteCmdSingleAsync <string>(BitcoinCommands.SendMany, smr, new JsonSerializerSettings()); if (result.Error == null) { var txId = result.Response; // check result if (string.IsNullOrEmpty(txId)) { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.SendMany} did not return a transaction id!"); } else { logger.Info(() => $"[{LogCategory}] Payout transaction id: {txId}"); } PersistPayments(balances, txId); NotifyPayoutSuccess(poolConfig.Id, balances, new[] { txId }, null); } else { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.SendMany} returned error: {result.Error.Message} code {result.Error.Code}"); NotifyPayoutFailure(poolConfig.Id, balances, $"{BitcoinCommands.SendMany} returned error: {result.Error.Message} code {result.Error.Code}", null); } }
public void Start(ClusterConfig clusterConfig) { Contract.RequiresNonNull(clusterConfig, nameof(clusterConfig)); this.clusterConfig = clusterConfig; logger.Info(() => $"Launching ..."); StartApi(clusterConfig); StartAdminApi(clusterConfig); }
public void Respond <T>(JsonRpcResponse <T> response) { Contract.RequiresNonNull(response, nameof(response)); lock (rpcCon) { rpcCon?.Send(response); } }
public void Notify <T>(JsonRpcRequest <T> request) { Contract.RequiresNonNull(request, nameof(request)); lock (rpcCon) { rpcCon?.Send(request); } }
public virtual void Configure(PoolConfig poolConfig, ClusterConfig clusterConfig) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); Contract.RequiresNonNull(clusterConfig, nameof(clusterConfig)); logger = LogUtil.GetPoolScopedLogger(typeof(PoolBase <TWorkerContext>), poolConfig); this.poolConfig = poolConfig; this.clusterConfig = clusterConfig; }
public async Task PayoutAsync(Balance[] balances) { Contract.RequiresNonNull(balances, nameof(balances)); // build args var amounts = balances .Where(x => x.Amount > 0) .ToDictionary(x => x.Address, x => Math.Round(x.Amount, 8)); if (amounts.Count == 0) { return; } logger.Info(() => $"[{LogCategory}] Paying out {FormatAmount(balances.Sum(x => x.Amount))} to {balances.Length} addresses"); var subtractFeesFrom = amounts.Keys.ToArray(); var args = new object[] { string.Empty, // default account amounts, // addresses and associated amounts 1, // only spend funds covered by this many confirmations "MiningCore Payout", // comment subtractFeesFrom // distribute transaction fee equally over all recipients }; // send command var result = await daemon.ExecuteCmdSingleAsync <string>(BitcoinCommands.SendMany, args, new JsonSerializerSettings()); if (result.Error == null) { var txId = result.Response; // check result if (string.IsNullOrEmpty(txId)) { logger.Error(() => $"[{LogCategory}] Daemon command '{BitcoinCommands.SendMany}' did not return a transaction id!"); } else { logger.Info(() => $"[{LogCategory}] Payout transaction id: {txId}"); } PersistPayments(balances, txId); NotifyPayoutSuccess(balances, new [] { txId }, null); } else { logger.Error(() => $"[{LogCategory}] Daemon command '{BitcoinCommands.SendMany}' returned error: {result.Error.Message} code {result.Error.Code}"); NotifyPayoutFailure(balances, $"Daemon command '{BitcoinCommands.SendMany}' returned error: {result.Error.Message} code {result.Error.Code}", null); } }
public virtual void Init(TBlockTemplate blockTemplate, string jobId, PoolConfig poolConfig, ClusterConfig clusterConfig, IMasterClock clock, IDestination poolAddressDestination, BitcoinNetworkType networkType, bool isPoS, double shareMultiplier, IHashAlgorithm coinbaseHasher, IHashAlgorithm headerHasher, IHashAlgorithm blockHasher) { Contract.RequiresNonNull(blockTemplate, nameof(blockTemplate)); Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); Contract.RequiresNonNull(clusterConfig, nameof(clusterConfig)); Contract.RequiresNonNull(clock, nameof(clock)); Contract.RequiresNonNull(poolAddressDestination, nameof(poolAddressDestination)); Contract.RequiresNonNull(coinbaseHasher, nameof(coinbaseHasher)); Contract.RequiresNonNull(headerHasher, nameof(headerHasher)); Contract.RequiresNonNull(blockHasher, nameof(blockHasher)); Contract.Requires <ArgumentException>(!string.IsNullOrEmpty(jobId), $"{nameof(jobId)} must not be empty"); this.poolConfig = poolConfig; this.clusterConfig = clusterConfig; this.clock = clock; this.poolAddressDestination = poolAddressDestination; this.networkType = networkType; BlockTemplate = blockTemplate; JobId = jobId; Difficulty = new Target(new NBitcoin.BouncyCastle.Math.BigInteger(BlockTemplate.Target, 16)).Difficulty; extraNoncePlaceHolderLength = BitcoinExtraNonceProvider.PlaceHolderLength; this.isPoS = isPoS; this.shareMultiplier = shareMultiplier; this.coinbaseHasher = coinbaseHasher; this.headerHasher = headerHasher; this.blockHasher = blockHasher; blockTargetValue = BigInteger.Parse(BlockTemplate.Target, NumberStyles.HexNumber); previousBlockHashReversedHex = BlockTemplate.PreviousBlockhash .HexToByteArray() .ReverseByteOrder() .ToHexString(); BuildMerkleBranches(); BuildCoinbase(); jobParams = new object[] { JobId, previousBlockHashReversedHex, coinbaseInitialHex, coinbaseFinalHex, merkleBranchesHex, BlockTemplate.Version.ToStringHex8(), BlockTemplate.Bits, BlockTemplate.CurTime.ToStringHex8(), false }; }
public EthereumJobManager( IComponentContext ctx, IMasterClock clock) : base(ctx) { Contract.RequiresNonNull(ctx, nameof(ctx)); Contract.RequiresNonNull(clock, nameof(clock)); this.clock = clock; }