private async Task <bool> EnsureBalance(decimal requiredAmount, CryptonoteCoinTemplate coin, CancellationToken ct) { var response = await walletDaemon.ExecuteCmdSingleAsync <GetBalanceResponse>(logger, CryptonoteWalletCommands.GetBalance, ct); if (response.Error != null) { logger.Error(() => $"[{LogCategory}] Daemon command '{CryptonoteWalletCommands.GetBalance}' returned error: {response.Error.Message} code {response.Error.Code}"); return(false); } var unlockedBalance = Math.Floor(response.Response.UnlockedBalance / coin.SmallestUnit); var balance = Math.Floor(response.Response.Balance / coin.SmallestUnit); if (unlockedBalance < requiredAmount) { logger.Info(() => $"[{LogCategory}] {FormatAmount(requiredAmount)} unlocked balance required for payment, but only have {FormatAmount(unlockedBalance)} of {FormatAmount(balance)} available yet. Will try again."); return(false); } logger.Info(() => $"[{LogCategory}] Current balance is {FormatAmount(unlockedBalance)}"); return(true); }
public virtual 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 args = new object[] { string.Empty, // default account amounts // addresses and associated amounts }; // 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}] {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}] {BitcoinCommands.SendMany} returned error: {result.Error.Message} code {result.Error.Code}"); NotifyPayoutFailure(balances, $"{BitcoinCommands.SendMany} returned error: {result.Error.Message} code {result.Error.Code}", null); } }
public async Task PayoutAsync(Balance[] balances) { // ensure we have peers var infoResponse = await daemon.ExecuteCmdSingleAsync <string>(logger, EC.GetPeerCount); if (networkType == EthereumNetworkType.Mainnet && (infoResponse.Error != null || string.IsNullOrEmpty(infoResponse.Response) || infoResponse.Response.IntegralFromHex <int>() < EthereumConstants.MinPayoutPeerCount)) { logger.Warn(() => $"[{LogCategory}] Payout aborted. Not enough peers (4 required)"); return; } var txHashes = new List <string>(); foreach (var balance in balances) { try { var txHash = await PayoutAsync(balance); txHashes.Add(txHash); } catch (Exception ex) { logger.Error(ex); NotifyPayoutFailure(poolConfig.Id, new[] { balance }, ex.Message, null); } } if (txHashes.Any()) { NotifyPayoutSuccess(poolConfig.Id, balances, txHashes.ToArray(), null); } }
public async Task ConfigureAsync(ClusterConfig clusterConfig, PoolConfig poolConfig) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); this.poolConfig = poolConfig; this.clusterConfig = clusterConfig; extraConfig = poolConfig.PaymentProcessing.Extra.SafeExtensionDataAs<CryptonotePoolPaymentProcessingConfigExtra>(); logger = LogUtil.GetPoolScopedLogger(typeof(CryptonotePayoutHandler), poolConfig); // configure standard daemon var jsonSerializerSettings = ctx.Resolve<JsonSerializerSettings>(); var daemonEndpoints = poolConfig.Daemons .Where(x => string.IsNullOrEmpty(x.Category)) .Select(x => { if (string.IsNullOrEmpty(x.HttpPath)) x.HttpPath = CryptonoteConstants.DaemonRpcLocation; return x; }) .ToArray(); daemon = new DaemonClient(jsonSerializerSettings, messageBus, clusterConfig.ClusterName ?? poolConfig.PoolName, poolConfig.Id); daemon.Configure(daemonEndpoints); // configure wallet daemon var walletDaemonEndpoints = poolConfig.Daemons .Where(x => x.Category?.ToLower() == CryptonoteConstants.WalletDaemonCategory) .Select(x => { if (string.IsNullOrEmpty(x.HttpPath)) x.HttpPath = CryptonoteConstants.DaemonRpcLocation; return x; }) .ToArray(); walletDaemon = new DaemonClient(jsonSerializerSettings, messageBus, clusterConfig.ClusterName ?? poolConfig.PoolName, poolConfig.Id); walletDaemon.Configure(walletDaemonEndpoints); // detect network await GetNetworkTypeAsync(); // detect transfer_split support var response = await walletDaemon.ExecuteCmdSingleAsync<TransferResponse>(logger, CryptonoteWalletCommands.TransferSplit); walletSupportsTransferSplit = response.Error.Code != CryptonoteConstants.MoneroRpcMethodNotFound; }
private async Task<bool> EnsureBalance(decimal requiredAmount, CryptonoteCoinTemplate coin) { var response = await walletDaemon.ExecuteCmdSingleAsync<GetBalanceResponse>(logger, CryptonoteWalletCommands.GetBalance); if (response.Error != null) { logger.Error(() => $"[{LogCategory}] Daemon command '{CryptonoteWalletCommands.GetBalance}' returned error: {response.Error.Message} code {response.Error.Code}"); return false; } var unlockedBalance = Math.Floor(response.Response.UnlockedBalance / coin.SmallestUnit); var balance = Math.Floor(response.Response.Balance / coin.SmallestUnit); if (response.Response.UnlockedBalance < requiredAmount) { logger.Error(() => $"[{LogCategory}] Need {FormatAmount(requiredAmount)} unlocked balance, but only have {FormatAmount(unlockedBalance)} ({FormatAmount(balance)})"); return false; } logger.Error(() => $"[{LogCategory}] Current balance is {FormatAmount(unlockedBalance)}"); return true; }
public virtual async Task <Block[]> ClassifyBlocksAsync(Block[] blocks) { Contract.RequiresNonNull(poolConfig, nameof(poolConfig)); Contract.RequiresNonNull(blocks, nameof(blocks)); var pageSize = 100; var pageCount = (int)Math.Ceiling(blocks.Length / (double)pageSize); var result = new List <Block>(); for (var i = 0; i < pageCount; i++) { // get a page full of blocks var page = blocks .Skip(i * pageSize) .Take(pageSize) .ToArray(); // build command batch (block.TransactionConfirmationData is the hash of the blocks coinbase transaction) var batch = page.Select(block => new[] { block.TransactionConfirmationData }).ToArray(); for (var j = 0; j < batch.Length; j++) { var cmdResult = await daemon.ExecuteCmdSingleAsync <JToken>(BitcoinCommands.GetTransaction, batch[j]); var transactionInfo = cmdResult.Response?.ToObject <Transaction>(); var block = page[j]; // check error if (cmdResult.Error != null) { // Code -5 interpreted as "orphaned" if (cmdResult.Error.Code == -5) { block.Status = BlockStatus.Orphaned; result.Add(block); } else { logger.Warn(() => $"[{LogCategory}] Daemon reports error '{cmdResult.Error.Message}' (Code {cmdResult.Error.Code}) for transaction {page[j].TransactionConfirmationData}"); } } // missing transaction details are interpreted as "orphaned" else if (transactionInfo?.Details == null || transactionInfo.Details.Length == 0) { block.Status = BlockStatus.Orphaned; result.Add(block); } else { switch (transactionInfo.Details[0].Category) { case "immature": // update progress var minConfirmations = extraPoolConfig?.MinimumConfirmations ?? BitcoinConstants.CoinbaseMinConfimations; block.ConfirmationProgress = Math.Min(1.0d, (double)transactionInfo.Confirmations / minConfirmations); block.Reward = transactionInfo.Details[0].Amount; result.Add(block); break; case "generate": // matured and spendable coinbase transaction block.Status = BlockStatus.Confirmed; block.ConfirmationProgress = 1; block.Reward = transactionInfo.Details[0].Amount; result.Add(block); logger.Info(() => $"[{LogCategory}] Unlocked block {block.BlockHeight} worth {FormatAmount(block.Reward)}"); break; default: logger.Info(() => $"[{LogCategory}] Block {block.BlockHeight} classified as orphaned. Category: {transactionInfo.Details[0].Category}"); block.Status = BlockStatus.Orphaned; block.Reward = 0; result.Add(block); break; } } } } return(result.ToArray()); }
public virtual async Task PayoutAsync(Balance[] balances) { Assertion.RequiresNonNull(balances, nameof(balances)); 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"); object[] args; if (extraPoolPaymentProcessingConfig?.MinersPayTxFees == true) { var comment = (poolConfig.PoolName ?? clusterConfig.ClusterName ?? "MiningCore").Trim() + " Payment"; var subtractFeesFrom = amounts.Keys.ToArray(); args = new object[] { string.Empty, amounts, 1, comment, subtractFeesFrom }; } else { args = new object[] { string.Empty, amounts, }; } var didUnlockWallet = false; tryTransfer: var result = await daemon.ExecuteCmdSingleAsync <string>(BitcoinCommands.SendMany, args, new JsonSerializerSettings()); if (result.Error == null) { if (didUnlockWallet) { logger.Info(() => $"[{LogCategory}] Locking wallet"); await daemon.ExecuteCmdSingleAsync <JToken>(BitcoinCommands.WalletLock); } var txId = result.Response; 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 { if (result.Error.Code == (int)BitcoinRPCErrorCode.RPC_WALLET_UNLOCK_NEEDED && !didUnlockWallet) { if (!string.IsNullOrEmpty(extraPoolPaymentProcessingConfig?.WalletPassword)) { logger.Info(() => $"[{LogCategory}] Unlocking wallet"); var unlockResult = await daemon.ExecuteCmdSingleAsync <JToken>(BitcoinCommands.WalletPassphrase, new[] { (object)extraPoolPaymentProcessingConfig.WalletPassword, (object)5 }); if (unlockResult.Error == null) { didUnlockWallet = true; goto tryTransfer; } else { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.WalletPassphrase} returned error: {result.Error.Message} code {result.Error.Code}"); } } else { logger.Error(() => $"[{LogCategory}] Wallet is locked but walletPassword was not configured. Unable to send funds."); } } 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); } } }
private async Task PayoutBatch(Balance[] balances) { // build request var request = new TransferRequest { Destinations = balances .Where(x => x.Amount > 0) .Select(x => new TransferDestination { Address = x.Address, Amount = (ulong)Math.Floor(x.Amount * MoneroConstants.SmallestUnit[poolConfig.Coin.Type]) }).ToArray(), GetTxKey = true }; if (request.Destinations.Length == 0) { return; } logger.Info(() => $"[{LogCategory}] Paying out {FormatAmount(balances.Sum(x => x.Amount))} to {balances.Length} addresses"); // send command var transferResponse = await walletDaemon.ExecuteCmdSingleAsync <TransferResponse>(MWC.Transfer, request); // gracefully handle error -4 (transaction would be too large. try /transfer_split) if (transferResponse.Error?.Code == -4) { if (walletSupportsTransferSplit) { logger.Info(() => $"[{LogCategory}] Retrying transfer using {MWC.TransferSplit}"); var transferSplitResponse = await walletDaemon.ExecuteCmdSingleAsync <TransferSplitResponse>(MWC.TransferSplit, request); // gracefully handle error -4 (transaction would be too large. try /transfer_split) if (transferResponse.Error?.Code != -4) { HandleTransferResponse(transferSplitResponse, balances); return; } } // retry paged logger.Info(() => $"[{LogCategory}] Retrying paged"); var validBalances = balances.Where(x => x.Amount > 0).ToArray(); var pageSize = 10; var pageCount = (int)Math.Ceiling((double)validBalances.Length / pageSize); for (var i = 0; i < pageCount; i++) { var page = validBalances .Skip(i * pageSize) .Take(pageSize) .ToArray(); // update request request.Destinations = page .Where(x => x.Amount > 0) .Select(x => new TransferDestination { Address = x.Address, Amount = (ulong)Math.Floor(x.Amount * MoneroConstants.SmallestUnit[poolConfig.Coin.Type]) }).ToArray(); logger.Info(() => $"[{LogCategory}] Page {i + 1}: Paying out {FormatAmount(page.Sum(x => x.Amount))} to {page.Length} addresses"); transferResponse = await walletDaemon.ExecuteCmdSingleAsync <TransferResponse>(MWC.Transfer, request); HandleTransferResponse(transferResponse, page); if (transferResponse.Error != null) { break; } } } else { HandleTransferResponse(transferResponse, balances); } }
public virtual 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"); object[] args; if (extraPoolPaymentProcessingConfig?.MinersPayTxFees == true) { var comment = (poolConfig.PoolName ?? clusterConfig.ClusterName ?? "MiningCore").Trim() + " Payment"; var subtractFeesFrom = amounts.Keys.ToArray(); if (!poolConfig.Template.As <BitcoinTemplate>().HasMasterNodes) { args = new object[] { string.Empty, // default account amounts, // addresses and associated amounts 1, // only spend funds covered by this many confirmations comment, // tx comment subtractFeesFrom // distribute transaction fee equally over all recipients }; } else { args = new object[] { string.Empty, // default account amounts, // addresses and associated amounts 1, // only spend funds covered by this many confirmations false, // Whether to add confirmations to transactions locked via InstantSend comment, // tx comment subtractFeesFrom, // distribute transaction fee equally over all recipients false, // use_is: Send this transaction as InstantSend false, // Use anonymized funds only }; } } else { args = new object[] { string.Empty, // default account amounts, // addresses and associated amounts }; } var didUnlockWallet = false; // send command tryTransfer: var result = await daemon.ExecuteCmdSingleAsync <string>(logger, BitcoinCommands.SendMany, args, new JsonSerializerSettings()); if (result.Error == null) { if (didUnlockWallet) { // lock wallet logger.Info(() => $"[{LogCategory}] Locking wallet"); await daemon.ExecuteCmdSingleAsync <JToken>(logger, BitcoinCommands.WalletLock); } // check result var txId = result.Response; if (string.IsNullOrEmpty(txId)) { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.SendMany} did not return a transaction id!"); } else { logger.Info(() => $"[{LogCategory}] Payout transaction id: {txId}"); } await PersistPaymentsAsync(balances, txId); NotifyPayoutSuccess(poolConfig.Id, balances, new[] { txId }, null); } else { if (result.Error.Code == (int)BitcoinRPCErrorCode.RPC_WALLET_UNLOCK_NEEDED && !didUnlockWallet) { if (!string.IsNullOrEmpty(extraPoolPaymentProcessingConfig?.WalletPassword)) { logger.Info(() => $"[{LogCategory}] Unlocking wallet"); var unlockResult = await daemon.ExecuteCmdSingleAsync <JToken>(logger, BitcoinCommands.WalletPassphrase, new[] { (object)extraPoolPaymentProcessingConfig.WalletPassword, (object)5 // unlock for N seconds }); if (unlockResult.Error == null) { didUnlockWallet = true; goto tryTransfer; } else { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.WalletPassphrase} returned error: {result.Error.Message} code {result.Error.Code}"); } } else { logger.Error(() => $"[{LogCategory}] Wallet is locked but walletPassword was not configured. Unable to send funds."); } } 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 virtual async Task <Share> SubmitShareAsync(StratumClient worker, object submission) { Contract.RequiresNonNull(worker, nameof(worker)); Contract.RequiresNonNull(submission, nameof(submission)); logger.LogInvoke(LogCat, new object[] { worker.ConnectionId }); if (!(submission is object[] submitParams)) { throw new StratumException(StratumError.Other, "invalid params"); } var context = worker.ContextAs <BitcoinWorkerContext>(); // extract params var workerValue = (submitParams[0] as string)?.Trim(); var jobId = submitParams[1] as string; var extraNonce2 = submitParams[2] as string; var nTime = submitParams[3] as string; var nonce = submitParams[4] as string; var solution = submitParams[5] as string; if (string.IsNullOrEmpty(workerValue)) { throw new StratumException(StratumError.Other, "missing or invalid workername"); } if (string.IsNullOrEmpty(solution)) { throw new StratumException(StratumError.Other, "missing or invalid solution"); } CommerciumJob job; lock (jobLock) { job = _validJobs.FirstOrDefault(x => x.JobId == jobId); } if (job == null) { throw new StratumException(StratumError.JobNotFound, "job not found"); } // extract worker/miner/payoutid var split = workerValue.Split('.'); var minerName = split[0]; var workerName = split.Length > 1 ? split[1] : null; // validate & process var(share, blockHex) = job.ProcessShare(worker, extraNonce2, nTime, nonce, solution); // if block candidate, submit & check if accepted by network if (share.IsBlockCandidate) { logger.Info(() => $"[{LogCat}] Submitting block {share.BlockHeight} [{share.BlockHash}]"); var acceptResponse = await SubmitBlockAsync(share, blockHex); // is it still a block candidate? share.IsBlockCandidate = acceptResponse.Accepted; if (share.IsBlockCandidate) { logger.Info(() => $"[{LogCat}] Daemon accepted block {share.BlockHeight} [{share.BlockHash}] submitted by {minerName}"); blockSubmissionSubject.OnNext(Unit.Default); // persist the coinbase transaction-hash to allow the payment processor // to verify later on that the pool has received the reward for the block share.TransactionConfirmationData = acceptResponse.CoinbaseTransaction; // check what is a value of CoinbaseTransaction and assign it to block reward var result = await _wallet.ExecuteCmdSingleAsync <JToken>(BitcoinCommands.GetTransaction, new [] { acceptResponse.CoinbaseTransaction }); Transaction transactionInfo = result.Response?.ToObject <Transaction>(); if (transactionInfo != null) { share.BlockReward = (decimal)transactionInfo.Details?[0]?.Amount; } } else { // clear fields that no longer apply share.TransactionConfirmationData = null; } } // enrich share with common data share.PoolId = poolConfig.Id; share.IpAddress = worker.RemoteEndpoint.Address.ToString(); share.Miner = minerName; share.Worker = workerName; share.UserAgent = context.UserAgent; share.Source = clusterConfig.ClusterName; share.NetworkDifficulty = job.Difficulty; share.Difficulty = share.Difficulty / ShareMultiplier; share.Created = _clock.Now; return(share); }
private string PayoutToAddress(string address, decimal amount, DaemonClient daemonClient) { string txId = string.Empty; object[] args = new object[] { address, // address amount, // amount "CX Trade", // comment "Payout", // comment_to false // subtractfeefromamount }; bool didUnlockWallet = false; // send command tryTransfer: var result = daemonClient.ExecuteCmdSingleAsync <string>(BlockchainConstants.BitcoinCommands.SendToAddress, args, _jsonSerializerSettings).Result; if (result.Error == null) { SafeLockWallet(daemonClient, didUnlockWallet); // check result txId = result.Response; if (string.IsNullOrEmpty(txId)) { _logger.Error($"{BlockchainConstants.BitcoinCommands.SendToAddress} did not return a transaction id!"); } } else { if (result.Error.Code == (int)BlockchainConstants.BitcoinRPCErrorCode.RPC_WALLET_UNLOCK_NEEDED && !didUnlockWallet) { if (!string.IsNullOrEmpty(daemonClient.WalletPassword)) { _logger.Info("Unlocking wallet"); var unlockResult = daemonClient.ExecuteCmdSingleAsync <JToken>(BlockchainConstants.BitcoinCommands.WalletPassphrase, new[] { (object)daemonClient.WalletPassword, (object)5 // unlock for N seconds }).Result; if (unlockResult.Error == null) { didUnlockWallet = true; goto tryTransfer; } else { _logger.Error($"{BlockchainConstants.BitcoinCommands.WalletPassphrase} returned error: {result.Error.Message} code {result.Error.Code}"); } } else { _logger.Error($"Wallet is locked but walletPassword was not configured. Unable to send funds."); } } else if (result.Error.Code == (int)BlockchainConstants.BitcoinRPCErrorCode.RPC_WALLET_INSUFFICIENT_FUNDS) { SafeLockWallet(daemonClient, didUnlockWallet); _notificationService.NotifyPaymentFailure(address, amount, "Not enough funds in wallet or account."); } else { _logger.Error($"{BlockchainConstants.BitcoinCommands.SendMany} returned error: {result.Error.Message} code {result.Error.Code}"); } } return(txId); }
public virtual 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"); object[] args; if (extraPoolPaymentProcessingConfig?.MinersPayTxFees == true) { var comment = (poolConfig.PoolName ?? clusterConfig.ClusterName ?? "MiningCore").Trim() + " Payment"; var subtractFeesFrom = amounts.Keys.ToArray(); args = new object[] { string.Empty, // default account amounts, // addresses and associated amounts 1, // only spend funds covered by this many confirmations comment, // tx comment subtractFeesFrom // distribute transaction fee equally over all recipients }; } else { args = new object[] { string.Empty, // default account amounts, // addresses and associated amounts }; } // 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}] {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 virtual 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"); object[] args; //Remarks: unfortunately 0.15 & 0.16 seem to be broken, sendmany not working anymore, instead using sendtoaddress var didUnlockWallet = false; foreach (string key in amounts.Keys) { args = new object[] { key, // address amounts[key], // amount "reward", // comment "pool payment", // comment_to true // subtractfeefromamount }; // send command tryTransfer: var result = await daemon.ExecuteCmdSingleAsync <string>(BitcoinCommands.SendToAddress, args, new JsonSerializerSettings()); Balance[] newBalances = balances.Where(b => b.Address == key).ToArray(); if (result.Error == null) { if (didUnlockWallet) { // lock wallet logger.Info(() => $"[{LogCategory}] Locking wallet"); await daemon.ExecuteCmdSingleAsync <JToken>(BitcoinCommands.WalletLock); } // check result var txId = result.Response; if (string.IsNullOrEmpty(txId)) { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.SendToAddress} did not return a transaction id!"); } else { logger.Info(() => $"[{LogCategory}] Payout transaction id: {txId}"); } PersistPayments(newBalances, txId); NotifyPayoutSuccess(poolConfig.Id, newBalances, new[] { txId }, null); } else { if (result.Error.Code == (int)BitcoinRPCErrorCode.RPC_WALLET_UNLOCK_NEEDED && !didUnlockWallet) { if (!string.IsNullOrEmpty(extraPoolPaymentProcessingConfig?.WalletPassword)) { logger.Info(() => $"[{LogCategory}] Unlocking wallet"); var unlockResult = await daemon.ExecuteCmdSingleAsync <JToken>(BitcoinCommands.WalletPassphrase, new[] { (object)extraPoolPaymentProcessingConfig.WalletPassword, (object)5 // unlock for N seconds }); if (unlockResult.Error == null) { didUnlockWallet = true; goto tryTransfer; } else { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.WalletPassphrase} returned error: {result.Error.Message} code {result.Error.Code}"); } } else { logger.Error(() => $"[{LogCategory}] Wallet is locked but walletPassword was not configured. Unable to send funds."); } } else { logger.Error(() => $"[{LogCategory}] {BitcoinCommands.SendToAddress} returned error: {result.Error.Message} code {result.Error.Code}"); NotifyPayoutFailure(poolConfig.Id, newBalances, $"{BitcoinCommands.SendMany} returned error: {result.Error.Message} code {result.Error.Code}", null); } } } }