private async Task <IBitcoinBasedTransaction> CreatePaymentTxAsync( Swap swap, string refundAddress, DateTimeOffset lockTime) { var currency = Currencies.Get <BitcoinBasedConfig>(swap.SoldCurrency); Log.Debug("Create swap payment {@currency} tx for swap {@swapId}", currency.Name, swap.Id); var amountInSatoshi = currency.CoinToSatoshi( AmountHelper.QtyToSellAmount( swap.Side, swap.Qty, swap.Price, currency.DigitsMultiplier)); // maker network fee if (swap.MakerNetworkFee > 0) { var makerNetworkFeeInSatoshi = currency.CoinToSatoshi(swap.MakerNetworkFee); if (makerNetworkFeeInSatoshi < amountInSatoshi) // network fee size check { amountInSatoshi += makerNetworkFeeInSatoshi; } } var tx = await _transactionFactory .CreateSwapPaymentTxAsync( fromOutputs : swap.FromOutputs, amount : amountInSatoshi, refundAddress : refundAddress, toAddress : swap.PartyAddress, lockTime : lockTime, secretHash : swap.SecretHash, secretSize : DefaultSecretSize, currencyConfig : currency) .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); }
private decimal RequiredAmountInTokens(Swap swap, Erc20Config erc20) { var requiredAmountInERC20 = AmountHelper.QtyToSellAmount(swap.Side, swap.Qty, swap.Price, erc20.DigitsMultiplier); // maker network fee if (swap.MakerNetworkFee > 0 && swap.MakerNetworkFee < requiredAmountInERC20) // network fee size check { requiredAmountInERC20 += AmountHelper.RoundDown(swap.MakerNetworkFee, erc20.DigitsMultiplier); } return(requiredAmountInERC20); }
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 <BitcoinBasedConfig>(Currency); var amountInSatoshi = currency.CoinToSatoshi( AmountHelper.QtyToSellAmount( 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, currency : currency, 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); }
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 <BitcoinBasedConfig>(Currency); var amountInSatoshi = currency.CoinToSatoshi( AmountHelper.QtyToSellAmount( swap.Side, swap.Qty, swap.Price, currency.DigitsMultiplier)); var tx = await _transactionFactory .CreateSwapRefundTxAsync( paymentTx : paymentTx, amount : amountInSatoshi, refundAddress : refundAddress, redeemScript : redeemScript, lockTime : lockTime, currency : currency) .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); }
protected virtual async Task <EthereumTransaction> CreatePaymentTxAsync( Swap swap, int lockTimeInSeconds, CancellationToken cancellationToken = default) { var ethConfig = EthConfig; Log.Debug("Create payment transaction from address {@address} for swap {@swapId}", swap.FromAddress, swap.Id); var requiredAmountInEth = AmountHelper.QtyToSellAmount(swap.Side, swap.Qty, swap.Price, ethConfig.DigitsMultiplier); // maker network fee if (swap.MakerNetworkFee > 0 && swap.MakerNetworkFee < requiredAmountInEth) // network fee size check { requiredAmountInEth += AmountHelper.RoundDown(swap.MakerNetworkFee, ethConfig.DigitsMultiplier); } var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeInSeconds)).ToUnixTimeSeconds(); var rewardForRedeemInEth = swap.PartyRewardForRedeem; var walletAddress = await _account .GetAddressAsync(swap.FromAddress, cancellationToken) .ConfigureAwait(false); var gasPrice = await ethConfig .GetGasPriceAsync(cancellationToken) .ConfigureAwait(false); var balanceInEth = walletAddress.Balance; Log.Debug("Available balance: {@balance}", balanceInEth); var feeAmountInEth = rewardForRedeemInEth == 0 ? ethConfig.InitiateFeeAmount(gasPrice) : ethConfig.InitiateWithRewardFeeAmount(gasPrice); if (balanceInEth < feeAmountInEth + requiredAmountInEth) { Log.Warning( "Insufficient funds at {@address}. Balance: {@balance}, required: {@required}, " + "feeAmount: {@feeAmount}, missing: {@result}.", walletAddress.Address, balanceInEth, requiredAmountInEth, feeAmountInEth, balanceInEth - feeAmountInEth - requiredAmountInEth); return(null); } var nonceResult = await((IEthereumBlockchainApi)ethConfig.BlockchainApi) .GetTransactionCountAsync(walletAddress.Address, pending: false, cancellationToken) .ConfigureAwait(false); if (nonceResult.HasError) { Log.Error($"Getting nonce error: {nonceResult.Error.Description}"); return(null); } TransactionInput txInput; var message = new InitiateFunctionMessage { HashedSecret = swap.SecretHash, Participant = swap.PartyAddress, RefundTimestamp = refundTimeStampUtcInSec, AmountToSend = EthereumConfig.EthToWei(requiredAmountInEth), FromAddress = walletAddress.Address, GasPrice = EthereumConfig.GweiToWei(gasPrice), Nonce = nonceResult.Value, RedeemFee = EthereumConfig.EthToWei(rewardForRedeemInEth) }; var initiateGasLimit = rewardForRedeemInEth == 0 ? ethConfig.InitiateGasLimit : ethConfig.InitiateWithRewardGasLimit; message.Gas = await EstimateGasAsync(message, new BigInteger(initiateGasLimit)) .ConfigureAwait(false); txInput = message.CreateTransactionInput(ethConfig.SwapContractAddress); return(new EthereumTransaction(ethConfig.Name, txInput) { Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment }); }
public static Task StartSwapSpentControlAsync( Swap swap, CurrencyConfig currency, DateTime refundTimeUtc, TimeSpan interval, Func <Swap, ITxPoint, CancellationToken, Task> completionHandler = null, Func <Swap, CancellationToken, Task> refundTimeReachedHandler = null, CancellationToken cancellationToken = default) { return(Task.Run(async() => { try { var bitcoinBased = (BitcoinBasedConfig)currency; var side = swap.Symbol .OrderSideForBuyCurrency(swap.PurchasedCurrency); var requiredAmount = AmountHelper.QtyToSellAmount(side, swap.Qty, swap.Price, bitcoinBased.DigitsMultiplier); var requiredAmountInSatoshi = bitcoinBased.CoinToSatoshi(requiredAmount); var lockTimeInSeconds = swap.IsInitiator ? CurrencySwap.DefaultInitiatorLockTimeInSeconds : CurrencySwap.DefaultAcceptorLockTimeInSeconds; var refundTimeUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeInSeconds)) .ToUnixTimeSeconds(); var redeemScript = swap.RefundAddress == null && swap.RedeemScript != null ? new Script(Convert.FromBase64String(swap.RedeemScript)) : BitcoinBasedSwapTemplate .GenerateHtlcP2PkhSwapPayment( aliceRefundAddress: swap.RefundAddress, bobAddress: swap.PartyAddress, lockTimeStamp: refundTimeUtcInSec, secretHash: swap.SecretHash, secretSize: CurrencySwap.DefaultSecretSize, expectedNetwork: bitcoinBased.Network); var swapOutput = ((IBitcoinBasedTransaction)swap.PaymentTx) .Outputs .Cast <BitcoinBasedTxOutput>() .FirstOrDefault(o => o.IsPayToScriptHash(redeemScript) && o.Value >= requiredAmountInSatoshi); if (swapOutput == null) { throw new InternalException( code: Errors.SwapError, description: "Payment tx have not swap output"); } while (!cancellationToken.IsCancellationRequested) { Log.Debug("Output spent control for {@currency} swap {@swapId}", currency.Name, swap.Id); var result = await currency .GetSpentPointAsync( hash: swap.PaymentTxId, index: swapOutput.Index, cancellationToken: cancellationToken) .ConfigureAwait(false); if (result != null && !result.HasError) { if (result.Value != null) { await completionHandler.Invoke(swap, result.Value, cancellationToken) .ConfigureAwait(false); break; } } if (DateTime.UtcNow >= refundTimeUtc) { await refundTimeReachedHandler.Invoke(swap, cancellationToken) .ConfigureAwait(false); break; } await Task.Delay(interval, cancellationToken) .ConfigureAwait(false); } } catch (OperationCanceledException) { Log.Debug("StartSwapSpentControlAsync canceled."); } catch (Exception e) { Log.Error(e, "StartSwapSpentControlAsync error"); } }, cancellationToken)); }
public static async Task <Result <bool> > IsInitiatedAsync( Swap swap, CurrencyConfig currency, TezosConfig tezos, long refundTimeStamp, CancellationToken cancellationToken = default) { try { Log.Debug("Tezos FA12: check initiated event"); var fa12 = (Fa12Config)currency; var side = swap.Symbol .OrderSideForBuyCurrency(swap.PurchasedCurrency) .Opposite(); var requiredAmountInTokenDigits = AmountHelper .QtyToSellAmount(side, swap.Qty, swap.Price, fa12.DigitsMultiplier) .ToTokenDigits(fa12.DigitsMultiplier); var requiredRewardForRedeemInTokenDigits = swap.IsAcceptor ? swap.RewardForRedeem.ToTokenDigits(fa12.DigitsMultiplier) : 0; var contractAddress = fa12.SwapContractAddress; var detectedAmountInTokenDigits = 0m; var detectedRedeemFeeAmountInTokenDigits = 0m; long detectedRefundTimestamp = 0; 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, swap.SecretHash.ToHexString(), fa12.TokenContractAddress, swap.ToAddress, refundTimeStamp)) { // init payment to secret hash! detectedPayment = true; detectedAmountInTokenDigits += GetAmount(tx); detectedRedeemFeeAmountInTokenDigits = GetRedeemFee(tx); detectedRefundTimestamp = GetRefundTimestamp(tx); } ///not implemented //else if (IsSwapAdd(tx, swap.SecretHash)) //{ // detectedPayment = true; // detectedAmountInMtz += tx.Amount; //} if (detectedPayment && detectedAmountInTokenDigits >= requiredAmountInTokenDigits) { if (swap.IsAcceptor && detectedRedeemFeeAmountInTokenDigits != requiredRewardForRedeemInTokenDigits) { Log.Debug( "Invalid redeem fee in initiated event. Expected value is {@expected}, actual is {@actual}", requiredRewardForRedeemInTokenDigits, detectedRedeemFeeAmountInTokenDigits); return(new Error( code: Errors.InvalidRewardForRedeem, description: $"Invalid redeem fee in initiated event. Expected value is {requiredRewardForRedeemInTokenDigits}, actual is {detectedRedeemFeeAmountInTokenDigits}")); } if (detectedRefundTimestamp != refundTimeStamp) { Log.Debug( "Invalid refund timestamp in initiated event. Expected value is {@expected}, actual is {@actual}", refundTimeStamp, detectedRefundTimestamp); return(new Error( code: Errors.InvalidRewardForRedeem, description: $"Invalid refund timestamp in initiated event. Expected value is {refundTimeStamp}, actual is {detectedRefundTimestamp}")); } return(true); // todo: check also token contract transfers } } 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 token swap initiated control task error"); return(new Error(Errors.InternalError, e.Message)); } return(false); }
protected virtual async Task <TezosTransaction> CreatePaymentTxAsync( Swap swap, int lockTimeSeconds, CancellationToken cancellationToken = default) { var xtzConfig = XtzConfig; Log.Debug("Create {@currency} payment transaction from address {@address} for swap {@swapId}", Currency, swap.FromAddress, swap.Id); var requiredAmountInMtz = AmountHelper .QtyToSellAmount(swap.Side, swap.Qty, swap.Price, xtzConfig.DigitsMultiplier) .ToMicroTez(); // maker network fee if (swap.MakerNetworkFee > 0) { var makerNetworkFeeInMtz = swap.MakerNetworkFee.ToMicroTez(); if (makerNetworkFeeInMtz < requiredAmountInMtz) // network fee size check { requiredAmountInMtz += makerNetworkFeeInMtz; } } var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeSeconds)).ToUnixTimeSeconds(); var rewardForRedeemInMtz = swap.IsInitiator ? swap.PartyRewardForRedeem.ToMicroTez() : 0; var walletAddress = await _account .GetAddressAsync(swap.FromAddress, cancellationToken) .ConfigureAwait(false); Log.Debug("Available balance: {@balance}", walletAddress.Balance); var balanceInMtz = walletAddress.Balance.ToMicroTez(); var isRevealed = await _account .IsRevealedSourceAsync(walletAddress.Address, cancellationToken) .ConfigureAwait(false); var feeAmountInMtz = xtzConfig.InitiateFee + (isRevealed ? 0 : xtzConfig.RevealFee); var storageLimitInMtz = xtzConfig.InitiateStorageLimit * xtzConfig.StorageFeeMultiplier; if (balanceInMtz < feeAmountInMtz + storageLimitInMtz + requiredAmountInMtz) { Log.Error( "Insufficient funds at {@address}. Balance: {@balance}, required: {@required}, " + "feeAmount: {@feeAmount}, storageLimit: {@storageLimit}, missing: {@result}.", walletAddress.Address, balanceInMtz, requiredAmountInMtz, feeAmountInMtz, storageLimitInMtz, balanceInMtz - feeAmountInMtz - storageLimitInMtz - requiredAmountInMtz); return(null); } return(new TezosTransaction { Currency = xtzConfig.Name, CreationTime = DateTime.UtcNow, From = walletAddress.Address, To = xtzConfig.SwapContractAddress, Amount = Math.Round(requiredAmountInMtz, 0), Fee = feeAmountInMtz, GasLimit = xtzConfig.InitiateGasLimit, StorageLimit = xtzConfig.InitiateStorageLimit, Params = CreateInitParams(swap, refundTimeStampUtcInSec, (long) rewardForRedeemInMtz), Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment, UseRun = true, UseSafeStorageLimit = true, UseOfflineCounter = true }); }
public async Task <IBlockchainTransaction> 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 CurrenciesProvider(Common.CurrenciesConfigurationString) .GetCurrencies(Atomex.Core.Network.TestNet); var bitcoin = tempCurrencies.Get <BitcoinConfig>("BTC"); bitcoin.BlockchainApi = bitcoinApi.Object; var litecoin = tempCurrencies.Get <LitecoinConfig>("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.QtyToSellAmount(swap.Side, swap.Qty, swap.Price, bitcoin.DigitsMultiplier)); var outputs = (await new BlockchainTxOutputSource(bitcoin) .GetAvailableOutputsAsync(new[] { aliceBtcAddress })) .Cast <BitcoinBasedTxOutput>(); var tx = await new BitcoinBasedSwapTransactionFactory() .CreateSwapPaymentTxAsync( fromOutputs: outputs, amount: amountInSatoshi, refundAddress: aliceBtcAddress, toAddress: bobBtcAddress, lockTime: DateTimeOffset.UtcNow.AddHours(1), secretHash: Common.SecretHash, secretSize: Common.Secret.Length, currencyConfig: bitcoin) .ConfigureAwait(false); Assert.NotNull(tx); //Assert.NotNull(redeemScript); return(tx); }
public static async Task <Result <bool> > IsInitiatedAsync( Swap swap, CurrencyConfig currency, long refundTimeStamp, CancellationToken cancellationToken = default) { try { Log.Debug("Ethereum: check initiated event"); var ethereum = (Atomex.EthereumConfig)currency; var sideOpposite = swap.Symbol .OrderSideForBuyCurrency(swap.PurchasedCurrency) .Opposite(); var requiredAmountInEth = AmountHelper.QtyToSellAmount(sideOpposite, swap.Qty, swap.Price, ethereum.DigitsMultiplier); var requiredAmountInWei = Atomex.EthereumConfig.EthToWei(requiredAmountInEth); var requiredRewardForRedeemInWei = Atomex.EthereumConfig.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 static async Task <Result <bool> > IsInitiatedAsync( Swap swap, CurrencyConfig currency, long lockTimeInSec, CancellationToken cancellationToken = default) { try { Log.Debug("Ethereum ERC20: check initiated event"); var erc20 = (Erc20Config)currency; var side = swap.Symbol .OrderSideForBuyCurrency(swap.PurchasedCurrency) .Opposite(); var refundTimeStamp = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeInSec)).ToUnixTimeSeconds(); var requiredAmountInERC20 = AmountHelper.QtyToSellAmount(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 erc20TransferValues = await GetTransferValuesAsync( currency : currency, from : initiatedEvent.Initiator.Substring(2), to : erc20.SwapContractAddress.Substring(2), blockNumber : contractInitEvent.HexBlockNumber, cancellationToken : cancellationToken) .ConfigureAwait(false); if (!erc20TransferValues.Contains(initiatedEvent.Value + initiatedEvent.RedeemFee)) { var actualTransferValue = string.Join(", ", erc20TransferValues.Select(v => v.ToString())); Log.Debug( "Invalid transfer value in erc20 initiated event. Expected value is {@expected}, actual is {@actual}", initiatedEvent.Value.ToString(), actualTransferValue); return(new Error( code: Errors.InvalidSwapPaymentTx, description: $"Invalid transfer value in erc20 initiated event. Expected value is {initiatedEvent.Value}, actual is {actualTransferValue}")); } 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())) { erc20TransferValues = await GetTransferValuesAsync( currency : currency, from : @event.Initiator.Substring(2), to : erc20.SwapContractAddress.Substring(2), blockNumber : contractInitEvent.HexBlockNumber, cancellationToken : cancellationToken) .ConfigureAwait(false); if (!erc20TransferValues.Contains(@event.Value - receivedAmountInDecimals)) { var actualTransferValue = string.Join(", ", erc20TransferValues.Select(v => v.ToString())); Log.Debug( "Invalid transfer value in added event. Expected value is {@expected}, actual is {@actual}", (@event.Value - receivedAmountInDecimals).ToString(), actualTransferValue); return(new Error( code: Errors.InvalidSwapPaymentTx, description: $"Invalid transfer value in initiated event. Expected value is {@event.Value - receivedAmountInDecimals}, actual is {actualTransferValue}")); } 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); }
public static async Task <Result <IBlockchainTransaction> > TryToFindPaymentAsync( Swap swap, CurrencyConfig currency, Side side, string toAddress, string refundAddress, long refundTimeStamp, string redeemScriptBase64 = null, CancellationToken cancellationToken = default) { try { Log.Debug("BitcoinBased: try to find payment tx"); var bitcoinBased = (BitcoinBasedConfig)currency; var requiredAmount = AmountHelper.QtyToSellAmount(side, swap.Qty, swap.Price, bitcoinBased.DigitsMultiplier); var requiredAmountInSatoshi = bitcoinBased.CoinToSatoshi(requiredAmount); var redeemScript = refundAddress == null && redeemScriptBase64 != null ? new Script(Convert.FromBase64String(redeemScriptBase64)) : BitcoinBasedSwapTemplate .GenerateHtlcP2PkhSwapPayment( aliceRefundAddress: refundAddress, bobAddress: toAddress, lockTimeStamp: refundTimeStamp, secretHash: swap.SecretHash, secretSize: CurrencySwap.DefaultSecretSize, expectedNetwork: bitcoinBased.Network); var redeemScriptAddress = redeemScript .PaymentScript .GetDestinationAddress(bitcoinBased.Network) .ToString(); var api = bitcoinBased.BlockchainApi as BitcoinBasedBlockchainApi; var outputsResult = await api .GetOutputsAsync(redeemScriptAddress, null, cancellationToken) .ConfigureAwait(false); if (outputsResult == null) { return(new Error(Errors.RequestError, $"Connection error while getting outputs for {redeemScriptAddress} address")); } if (outputsResult.HasError) { return(outputsResult.Error); } foreach (var output in outputsResult.Value) { var o = output as BitcoinBasedTxOutput; var outputScriptHex = o.Coin.TxOut.ScriptPubKey.ToHex(); if (redeemScript.PaymentScript.ToHex() != outputScriptHex) { continue; } if (o.Value < requiredAmountInSatoshi) { continue; } var txResult = await api .GetTransactionAsync(o.TxId, cancellationToken) .ConfigureAwait(false); if (txResult == null) { return(new Error(Errors.RequestError, $"Connection error while getting tx {o.TxId}")); } if (txResult.HasError) { return(txResult.Error); } if (txResult.Value == null) { continue; } return(txResult.Value as BitcoinBasedTransaction); } return(new Result <IBlockchainTransaction>((IBitcoinBasedTransaction)null)); } catch (Exception e) { Log.Error(e, "BitcoinBased swap initiated control task error"); return(new Error(Errors.InternalError, e.Message)); } }