private async Task BroadcastTxAsync( Swap swap, EthereumTransaction tx, CancellationToken cancellationToken = default, bool updateBalance = true, bool notifyIfUnconfirmed = true, bool notifyIfBalanceUpdated = true) { var broadcastResult = await Erc20.BlockchainApi .BroadcastAsync(tx, cancellationToken) .ConfigureAwait(false); if (broadcastResult.HasError) { throw new InternalException(broadcastResult.Error); } var txId = broadcastResult.Value; if (txId == null) { throw new Exception("Transaction Id is null"); } Log.Debug("TxId {@id} for swap {@swapId}", txId, swap.Id); if (tx.Type.HasFlag(BlockchainTransactionType.SwapPayment)) { tx = tx.ParseERC20Input(); } // account new unconfirmed transaction await Erc20Account .UpsertTransactionAsync( tx : tx, updateBalance : updateBalance, notifyIfUnconfirmed : notifyIfUnconfirmed, notifyIfBalanceUpdated : notifyIfBalanceUpdated, cancellationToken : cancellationToken) .ConfigureAwait(false); var ethTx = tx.Clone(); ethTx.Currency = Eth; ethTx.Amount = 0; ethTx.Type = BlockchainTransactionType.Output | (ethTx.Type.HasFlag(BlockchainTransactionType.TokenApprove) ? BlockchainTransactionType.TokenCall : BlockchainTransactionType.SwapCall); await EthereumAccount .UpsertTransactionAsync( tx : ethTx, updateBalance : updateBalance, notifyIfUnconfirmed : notifyIfUnconfirmed, notifyIfBalanceUpdated : notifyIfBalanceUpdated, cancellationToken : cancellationToken) .ConfigureAwait(false); // todo: transaction receipt status control }
public Erc20Swap( Erc20Account account, EthereumAccount ethereumAccount, ICurrencies currencies) : base(account.Currency, currencies) { Erc20Account = account ?? throw new ArgumentNullException(nameof(account)); EthereumAccount = ethereumAccount ?? throw new ArgumentNullException(nameof(ethereumAccount)); }
private async Task <bool> SignTransactionAsync( EthereumTransaction tx, CancellationToken cancellationToken = default) { var walletAddress = await Erc20Account .GetAddressAsync( address : tx.From, cancellationToken : cancellationToken) .ConfigureAwait(false); return(await Erc20Account.Wallet .SignAsync( tx : tx, address : walletAddress, cancellationToken : cancellationToken) .ConfigureAwait(false)); }
private async void RedeemBySomeoneCanceledEventHandler( Swap swap, DateTime refundTimeUtc, CancellationToken cancellationToken) { Log.Debug("Handle redeem party control canceled event for swap {@swapId}", swap.Id); try { if (swap.Secret?.Length > 0) { var walletAddress = (await Erc20Account .GetUnspentAddressesAsync( toAddress: swap.ToAddress, amount: 0, fee: 0, feePrice: 0, feeUsagePolicy: FeeUsagePolicy.EstimatedFee, addressUsagePolicy: AddressUsagePolicy.UseOnlyOneAddress, transactionType: BlockchainTransactionType.SwapRedeem, cancellationToken: cancellationToken) .ConfigureAwait(false)) .FirstOrDefault(); if (walletAddress == null) //todo: make some panic here { Log.Error( "Counter counterParty redeem need to be made for swap {@swapId}, using secret {@Secret}", swap.Id, Convert.ToBase64String(swap.Secret)); return; } await RedeemAsync(swap, cancellationToken) .ConfigureAwait(false); } } catch (Exception e) { Log.Error(e, "Redeem party control canceled event error"); } }
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); }
public override async Task RefundAsync( Swap swap, CancellationToken cancellationToken = default) { var erc20 = Erc20; if (swap.StateFlags.HasFlag(SwapStateFlags.IsRefundBroadcast)) { TrackTransactionConfirmationAsync( swap: swap, currency: erc20, txId: swap.RefundTx.Id, confirmationHandler: RefundConfirmedEventHandler, cancellationToken: cancellationToken) .FireAndForget(); return; } Log.Debug("Create refund for swap {@swap}", swap.Id); var walletAddress = (await Erc20Account .GetUnspentAddressesAsync( toAddress: null, // get refund address amount: 0, fee: 0, feePrice: 0, feeUsagePolicy: FeeUsagePolicy.EstimatedFee, addressUsagePolicy: AddressUsagePolicy.UseOnlyOneAddress, transactionType: BlockchainTransactionType.SwapRefund, cancellationToken: cancellationToken) .ConfigureAwait(false)) .FirstOrDefault(); if (walletAddress == null) { Log.Error("Insufficient funds for refund"); return; } 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; } var message = new ERC20RefundFunctionMessage { FromAddress = walletAddress.Address, HashedSecret = swap.SecretHash, GasPrice = Atomex.Ethereum.GweiToWei(erc20.GasPriceInGwei), Nonce = nonceResult.Value, }; message.Gas = await EstimateGasAsync(message, new BigInteger(erc20.RefundGasLimit)) .ConfigureAwait(false); var txInput = message.CreateTransactionInput(erc20.SwapContractAddress); var refundTx = new EthereumTransaction(erc20, txInput) { Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapRefund }; var signResult = await SignTransactionAsync(refundTx, cancellationToken) .ConfigureAwait(false); if (!signResult) { Log.Error("Transaction signing error"); return; } swap.RefundTx = refundTx; swap.StateFlags |= SwapStateFlags.IsRefundSigned; RaiseSwapUpdated(swap, SwapStateFlags.IsRefundSigned); await BroadcastTxAsync(swap, refundTx, cancellationToken) .ConfigureAwait(false); swap.RefundTx = refundTx; swap.StateFlags |= SwapStateFlags.IsRefundBroadcast; RaiseSwapUpdated(swap, SwapStateFlags.IsRefundBroadcast); TrackTransactionConfirmationAsync( swap: swap, currency: erc20, txId: refundTx.Id, confirmationHandler: RefundConfirmedEventHandler, cancellationToken: cancellationToken) .FireAndForget(); }
public override async Task RedeemForPartyAsync( Swap swap, CancellationToken cancellationToken = default) { Log.Debug("Create redeem for counterParty for swap {@swapId}", swap.Id); var erc20 = Erc20; var walletAddress = (await Erc20Account .GetUnspentAddressesAsync( toAddress: null, // todo: get participant address amount: 0, fee: 0, feePrice: 0, feeUsagePolicy: FeeUsagePolicy.EstimatedFee, addressUsagePolicy: AddressUsagePolicy.UseOnlyOneAddress, transactionType: BlockchainTransactionType.SwapRedeem, cancellationToken: cancellationToken) .ConfigureAwait(false)) .FirstOrDefault(); if (walletAddress == null) { Log.Error("Insufficient balance for party redeem. Cannot find the address containing the required amount of funds."); return; } var nonceResult = await EthereumNonceManager.Instance .GetNonceAsync(erc20, walletAddress.Address, cancellationToken) .ConfigureAwait(false); if (nonceResult.HasError) { Log.Error("Nonce getting error with code {@code} and description {@description}", nonceResult.Error.Code, nonceResult.Error.Description); return; } var message = new RedeemFunctionMessage { FromAddress = walletAddress.Address, HashedSecret = swap.SecretHash, Secret = swap.Secret, Nonce = nonceResult.Value, GasPrice = Atomex.Ethereum.GweiToWei(erc20.GasPriceInGwei), }; message.Gas = await EstimateGasAsync(message, new BigInteger(erc20.RedeemGasLimit)) .ConfigureAwait(false); var txInput = message.CreateTransactionInput(erc20.SwapContractAddress); var redeemTx = new EthereumTransaction(erc20, txInput) { Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapRedeem }; var signResult = await SignTransactionAsync(redeemTx, cancellationToken) .ConfigureAwait(false); if (!signResult) { Log.Error("Transaction signing error"); return; } await BroadcastTxAsync(swap, redeemTx, cancellationToken) .ConfigureAwait(false); }
private async Task <IList <EthereumTransaction> > CreateApproveTxsAsync( Swap swap, EthereumTransaction paymentTx, CancellationToken cancellationToken = default) { var erc20 = Erc20Config; Log.Debug("Create approve transactions for swap {@swapId}", swap.Id); var transactions = new List <EthereumTransaction>(); var walletAddress = await Erc20Account .GetAddressAsync(paymentTx.From, cancellationToken) .ConfigureAwait(false); var gasPrice = await EthConfig .GetGasPriceAsync(cancellationToken) .ConfigureAwait(false); 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); var nonceResult = await((IEthereumBlockchainApi)erc20.BlockchainApi) .GetTransactionCountAsync(walletAddress.Address, pending: false, cancellationToken) .ConfigureAwait(false); if (nonceResult.HasError) { Log.Error($"Getting nonce error: {nonceResult.Error.Description}"); return(new List <EthereumTransaction>()); } if (allowance.Value > 0) { var tx = await CreateApproveTx( walletAddress : walletAddress.Address, nonce : nonceResult.Value, value : 0, gasPrice : gasPrice) .ConfigureAwait(false); transactions.Add(tx); } var requiredAmountInErc20 = RequiredAmountInTokens(swap, erc20); var amountInErc20 = AmountHelper.DustProofMin( walletAddress.Balance, requiredAmountInErc20, erc20.DigitsMultiplier, erc20.DustDigitsMultiplier); var approveTx = await CreateApproveTx( walletAddress : walletAddress.Address, nonce : nonceResult.Value, value : erc20.TokensToTokenDigits(amountInErc20), gasPrice : gasPrice) .ConfigureAwait(false); transactions.Add(approveTx); return(transactions); }
protected async Task <EthereumTransaction> CreatePaymentTxAsync( Swap swap, int lockTimeInSeconds, CancellationToken cancellationToken = default) { var erc20Config = Erc20Config; Log.Debug("Create payment transaction from address {@adderss} for swap {@swapId}", swap.FromAddress, swap.Id); var requiredAmountInERC20 = RequiredAmountInTokens(swap, erc20Config); var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeInSeconds)).ToUnixTimeSeconds(); var rewardForRedeemInERC20 = swap.PartyRewardForRedeem; var walletAddress = await Erc20Account .GetAddressAsync(swap.FromAddress, cancellationToken) .ConfigureAwait(false); var gasPrice = await EthConfig .GetGasPriceAsync(cancellationToken) .ConfigureAwait(false); var balanceInEth = (await EthereumAccount .GetAddressBalanceAsync( address: walletAddress.Address, cancellationToken: cancellationToken) .ConfigureAwait(false)) .Available; var feeAmountInEth = rewardForRedeemInERC20 == 0 ? erc20Config.InitiateFeeAmount(gasPrice) : erc20Config.InitiateWithRewardFeeAmount(gasPrice); if (balanceInEth < feeAmountInEth) { Log.Error( "Insufficient funds at {@address} for fee. Balance: {@balance}, feeAmount: {@feeAmount}, result: {@result}.", walletAddress.Address, balanceInEth, feeAmountInEth, balanceInEth - feeAmountInEth); return(null); } var balanceInErc20 = walletAddress.Balance; Log.Debug("Available balance: {@balance}", balanceInErc20); if (balanceInErc20 < requiredAmountInERC20) { Log.Error( "Insufficient funds at {@address}. Balance: {@balance}, required: {@result}, missing: {@missing}.", walletAddress.Address, balanceInErc20, requiredAmountInERC20, balanceInErc20 - requiredAmountInERC20); return(null); } var amountInErc20 = AmountHelper.DustProofMin( balanceInErc20, requiredAmountInERC20, erc20Config.DigitsMultiplier, erc20Config.DustDigitsMultiplier); var nonceResult = await((IEthereumBlockchainApi)erc20Config.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 initMessage = new ERC20InitiateFunctionMessage { HashedSecret = swap.SecretHash, ERC20Contract = erc20Config.ERC20ContractAddress, Participant = swap.PartyAddress, RefundTimestamp = refundTimeStampUtcInSec, Countdown = lockTimeInSeconds, Value = erc20Config.TokensToTokenDigits(amountInErc20), RedeemFee = erc20Config.TokensToTokenDigits(rewardForRedeemInERC20), Active = true, FromAddress = walletAddress.Address, GasPrice = EthereumConfig.GweiToWei(gasPrice), Nonce = nonceResult.Value }; var initiateGasLimit = rewardForRedeemInERC20 == 0 ? erc20Config.InitiateGasLimit : erc20Config.InitiateWithRewardGasLimit; initMessage.Gas = await EstimateGasAsync(initMessage, new BigInteger(initiateGasLimit)) .ConfigureAwait(false); txInput = initMessage.CreateTransactionInput(erc20Config.SwapContractAddress); return(new EthereumTransaction(erc20Config.Name, txInput) { Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment }); }