Пример #1
0
        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
        }
Пример #2
0
 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));
 }
Пример #3
0
        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));
        }
Пример #4
0
        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");
            }
        }
Пример #5
0
        protected override async Task <IEnumerable <EthereumTransaction> > CreatePaymentTxsAsync(
            Swap swap,
            int lockTimeInSeconds,
            CancellationToken cancellationToken = default)
        {
            var erc20 = Erc20;

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

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

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

            var transactions = new List <EthereumTransaction>();

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

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

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

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

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

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

                    continue;
                }

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

                requiredAmountInERC20 -= amountInERC20;

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

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

                    return(null);
                }

                var nonce = nonceResult.Value;

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

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

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

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

                TransactionInput txInput;

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

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

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

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

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

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

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

                if (isInitTx)
                {
                    isInitTx = false;
                }

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

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

            return(transactions);
        }
Пример #6
0
        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();
        }
Пример #7
0
        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);
        }
Пример #8
0
        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);
        }
Пример #9
0
        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
            });
        }