private async Task SaveCommandActionAsync()
        {
            try
            {
                var nextAddressResponse = await App.Current.Synchronizer.WalletRpcClient.NextExternalAddressAsync(SelectedVotingAccount.Account);

                var pubKeyAddress = nextAddressResponse.Item2;

                var bestApiVersion = PoolApiClient.BestSupportedApiVersion(SelectedStakePool.SupportedApiVersions);
                var client         = new PoolApiClient(bestApiVersion, SelectedStakePool.Uri, SelectedPoolApiKey, _httpClient);

                // Sumbit the pubkey.  In the current stakepool api version only a single pubkey can
                // be submitted per user, so if one was already submitted, simply ignore the error.
                try
                {
                    await client.CreateVotingAddressAsync(pubKeyAddress);
                }
                catch (PoolApiResponseException ex) when(ex.Code == StatusCode.AlreadyExists)
                {
                }

                var poolInfo = await client.GetPurchaseInfoAsync();

                var configEntry = new StakePoolUserConfig.Entry
                {
                    ApiKey             = SelectedPoolApiKey,
                    Host               = SelectedStakePool.Uri.Host,
                    MultisigVoteScript = poolInfo.RedeemScriptHex,
                    VotingAccount      = SelectedVotingAccount.Account.AccountNumber,
                };
                _configuredPools[configEntry.Host] = configEntry;

                var config = new StakePoolUserConfig {
                    Entries = _configuredPools.Values.ToArray()
                };
                await WriteConfig(config, _configPath);

                SelectedConfiguredPool = configEntry;

                _saveCommand.Executable = false;
                NeedsSaving             = false;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error communicating with stake pool");
            }
        }
        private Task UpdateStakepoolVotePreferences()
        {
            var voteBits    = CalculateVoteBits();
            var updateTasks = ConfiguredStakePools.OfType <StakePoolSelection>()
                              .Select(sp =>
            {
                var bestApiVersion = PoolApiClient.BestSupportedApiVersion(sp.PoolInfo.SupportedApiVersions);
                return(TupleValue.Create(sp, bestApiVersion));
            })
                              .Where(t => t.Item2 >= 2)
                              .Select(t =>
            {
                var sp             = t.Item1;
                var bestApiVersion = t.Item2;
                var client         = new PoolApiClient(bestApiVersion, sp.PoolInfo.Uri, sp.ApiToken, _httpClient);
                return(client.SetVoteBitsAsync(voteBits));
            });

            return(Task.WhenAll(updateTasks));
        }
        private async Task <bool> PurchaseTicketsWithPassphrase(string passphrase)
        {
            var synchronizer = App.Current.Synchronizer;
            var walletClient = synchronizer.WalletRpcClient;

            var  account          = SelectedSourceAccount.Account;
            var  spendLimit       = synchronizer.TicketPrice;
            int  requiredConfirms = 2; // TODO allow user to set
            uint expiryHeight     = _expiry + (uint)synchronizer.SyncedBlockHeight;

            Amount splitFeeLocal  = _splitFee;
            Amount ticketFeeLocal = _ticketFee;

            Address votingAddress;
            Address poolFeeAddress;
            decimal poolFees;

            if (SelectedStakePool is StakePoolSelection)
            {
                var selection      = (StakePoolSelection)SelectedStakePool;
                var bestApiVersion = PoolApiClient.BestSupportedApiVersion(selection.PoolInfo.SupportedApiVersions);
                var client         = new PoolApiClient(bestApiVersion, selection.PoolInfo.Uri, selection.ApiToken, _httpClient);
                var purchaseInfo   = await client.GetPurchaseInfoAsync();

                // Import the 1-of-2 multisig vote script.  This has to be done here rather than from
                // the pool management dialog since importing requires an unlocked wallet and we are
                // unable to open nested dialog windows to prompt for a passphrase.
                //
                // This does not need to re-import the script every time ticktes are purchased using
                // a pool, but for code simplicity it is done this way.  Also, in future versions of the
                // API when it may be possible to generate a new reward address for each ticket, we will
                // need to import these scripts ever time.
                await walletClient.ImportScriptAsync(selection.MultisigVoteScript, false, 0, passphrase);

                votingAddress  = Address.Decode(purchaseInfo.VotingAddress);
                poolFeeAddress = Address.Decode(purchaseInfo.FeeAddress);
                poolFees       = purchaseInfo.Fee;
            }
            else
            {
                votingAddress  = _votingAddress;
                poolFeeAddress = _poolFeeAddress;
                poolFees       = _poolFees / 100m;
            }

            List <Blake256Hash> purchaseResponse;

            try
            {
                purchaseResponse = await walletClient.PurchaseTicketsAsync(account, spendLimit,
                                                                           requiredConfirms, votingAddress, _ticketsToPurchase, poolFeeAddress,
                                                                           poolFees, expiryHeight, _splitFee, _ticketFee, passphrase);
            }
            catch (Grpc.Core.RpcException ex)
            {
                MessageBox.Show(ex.Status.Detail, "Unexpected error");
                return(false);
            }

            ResponseString = "Success! Ticket hashes:\n" + string.Join("\n", purchaseResponse);
            return(true);
        }
        private async Task <bool> PurchaseTicketsWithPassphrase(string passphrase)
        {
            var synchronizer = App.Current.Synchronizer;
            var walletClient = synchronizer.WalletRpcClient;

            var  account          = SelectedSourceAccount.Account;
            var  spendLimit       = synchronizer.TicketPrice;
            int  requiredConfirms = 2; // TODO allow user to set
            uint expiryHeight     = _expiry + (uint)synchronizer.SyncedBlockHeight;

            Address votingAddress;
            Address poolFeeAddress;
            decimal poolFees;

            if (SelectedStakePool is StakePoolSelection)
            {
                var selection      = (StakePoolSelection)SelectedStakePool;
                var bestApiVersion = PoolApiClient.BestSupportedApiVersion(selection.PoolInfo.SupportedApiVersions);
                var client         = new PoolApiClient(bestApiVersion, selection.PoolInfo.Uri, selection.ApiToken, _httpClient);
                var purchaseInfo   = await client.GetPurchaseInfoAsync();

                // Import the 1-of-2 multisig vote script.  This has to be done here rather than from
                // the pool management dialog since importing requires an unlocked wallet and we are
                // unable to open nested dialog windows to prompt for a passphrase.
                //
                // This does not need to re-import the script every time ticktes are purchased using
                // a pool, but for code simplicity it is done this way.  Also, in future versions of the
                // API when it may be possible to generate a new reward address for each ticket, we will
                // need to import these scripts ever time.
                string fraudReason = null;
                try
                {
                    var importResp = await walletClient.ImportScriptAsync(selection.MultisigVoteScript, false, 0, passphrase,
                                                                          requireRedeemable : true);

                    if (importResp.Item1 != purchaseInfo.VotingAddress)
                    {
                        fraudReason = "The stakepool voting address is not the P2SH address of the voting redeem script.";
                    }
                }
                catch (RpcException ex) when(ex.Status.StatusCode == Grpc.Core.StatusCode.FailedPrecondition)
                {
                    fraudReason = "The stakepool 1-of-2 voting script is not redeemable by your wallet.";
                }
                if (fraudReason != null)
                {
                    MessageBox.Show(fraudReason + "\n\n" +
                                    $"Please report this to the stakepool administrator and the {BlockChain.CurrencyName} developers.",
                                    "Warning");
                    return(false);
                }

                votingAddress  = Address.Decode(purchaseInfo.VotingAddress);
                poolFeeAddress = Address.Decode(purchaseInfo.FeeAddress);
                poolFees       = purchaseInfo.Fee;
            }
            else
            {
                votingAddress  = _votingAddress;
                poolFeeAddress = _poolFeeAddress;
                poolFees       = _poolFees / 100m;
            }

            List <Blake256Hash> purchaseResponse;

            try
            {
                purchaseResponse = await walletClient.PurchaseTicketsAsync(account, spendLimit,
                                                                           requiredConfirms, votingAddress, _ticketsToPurchase, poolFeeAddress,
                                                                           poolFees, expiryHeight, TransactionFees.DefaultFeePerKb, _ticketFee, passphrase);
            }
            catch (Grpc.Core.RpcException ex)
            {
                MessageBox.Show(ex.Status.Detail, "Unexpected error");
                return(false);
            }

            ResponseString = "Success! Ticket hashes:\n" + string.Join("\n", purchaseResponse);
            return(true);
        }