public override async Task <IEnumerable <WalletAddress> > GetUnspentAddressesAsync(
            string toAddress,
            decimal amount,
            decimal fee,
            decimal feePrice,
            FeeUsagePolicy feeUsagePolicy,
            AddressUsagePolicy addressUsagePolicy,
            BlockchainTransactionType transactionType,
            CancellationToken cancellationToken = default)
        {
            var unspentAddresses = (await DataRepository
                                    .GetUnspentAddressesAsync(Currency)
                                    .ConfigureAwait(false))
                                   .ToList();

            var selectedAddresses = (await SelectUnspentAddresses(
                                         from: unspentAddresses,
                                         amount: amount,
                                         fee: fee,
                                         feePrice: feePrice,
                                         feeUsagePolicy: feeUsagePolicy,
                                         addressUsagePolicy: addressUsagePolicy,
                                         transactionType: transactionType)
                                     .ConfigureAwait(false))
                                    .ToList();

            if (toAddress != null && selectedAddresses.Any(a => a.WalletAddress.Address == toAddress))
            {
                selectedAddresses.RemoveAll(a => a.WalletAddress.Address != toAddress);
            }

            return(ResolvePublicKeys(selectedAddresses
                                     .Select(w => w.WalletAddress)
                                     .ToList()));
        }
        protected IEnumerable <WalletAddress> ApplyAddressUsagePolicy(
            List <WalletAddress> addresses,
            decimal amount,
            decimal fee,
            decimal feePrice,
            AddressUsagePolicy addressUsagePolicy)
        {
            var currency = Currencies.GetByName(Currency);

            switch (addressUsagePolicy)
            {
            case AddressUsagePolicy.UseMinimalBalanceFirst:
                addresses = addresses.SortList(new AvailableBalanceAscending());
                break;

            case AddressUsagePolicy.UseMaximumBalanceFirst:
                addresses = addresses.SortList(new AvailableBalanceDescending());
                break;

            case AddressUsagePolicy.UseOnlyOneAddress:
                var walletAddress = addresses
                                    .FirstOrDefault(w => w.AvailableBalance() >= amount + currency.GetFeeAmount(fee, feePrice));

                return(walletAddress != null
                        ? new List <WalletAddress> {
                    walletAddress
                }
                        : Enumerable.Empty <WalletAddress>());

            default:
                throw new Exception("Address usage policy not supported");
            }

            return(addresses);
        }
Example #3
0
        public override async Task <IEnumerable <WalletAddress> > GetUnspentAddressesAsync(
            decimal amount,
            decimal fee,
            decimal feePrice,
            bool isFeePerTransaction,
            AddressUsagePolicy addressUsagePolicy,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            var unspentAddresses = (await DataRepository
                                    .GetUnspentAddressesAsync(Currency)
                                    .ConfigureAwait(false))
                                   .ToList();

            var selectedAddresses = await SelectUnspentAddressesAsync(
                from : unspentAddresses,
                to : null,
                amount : amount,
                fee : fee,
                isFeePerTransaction : isFeePerTransaction,
                addressUsagePolicy : addressUsagePolicy,
                cancellationToken : cancellationToken)
                                    .ConfigureAwait(false);

            return(ResolvePublicKeys(selectedAddresses
                                     .Select(w => w.WalletAddress)
                                     .ToList()));
        }
Example #4
0
        protected IEnumerable <WalletAddress> ApplyAddressUsagePolicy(
            List <WalletAddress> addresses,
            decimal amount,
            decimal fee,
            decimal feePrice,
            AddressUsagePolicy addressUsagePolicy)
        {
            switch (addressUsagePolicy)
            {
            case AddressUsagePolicy.UseMinimalBalanceFirst:
                addresses = addresses
                            .SortList(comparison: (a, b) => a.AvailableBalance().CompareTo(b.AvailableBalance()));
                break;

            case AddressUsagePolicy.UseMaximumBalanceFirst:
                addresses = addresses
                            .SortList(comparison: (a, b) => b.AvailableBalance().CompareTo(a.AvailableBalance()));
                break;

            case AddressUsagePolicy.UseOnlyOneAddress:
                var walletAddress = addresses
                                    .FirstOrDefault(w => w.AvailableBalance() >= amount + Currency.GetFeeAmount(fee, feePrice));

                return(walletAddress != null
                        ? new List <WalletAddress> {
                    walletAddress
                }
                        : Enumerable.Empty <WalletAddress>());

            default:
                throw new Exception("Address usage policy not supported");
            }

            return(addresses);
        }
Example #5
0
 public abstract Task <IEnumerable <WalletAddress> > GetUnspentAddressesAsync(
     decimal amount,
     decimal fee,
     decimal feePrice,
     bool isFeePerTransaction,
     AddressUsagePolicy addressUsagePolicy,
     CancellationToken cancellationToken = default(CancellationToken));
 public abstract Task <IEnumerable <WalletAddress> > GetUnspentAddressesAsync(
     string toAddress,
     decimal amount,
     decimal fee,
     decimal feePrice,
     FeeUsagePolicy feeUsagePolicy,
     AddressUsagePolicy addressUsagePolicy,
     BlockchainTransactionType transactionType,
     CancellationToken cancellationToken = default);
        public override async Task <IEnumerable <WalletAddress> > GetUnspentAddressesAsync(
            decimal amount,
            decimal fee,
            decimal feePrice,
            bool isFeePerTransaction,
            AddressUsagePolicy addressUsagePolicy,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            // todo: unspent address using policy (transaction count minimization?)

            var unspentAddresses = (await DataRepository
                                    .GetUnspentAddressesAsync(Currency)
                                    .ConfigureAwait(false))
                                   .ToList();

            unspentAddresses = ApplyAddressUsagePolicy(
                addresses: unspentAddresses,
                amount: amount,
                fee: fee,
                feePrice: feePrice,
                addressUsagePolicy: addressUsagePolicy)
                               .ToList();

            if (unspentAddresses.Count == 0)
            {
                return(unspentAddresses);
            }

            var requiredAmount = amount + fee;

            var usedAddresses = new List <WalletAddress>();
            var usedAmount    = 0m;

            foreach (var walletAddress in unspentAddresses)
            {
                if (usedAmount >= requiredAmount)
                {
                    break;
                }

                usedAddresses.Add(walletAddress);
                usedAmount += walletAddress.AvailableBalance();
            }

            if (requiredAmount > 0 && usedAmount < requiredAmount)
            {
                return(Enumerable.Empty <WalletAddress>());
            }

            return(ResolvePublicKeys(usedAddresses));
        }
Example #8
0
 public Task <IEnumerable <WalletAddress> > GetUnspentAddressesAsync(
     Currency currency,
     decimal amount,
     decimal fee,
     decimal feePrice,
     bool isFeePerTransaction,
     AddressUsagePolicy addressUsagePolicy,
     CancellationToken cancellationToken = default(CancellationToken))
 {
     return(GetAccountByCurrency(currency)
            .GetUnspentAddressesAsync(
                amount: amount,
                fee: fee,
                feePrice: feePrice,
                isFeePerTransaction: isFeePerTransaction,
                addressUsagePolicy: addressUsagePolicy,
                cancellationToken: cancellationToken));
 }
Example #9
0
 public Task <IEnumerable <WalletAddress> > GetUnspentAddressesAsync(
     string currency,
     string toAddress,
     decimal amount,
     decimal fee,
     decimal feePrice,
     FeeUsagePolicy feeUsagePolicy,
     AddressUsagePolicy addressUsagePolicy,
     BlockchainTransactionType transactionType,
     CancellationToken cancellationToken = default)
 {
     return(GetCurrencyAccount(currency)
            .GetUnspentAddressesAsync(
                toAddress: toAddress,
                amount: amount,
                fee: fee,
                feePrice: feePrice,
                feeUsagePolicy: feeUsagePolicy,
                addressUsagePolicy: addressUsagePolicy,
                transactionType: transactionType,
                cancellationToken: cancellationToken));
 }
Example #10
0
        public override async Task <IEnumerable <WalletAddress> > GetUnspentAddressesAsync(
            string toAddress,
            decimal amount,
            decimal fee,
            decimal feePrice,
            FeeUsagePolicy feeUsagePolicy,
            AddressUsagePolicy addressUsagePolicy,
            BlockchainTransactionType transactionType,
            CancellationToken cancellationToken = default)
        {
            var unspentAddresses = (await DataRepository
                                    .GetUnspentAddressesAsync(Currency)
                                    .ConfigureAwait(false))
                                   .ToList();

            if (!transactionType.HasFlag(BlockchainTransactionType.SwapRedeem) &&
                !transactionType.HasFlag(BlockchainTransactionType.SwapRefund))
            {
                unspentAddresses = unspentAddresses
                                   .Where(w => w.Address != toAddress)
                                   .ToList();
            }

            var selectedAddresses = await SelectUnspentAddressesAsync(
                from : unspentAddresses,
                to : toAddress,
                amount : amount,
                fee : fee,
                feePrice : 0,
                feeUsagePolicy : feeUsagePolicy,
                addressUsagePolicy : addressUsagePolicy,
                transactionType : transactionType,
                cancellationToken : cancellationToken)
                                    .ConfigureAwait(false);

            return(ResolvePublicKeys(selectedAddresses
                                     .Select(w => w.WalletAddress)
                                     .ToList()));
        }
Example #11
0
        public override async Task <IEnumerable <WalletAddress> > GetUnspentAddressesAsync(
            string toAddress,
            decimal amount,
            decimal fee,
            decimal feePrice,
            FeeUsagePolicy feeUsagePolicy,
            AddressUsagePolicy addressUsagePolicy,
            BlockchainTransactionType transactionType,
            CancellationToken cancellationToken = default)
        {
            if (feeUsagePolicy == FeeUsagePolicy.EstimatedFee)
            {
                var estimatedFee = await EstimateFeeAsync(
                    to : toAddress,
                    amount : amount,
                    type : transactionType,
                    cancellationToken : cancellationToken)
                                   .ConfigureAwait(false);

                if (estimatedFee == null)
                {
                    return(Enumerable.Empty <WalletAddress>());
                }

                fee = estimatedFee.Value;
            }

            var unspentAddresses = (await DataRepository
                                    .GetUnspentAddressesAsync(Currency)
                                    .ConfigureAwait(false))
                                   .ToList();

            unspentAddresses = ApplyAddressUsagePolicy(
                addresses: unspentAddresses,
                amount: amount,
                fee: fee,
                feePrice: feePrice,
                addressUsagePolicy: addressUsagePolicy)
                               .ToList();

            if (unspentAddresses.Count == 0)
            {
                return(unspentAddresses);
            }

            var requiredAmount = amount + fee;

            var usedAddresses = new List <WalletAddress>();
            var usedAmount    = 0m;

            foreach (var walletAddress in unspentAddresses)
            {
                if (usedAmount >= requiredAmount)
                {
                    break;
                }

                usedAddresses.Add(walletAddress);

                usedAmount += walletAddress.AvailableBalance();
            }

            if (requiredAmount > 0 && usedAmount < requiredAmount)
            {
                return(Enumerable.Empty <WalletAddress>());
            }

            return(ResolvePublicKeys(usedAddresses));
        }
Example #12
0
        private async Task <IEnumerable <SelectedWalletAddress> > SelectUnspentAddressesAsync(
            IList <WalletAddress> from,
            string to,
            decimal amount,
            decimal fee,
            FeeUsagePolicy feeUsagePolicy,
            AddressUsagePolicy addressUsagePolicy,
            BlockchainTransactionType transactionType,
            CancellationToken cancellationToken = default)
        {
            var xtz = Xtz;

            if (addressUsagePolicy == AddressUsagePolicy.UseMinimalBalanceFirst)
            {
                from = from.ToList().SortList(new AvailableBalanceAscending());
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseMaximumBalanceFirst)
            {
                from = from.ToList().SortList(new AvailableBalanceDescending());
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseMaximumChainBalanceFirst)
            {
                var xtzUnspentAddresses = (await DataRepository
                                           .GetUnspentAddressesAsync(xtz.Name)
                                           .ConfigureAwait(false))
                                          .ToList();

                if (!xtzUnspentAddresses.Any())
                {
                    Log.Debug("Unsufficient XTZ ammount for FA2 token processing");
                    return(Enumerable.Empty <SelectedWalletAddress>());
                }

                xtzUnspentAddresses = xtzUnspentAddresses.ToList().SortList(new AvailableBalanceDescending());

                from = xtzUnspentAddresses.FindAll(a => from.Select(b => b.Address).ToList().Contains(a.Address));
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseOnlyOneAddress)
            {
                var xtzUnspentAddresses = (await DataRepository
                                           .GetUnspentAddressesAsync(xtz.Name)
                                           .ConfigureAwait(false))
                                          .ToList();

                xtzUnspentAddresses = xtzUnspentAddresses.ToList().SortList(new AvailableBalanceAscending());

                if (!xtzUnspentAddresses.Any())
                {
                    Log.Debug("Unsufficient XTZ ammount for FA2 token processing");
                    return(Enumerable.Empty <SelectedWalletAddress>());
                }

                from = from.ToList()
                       .Concat(xtzUnspentAddresses.FindAll(a => from.Select(b => b.Address).ToList().Contains(a.Address) == false))
                       .ToList();

                //"to" used for redeem and refund - using the destination address is prefered
                from = from.Where(a => a.Address == to).Concat(from.Where(a => a.Address != to)).ToList();
            }

            var result         = new List <SelectedWalletAddress>();
            var requiredAmount = amount;

            var isFirstTx = true;
            var completed = false;

            foreach (var address in from)
            {
                var availableBalanceInTokens = address.AvailableBalance();

                var xtzAddress = await DataRepository
                                 .GetWalletAddressAsync(xtz.Name, address.Address)
                                 .ConfigureAwait(false);

                var availableBalanceInTez = xtzAddress != null?xtzAddress.AvailableBalance() : 0m;

                var txFeeInTez = feeUsagePolicy == FeeUsagePolicy.FeePerTransaction
                    ? fee
                    : await FeeByType(
                    type : transactionType,
                    from : address.Address,
                    cancellationToken : cancellationToken)
                                 .ConfigureAwait(false);

                var storageFeeInTez = StorageFeeByTypeAsync(
                    type: transactionType);

                var leftBalanceInTez = availableBalanceInTez - txFeeInTez - storageFeeInTez - xtz.MicroTezReserve.ToTez();

                if (leftBalanceInTez < 0) // ignore address with balance less than fee
                {
                    Log.Debug("Unsufficient XTZ ammount for FA2 token processing on address {@address} with available balance {@balance} and needed amount {@amount}",
                              address.Address,
                              availableBalanceInTez,
                              txFeeInTez + storageFeeInTez + xtz.MicroTezReserve.ToTez());
                    continue;
                }

                if (addressUsagePolicy == AddressUsagePolicy.UseOnlyOneAddress)
                {
                    if (Math.Min(availableBalanceInTokens, requiredAmount) == requiredAmount)
                    {
                        return new List <SelectedWalletAddress> {
                                   new SelectedWalletAddress
                                   {
                                       WalletAddress  = address,
                                       UsedAmount     = amount,
                                       UsedFee        = txFeeInTez,
                                       UsedStorageFee = storageFeeInTez
                                   }
                        }
                    }
                    ;
                    continue;
                }

                decimal amountToUse = 0;

                amountToUse     = Math.Min(availableBalanceInTokens, requiredAmount);
                requiredAmount -= amountToUse;

                if (amountToUse > 0)
                {
                    result.Add(new SelectedWalletAddress
                    {
                        WalletAddress  = address,
                        UsedAmount     = amountToUse,
                        UsedFee        = txFeeInTez,
                        UsedStorageFee = storageFeeInTez
                    });
                }

                if (requiredAmount <= 0)
                {
                    completed = true;

                    if (feeUsagePolicy == FeeUsagePolicy.FeeForAllTransactions)
                    {
                        var estimatedFee = result.Sum(s => s.UsedFee);

                        var remainingFee = fee - estimatedFee;

                        var extraFee = 0m;

                        if (remainingFee > 0)
                        {
                            foreach (var s in result)
                            {
                                xtzAddress = await DataRepository
                                             .GetWalletAddressAsync(xtz.Name, s.WalletAddress.Address)
                                             .ConfigureAwait(false);

                                extraFee = s == result.Last()
                                    ? Math.Min(xtzAddress.AvailableBalance() - s.UsedFee - s.UsedStorageFee - xtz.MicroTezReserve.ToTez(), remainingFee)
                                    : Math.Min(xtzAddress.AvailableBalance() - s.UsedFee - s.UsedStorageFee - xtz.MicroTezReserve.ToTez(), Math.Round(remainingFee * s.UsedFee / estimatedFee, xtz.Digits));

                                remainingFee -= extraFee;
                                estimatedFee -= s.UsedFee;
                                s.UsedFee    += extraFee;

                                if (remainingFee <= 0)
                                {
                                    break;
                                }
                            }
                        }
                        else if (remainingFee < 0) //todo: delete
                        {
                            Log.Error("Error fee is too small for transaction, fee is {@fee} with estimated fee {@estimatedFee}",
                                      fee,
                                      estimatedFee);
                            return(Enumerable.Empty <SelectedWalletAddress>());
                        }
                    }

                    break;
                }
                if (isFirstTx)
                {
                    isFirstTx = false;
                }
            }
            if (completed)
            {
                return(result);
            }

            if (feeUsagePolicy == FeeUsagePolicy.FeeForAllTransactions) //todo: delete
            {
                Log.Error("Error fee is too big for transaction, fee is {@fee} with estimated fee {@estimatedFee}",
                          fee,
                          result.Sum(s => s.UsedFee));
            }

            return(Enumerable.Empty <SelectedWalletAddress>());
        }
Example #13
0
        private async Task <IEnumerable <SelectedWalletAddress> > SelectUnspentAddressesAsync(
            IList <WalletAddress> from,
            string to,
            decimal amount,
            decimal fee,
            bool isFeePerTransaction,
            AddressUsagePolicy addressUsagePolicy,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            var activationFeeTez = 0m;

            if (to != null)
            {
                var api = (ITezosBlockchainApi)Xtz.BlockchainApi;

                var isActive = await api
                               .IsActiveAddress(to, cancellationToken)
                               .ConfigureAwait(false);

                activationFeeTez = !isActive
                    ? Xtz.ActivationFee.ToTez()
                    : 0;
            }

            if (addressUsagePolicy == AddressUsagePolicy.UseMinimalBalanceFirst)
            {
                from = from.ToList().SortList((a, b) => a.AvailableBalance().CompareTo(b.AvailableBalance()));
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseMaximumBalanceFirst)
            {
                from = from.ToList().SortList((a, b) => b.AvailableBalance().CompareTo(a.AvailableBalance()));
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseOnlyOneAddress)
            {
                var address = from.FirstOrDefault(w => w.AvailableBalance() >= amount + fee + activationFeeTez);

                return(address != null
                    ? new List <SelectedWalletAddress> {
                    new SelectedWalletAddress
                    {
                        WalletAddress = address,
                        UsedAmount = amount,
                        UsedFee = fee + activationFeeTez
                    }
                }
                    : Enumerable.Empty <SelectedWalletAddress>());
            }

            for (var txCount = 1; txCount <= from.Count; ++txCount)
            {
                var result         = new List <SelectedWalletAddress>();
                var requiredAmount = amount;

                var feePerTx = isFeePerTransaction
                    ? fee
                    : fee / txCount;

                var firstTx   = true;
                var completed = false;

                foreach (var address in from)
                {
                    var availableBalance = address.AvailableBalance();

                    var txFee = firstTx
                        ? feePerTx + activationFeeTez
                        : feePerTx;

                    if (availableBalance <= txFee) // ignore address with balance less than fee
                    {
                        continue;
                    }

                    var amountToUse = Math.Min(Math.Max(availableBalance - txFee, 0), requiredAmount);

                    result.Add(new SelectedWalletAddress
                    {
                        WalletAddress = address,
                        UsedAmount    = amountToUse,
                        UsedFee       = txFee
                    });
                    requiredAmount -= amountToUse;

                    if (requiredAmount <= 0)
                    {
                        completed = true;
                        break;
                    }

                    if (result.Count == txCount) // will need more transactions
                    {
                        break;
                    }

                    if (firstTx)
                    {
                        firstTx = false;
                    }
                }

                if (completed)
                {
                    return(result);
                }
            }

            return(Enumerable.Empty <SelectedWalletAddress>());
        }
Example #14
0
        public override Task <IEnumerable <SelectedWalletAddress> > SelectUnspentAddressesAsync(
            IList <WalletAddress> from,
            string to,
            decimal amount,
            decimal fee,
            decimal feePrice,
            FeeUsagePolicy feeUsagePolicy,
            AddressUsagePolicy addressUsagePolicy,
            BlockchainTransactionType transactionType,
            CancellationToken cancellationToken = default)
        {
            var eth = Eth;

            if (addressUsagePolicy == AddressUsagePolicy.UseMinimalBalanceFirst)
            {
                from = from.ToList().SortList(new AvailableBalanceAscending());
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseMaximumBalanceFirst)
            {
                from = from.ToList().SortList(new AvailableBalanceDescending());
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseOnlyOneAddress)
            {
                var feeInEth = feeUsagePolicy == FeeUsagePolicy.EstimatedFee
                    ? eth.GetFeeAmount(GasLimitByType(transactionType, isFirstTx: true), eth.GasPriceInGwei)
                    : eth.GetFeeAmount(fee, feePrice);

                var address = from.FirstOrDefault(w => w.AvailableBalance() >= amount + feeInEth);

                return(address != null
                    ? Task.FromResult <IEnumerable <SelectedWalletAddress> >(new List <SelectedWalletAddress>
                {
                    new SelectedWalletAddress
                    {
                        WalletAddress = address,
                        UsedAmount = amount,
                        UsedFee = feeInEth
                    }
                })
                    : Task.FromResult(Enumerable.Empty <SelectedWalletAddress>()));
            }

            for (var txCount = 1; txCount <= from.Count; ++txCount)
            {
                var result         = new List <SelectedWalletAddress>();
                var requiredAmount = amount;

                var isFirstTx = true;
                var completed = false;

                foreach (var address in from)
                {
                    var availableBalance = address.AvailableBalance();

                    var txFee = feeUsagePolicy == FeeUsagePolicy.EstimatedFee
                        ? eth.GetFeeAmount(GasLimitByType(transactionType, isFirstTx), eth.GasPriceInGwei)
                        : feeUsagePolicy == FeeUsagePolicy.FeeForAllTransactions
                            ? Math.Round(eth.GetFeeAmount(fee, feePrice) / txCount, eth.Digits)
                            : eth.GetFeeAmount(fee, feePrice);

                    if (availableBalance <= txFee) // ignore address with balance less than fee
                    {
                        if (result.Count + from.Count - from.IndexOf(address) <= txCount)
                        {
                            break;
                        }
                        else
                        {
                            continue;
                        }
                    }

                    var amountToUse = Math.Min(Math.Max(availableBalance - txFee, 0), requiredAmount);

                    result.Add(new SelectedWalletAddress
                    {
                        WalletAddress = address,
                        UsedAmount    = amountToUse,
                        UsedFee       = txFee
                    });
                    requiredAmount -= amountToUse;

                    if (requiredAmount <= 0)
                    {
                        completed = true;
                        break;
                    }

                    if (result.Count == txCount) // will need more transactions
                    {
                        break;
                    }

                    if (isFirstTx)
                    {
                        isFirstTx = false;
                    }
                }

                if (completed)
                {
                    return(Task.FromResult <IEnumerable <SelectedWalletAddress> >(result));
                }
            }

            return(Task.FromResult(Enumerable.Empty <SelectedWalletAddress>()));
        }
Example #15
0
        private async Task <IEnumerable <SelectedWalletAddress> > SelectUnspentAddresses(
            List <WalletAddress> from,
            decimal amount,
            decimal fee,
            decimal feePrice,
            FeeUsagePolicy feeUsagePolicy,
            AddressUsagePolicy addressUsagePolicy,
            BlockchainTransactionType transactionType)
        {
            var erc20 = Erc20;
            var eth   = Eth;

            if (addressUsagePolicy == AddressUsagePolicy.UseMinimalBalanceFirst)
            {
                from = from.ToList().SortList((a, b) => a.AvailableBalance().CompareTo(b.AvailableBalance()));
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseMaximumBalanceFirst)
            {
                from = from.ToList().SortList((a, b) => b.AvailableBalance().CompareTo(a.AvailableBalance()));
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseMaximumChainBalanceFirst)
            {
                var ethUnspentAddresses = (await DataRepository
                                           .GetUnspentAddressesAsync(eth.Name)
                                           .ConfigureAwait(false))
                                          .ToList();

                if (!ethUnspentAddresses.Any())
                {
                    Log.Debug("Unsufficient ETH ammount for ERC20 token processing");
                    return(Enumerable.Empty <SelectedWalletAddress>());
                }

                ethUnspentAddresses = ethUnspentAddresses.SortList((a, b) => b.AvailableBalance().CompareTo(a.AvailableBalance()));

                from = from.FindAll(
                    a => ethUnspentAddresses.Select(b => b.Address)
                    .ToList()
                    .Contains(a.Address));
            }

            else if (addressUsagePolicy == AddressUsagePolicy.UseOnlyOneAddress)
            {
                var result = new List <SelectedWalletAddress>();

                var feeInEth = feeUsagePolicy == FeeUsagePolicy.EstimatedFee
                    ? erc20.GetFeeAmount(GasLimitByType(transactionType, isFirstTx: true), erc20.GasPriceInGwei)
                    : erc20.GetFeeAmount(fee, feePrice);

                //take erc20 non zero addresses first
                foreach (var address in from.TakeWhile(x => x.AvailableBalance() >= amount))
                {
                    var ethAddress = await DataRepository
                                     .GetWalletAddressAsync(eth.Name, address.Address)
                                     .ConfigureAwait(false);

                    if (ethAddress == null || ethAddress.AvailableBalance() < feeInEth)
                    {
                        Log.Debug("Unsufficient ETH ammount for ERC20 token processing on address {@address} with available balance {@balance} and needed amount {@amount}",
                                  ethAddress.Address,
                                  ethAddress.AvailableBalance(),
                                  feeInEth);
                        continue;
                    }

                    result.Add(new SelectedWalletAddress
                    {
                        WalletAddress = address,
                        UsedAmount    = amount,
                        UsedFee       = feeInEth
                    });
                }

                if (result.Any() || amount != 0m)
                {
                    return(result);
                }

                //take non zero eth addresses
                var ethUnspentAddresses = (await DataRepository
                                           .GetUnspentAddressesAsync(eth.Name)
                                           .ConfigureAwait(false))
                                          .ToList();

                if (!ethUnspentAddresses.Any())
                {
                    Log.Debug("Unsufficient ETH ammount for ERC20 token processing");
                    return(Enumerable.Empty <SelectedWalletAddress>());
                }

                ethUnspentAddresses = ethUnspentAddresses.FindAll(a => a.AvailableBalance() > feeInEth);
                ethUnspentAddresses = ethUnspentAddresses.SortList((a, b) => a.AvailableBalance().CompareTo(b.AvailableBalance()));

                foreach (var address in ethUnspentAddresses)
                {
                    result.Add(new SelectedWalletAddress
                    {
                        WalletAddress = address,
                        UsedAmount    = amount,
                        UsedFee       = feeInEth
                    });
                }

                return(result);
            }

            for (var txCount = 1; txCount <= from.Count; ++txCount)
            {
                var result         = new List <SelectedWalletAddress>();
                var requiredAmount = amount;

                var isFirstTx = true;
                var completed = false;

                foreach (var address in from)
                {
                    var availableBalance = address.AvailableBalance();

                    if (!(availableBalance > 0))
                    {
                        continue;
                    }

                    var ethAddress = await DataRepository
                                     .GetWalletAddressAsync(eth.Name, address.Address)
                                     .ConfigureAwait(false);

                    var ethAvailableBalance = ethAddress != null?ethAddress.AvailableBalance() : 0;

                    var txFee = feeUsagePolicy == FeeUsagePolicy.EstimatedFee
                        ? eth.GetFeeAmount(GasLimitByType(transactionType, isFirstTx), erc20.GasPriceInGwei)
                        : feeUsagePolicy == FeeUsagePolicy.FeeForAllTransactions
                            ? Math.Round(eth.GetFeeAmount(fee, feePrice) / txCount, eth.Digits)
                            : eth.GetFeeAmount(fee, feePrice);

                    if (ethAvailableBalance < txFee) // ignore address with balance less than fee
                    {
                        Log.Debug("Unsufficient ETH ammount for ERC20 token processing on address {@address} with available balance {@balance} and needed amount {@amount}",
                                  ethAddress.Address,
                                  ethAddress.AvailableBalance(),
                                  txFee);
                        if (result.Count + from.Count - from.IndexOf(address) <= txCount)
                        {
                            break;
                        }
                        else
                        {
                            continue;
                        }
                    }

                    var amountToUse = Math.Min(availableBalance, requiredAmount);

                    result.Add(new SelectedWalletAddress
                    {
                        WalletAddress = address,
                        UsedAmount    = amountToUse,
                        UsedFee       = txFee
                    });
                    requiredAmount -= amountToUse;

                    if (requiredAmount <= 0)
                    {
                        completed = true;
                        break;
                    }

                    if (result.Count == txCount) // will need more transactions
                    {
                        break;
                    }

                    if (isFirstTx)
                    {
                        isFirstTx = false;
                    }
                }

                if (completed)
                {
                    return(result);
                }
            }

            return(Enumerable.Empty <SelectedWalletAddress>());
        }
        private IEnumerable <(WalletAddress, decimal)> SelectUnspentAddresses(
            IList <WalletAddress> from,
            decimal amount,
            decimal fee,
            decimal feePrice,
            bool isFeePerTransaction,
            AddressUsagePolicy addressUsagePolicy)
        {
            if (addressUsagePolicy == AddressUsagePolicy.UseMinimalBalanceFirst)
            {
                from = from.ToList().SortList((a, b) => a.AvailableBalance().CompareTo(b.AvailableBalance()));
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseMaximumBalanceFirst)
            {
                from = from.ToList().SortList((a, b) => b.AvailableBalance().CompareTo(a.AvailableBalance()));
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseOnlyOneAddress)
            {
                var address = from.FirstOrDefault(w => w.AvailableBalance() >= amount + Currency.GetFeeAmount(fee, feePrice));

                return(address != null
                    ? new List <(WalletAddress, decimal)> {
                    (address, amount + Currency.GetFeeAmount(fee, feePrice))
                }
                    : Enumerable.Empty <(WalletAddress, decimal)>());
            }

            for (var txCount = 1; txCount <= from.Count; ++txCount)
            {
                var result         = new List <(WalletAddress, decimal)>();
                var requiredAmount = amount;

                var feePerTx = isFeePerTransaction
                    ? Currency.GetFeeAmount(fee, feePrice)
                    : Currency.GetFeeAmount(fee, feePrice) / txCount;

                var completed = false;

                foreach (var address in from)
                {
                    var availableBalance = address.AvailableBalance();

                    if (availableBalance <= feePerTx) // ignore address with balance less than fee
                    {
                        continue;
                    }

                    var amountToUse = Math.Min(Math.Max(availableBalance - feePerTx, 0), requiredAmount);

                    result.Add((address, amountToUse));
                    requiredAmount -= amountToUse;

                    if (requiredAmount <= 0)
                    {
                        completed = true;
                        break;
                    }

                    if (result.Count == txCount) // will need more transactions
                    {
                        break;
                    }
                }

                if (completed)
                {
                    return(result);
                }
            }

            return(Enumerable.Empty <(WalletAddress, decimal)>());
        }
Example #17
0
        public override async Task <IEnumerable <SelectedWalletAddress> > SelectUnspentAddressesAsync(
            IList <WalletAddress> from,
            string to,
            decimal amount,
            decimal fee,
            decimal feePrice,
            FeeUsagePolicy feeUsagePolicy,
            AddressUsagePolicy addressUsagePolicy,
            BlockchainTransactionType transactionType,
            CancellationToken cancellationToken = default)
        {
            var xtz = Xtz;

            if (addressUsagePolicy == AddressUsagePolicy.UseMinimalBalanceFirst)
            {
                from = from.ToList().SortList(new AvailableBalanceAscending());
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseMaximumBalanceFirst)
            {
                from = from.ToList().SortList(new AvailableBalanceDescending());
            }
            else if (addressUsagePolicy == AddressUsagePolicy.UseOnlyOneAddress)
            {
                //"to" used for redeem and refund - using the destination address is prefered
                from = from.Where(a => a.Address == to).Concat(from.Where(a => a.Address != to)).ToList();
            }

            var result         = new List <SelectedWalletAddress>();
            var requiredAmount = amount;

            if (feeUsagePolicy == FeeUsagePolicy.FeeForAllTransactions)
            {
                requiredAmount += fee;
            }

            var isFirstTx = true;
            var completed = false;

            foreach (var address in from)
            {
                var availableBalanceInTez = address.AvailableBalance();

                var txFeeInTez = feeUsagePolicy == FeeUsagePolicy.FeePerTransaction
                    ? fee
                    : await FeeByType(
                    type : transactionType,
                    from : address.Address,
                    isFirstTx : isFirstTx,
                    cancellationToken : cancellationToken)
                                 .ConfigureAwait(false);

                var storageFeeInTez = await StorageFeeByTypeAsync(
                    type : transactionType,
                    to : to,
                    isFirstTx : isFirstTx,
                    cancellationToken : cancellationToken)
                                      .ConfigureAwait(false);

                var netAvailableBalanceInTez = availableBalanceInTez - txFeeInTez - storageFeeInTez - xtz.MicroTezReserve.ToTez();

                if (netAvailableBalanceInTez <= 0) // ignore address with balance less than fee
                {
                    continue;
                }

                if (addressUsagePolicy == AddressUsagePolicy.UseOnlyOneAddress)
                {
                    if (Math.Min(netAvailableBalanceInTez, requiredAmount) == requiredAmount)
                    {
                        return new List <SelectedWalletAddress> {
                                   new SelectedWalletAddress
                                   {
                                       WalletAddress  = address,
                                       UsedAmount     = amount,
                                       UsedFee        = txFeeInTez,
                                       UsedStorageFee = storageFeeInTez
                                   }
                        }
                    }
                    ;
                    continue;
                }

                decimal amountToUse = 0;

                if (feeUsagePolicy == FeeUsagePolicy.EstimatedFee)
                {
                    amountToUse     = Math.Min(netAvailableBalanceInTez, requiredAmount);
                    requiredAmount -= amountToUse;
                }
                else if (feeUsagePolicy == FeeUsagePolicy.FeeForAllTransactions)
                {
                    amountToUse     = Math.Min(netAvailableBalanceInTez, requiredAmount - txFeeInTez);
                    requiredAmount -= (amountToUse + txFeeInTez);
                }

                if (amountToUse > 0)
                {
                    result.Add(new SelectedWalletAddress
                    {
                        WalletAddress  = address,
                        UsedAmount     = amountToUse,
                        UsedFee        = txFeeInTez,
                        UsedStorageFee = storageFeeInTez
                    });
                }

                if (requiredAmount <= 0)
                {
                    completed = true;

                    if (feeUsagePolicy == FeeUsagePolicy.FeeForAllTransactions)
                    {
                        requiredAmount = amount;

                        var estimatedFee = result.Sum(s => s.UsedFee);
                        var remainingFee = fee - estimatedFee;

                        var extraFee = 0m;

                        if (remainingFee > 0)
                        {
                            var res = result.ToList();
                            result = new List <SelectedWalletAddress>();

                            foreach (var s in res)
                            {
                                extraFee = Math.Round(remainingFee * s.UsedFee / estimatedFee, xtz.Digits);

                                if (extraFee + requiredAmount <= s.UsedAmount)
                                {
                                    s.UsedAmount = requiredAmount;
                                    s.UsedFee   += extraFee;
                                    result.Add(s);
                                    break;
                                }

                                if (s == res.Last())
                                {
                                    s.UsedAmount = requiredAmount;
                                    s.UsedFee   += Math.Min(s.WalletAddress.AvailableBalance() - s.UsedAmount - s.UsedFee - s.UsedStorageFee - xtz.MicroTezReserve.ToTez(), remainingFee);
                                    if (s.WalletAddress.AvailableBalance() - s.UsedAmount - s.UsedFee - s.UsedStorageFee - xtz.MicroTezReserve.ToTez() < 0) //check if possible
                                    {
                                        Log.Error("Error in fee distribution for transactions, fee is {@fee} with used fee {@usedFee}",
                                                  fee,
                                                  s.UsedFee + s.UsedStorageFee);
                                    }
                                }
                                else
                                {
                                    if (extraFee >= s.UsedAmount)
                                    {
                                        //remainingFee -= s.UsedAmount; //todo: use it when replacing allocation fee
                                        //estimatedFee -= s.UsedFee;
                                        //continue;
                                        extraFee = Math.Min(s.UsedAmount - 1 / xtz.DigitsMultiplier, extraFee);
                                    }

                                    remainingFee -= extraFee;
                                    estimatedFee -= s.UsedFee;
                                    s.UsedAmount -= extraFee;
                                    s.UsedFee    += extraFee;
                                }

                                requiredAmount -= Math.Min(requiredAmount, s.UsedAmount);

                                result.Add(s);

                                if (requiredAmount <= 0)
                                {
                                    break;
                                }
                            }
                        }
                        else //todo: delete
                        {
                            Log.Error("Error fee is too small for transaction, fee is {@fee} with estimated fee {@estimatedFee}",
                                      fee,
                                      estimatedFee);
                        }
                    }

                    break;
                }
                if (isFirstTx)
                {
                    isFirstTx = false;
                }
            }
            if (completed)
            {
                return(result);
            }

            if (feeUsagePolicy == FeeUsagePolicy.FeeForAllTransactions) //todo: delete
            {
                Log.Error("Error fee is too big for transaction, fee is {@fee} with estimated fee {@estimatedFee}",
                          fee,
                          result.Sum(s => s.UsedFee));
            }

            return(Enumerable.Empty <SelectedWalletAddress>());
        }

        #endregion Addresses
    }
Example #18
0
 public override Task <IEnumerable <SelectedWalletAddress> > SelectUnspentAddressesAsync(IList <WalletAddress> from, string to, decimal amount, decimal fee, decimal feePrice, FeeUsagePolicy feeUsagePolicy, AddressUsagePolicy addressUsagePolicy, BlockchainTransactionType transactionType, CancellationToken cancellationToken = default)
 {
     throw new NotImplementedException();
 }