예제 #1
0
        private async Task <IBitcoinBasedTransaction> CreateRefundTxAsync(
            Swap swap,
            IBitcoinBasedTransaction paymentTx,
            string refundAddress,
            DateTimeOffset lockTime,
            byte[] redeemScript)
        {
            Log.Debug("Create refund tx for swap {@swapId}", swap.Id);

            var currency = Currencies.Get <BitcoinBasedCurrency>(Currency);

            var amountInSatoshi = currency.CoinToSatoshi(AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price, currency.DigitsMultiplier));

            var tx = await _transactionFactory
                     .CreateSwapRefundTxAsync(
                paymentTx : paymentTx,
                amount : amountInSatoshi,
                refundAddress : refundAddress,
                lockTime : lockTime,
                redeemScript : redeemScript)
                     .ConfigureAwait(false);

            if (tx == null)
            {
                throw new InternalException(
                          code: Errors.TransactionCreationError,
                          description: $"Refund tx creation error for swap {swap.Id}");
            }

            tx.Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapRefund;

            Log.Debug("Refund tx successfully created for swap {@swapId}", swap.Id);

            return(tx);
        }
        public decimal EstimatedDealPrice(Side side, decimal amount)
        {
            var amountToFill = amount;

            lock (SyncRoot)
            {
                var book = side == Side.Buy
                    ? Sells
                    : Buys;

                if (amount == 0)
                {
                    return(book.Any() ? book.First().Key : 0);
                }

                foreach (var entryPair in book)
                {
                    var qty             = entryPair.Value.Qty();
                    var availableAmount = AmountHelper.QtyToAmount(side, qty, entryPair.Key);

                    amountToFill -= availableAmount;

                    if (amountToFill <= 0)
                    {
                        return(entryPair.Key);
                    }
                }
            }

            return(0m);
        }
        public static SwapViewModel CreateSwapViewModel(ClientSwap swap)
        {
            var fromCurrency = CurrencyViewModelCreator.CreateViewModel(
                currency: swap.SoldCurrency,
                subscribeToUpdates: false);

            var toCurrency = CurrencyViewModelCreator.CreateViewModel(
                currency: swap.PurchasedCurrency,
                subscribeToUpdates: false);

            var fromAmount = AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price);
            var toAmount   = AmountHelper.QtyToAmount(swap.Side.Opposite(), swap.Qty, swap.Price);

            return(new SwapViewModel
            {
                Id = swap.Id.ToString(),
                CompactState = CompactStateBySwap(swap),
                Mode = ModeBySwap(swap),
                Time = swap.TimeStamp,

                FromBrush = new SolidColorBrush(fromCurrency.AmountColor),
                FromAmount = fromAmount,
                FromAmountFormat = fromCurrency.CurrencyFormat,
                FromCurrencyCode = fromCurrency.CurrencyCode,

                ToBrush = new SolidColorBrush(toCurrency.AmountColor),
                ToAmount = toAmount,
                ToAmountFormat = toCurrency.CurrencyFormat,
                ToCurrencyCode = toCurrency.CurrencyCode,

                Price = swap.Price,
                PriceFormat = $"F{swap.Symbol.Quote.Digits}"
            });
        }
        public static SwapViewModel CreateSwapViewModel(Swap swap, ICurrencies currencies, IAccount account)
        {
            var soldCurrency      = currencies.GetByName(swap.SoldCurrency);
            var purchasedCurrency = currencies.GetByName(swap.PurchasedCurrency);

            var fromAmount = AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price, soldCurrency.DigitsMultiplier);
            var toAmount   = AmountHelper.QtyToAmount(swap.Side.Opposite(), swap.Qty, swap.Price, purchasedCurrency.DigitsMultiplier);

            var quoteCurrency = swap.Symbol.QuoteCurrency() == swap.SoldCurrency
                ? soldCurrency
                : purchasedCurrency;

            var swapViewModel = new SwapViewModel
            {
                Id   = swap.Id.ToString(),
                Mode = ModeBySwap(swap),
                Time = swap.TimeStamp,

                FromAmount       = fromAmount,
                FromCurrencyCode = soldCurrency.Name,

                ToAmount       = toAmount,
                ToCurrencyCode = purchasedCurrency.Name,

                Price       = swap.Price,
                PriceFormat = $"F{quoteCurrency.Digits}",

                Account = account
            };

            swapViewModel.UpdateSwap(swap);

            return(swapViewModel);
        }
예제 #5
0
        public static SwapViewModel CreateSwapViewModel(Swap swap, ICurrencies currencies)
        {
            try
            {
                var soldCurrency      = currencies.GetByName(swap.SoldCurrency);
                var purchasedCurrency = currencies.GetByName(swap.PurchasedCurrency);

                var fromCurrencyViewModel = CurrencyViewModelCreator.CreateViewModel(
                    currencyConfig: soldCurrency,
                    subscribeToUpdates: false);

                var toCurrencyViewModel = CurrencyViewModelCreator.CreateViewModel(
                    currencyConfig: purchasedCurrency,
                    subscribeToUpdates: false);

                var fromAmount = AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price, soldCurrency.DigitsMultiplier);
                var toAmount   = AmountHelper.QtyToAmount(swap.Side.Opposite(), swap.Qty, swap.Price, purchasedCurrency.DigitsMultiplier);

                var quoteCurrency = swap.Symbol.QuoteCurrency() == swap.SoldCurrency
                    ? soldCurrency
                    : purchasedCurrency;

                return(new SwapViewModel
                {
                    Id = swap.Id.ToString(),
                    CompactState = CompactStateBySwap(swap),
                    Mode = ModeBySwap(swap),
                    Time = swap.TimeStamp,

                    FromBrush = new SolidColorBrush(fromCurrencyViewModel.AmountColor),
                    FromAmount = fromAmount,
                    FromAmountFormat = fromCurrencyViewModel.CurrencyFormat,
                    FromCurrencyCode = fromCurrencyViewModel.CurrencyCode,

                    ToBrush = new SolidColorBrush(toCurrencyViewModel.AmountColor),
                    ToAmount = toAmount,
                    ToAmountFormat = toCurrencyViewModel.CurrencyFormat,
                    ToCurrencyCode = toCurrencyViewModel.CurrencyCode,

                    Price = swap.Price,
                    PriceFormat = $"F{quoteCurrency.Digits}"
                });
            }
            catch (Exception e)
            {
                Log.Error(e, $"Error while create SwapViewModel for {swap.Symbol} swap with id {swap.Id}");

                return(null);
            }
        }
예제 #6
0
        public Task <IBitcoinBasedTransaction> CreateSwapRefundTxAsync(
            IBitcoinBasedTransaction paymentTx,
            ClientSwap swap,
            string refundAddress,
            DateTimeOffset lockTime)
        {
            var currency    = (BitcoinBasedCurrency)paymentTx.Currency;
            var orderAmount = (long)(AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price) *
                                     currency.DigitsMultiplier);

            var swapOutputs = paymentTx.Outputs
                              .Cast <BitcoinBasedTxOutput>()
                              .Where(o => o.Value == orderAmount && o.IsSwapPayment)
                              .ToList();

            if (swapOutputs.Count != 1)
            {
                throw new Exception("Payment tx must have only one swap payment output");
            }

            var estimatedSigSize = EstimateSigSize(swapOutputs, forRefund: true);

            var txSize = currency
                         .CreateSwapRefundTx(
                unspentOutputs: swapOutputs,
                destinationAddress: refundAddress,
                changeAddress: refundAddress,
                amount: orderAmount,
                fee: 0,
                lockTime: lockTime)
                         .VirtualSize();

            var fee = (long)(currency.FeeRate * (txSize + estimatedSigSize));

            if (orderAmount - fee < 0)
            {
                throw new Exception($"Insufficient funds for fee. Available {orderAmount}, required {fee}");
            }

            var tx = currency.CreateSwapRefundTx(
                unspentOutputs: swapOutputs,
                destinationAddress: refundAddress,
                changeAddress: refundAddress,
                amount: orderAmount - fee,
                fee: fee,
                lockTime: lockTime);

            return(Task.FromResult(tx));
        }
예제 #7
0
        public (decimal, decimal) EstimateOrderPrices(
            Side side,
            decimal amount,
            decimal amountDigitsMultiplier,
            decimal qtyDigitsMultiplier)
        {
            var requiredAmount = amount;

            lock (SyncRoot)
            {
                var book = side == Side.Buy
                    ? Sells
                    : Buys;

                if (amount == 0)
                {
                    return(book.Any()
                        ? (book.First().Key, book.First().Key)
                        : (0m, 0m));
                }

                var totalUsedQuoteAmount = 0m;
                var totalUsedQty         = 0m;

                foreach (var entryPair in book)
                {
                    var qty   = entryPair.Value.Qty();
                    var price = entryPair.Key;

                    var availableAmount = AmountHelper.QtyToAmount(side, qty, price, amountDigitsMultiplier);

                    var usedAmount = Math.Min(requiredAmount, availableAmount);
                    var usedQty    = AmountHelper.AmountToQty(side, usedAmount, price, qtyDigitsMultiplier);

                    totalUsedQuoteAmount += usedQty * price;
                    totalUsedQty         += usedQty;

                    requiredAmount -= usedAmount;

                    if (requiredAmount <= 0)
                    {
                        return(price, totalUsedQuoteAmount / totalUsedQty);
                    }
                }
            }

            return(0m, 0m);
        }
예제 #8
0
        private async Task <(IBitcoinBasedTransaction, byte[])> CreatePaymentTxAsync(
            Swap swap,
            string refundAddress,
            DateTimeOffset lockTime)
        {
            var currency = Currencies.Get <BitcoinBasedCurrency>(swap.SoldCurrency);

            Log.Debug("Create swap payment {@currency} tx for swap {@swapId}",
                      currency.Name,
                      swap.Id);

            var unspentAddresses = (await _account
                                    .GetUnspentAddressesAsync()
                                    .ConfigureAwait(false))
                                   .ToList()
                                   .SortList(new AvailableBalanceAscending())
                                   .Select(a => a.Address);

            var amountInSatoshi = currency.CoinToSatoshi(AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price, currency.DigitsMultiplier));

            var(tx, redeemScript) = await _transactionFactory
                                    .CreateSwapPaymentTxAsync(
                currency : currency,
                amount : amountInSatoshi,
                fromWallets : unspentAddresses,
                refundAddress : refundAddress,
                toAddress : swap.PartyAddress,
                lockTime : lockTime,
                secretHash : swap.SecretHash,
                secretSize : DefaultSecretSize,
                outputsSource : new LocalTxOutputSource(_account))
                                    .ConfigureAwait(false);

            if (tx == null)
            {
                throw new InternalException(
                          code: Errors.TransactionCreationError,
                          description: $"Payment tx creation error for swap {swap.Id}");
            }

            tx.Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment;

            Log.Debug("Payment tx successfully created for swap {@swapId}", swap.Id);

            return(tx, redeemScript);
        }
예제 #9
0
        private async Task <IBitcoinBasedTransaction> CreateRedeemTxAsync(
            Swap swap,
            IBitcoinBasedTransaction paymentTx,
            string redeemAddress,
            byte[] redeemScript,
            bool increaseSequenceNumber = false)
        {
            Log.Debug("Create redeem tx for swap {@swapId}", swap.Id);

            var currency = Currencies.Get <BitcoinBasedCurrency>(Currency);

            var amountInSatoshi = currency.CoinToSatoshi(AmountHelper.QtyToAmount(swap.Side.Opposite(), swap.Qty, swap.Price, currency.DigitsMultiplier));

            var sequenceNumber = 0u;

            if (increaseSequenceNumber)
            {
                var previousSequenceNumber = (swap?.RedeemTx as IBitcoinBasedTransaction)?.GetSequenceNumber(0) ?? 0;

                sequenceNumber = previousSequenceNumber == 0
                    ? Sequence.SEQUENCE_FINAL - 1024
                    : (previousSequenceNumber == Sequence.SEQUENCE_FINAL
                        ? Sequence.SEQUENCE_FINAL
                        : previousSequenceNumber + 1);
            }

            var tx = await _transactionFactory
                     .CreateSwapRedeemTxAsync(
                paymentTx : paymentTx,
                amount : amountInSatoshi,
                redeemAddress : redeemAddress,
                redeemScript : redeemScript,
                sequenceNumber : sequenceNumber)
                     .ConfigureAwait(false);

            if (tx == null)
            {
                throw new InternalException(
                          code: Errors.TransactionCreationError,
                          description: $"Redeem tx creation error for swap {swap.Id}");
            }

            tx.Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapRedeem;

            return(tx);
        }
예제 #10
0
        public Task <IBitcoinBasedTransaction> CreateSwapRedeemTxAsync(
            IBitcoinBasedTransaction paymentTx,
            ClientSwap swap,
            string redeemAddress)
        {
            var currency    = (BitcoinBasedCurrency)paymentTx.Currency;
            var orderAmount = (long)(AmountHelper.QtyToAmount(swap.Side.Opposite(), swap.Qty, swap.Price) *
                                     currency.DigitsMultiplier);

            var swapOutputs = paymentTx
                              .SwapOutputs()
                              .ToList();

            if (swapOutputs.Count != 1)
            {
                throw new Exception("Payment tx must have only one swap payment output");
            }

            var estimatedSigSize = EstimateSigSize(swapOutputs);

            var txSize = currency
                         .CreateP2PkhTx(
                unspentOutputs: swapOutputs,
                destinationAddress: redeemAddress,
                changeAddress: redeemAddress,
                amount: orderAmount,
                fee: 0)
                         .VirtualSize();

            var fee = (long)(currency.FeeRate * (txSize + estimatedSigSize));

            if (orderAmount - fee < 0)
            {
                throw new Exception($"Insufficient funds for fee. Available {orderAmount}, required {fee}");
            }

            var tx = currency.CreateP2PkhTx(
                unspentOutputs: swapOutputs,
                destinationAddress: redeemAddress,
                changeAddress: redeemAddress,
                amount: orderAmount - fee,
                fee: fee);

            return(Task.FromResult(tx));
        }
예제 #11
0
        public decimal EstimateMaxAmount(Side side, long digitsMultiplier)
        {
            var amount = 0m;

            lock (SyncRoot)
            {
                var book = side == Side.Buy
                    ? Sells
                    : Buys;

                foreach (var entryPair in book)
                {
                    amount += AmountHelper.QtyToAmount(side, entryPair.Value.Qty(), entryPair.Key, digitsMultiplier);
                }
            }

            return(amount);
        }
        public static SwapViewModel CreateSwapViewModel(ISwapState s)
        {
            if (s is SwapState swap)
            {
                var order = swap.Order;

                var fromCurrency = CurrencyViewModelCreator.CreateViewModel(
                    currency: order.SoldCurrency(),
                    subscribeToUpdates: false);

                var toCurrency = CurrencyViewModelCreator.CreateViewModel(
                    currency: order.PurchasedCurrency(),
                    subscribeToUpdates: false);

                var fromAmount = AmountHelper.QtyToAmount(order.Side, order.LastQty, order.LastPrice);
                var toAmount   = AmountHelper.QtyToAmount(order.Side.Opposite(), order.LastQty, order.LastPrice);

                return(new SwapViewModel
                {
                    Id = swap.Id.ToString(),
                    CompactState = CompactStateBySwap(swap),
                    Mode = ModeBySwap(swap),
                    Time = order.TimeStamp,

                    FromBrush = new SolidColorBrush(fromCurrency.AmountColor),
                    FromAmount = fromAmount,
                    FromAmountFormat = fromCurrency.CurrencyFormat,
                    FromCurrencyCode = fromCurrency.CurrencyCode,

                    ToBrush = new SolidColorBrush(toCurrency.AmountColor),
                    ToAmount = toAmount,
                    ToAmountFormat = toCurrency.CurrencyFormat,
                    ToCurrencyCode = toCurrency.CurrencyCode,

                    Price = order.LastPrice,
                    PriceFormat = $"F{order.Symbol.PriceDigits}"
                });
            }

            throw new NotSupportedException("Swap not supported");
        }
        public decimal EstimateMaxAmount(Side side)
        {
            var amount = 0m;

            lock (SyncRoot)
            {
                var book = side == Side.Buy
                    ? Sells
                    : Buys;

                foreach (var entryPair in book)
                {
                    amount += AmountHelper.QtyToAmount(
                        side: side,
                        qty: entryPair.Value.Qty(),
                        price: entryPair.Key);
                }
            }

            return(amount);
        }
예제 #14
0
        protected virtual async Task <IEnumerable <TezosTransaction> > CreatePaymentTxsAsync(
            Swap swap,
            int lockTimeSeconds,
            CancellationToken cancellationToken = default)
        {
            var xtz = Xtz;

            Log.Debug("Create payment transactions for swap {@swapId}", swap.Id);

            var requiredAmountInMtz = AmountHelper
                                      .QtyToAmount(swap.Side, swap.Qty, swap.Price, xtz.DigitsMultiplier)
                                      .ToMicroTez();

            var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeSeconds)).ToUnixTimeSeconds();
            var isInitTx             = true;
            var rewardForRedeemInMtz = swap.IsInitiator
                ? swap.PartyRewardForRedeem.ToMicroTez()
                : 0;

            var unspentAddresses = (await _account
                                    .GetUnspentAddressesAsync(cancellationToken)
                                    .ConfigureAwait(false))
                                   .ToList()
                                   .SortList(new AvailableBalanceAscending());

            var transactions = new List <TezosTransaction>();

            foreach (var walletAddress in unspentAddresses)
            {
                Log.Debug("Create swap payment tx from address {@address} for swap {@swapId}",
                          walletAddress.Address,
                          swap.Id);

                var balanceInTz = (await _account
                                   .GetAddressBalanceAsync(
                                       address: walletAddress.Address,
                                       cancellationToken: cancellationToken)
                                   .ConfigureAwait(false))
                                  .Available;

                Log.Debug("Available balance: {@balance}", balanceInTz);

                var balanceInMtz = balanceInTz.ToMicroTez();

                var isRevealed = await _account
                                 .IsRevealedSourceAsync(walletAddress.Address, cancellationToken)
                                 .ConfigureAwait(false);

                var feeAmountInMtz = isInitTx
                    ? xtz.InitiateFee + (isRevealed ? 0 : xtz.RevealFee)
                    : xtz.AddFee + (isRevealed ? 0 : xtz.RevealFee);

                var storageLimitInMtz = isInitTx
                    ? xtz.InitiateStorageLimit * xtz.StorageFeeMultiplier
                    : xtz.AddStorageLimit * xtz.StorageFeeMultiplier;

                var amountInMtz = Math.Min(balanceInMtz - feeAmountInMtz - storageLimitInMtz, requiredAmountInMtz);

                if (amountInMtz <= 0)
                {
                    Log.Warning(
                        "Insufficient funds at {@address}. Balance: {@balance}, " +
                        "feeAmount: {@feeAmount}, storageLimit: {@storageLimit}, result: {@result}.",
                        walletAddress.Address,
                        balanceInMtz,
                        feeAmountInMtz,
                        storageLimitInMtz,
                        amountInMtz);

                    continue;
                }

                requiredAmountInMtz -= amountInMtz;

                if (isInitTx)
                {
                    transactions.Add(new TezosTransaction
                    {
                        Currency      = xtz,
                        CreationTime  = DateTime.UtcNow,
                        From          = walletAddress.Address,
                        To            = xtz.SwapContractAddress,
                        Amount        = Math.Round(amountInMtz, 0),
                        Fee           = feeAmountInMtz,
                        GasLimit      = xtz.InitiateGasLimit,
                        StorageLimit  = xtz.InitiateStorageLimit,
                        Params        = InitParams(swap, refundTimeStampUtcInSec, (long)rewardForRedeemInMtz),
                        UseDefaultFee = true,
                        Type          = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment
                    });
                }
                else
                {
                    transactions.Add(new TezosTransaction
                    {
                        Currency      = xtz,
                        CreationTime  = DateTime.UtcNow,
                        From          = walletAddress.Address,
                        To            = xtz.SwapContractAddress,
                        Amount        = Math.Round(amountInMtz, 0),
                        Fee           = feeAmountInMtz,
                        GasLimit      = xtz.AddGasLimit,
                        StorageLimit  = xtz.AddStorageLimit,
                        UseDefaultFee = true,
                        Params        = AddParams(swap),
                        Type          = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment
                    });
                }

                if (isInitTx)
                {
                    isInitTx = false;
                }

                if (requiredAmountInMtz == 0)
                {
                    break;
                }
            }

            if (requiredAmountInMtz > 0)
            {
                Log.Warning("Insufficient funds (left {@requredAmount}).", requiredAmountInMtz);
                return(Enumerable.Empty <TezosTransaction>());
            }

            return(transactions);
        }
예제 #15
0
        protected virtual async Task <IEnumerable <EthereumTransaction> > CreatePaymentTxsAsync(
            Swap swap,
            int lockTimeInSeconds,
            CancellationToken cancellationToken = default)
        {
            var eth = Eth;

            Log.Debug("Create payment transactions for swap {@swapId}", swap.Id);

            var requiredAmountInEth     = AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price, eth.DigitsMultiplier);
            var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeInSeconds)).ToUnixTimeSeconds();
            var isInitTx             = true;
            var rewardForRedeemInEth = swap.PartyRewardForRedeem;

            var unspentAddresses = (await _account
                                    .GetUnspentAddressesAsync(cancellationToken)
                                    .ConfigureAwait(false))
                                   .ToList()
                                   .SortList(new AvailableBalanceAscending());

            var transactions = new List <EthereumTransaction>();

            foreach (var walletAddress in unspentAddresses)
            {
                Log.Debug("Create swap payment tx from address {@address} for swap {@swapId}", walletAddress.Address, swap.Id);

                var balanceInEth = (await _account
                                    .GetAddressBalanceAsync(
                                        address: walletAddress.Address,
                                        cancellationToken: cancellationToken)
                                    .ConfigureAwait(false))
                                   .Available;

                Log.Debug("Available balance: {@balance}", balanceInEth);

                var feeAmountInEth = isInitTx
                    ? rewardForRedeemInEth == 0
                        ? eth.InitiateFeeAmount
                        : eth.InitiateWithRewardFeeAmount
                    : eth.AddFeeAmount;

                var amountInEth = Math.Min(balanceInEth - feeAmountInEth, requiredAmountInEth);

                if (amountInEth <= 0)
                {
                    Log.Warning(
                        "Insufficient funds at {@address}. Balance: {@balance}, feeAmount: {@feeAmount}, result: {@result}.",
                        walletAddress.Address,
                        balanceInEth,
                        feeAmountInEth,
                        amountInEth);

                    continue;
                }

                requiredAmountInEth -= amountInEth;

                var nonceResult = await EthereumNonceManager.Instance
                                  .GetNonceAsync(eth, walletAddress.Address)
                                  .ConfigureAwait(false);

                if (nonceResult.HasError)
                {
                    Log.Error("Nonce getting error with code {@code} and description {@description}",
                              nonceResult.Error.Code,
                              nonceResult.Error.Description);

                    return(null);
                }

                TransactionInput txInput;

                if (isInitTx)
                {
                    var message = new InitiateFunctionMessage
                    {
                        HashedSecret    = swap.SecretHash,
                        Participant     = swap.PartyAddress,
                        RefundTimestamp = refundTimeStampUtcInSec,
                        AmountToSend    = Atomex.Ethereum.EthToWei(amountInEth),
                        FromAddress     = walletAddress.Address,
                        GasPrice        = Atomex.Ethereum.GweiToWei(eth.GasPriceInGwei),
                        Nonce           = nonceResult.Value,
                        RedeemFee       = Atomex.Ethereum.EthToWei(rewardForRedeemInEth)
                    };

                    var initiateGasLimit = rewardForRedeemInEth == 0
                        ? eth.InitiateGasLimit
                        : eth.InitiateWithRewardGasLimit;

                    message.Gas = await EstimateGasAsync(message, new BigInteger(initiateGasLimit))
                                  .ConfigureAwait(false);

                    txInput = message.CreateTransactionInput(eth.SwapContractAddress);
                }
                else
                {
                    var message = new AddFunctionMessage
                    {
                        HashedSecret = swap.SecretHash,
                        AmountToSend = Atomex.Ethereum.EthToWei(amountInEth),
                        FromAddress  = walletAddress.Address,
                        GasPrice     = Atomex.Ethereum.GweiToWei(Eth.GasPriceInGwei),
                        Nonce        = nonceResult.Value,
                    };

                    message.Gas = await EstimateGasAsync(message, new BigInteger(eth.AddGasLimit))
                                  .ConfigureAwait(false);

                    txInput = message.CreateTransactionInput(eth.SwapContractAddress);
                }

                transactions.Add(new EthereumTransaction(eth, txInput)
                {
                    Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment
                });

                if (isInitTx)
                {
                    isInitTx = false;
                }

                if (requiredAmountInEth == 0)
                {
                    break;
                }
            }

            if (requiredAmountInEth > 0)
            {
                Log.Warning("Insufficient funds (left {@requiredAmount}).", requiredAmountInEth);
                return(Enumerable.Empty <EthereumTransaction>());
            }

            return(transactions);
        }
예제 #16
0
        public async Task <IBitcoinBasedTransaction> CreateSwapPaymentTxAsync(
            BitcoinBasedCurrency currency,
            ClientSwap swap,
            IEnumerable <string> fromWallets,
            string refundAddress,
            string toAddress,
            DateTimeOffset lockTime,
            byte[] secretHash,
            int secretSize,
            ITxOutputSource outputsSource)
        {
            var availableOutputs = (await outputsSource
                                    .GetAvailableOutputsAsync(currency, fromWallets)
                                    .ConfigureAwait(false))
                                   .ToList();

            var fee         = 0L;
            var orderAmount = (long)(AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price) *
                                     currency.DigitsMultiplier);

            var requiredAmount = orderAmount + fee;

            long usedAmount;
            IList <ITxOutput>        usedOutputs;
            IBitcoinBasedTransaction tx;

            do
            {
                usedOutputs = availableOutputs
                              .SelectOutputsForAmount(requiredAmount)
                              .ToList();

                usedAmount = usedOutputs.Sum(o => o.Value);

                if (usedAmount < requiredAmount)
                {
                    throw new Exception($"Insufficient funds. Available {usedAmount}, required {requiredAmount}");
                }

                var estimatedSigSize = EstimateSigSize(usedOutputs);

                tx = currency.CreateHtlcP2PkhSwapPaymentTx(
                    unspentOutputs: usedOutputs,
                    aliceRefundAddress: refundAddress,
                    bobAddress: toAddress,
                    lockTime: lockTime,
                    secretHash: secretHash,
                    secretSize: secretSize,
                    amount: orderAmount,
                    fee: fee);

                var txSize = tx.VirtualSize();

                fee = (long)(currency.FeeRate * (txSize + estimatedSigSize));

                requiredAmount = orderAmount + fee;
            } while (usedAmount < requiredAmount);

            tx = currency.CreateHtlcP2PkhSwapPaymentTx(
                unspentOutputs: usedOutputs,
                aliceRefundAddress: refundAddress,
                bobAddress: toAddress,
                lockTime: lockTime,
                secretHash: secretHash,
                secretSize: secretSize,
                amount: orderAmount,
                fee: fee);

            return(tx);
        }
예제 #17
0
        public override async Task <bool> CheckCompletion()
        {
            try
            {
                Log.Debug("Ethereum: check initiated event");

                AttemptsCount++;
                if (AttemptsCount == MaxAttemptsCount)
                {
                    Log.Warning("Ethereum: maximum number of attempts to check initiated event reached");

                    CancelHandler?.Invoke(this);
                    return(true);
                }

                var side = Swap.Symbol
                           .OrderSideForBuyCurrency(Swap.PurchasedCurrency)
                           .Opposite();

                var requiredAmountInEth          = AmountHelper.QtyToAmount(side, Swap.Qty, Swap.Price);
                var requiredAmountInWei          = Atomix.Ethereum.EthToWei(requiredAmountInEth);
                var requiredRewardForRedeemInWei = Atomix.Ethereum.EthToWei(Swap.RewardForRedeem);

                var wsUri = Web3BlockchainApi.WsUriByChain(Eth.Chain);
                var web3  = new Web3(new WebSocketClient(wsUri));

                var contractAddress = Eth.SwapContractAddress;

                if (!Initiated)
                {
                    var eventHandlerInitiated = web3.Eth.GetEvent <InitiatedEventDTO>(contractAddress);

                    var filterIdInitiated = await eventHandlerInitiated
                                            .CreateFilterAsync(
                        Swap.SecretHash,
                        Swap.ToAddress)
                                            .ConfigureAwait(false);

                    var eventInitiated = await eventHandlerInitiated
                                         //.GetFilterChanges(filterId)
                                         .GetAllChanges(filterIdInitiated)
                                         .ConfigureAwait(false);

                    if (eventInitiated.Count == 0)
                    {
                        return(false);
                    }

                    Initiated = true;

                    if (eventInitiated[0].Event.Value >= requiredAmountInWei - requiredRewardForRedeemInWei)
                    {
                        if (Swap.IsAcceptor)
                        {
                            if (eventInitiated[0].Event.RedeemFee != requiredRewardForRedeemInWei)
                            {
                                Log.Debug(
                                    "Invalid redeem fee in initiated event. Expected value is {@expected}, actual is {@actual}",
                                    requiredRewardForRedeemInWei,
                                    (long)eventInitiated[0].Event.RedeemFee);

                                CancelHandler?.Invoke(this);
                                return(true);
                            }

                            if (eventInitiated[0].Event.RefundTimestamp != RefundTimestamp)
                            {
                                Log.Debug(
                                    "Invalid refund time in initiated event. Expected value is {@expected}, actual is {@actual}",
                                    RefundTimestamp,
                                    eventInitiated[0].Event.RefundTimestamp);

                                CancelHandler?.Invoke(this);
                                return(true);
                            }
                        }

                        CompleteHandler?.Invoke(this);
                        return(true);
                    }

                    Log.Debug(
                        "Eth value is not enough. Expected value is {@expected}. Actual value is {@actual}",
                        requiredAmountInWei - requiredRewardForRedeemInWei,
                        (long)eventInitiated[0].Event.Value);
                }

                if (Initiated)
                {
                    var eventHandlerAdded = web3.Eth.GetEvent <AddedEventDTO>(contractAddress);

                    var filterIdAdded = await eventHandlerAdded
                                        .CreateFilterAsync <byte[]>(Swap.SecretHash)
                                        .ConfigureAwait(false);

                    var eventsAdded = await eventHandlerAdded
                                      //.GetFilterChanges(filterId)
                                      .GetAllChanges(filterIdAdded)
                                      .ConfigureAwait(false);

                    if (eventsAdded.Count == 0)
                    {
                        return(false);
                    }

                    foreach (var @event in eventsAdded)
                    {
                        if (@event.Event.Value >= requiredAmountInWei - requiredRewardForRedeemInWei)
                        {
                            CompleteHandler?.Invoke(this);
                            return(true);
                        }

                        Log.Debug(
                            "Eth value is not enough. Expected value is {@expected}. Actual value is {@actual}",
                            requiredAmountInWei - requiredRewardForRedeemInWei,
                            (long)@event.Event.Value);
                    }
                }
            }
            catch (Exception e)
            {
                Log.Error(e, "Ethereum swap initiated control task error");
            }

            return(false);
        }
예제 #18
0
        protected override async Task <IEnumerable <EthereumTransaction> > CreatePaymentTxsAsync(
            Swap swap,
            int lockTimeInSeconds,
            CancellationToken cancellationToken = default)
        {
            var erc20 = Erc20;

            Log.Debug("Create payment transactions for swap {@swapId}", swap.Id);

            var requiredAmountInERC20   = AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price, erc20.DigitsMultiplier);
            var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeInSeconds)).ToUnixTimeSeconds();
            var isInitTx = true;
            var rewardForRedeemInERC20 = swap.PartyRewardForRedeem;

            var unspentAddresses = (await Erc20Account
                                    .GetUnspentAddressesAsync(cancellationToken)
                                    .ConfigureAwait(false))
                                   .ToList()
                                   .SortList((a, b) => a.AvailableBalance().CompareTo(b.AvailableBalance()));

            var transactions = new List <EthereumTransaction>();

            foreach (var walletAddress in unspentAddresses)
            {
                Log.Debug("Create swap payment tx from address {@address} for swap {@swapId}", walletAddress.Address, swap.Id);

                var balanceInEth = (await EthereumAccount
                                    .GetAddressBalanceAsync(
                                        address: walletAddress.Address,
                                        cancellationToken: cancellationToken)
                                    .ConfigureAwait(false))
                                   .Available;

                var balanceInERC20 = (await Erc20Account
                                      .GetAddressBalanceAsync(
                                          address: walletAddress.Address,
                                          cancellationToken: cancellationToken)
                                      .ConfigureAwait(false))
                                     .Available;

                Log.Debug("Available balance: {@balance}", balanceInERC20);

                var feeAmountInEth = (isInitTx
                    ? rewardForRedeemInERC20 == 0
                        ? erc20.InitiateFeeAmount
                        : erc20.InitiateWithRewardFeeAmount
                    : erc20.AddFeeAmount) + erc20.ApproveFeeAmount;

                if (balanceInEth - feeAmountInEth <= 0)
                {
                    Log.Warning(
                        "Insufficient funds at {@address}. Balance: {@balance}, feeAmount: {@feeAmount}, result: {@result}.",
                        walletAddress.Address,
                        balanceInEth,
                        feeAmountInEth,
                        balanceInEth - feeAmountInEth);

                    continue;
                }

                var amountInERC20 = requiredAmountInERC20 > 0
                    ? AmountHelper.DustProofMin(balanceInERC20, requiredAmountInERC20, erc20.DigitsMultiplier, erc20.DustDigitsMultiplier)
                    : 0;

                requiredAmountInERC20 -= amountInERC20;

                var nonceResult = await EthereumNonceManager.Instance
                                  .GetNonceAsync(erc20, walletAddress.Address)
                                  .ConfigureAwait(false);

                if (nonceResult.HasError)
                {
                    Log.Error("Nonce getting error with code {@code} and description {@description}",
                              nonceResult.Error.Code,
                              nonceResult.Error.Description);

                    return(null);
                }

                var nonce = nonceResult.Value;

                var allowanceMessage = new ERC20AllowanceFunctionMessage()
                {
                    Owner       = walletAddress.Address,
                    Spender     = erc20.SwapContractAddress,
                    FromAddress = walletAddress.Address
                };

                var allowance = await((IEthereumBlockchainApi)erc20.BlockchainApi)
                                .GetERC20AllowanceAsync(
                    erc20: erc20,
                    tokenAddress: erc20.ERC20ContractAddress,
                    allowanceMessage: allowanceMessage,
                    cancellationToken: cancellationToken)
                                .ConfigureAwait(false);

                if (allowance.Value > 0)
                {
                    transactions.Add(await CreateApproveTx(walletAddress, nonceResult.Value, 0)
                                     .ConfigureAwait(false));
                    nonce += 1;
                }
                else
                {
                    transactions.Add(new EthereumTransaction());
                }

                transactions.Add(await CreateApproveTx(walletAddress, nonce, erc20.TokensToTokenDigits(amountInERC20))
                                 .ConfigureAwait(false));
                nonce += 1;

                TransactionInput txInput;

                //actual transfer
                if (isInitTx)
                {
                    var initMessage = new ERC20InitiateFunctionMessage
                    {
                        HashedSecret    = swap.SecretHash,
                        ERC20Contract   = erc20.ERC20ContractAddress,
                        Participant     = swap.PartyAddress,
                        RefundTimestamp = refundTimeStampUtcInSec,
                        Countdown       = lockTimeInSeconds,
                        Value           = erc20.TokensToTokenDigits(amountInERC20),
                        RedeemFee       = erc20.TokensToTokenDigits(rewardForRedeemInERC20),
                        Active          = true,
                        FromAddress     = walletAddress.Address,
                        GasPrice        = Atomex.Ethereum.GweiToWei(erc20.GasPriceInGwei),
                        Nonce           = nonce
                    };

                    var initiateGasLimit = rewardForRedeemInERC20 == 0
                        ? erc20.InitiateGasLimit
                        : erc20.InitiateWithRewardGasLimit;

                    initMessage.Gas = await EstimateGasAsync(initMessage, new BigInteger(initiateGasLimit))
                                      .ConfigureAwait(false);

                    txInput = initMessage.CreateTransactionInput(erc20.SwapContractAddress);
                }
                else
                {
                    var addMessage = new ERC20AddFunctionMessage
                    {
                        HashedSecret = swap.SecretHash,
                        Value        = erc20.TokensToTokenDigits(amountInERC20),
                        FromAddress  = walletAddress.Address,
                        GasPrice     = Atomex.Ethereum.GweiToWei(erc20.GasPriceInGwei),
                        Nonce        = nonce
                    };

                    addMessage.Gas = await EstimateGasAsync(addMessage, new BigInteger(erc20.AddGasLimit))
                                     .ConfigureAwait(false);

                    txInput = addMessage.CreateTransactionInput(erc20.SwapContractAddress);
                }

                transactions.Add(new EthereumTransaction(erc20, txInput)
                {
                    Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment
                });

                if (isInitTx)
                {
                    isInitTx = false;
                }

                if (requiredAmountInERC20 <= 0)
                {
                    break;
                }
            }

            if (requiredAmountInERC20 > 0)
            {
                Log.Warning("Insufficient ERC20 or Eth funds (left {@requiredAmount}).", requiredAmountInERC20);
                return(Enumerable.Empty <EthereumTransaction>());
            }

            return(transactions);
        }
예제 #19
0
        protected override async Task <IEnumerable <TezosTransaction> > CreatePaymentTxsAsync(
            Swap swap,
            int lockTimeSeconds,
            CancellationToken cancellationToken = default)
        {
            Log.Debug("Create payment transactions for swap {@swapId}", swap.Id);

            var fa12    = Fa12;
            var fa12Api = fa12.BlockchainApi as ITokenBlockchainApi;

            var requiredAmountInTokens = AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price, fa12.DigitsMultiplier);

            var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeSeconds)).ToUnixTimeSeconds();
            var isInitTx = true;
            var rewardForRedeemInTokenDigits = swap.IsInitiator
                ? swap.PartyRewardForRedeem.ToTokenDigits(fa12.DigitsMultiplier)
                : 0;

            var unspentAddresses = (await Fa12Account
                                    .GetUnspentAddressesAsync(cancellationToken)
                                    .ConfigureAwait(false))
                                   .ToList()
                                   .SortList(new AvailableBalanceAscending());

            var transactions = new List <TezosTransaction>();

            foreach (var walletAddress in unspentAddresses)
            {
                Log.Debug("Create swap payment tx from address {@address} for swap {@swapId}",
                          walletAddress.Address,
                          swap.Id);

                var balanceInTz = (await TezosAccount
                                   .GetAddressBalanceAsync(
                                       address: walletAddress.Address,
                                       cancellationToken: cancellationToken)
                                   .ConfigureAwait(false))
                                  .Available;

                var balanceInTokens = (await Fa12Account
                                       .GetAddressBalanceAsync(
                                           address: walletAddress.Address,
                                           cancellationToken: cancellationToken)
                                       .ConfigureAwait(false))
                                      .Available;

                Log.Debug("Available balance: {@balance}", balanceInTokens);

                var balanceInMtz         = balanceInTz.ToMicroTez();
                var balanceInTokenDigits = balanceInTokens.ToTokenDigits(fa12.DigitsMultiplier);

                var isRevealed = await _account
                                 .IsRevealedSourceAsync(walletAddress.Address, cancellationToken)
                                 .ConfigureAwait(false);

                var feeAmountInMtz = fa12.ApproveFee * 2 +
                                     (isInitTx ? fa12.InitiateFee : fa12.AddFee) +
                                     (isRevealed ? 0 : fa12.RevealFee);

                var storageLimitInMtz = (fa12.ApproveStorageLimit * 2 +
                                         (isInitTx ? fa12.InitiateStorageLimit : fa12.AddStorageLimit)) *
                                        fa12.StorageFeeMultiplier;

                if (balanceInMtz - feeAmountInMtz - storageLimitInMtz - Xtz.MicroTezReserve <= 0)
                {
                    Log.Warning(
                        "Insufficient funds at {@address}. Balance: {@balance}, " +
                        "feeAmount: {@feeAmount}, storageLimit: {@storageLimit}.",
                        walletAddress.Address,
                        balanceInMtz,
                        feeAmountInMtz,
                        storageLimitInMtz);

                    continue;
                }

                var amountInTokens = requiredAmountInTokens > 0
                    ? AmountHelper.DustProofMin(balanceInTokens, requiredAmountInTokens, fa12.DigitsMultiplier, fa12.DustDigitsMultiplier)
                    : 0;

                if (amountInTokens == 0)
                {
                    break;
                }

                requiredAmountInTokens -= amountInTokens;

                using var callingAddressPublicKey = new SecureBytes((await Fa12Account.GetAddressAsync(walletAddress.Address)
                                                                     .ConfigureAwait(false))
                                                                    .PublicKeyBytes());

                var allowanceResult = await fa12Api
                                      .TryGetTokenAllowanceAsync(
                    holderAddress : walletAddress.Address,
                    spenderAddress : fa12.SwapContractAddress,
                    callingAddress : walletAddress.Address,
                    securePublicKey : callingAddressPublicKey,
                    cancellationToken : cancellationToken)
                                      .ConfigureAwait(false);

                if (allowanceResult.HasError)
                {
                    Log.Error("Error while getting token allowance for {@address} with code {@code} and description {@description}",
                              walletAddress.Address,
                              allowanceResult.Error.Code,
                              allowanceResult.Error.Description);

                    continue; // todo: maybe add approve 0
                }

                if (allowanceResult.Value > 0)
                {
                    transactions.Add(new TezosTransaction
                    {
                        Currency      = fa12,
                        CreationTime  = DateTime.UtcNow,
                        From          = walletAddress.Address,
                        To            = fa12.TokenContractAddress,
                        Fee           = fa12.ApproveFee,
                        GasLimit      = fa12.ApproveGasLimit,
                        StorageLimit  = fa12.ApproveStorageLimit,
                        Params        = ApproveParams(fa12.SwapContractAddress, 0),
                        UseDefaultFee = true,
                        Type          = BlockchainTransactionType.TokenApprove
                    });
                }

                transactions.Add(new TezosTransaction
                {
                    Currency      = fa12,
                    CreationTime  = DateTime.UtcNow,
                    From          = walletAddress.Address,
                    To            = fa12.TokenContractAddress,
                    Fee           = fa12.ApproveFee,
                    GasLimit      = fa12.ApproveGasLimit,
                    StorageLimit  = fa12.ApproveStorageLimit,
                    Params        = ApproveParams(fa12.SwapContractAddress, amountInTokens.ToTokenDigits(fa12.DigitsMultiplier)),
                    UseDefaultFee = true,
                    Type          = BlockchainTransactionType.TokenApprove
                });

                if (isInitTx)
                {
                    transactions.Add(new TezosTransaction
                    {
                        Currency      = fa12,
                        CreationTime  = DateTime.UtcNow,
                        From          = walletAddress.Address,
                        To            = fa12.SwapContractAddress,
                        Fee           = feeAmountInMtz,
                        GasLimit      = fa12.InitiateGasLimit,
                        StorageLimit  = fa12.InitiateStorageLimit,
                        Params        = InitParams(swap, fa12.TokenContractAddress, amountInTokens.ToTokenDigits(fa12.DigitsMultiplier), refundTimeStampUtcInSec, (long)rewardForRedeemInTokenDigits),
                        UseDefaultFee = true,
                        Type          = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment
                    });
                }
                //else
                //{
                //    transactions.Add(new TezosTransaction
                //    {
                //        Currency = Xtz,
                //        CreationTime = DateTime.UtcNow,
                //        From = walletAddress.Address,
                //        To = Xtz.SwapContractAddress,
                //        Fee = feeAmountInMtz,
                //        GasLimit = Xtz.AddGasLimit,
                //        StorageLimit = Xtz.AddStorageLimit,
                //        UseDefaultFee = true,
                //        Params = AddParams(swap),
                //        Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment
                //    });
                //}

                if (isInitTx)
                {
                    isInitTx = false;
                }

                if (requiredAmountInTokens <= 0)
                {
                    break;
                }
            }

            if (requiredAmountInTokens > 0)
            {
                Log.Warning("Insufficient funds (left {@requredAmount}).", requiredAmountInTokens);
                return(Enumerable.Empty <TezosTransaction>());
            }

            return(transactions);
        }
        public static async Task <Result <bool> > IsInitiatedAsync(
            Swap swap,
            Currency currency,
            long refundTimeStamp,
            CancellationToken cancellationToken = default)
        {
            try
            {
                Log.Debug("Ethereum: check initiated event");

                var ethereum = (Atomex.Ethereum)currency;

                var sideOpposite = swap.Symbol
                                   .OrderSideForBuyCurrency(swap.PurchasedCurrency)
                                   .Opposite();

                var requiredAmountInEth          = AmountHelper.QtyToAmount(sideOpposite, swap.Qty, swap.Price, ethereum.DigitsMultiplier);
                var requiredAmountInWei          = Atomex.Ethereum.EthToWei(requiredAmountInEth);
                var requiredRewardForRedeemInWei = Atomex.Ethereum.EthToWei(swap.RewardForRedeem);


                var api = new EtherScanApi(ethereum);

                var initiateEventsResult = await api
                                           .GetContractEventsAsync(
                    address : ethereum.SwapContractAddress,
                    fromBlock : ethereum.SwapContractBlockNumber,
                    toBlock : ulong.MaxValue,
                    topic0 : EventSignatureExtractor.GetSignatureHash <InitiatedEventDTO>(),
                    topic1 : "0x" + swap.SecretHash.ToHexString(),
                    topic2 : "0x000000000000000000000000" + swap.ToAddress.Substring(2),
                    cancellationToken : cancellationToken)
                                           .ConfigureAwait(false);

                if (initiateEventsResult == null)
                {
                    return(new Error(Errors.RequestError, $"Connection error while getting contract {ethereum.SwapContractAddress} initiate event"));
                }

                if (initiateEventsResult.HasError)
                {
                    return(initiateEventsResult.Error);
                }

                var events = initiateEventsResult.Value?.ToList();

                if (events == null || !events.Any())
                {
                    return(false);
                }

                var initiatedEvent = events.First().ParseInitiatedEvent();

                if (initiatedEvent.Value >= requiredAmountInWei - requiredRewardForRedeemInWei)
                {
                    if (initiatedEvent.RefundTimestamp != refundTimeStamp)
                    {
                        Log.Debug(
                            "Invalid refund time in initiated event. Expected value is {@expected}, actual is {@actual}",
                            refundTimeStamp,
                            (long)initiatedEvent.RefundTimestamp);

                        return(new Error(
                                   code: Errors.InvalidRefundLockTime,
                                   description: $"Invalid refund time in initiated event. Expected value is {refundTimeStamp}, actual is {(long)initiatedEvent.RefundTimestamp}"));
                    }

                    if (swap.IsAcceptor)
                    {
                        if (initiatedEvent.RedeemFee != requiredRewardForRedeemInWei)
                        {
                            Log.Debug(
                                "Invalid redeem fee in initiated event. Expected value is {@expected}, actual is {@actual}",
                                requiredRewardForRedeemInWei,
                                (long)initiatedEvent.RedeemFee);

                            return(new Error(
                                       code: Errors.InvalidRewardForRedeem,
                                       description: $"Invalid redeem fee in initiated event. Expected value is {requiredRewardForRedeemInWei}, actual is {(long)initiatedEvent.RedeemFee}"));
                        }
                    }

                    return(true);
                }

                Log.Debug(
                    "Eth value is not enough. Expected value is {@expected}. Actual value is {@actual}",
                    (decimal)(requiredAmountInWei - requiredRewardForRedeemInWei),
                    (decimal)initiatedEvent.Value);

                var addEventsResult = await api
                                      .GetContractEventsAsync(
                    address : ethereum.SwapContractAddress,
                    fromBlock : ethereum.SwapContractBlockNumber,
                    toBlock : ulong.MaxValue,
                    topic0 : EventSignatureExtractor.GetSignatureHash <AddedEventDTO>(),
                    topic1 : "0x" + swap.SecretHash.ToHexString(),
                    cancellationToken : cancellationToken)
                                      .ConfigureAwait(false);

                if (addEventsResult == null)
                {
                    return(new Error(Errors.RequestError, $"Connection error while getting contract {ethereum.SwapContractAddress} add event"));
                }

                if (addEventsResult.HasError)
                {
                    return(addEventsResult.Error);
                }

                events = addEventsResult.Value?.ToList();

                if (events == null || !events.Any())
                {
                    return(false);
                }

                foreach (var @event in events.Select(e => e.ParseAddedEvent()))
                {
                    if (@event.Value >= requiredAmountInWei - requiredRewardForRedeemInWei)
                    {
                        return(true);
                    }

                    Log.Debug(
                        "Eth value is not enough. Expected value is {@expected}. Actual value is {@actual}",
                        requiredAmountInWei - requiredRewardForRedeemInWei,
                        (long)@event.Value);
                }
            }
            catch (Exception e)
            {
                Log.Error(e, "Ethereum swap initiated control task error");

                return(new Error(Errors.InternalError, e.Message));
            }

            return(false);
        }
예제 #21
0
        public override async Task <bool> CheckCompletion()
        {
            try
            {
                Log.Debug("Tezos: check initiated event");

                AttemptsCount++;
                if (AttemptsCount == MaxAttemptsCount)
                {
                    Log.Warning("Tezos: maximum number of attempts to check initiated event reached");

                    CancelHandler?.Invoke(this);
                    return(true);
                }

                var side = Swap.Symbol
                           .OrderSideForBuyCurrency(Swap.PurchasedCurrency)
                           .Opposite();

                var requiredAmountInTz           = AmountHelper.QtyToAmount(side, Swap.Qty, Swap.Price);
                var requiredAmountInMtz          = requiredAmountInTz.ToMicroTez();
                var requiredRewardForRedeemInMtz = Swap.RewardForRedeem.ToMicroTez();

                var contractAddress = Xtz.SwapContractAddress;

                var api = (ITezosBlockchainApi)Xtz.BlockchainApi;

                var detectedAmountInMtz          = 0m;
                var detectedRedeemFeeAmountInMtz = 0m;

                for (var page = 0;; page++)
                {
                    var txs = (await api
                               .GetTransactionsAsync(contractAddress, page)
                               .ConfigureAwait(false))
                              .Cast <TezosTransaction>()
                              .ToList();

                    if (txs.Count == 0)
                    {
                        break;
                    }

                    foreach (var tx in txs)
                    {
                        if (tx.IsConfirmed() && tx.To == contractAddress)
                        {
                            var detectedPayment = false;

                            if (tx.IsSwapInit(RefundTimestamp, Swap.SecretHash, Swap.ToAddress))
                            {
                                // init payment to secret hash!
                                detectedPayment              = true;
                                detectedAmountInMtz         += tx.Amount;
                                detectedRedeemFeeAmountInMtz = tx.GetRedeemFee();
                            }
                            else if (tx.IsSwapAdd(Swap.SecretHash))
                            {
                                detectedPayment      = true;
                                detectedAmountInMtz += tx.Amount;
                            }

                            if (detectedPayment && detectedAmountInMtz >= requiredAmountInMtz)
                            {
                                if (Swap.IsAcceptor && detectedRedeemFeeAmountInMtz != requiredRewardForRedeemInMtz)
                                {
                                    CancelHandler?.Invoke(this);
                                    return(true);
                                }

                                CompleteHandler?.Invoke(this);
                                return(true);
                            }
                        }

                        var blockTimeUtc = tx.BlockInfo.BlockTime.ToUniversalTime();
                        var swapTimeUtc  = Swap.TimeStamp.ToUniversalTime();

                        if (blockTimeUtc < swapTimeUtc)
                        {
                            return(false);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Log.Error(e, "Tezos swap initiated control task error");
            }

            return(false);
        }
예제 #22
0
        public static async Task <Result <bool> > IsInitiatedAsync(
            Swap swap,
            Currency currency,
            long lockTimeInSec,
            CancellationToken cancellationToken = default)
        {
            try
            {
                Log.Debug("Ethereum ERC20: check initiated event");

                var erc20 = (EthereumTokens.ERC20)currency;

                var side = swap.Symbol
                           .OrderSideForBuyCurrency(swap.PurchasedCurrency)
                           .Opposite();

                var refundTimeStamp                   = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeInSec)).ToUnixTimeSeconds();
                var requiredAmountInERC20             = AmountHelper.QtyToAmount(side, swap.Qty, swap.Price, erc20.DigitsMultiplier);
                var requiredAmountInDecimals          = erc20.TokensToTokenDigits(requiredAmountInERC20);
                var receivedAmountInDecimals          = new BigInteger(0);
                var requiredRewardForRedeemInDecimals = swap.IsAcceptor
                    ? erc20.TokensToTokenDigits(swap.RewardForRedeem)
                    : 0;

                var api = new EtherScanApi(erc20);

                var initiateEventsResult = await api
                                           .GetContractEventsAsync(
                    address : erc20.SwapContractAddress,
                    fromBlock : erc20.SwapContractBlockNumber,
                    toBlock : ulong.MaxValue,
                    topic0 : EventSignatureExtractor.GetSignatureHash <ERC20InitiatedEventDTO>(),
                    topic1 : "0x" + swap.SecretHash.ToHexString(),
                    topic2 : "0x000000000000000000000000" + erc20.ERC20ContractAddress.Substring(2),     //??
                    topic3 : "0x000000000000000000000000" + swap.ToAddress.Substring(2),
                    cancellationToken : cancellationToken)
                                           .ConfigureAwait(false);

                if (initiateEventsResult == null)
                {
                    return(new Error(Errors.RequestError, $"Connection error while trying to get contract {erc20.SwapContractAddress} initiate event"));
                }

                if (initiateEventsResult.HasError)
                {
                    return(initiateEventsResult.Error);
                }

                var events = initiateEventsResult.Value?.ToList();

                if (events == null || !events.Any())
                {
                    return(false);
                }

                var contractInitEvent = events.Last();

                var initiatedEvent = contractInitEvent.ParseERC20InitiatedEvent();

                if (initiatedEvent.RefundTimestamp != refundTimeStamp)
                {
                    Log.Debug(
                        "Invalid refund time in initiated event. Expected value is {@expected}, actual is {@actual}",
                        refundTimeStamp,
                        (long)initiatedEvent.RefundTimestamp);

                    return(new Error(
                               code: Errors.InvalidRefundLockTime,
                               description: $"Invalid refund time in initiated event. Expected value is {refundTimeStamp}, actual is {(long)initiatedEvent.RefundTimestamp}"));
                }

                if (initiatedEvent.Countdown != lockTimeInSec)  //todo: use it
                {
                    Log.Debug(
                        "Invalid countdown in initiated event. Expected value is {@expected}, actual is {@actual}",
                        lockTimeInSec,
                        (long)initiatedEvent.Countdown);

                    return(new Error(
                               code: Errors.InvalidRewardForRedeem,
                               description: $"Invalid countdown in initiated event. Expected value is {lockTimeInSec}, actual is {(long)initiatedEvent.Countdown}"));
                }

                if (initiatedEvent.RedeemFee != requiredRewardForRedeemInDecimals)
                {
                    Log.Debug(
                        "Invalid redeem fee in initiated event. Expected value is {@expected}, actual is {@actual}",
                        requiredRewardForRedeemInDecimals,
                        (long)initiatedEvent.RedeemFee);

                    return(new Error(
                               code: Errors.InvalidRewardForRedeem,
                               description: $"Invalid redeem fee in initiated event. Expected value is {requiredRewardForRedeemInDecimals}, actual is {(long)initiatedEvent.RedeemFee}"));
                }

                if (!initiatedEvent.Active)
                {
                    Log.Debug(
                        "Invalid active value in initiated event. Expected value is {@expected}, actual is {@actual}",
                        true,
                        initiatedEvent.Active);

                    return(new Error(
                               code: Errors.InvalidRewardForRedeem,
                               description: $"Invalid active value in initiated event. Expected value is {true}, actual is {initiatedEvent.Active}"));
                }

                var erc20TransferValue = await GetTransferValue(
                    currency : currency,
                    from : initiatedEvent.Initiator.Substring(2),
                    to : erc20.SwapContractAddress.Substring(2),
                    blockNumber : contractInitEvent.HexBlockNumber,
                    cancellationToken : cancellationToken)
                                         .ConfigureAwait(false);

                if (erc20TransferValue != initiatedEvent.Value + initiatedEvent.RedeemFee)
                {
                    Log.Debug(
                        "Invalid transfer value in erc20 initiated event. Expected value is {@expected}, actual is {@actual}",
                        initiatedEvent.Value,
                        erc20TransferValue);

                    return(new Error(
                               code: Errors.InvalidSwapPaymentTx,
                               description: $"Invalid transfer value in erc20 initiated event. Expected value is {initiatedEvent.Value}, actual is {initiatedEvent.Active}"));
                }

                receivedAmountInDecimals = initiatedEvent.Value;

                if (receivedAmountInDecimals >= requiredAmountInDecimals - requiredRewardForRedeemInDecimals)
                {
                    return(true);
                }

                Log.Debug(
                    "Ethereum ERC20 value is not enough. Expected value is {@expected}. Actual value is {@actual}",
                    (decimal)(requiredAmountInDecimals - requiredRewardForRedeemInDecimals),
                    (decimal)initiatedEvent.Value);

                var addEventsResult = await api
                                      .GetContractEventsAsync(
                    address : erc20.SwapContractAddress,
                    fromBlock : erc20.SwapContractBlockNumber,
                    toBlock : ulong.MaxValue,
                    topic0 : EventSignatureExtractor.GetSignatureHash <ERC20AddedEventDTO>(),
                    topic1 : "0x" + swap.SecretHash.ToHexString(),
                    cancellationToken : cancellationToken)
                                      .ConfigureAwait(false);

                if (addEventsResult == null)
                {
                    return(new Error(Errors.RequestError, $"Connection error while trying to get contract {erc20.SwapContractAddress} add event"));
                }

                if (addEventsResult.HasError)
                {
                    return(addEventsResult.Error);
                }

                events = addEventsResult.Value?.ToList();

                if (events == null || !events.Any())
                {
                    return(false);
                }

                foreach (var @event in events.Select(e => e.ParseERC20AddedEvent()))
                {
                    erc20TransferValue = await GetTransferValue(
                        currency : currency,
                        from : @event.Initiator.Substring(2),
                        to : erc20.SwapContractAddress.Substring(2),
                        blockNumber : contractInitEvent.HexBlockNumber,
                        cancellationToken : cancellationToken)
                                         .ConfigureAwait(false);

                    if (erc20TransferValue != @event.Value - receivedAmountInDecimals)
                    {
                        Log.Debug(
                            "Invalid transfer value in added event. Expected value is {@expected}, actual is {@actual}",
                            @event.Value - receivedAmountInDecimals,
                            erc20TransferValue);

                        return(new Error(
                                   code: Errors.InvalidSwapPaymentTx,
                                   description: $"Invalid transfer value in initiated event. Expected value is {@event.Value - receivedAmountInDecimals}, actual is {erc20TransferValue}"));
                    }

                    receivedAmountInDecimals = @event.Value;

                    if (receivedAmountInDecimals >= requiredAmountInDecimals - requiredRewardForRedeemInDecimals)
                    {
                        return(true);
                    }

                    Log.Debug(
                        "Ethereum ERC20 value is not enough. Expected value is {@expected}. Actual value is {@actual}",
                        requiredAmountInDecimals - requiredRewardForRedeemInDecimals,
                        (long)@event.Value);
                }
            }
            catch (Exception e)
            {
                Log.Error(e, "Ethereum ERC20 swap initiated control task error");

                return(new Error(Errors.InternalError, e.Message));
            }

            return(false);
        }
예제 #23
0
        private async Task <IEnumerable <EthereumTransaction> > CreatePaymentTxsAsync(
            ClientSwap swap,
            int lockTimeInSeconds,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            Log.Debug("Create payment transactions for swap {@swapId}", swap.Id);

            var requiredAmountInEth     = AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price);
            var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeInSeconds)).ToUnixTimeSeconds();
            var isInitTx             = true;
            var rewardForRedeemInEth = swap.PartyRewardForRedeem;

            var unspentAddresses = (await Account
                                    .GetUnspentAddressesAsync(Eth, cancellationToken)
                                    .ConfigureAwait(false))
                                   .ToList()
                                   .SortList((a, b) => a.AvailableBalance().CompareTo(b.AvailableBalance()));

            var transactions = new List <EthereumTransaction>();

            foreach (var walletAddress in unspentAddresses)
            {
                Log.Debug("Create swap payment tx from address {@address} for swap {@swapId}", walletAddress.Address, swap.Id);

                var balanceInEth = (await Account
                                    .GetAddressBalanceAsync(
                                        currency: Eth,
                                        address: walletAddress.Address,
                                        cancellationToken: cancellationToken)
                                    .ConfigureAwait(false))
                                   .Available;

                Log.Debug("Available balance: {@balance}", balanceInEth);

                var feeAmountInEth = isInitTx
                    ? (rewardForRedeemInEth == 0
                        ? Eth.InitiateFeeAmount
                        : Eth.InitiateWithRewardFeeAmount)
                    : Eth.AddFeeAmount;

                var amountInEth = Math.Min(balanceInEth - feeAmountInEth, requiredAmountInEth);

                if (amountInEth <= 0)
                {
                    Log.Warning(
                        "Insufficient funds at {@address}. Balance: {@balance}, feeAmount: {@feeAmount}, result: {@result}.",
                        walletAddress.Address,
                        balanceInEth,
                        feeAmountInEth,
                        amountInEth);

                    continue;
                }

                requiredAmountInEth -= amountInEth;

                var nonce = await EthereumNonceManager.Instance
                            .GetNonce(Eth, walletAddress.Address)
                            .ConfigureAwait(false);

                TransactionInput txInput;

                if (isInitTx)
                {
                    var message = new InitiateFunctionMessage
                    {
                        HashedSecret    = swap.SecretHash,
                        Participant     = swap.PartyAddress,
                        RefundTimestamp = refundTimeStampUtcInSec,
                        AmountToSend    = Atomix.Ethereum.EthToWei(amountInEth),
                        FromAddress     = walletAddress.Address,
                        GasPrice        = Atomix.Ethereum.GweiToWei(Eth.GasPriceInGwei),
                        Nonce           = nonce,
                        RedeemFee       = Atomix.Ethereum.EthToWei(rewardForRedeemInEth)
                    };

                    var initiateGasLimit = rewardForRedeemInEth == 0
                        ? Eth.InitiateGasLimit
                        : Eth.InitiateWithRewardGasLimit;

                    message.Gas = await EstimateGasAsync(message, new BigInteger(initiateGasLimit))
                                  .ConfigureAwait(false);

                    txInput = message.CreateTransactionInput(Eth.SwapContractAddress);
                }
                else
                {
                    var message = new AddFunctionMessage
                    {
                        HashedSecret = swap.SecretHash,
                        AmountToSend = Atomix.Ethereum.EthToWei(amountInEth),
                        FromAddress  = walletAddress.Address,
                        GasPrice     = Atomix.Ethereum.GweiToWei(Eth.GasPriceInGwei),
                        Nonce        = nonce,
                    };

                    message.Gas = await EstimateGasAsync(message, new BigInteger(Eth.AddGasLimit))
                                  .ConfigureAwait(false);

                    txInput = message.CreateTransactionInput(Eth.SwapContractAddress);
                }

                transactions.Add(new EthereumTransaction(Eth, txInput)
                {
                    Type = EthereumTransaction.OutputTransaction
                });

                if (isInitTx)
                {
                    isInitTx = false;
                }

                if (requiredAmountInEth == 0)
                {
                    break;
                }
            }

            if (requiredAmountInEth > 0)
            {
                Log.Warning("Insufficient funds (left {@requredAmount}).", requiredAmountInEth);
                return(Enumerable.Empty <EthereumTransaction>());
            }

            return(transactions);
        }
예제 #24
0
        private async Task <Error> ConvertAsync()
        {
            try
            {
                var account         = App.Account;
                var currencyAccount = account
                                      .GetCurrencyAccount <ILegacyCurrencyAccount>(FromCurrency.Name);

                var fromWallets = (await currencyAccount
                                   .GetUnspentAddressesAsync(
                                       toAddress: null,
                                       amount: Amount,
                                       fee: 0,
                                       feePrice: await FromCurrency.GetDefaultFeePriceAsync(),
                                       feeUsagePolicy: FeeUsagePolicy.EstimatedFee,
                                       addressUsagePolicy: AddressUsagePolicy.UseMinimalBalanceFirst,
                                       transactionType: BlockchainTransactionType.SwapPayment))
                                  .ToList();

                foreach (var fromWallet in fromWallets)
                {
                    if (fromWallet.Currency != FromCurrency.Name)
                    {
                        fromWallet.Currency = FromCurrency.Name;
                    }
                }

                // check balances
                var errors = await BalanceChecker.CheckBalancesAsync(App.Account, fromWallets);

                if (errors.Any())
                {
                    return(new Error(Errors.SwapError, GetErrorsDescription(errors)));
                }

                if (Amount == 0)
                {
                    return(new Error(Errors.SwapError, Resources.CvZeroAmount));
                }

                if (Amount > 0 && !fromWallets.Any())
                {
                    return(new Error(Errors.SwapError, Resources.CvInsufficientFunds));
                }

                var symbol = App.SymbolsProvider
                             .GetSymbols(App.Account.Network)
                             .SymbolByCurrencies(FromCurrency, ToCurrency);

                var baseCurrency = App.Account.Currencies.GetByName(symbol.Base);
                var side         = symbol.OrderSideForBuyCurrency(ToCurrency);
                var terminal     = App.Terminal;
                var price        = EstimatedPrice;
                var orderPrice   = EstimatedOrderPrice;

                if (price == 0)
                {
                    return(new Error(Errors.NoLiquidity, Resources.CvNoLiquidity));
                }

                var qty = AmountHelper.AmountToQty(side, Amount, price, baseCurrency.DigitsMultiplier);

                if (qty < symbol.MinimumQty)
                {
                    var minimumAmount = AmountHelper.QtyToAmount(side, symbol.MinimumQty, price, FromCurrency.DigitsMultiplier);
                    var message       = string.Format(CultureInfo.InvariantCulture, Resources.CvMinimumAllowedQtyWarning, minimumAmount, FromCurrency.Name);

                    return(new Error(Errors.SwapError, message));
                }

                var order = new Order
                {
                    Symbol          = symbol.Name,
                    TimeStamp       = DateTime.UtcNow,
                    Price           = orderPrice,
                    Qty             = qty,
                    Side            = side,
                    Type            = OrderType.FillOrKill,
                    FromWallets     = fromWallets.ToList(),
                    MakerNetworkFee = EstimatedMakerNetworkFee
                };

                await order.CreateProofOfPossessionAsync(account);

                terminal.OrderSendAsync(order);

                // wait for swap confirmation
                var timeStamp = DateTime.UtcNow;

                while (DateTime.UtcNow < timeStamp + SwapTimeout)
                {
                    await Task.Delay(SwapCheckInterval);

                    var currentOrder = terminal.Account.GetOrderById(order.ClientOrderId);

                    if (currentOrder == null)
                    {
                        continue;
                    }

                    if (currentOrder.Status == OrderStatus.Pending)
                    {
                        continue;
                    }

                    if (currentOrder.Status == OrderStatus.PartiallyFilled || currentOrder.Status == OrderStatus.Filled)
                    {
                        var swap = (await terminal.Account
                                    .GetSwapsAsync())
                                   .FirstOrDefault(s => s.OrderId == currentOrder.Id);

                        if (swap == null)
                        {
                            continue;
                        }

                        return(null);
                    }

                    if (currentOrder.Status == OrderStatus.Canceled)
                    {
                        return(new Error(Errors.PriceHasChanged, Resources.SvPriceHasChanged));
                    }

                    if (currentOrder.Status == OrderStatus.Rejected)
                    {
                        return(new Error(Errors.OrderRejected, Resources.SvOrderRejected));
                    }
                }

                return(new Error(Errors.TimeoutReached, Resources.SvTimeoutReached));
            }
            catch (Exception e)
            {
                Log.Error(e, "Conversion error");

                return(new Error(Errors.SwapError, Resources.CvConversionError));
            }
        }
예제 #25
0
        private async Task <IEnumerable <TezosTransaction> > CreatePaymentTxsAsync(
            ClientSwap swap,
            int lockTimeSeconds,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            Log.Debug("Create payment transactions for swap {@swapId}", swap.Id);

            var requiredAmountInMtz = AmountHelper
                                      .QtyToAmount(swap.Side, swap.Qty, swap.Price)
                                      .ToMicroTez();

            var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeSeconds)).ToUnixTimeSeconds();
            var isInitTx             = true;
            var rewardForRedeemInMtz = swap.IsInitiator
                ? swap.PartyRewardForRedeem.ToMicroTez()
                : 0;

            var unspentAddresses = (await Account
                                    .GetUnspentAddressesAsync(Xtz, cancellationToken)
                                    .ConfigureAwait(false))
                                   .ToList()
                                   .SortList((a, b) => a.AvailableBalance().CompareTo(b.AvailableBalance()));

            var transactions = new List <TezosTransaction>();

            foreach (var walletAddress in unspentAddresses)
            {
                Log.Debug("Create swap payment tx from address {@address} for swap {@swapId}",
                          walletAddress.Address,
                          swap.Id);

                var balanceInTz = (await Account
                                   .GetAddressBalanceAsync(
                                       currency: Xtz,
                                       address: walletAddress.Address,
                                       cancellationToken: cancellationToken)
                                   .ConfigureAwait(false))
                                  .Available;

                Log.Debug("Available balance: {@balance}", balanceInTz);

                var balanceInMtz   = balanceInTz.ToMicroTez();
                var feeAmountInMtz = isInitTx
                    ? Xtz.InitiateFee + Xtz.InitiateStorageLimit
                    : Xtz.AddFee + Xtz.AddStorageLimit;

                var amountInMtz = Math.Min(balanceInMtz - feeAmountInMtz, requiredAmountInMtz);

                if (amountInMtz <= 0)
                {
                    Log.Warning(
                        "Insufficient funds at {@address}. Balance: {@balance}, " +
                        "feeAmount: {@feeAmount}, result: {@result}.",
                        walletAddress.Address,
                        balanceInMtz,
                        feeAmountInMtz,
                        amountInMtz);

                    continue;
                }

                requiredAmountInMtz -= amountInMtz;

                if (isInitTx)
                {
                    transactions.Add(new TezosTransaction(Xtz)
                    {
                        From         = walletAddress.Address,
                        To           = Xtz.SwapContractAddress,
                        Amount       = Math.Round(amountInMtz, 0),
                        Fee          = feeAmountInMtz,
                        GasLimit     = Xtz.InitiateGasLimit,
                        StorageLimit = Xtz.InitiateStorageLimit,
                        Params       = InitParams(swap, refundTimeStampUtcInSec, (long)rewardForRedeemInMtz),
                        Type         = TezosTransaction.OutputTransaction
                    });
                }
                else
                {
                    transactions.Add(new TezosTransaction(Xtz)
                    {
                        From         = walletAddress.Address,
                        To           = Xtz.SwapContractAddress,
                        Amount       = Math.Round(amountInMtz, 0),
                        Fee          = feeAmountInMtz,
                        GasLimit     = Xtz.AddGasLimit,
                        StorageLimit = Xtz.AddStorageLimit,
                        Params       = AddParams(swap),
                        Type         = TezosTransaction.OutputTransaction
                    });
                }

                if (isInitTx)
                {
                    isInitTx = false;
                }

                if (requiredAmountInMtz == 0)
                {
                    break;
                }
            }

            if (requiredAmountInMtz > 0)
            {
                Log.Warning("Insufficient funds (left {@requredAmount}).", requiredAmountInMtz);
                return(Enumerable.Empty <TezosTransaction>());
            }

            return(transactions);
        }
예제 #26
0
        public static async Task <Result <bool> > IsInitiatedAsync(
            Swap swap,
            Currency currency,
            long refundTimeStamp,
            CancellationToken cancellationToken = default)
        {
            try
            {
                Log.Debug("Tezos: check initiated event");

                var tezos = (Atomex.Tezos)currency;

                var side = swap.Symbol
                           .OrderSideForBuyCurrency(swap.PurchasedCurrency)
                           .Opposite();

                var requiredAmountInMtz = AmountHelper
                                          .QtyToAmount(side, swap.Qty, swap.Price, tezos.DigitsMultiplier)
                                          .ToMicroTez();

                var requiredRewardForRedeemInMtz = swap.RewardForRedeem.ToMicroTez();

                var contractAddress              = tezos.SwapContractAddress;
                var detectedAmountInMtz          = 0m;
                var detectedRedeemFeeAmountInMtz = 0m;

                var blockchainApi = (ITezosBlockchainApi)tezos.BlockchainApi;

                var txsResult = await blockchainApi
                                .TryGetTransactionsAsync(contractAddress, cancellationToken : cancellationToken)
                                .ConfigureAwait(false);

                if (txsResult == null)
                {
                    return(new Error(Errors.RequestError, $"Connection error while getting txs from contract {contractAddress}"));
                }

                if (txsResult.HasError)
                {
                    Log.Error("Error while get transactions from contract {@contract}. Code: {@code}. Description: {@desc}",
                              contractAddress,
                              txsResult.Error.Code,
                              txsResult.Error.Description);

                    return(txsResult.Error);
                }

                var txs = txsResult.Value
                          ?.Cast <TezosTransaction>()
                          .ToList();

                if (txs == null || !txs.Any())
                {
                    return(false);
                }

                foreach (var tx in txs)
                {
                    if (tx.IsConfirmed && tx.To == contractAddress)
                    {
                        var detectedPayment = false;

                        if (IsSwapInit(tx, refundTimeStamp, swap.SecretHash, swap.ToAddress))
                        {
                            // init payment to secret hash!
                            detectedPayment              = true;
                            detectedAmountInMtz         += tx.Amount;
                            detectedRedeemFeeAmountInMtz = GetRedeemFee(tx);
                        }
                        else if (IsSwapAdd(tx, swap.SecretHash))
                        {
                            detectedPayment      = true;
                            detectedAmountInMtz += tx.Amount;
                        }

                        if (detectedPayment && detectedAmountInMtz >= requiredAmountInMtz)
                        {
                            if (swap.IsAcceptor && detectedRedeemFeeAmountInMtz != requiredRewardForRedeemInMtz)
                            {
                                Log.Debug(
                                    "Invalid redeem fee in initiated event. Expected value is {@expected}, actual is {@actual}",
                                    requiredRewardForRedeemInMtz,
                                    detectedRedeemFeeAmountInMtz);

                                return(new Error(
                                           code: Errors.InvalidRewardForRedeem,
                                           description: $"Invalid redeem fee in initiated event. Expected value is {requiredRewardForRedeemInMtz}, actual is {detectedRedeemFeeAmountInMtz}"));
                            }

                            return(true);
                        }
                    }

                    if (tx.BlockInfo?.BlockTime == null)
                    {
                        continue;
                    }

                    var blockTimeUtc = tx.BlockInfo.BlockTime.Value.ToUniversalTime();
                    var swapTimeUtc  = swap.TimeStamp.ToUniversalTime();

                    if (blockTimeUtc < swapTimeUtc)
                    {
                        return(false);
                    }
                }
            }
            catch (Exception e)
            {
                Log.Error(e, "Tezos swap initiated control task error");

                return(new Error(Errors.InternalError, e.Message));
            }

            return(false);
        }
        public static bool TryVerifyPartyPaymentTx(
            IBitcoinBasedTransaction tx,
            Swap swap,
            ICurrencies currencies,
            byte[] secretHash,
            long refundLockTime,
            out Error error)
        {
            if (tx == null)
            {
                throw new ArgumentNullException(nameof(tx));
            }

            if (swap == null)
            {
                throw new ArgumentNullException(nameof(swap));
            }

            var currency = currencies.Get <BitcoinBasedCurrency>(swap.PurchasedCurrency);

            var partyRedeemScript = new Script(Convert.FromBase64String(swap.PartyRedeemScript));

            var targetAddressHash = new BitcoinPubKeyAddress(swap.ToAddress, currency.Network)
                                    .Hash
                                    .ToBytes();

            var hasSwapOutput = false;

            foreach (var txOutput in tx.Outputs)
            {
                try
                {
                    var output = (BitcoinBasedTxOutput)txOutput;

                    if (!output.IsPayToScriptHash(partyRedeemScript))
                    {
                        continue;
                    }

                    // check address
                    var outputTargetAddressHash = BitcoinBasedSwapTemplate.ExtractTargetPkhFromHtlcP2PkhSwapPayment(
                        script: partyRedeemScript);

                    if (!outputTargetAddressHash.SequenceEqual(targetAddressHash))
                    {
                        continue;
                    }

                    var outputSecretHash = BitcoinBasedSwapTemplate.ExtractSecretHashFromHtlcP2PkhSwapPayment(
                        script: partyRedeemScript);

                    if (!outputSecretHash.SequenceEqual(secretHash))
                    {
                        continue;
                    }

                    hasSwapOutput = true;

                    // check swap output refund lock time
                    var outputLockTime = BitcoinBasedSwapTemplate.ExtractLockTimeFromHtlcP2PkhSwapPayment(
                        script: partyRedeemScript);

                    var swapTimeUnix = (long)swap.TimeStamp.ToUniversalTime().ToUnixTime();

                    if (outputLockTime - swapTimeUnix < refundLockTime)
                    {
                        error = new Error(
                            code: Errors.InvalidRefundLockTime,
                            description: "Invalid refund time",
                            swap: swap);

                        return(false);
                    }

                    // check swap amount
                    var side = swap.Symbol
                               .OrderSideForBuyCurrency(currency.Name)
                               .Opposite();

                    var requiredAmount          = AmountHelper.QtyToAmount(side, swap.Qty, swap.Price, currency.DigitsMultiplier);
                    var requiredAmountInSatoshi = currency.CoinToSatoshi(requiredAmount);

                    if (output.Value < requiredAmountInSatoshi)
                    {
                        error = new Error(
                            code: Errors.InvalidSwapPaymentTxAmount,
                            description: "Invalid payment tx amount",
                            swap: swap);

                        return(false);
                    }
                }
                catch (Exception e)
                {
                    Log.Error(e, "Transaction verification error");

                    error = new Error(
                        code: Errors.TransactionVerificationError,
                        description: e.Message,
                        swap: swap);

                    return(false);
                }
            }

            if (!hasSwapOutput)
            {
                error = new Error(
                    code: Errors.TransactionVerificationError,
                    description: $"No swap outputs in tx @{tx.Id} for address {swap.ToAddress}",
                    swap: swap);

                return(false);
            }

            // todo: check fee
            // todo: try to verify

            error = null;
            return(true);
        }
        public async Task <(IBlockchainTransaction, byte[])> CreateSwapPaymentTxTest()
        {
            var bitcoinApi = new Mock <IInOutBlockchainApi>();

            bitcoinApi.Setup(a => a.GetUnspentOutputsAsync(It.IsAny <string>(), null, new CancellationToken()))
            .Returns(Task.FromResult(GetTestOutputs(Common.Alice.PubKey, NBitcoin.Network.TestNet)));

            var litecoinApi = new Mock <IInOutBlockchainApi>();

            litecoinApi.Setup(a => a.GetUnspentOutputsAsync(It.IsAny <string>(), null, new CancellationToken()))
            .Returns(Task.FromResult(GetTestOutputs(Common.Bob.PubKey, AltNetworkSets.Litecoin.Testnet)));

            var tempCurrencies = new Currencies(Common.CurrenciesConfiguration.GetSection(Atomex.Core.Network.TestNet.ToString()));

            var bitcoin = tempCurrencies.Get <Bitcoin>("BTC");

            bitcoin.BlockchainApi = bitcoinApi.Object;

            var litecoin = tempCurrencies.Get <Litecoin>("LTC");

            litecoin.BlockchainApi = litecoinApi.Object;

            var aliceBtcAddress = Common.Alice.PubKey
                                  .GetAddress(ScriptPubKeyType.Legacy, bitcoin.Network)
                                  .ToString();

            var bobBtcAddress = Common.Bob.PubKey
                                .GetAddress(ScriptPubKeyType.Legacy, bitcoin.Network)
                                .ToString();

            const decimal lastPrice = 0.000001m;
            const decimal lastQty   = 10m;

            var swap = new Swap
            {
                Symbol = "LTC/BTC",
                Side   = Side.Buy,
                Price  = lastPrice,
                Qty    = lastQty
            };

            var amountInSatoshi = bitcoin.CoinToSatoshi(AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price, bitcoin.DigitsMultiplier));

            var(tx, redeemScript) = await new BitcoinBasedSwapTransactionFactory()
                                    .CreateSwapPaymentTxAsync(
                currency: bitcoin,
                amount: amountInSatoshi,
                fromWallets: new [] { aliceBtcAddress },
                refundAddress: aliceBtcAddress,
                toAddress: bobBtcAddress,
                lockTime: DateTimeOffset.UtcNow.AddHours(1),
                secretHash: Common.SecretHash,
                secretSize: Common.Secret.Length,
                outputsSource: new BlockchainTxOutputSource(bitcoin))
                                    .ConfigureAwait(false);

            Assert.NotNull(tx);
            Assert.NotNull(redeemScript);

            return(tx, redeemScript);
        }