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); }
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); } }
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)); }
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); }
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); }
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); }
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)); }
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); }
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); }
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); }
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); }
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); }
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); }
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); }
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); }
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); }
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); }
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)); } }
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); }
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); }