示例#1
0
 public Fa12Swap(
     Fa12Account account,
     TezosAccount tezosAccount,
     ICurrencies currencies)
     : base(account.Currency, currencies)
 {
     Fa12Account  = account ?? throw new ArgumentNullException(nameof(account));
     TezosAccount = tezosAccount ?? throw new ArgumentNullException(nameof(account));
 }
示例#2
0
        private async Task BroadcastTxAsync(
            Swap swap,
            TezosTransaction tx,
            CancellationToken cancellationToken = default,
            bool updateBalance          = true,
            bool notifyIfUnconfirmed    = true,
            bool notifyIfBalanceUpdated = true)
        {
            var broadcastResult = await Xtz.BlockchainApi
                                  .TryBroadcastAsync(tx, cancellationToken : cancellationToken)
                                  .ConfigureAwait(false);

            if (broadcastResult.HasError)
            {
                throw new Exception($"Error while broadcast transaction with code {broadcastResult.Error.Code} and description {broadcastResult.Error.Description}");
            }

            var txId = broadcastResult.Value;

            if (txId == null)
            {
                throw new Exception("Transaction Id is null");
            }

            Log.Debug("TxId {@id} for swap {@swapId}", txId, swap.Id);

            // account new unconfirmed transaction
            await Fa12Account
            .UpsertTransactionAsync(
                tx : tx,
                updateBalance : updateBalance,
                notifyIfUnconfirmed : notifyIfUnconfirmed,
                notifyIfBalanceUpdated : notifyIfBalanceUpdated,
                cancellationToken : cancellationToken)
            .ConfigureAwait(false);

            var xtzTx = tx.Clone();

            xtzTx.Currency = Xtz;
            xtzTx.Amount   = 0;
            xtzTx.Type     = BlockchainTransactionType.Output | (tx.Type.HasFlag(BlockchainTransactionType.TokenApprove)
                ? BlockchainTransactionType.TokenCall
                : BlockchainTransactionType.SwapCall);

            await TezosAccount
            .UpsertTransactionAsync(
                tx : xtzTx,
                updateBalance : updateBalance,
                notifyIfUnconfirmed : notifyIfUnconfirmed,
                notifyIfBalanceUpdated : notifyIfBalanceUpdated,
                cancellationToken : cancellationToken)
            .ConfigureAwait(false);

            // todo: transaction receipt status control
        }
示例#3
0
        private async Task <bool> SignTransactionAsync(
            TezosTransaction tx,
            CancellationToken cancellationToken = default)
        {
            var walletAddress = await Fa12Account
                                .GetAddressAsync(
                address : tx.From,
                cancellationToken : cancellationToken)
                                .ConfigureAwait(false);

            return(await Fa12Account.Wallet
                   .SignAsync(
                       tx : tx,
                       address : walletAddress,
                       cancellationToken : cancellationToken)
                   .ConfigureAwait(false));
        }
示例#4
0
        private async Task <bool> WaitPaymentConfirmationAsync(
            string txId,
            TimeSpan timeout,
            CancellationToken cancellationToken = default)
        {
            var timeStamp = DateTime.UtcNow;

            while (DateTime.UtcNow < timeStamp + timeout)
            {
                await Task.Delay(InitiationCheckInterval, cancellationToken)
                .ConfigureAwait(false);

                var tx = await Fa12Account
                         .GetTransactionByIdAsync(txId)
                         .ConfigureAwait(false);

                if (tx != null && tx.IsConfirmed)
                {
                    return(true);
                }
            }

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

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

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

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

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

            var transactions = new List <TezosTransaction>();

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

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

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

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

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

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

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

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

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

                    continue;
                }

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

                if (amountInTokens == 0)
                {
                    break;
                }

                requiredAmountInTokens -= amountInTokens;

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

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

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

                    continue; // todo: maybe add approve 0
                }

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

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

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

                if (isInitTx)
                {
                    isInitTx = false;
                }

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

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

            return(transactions);
        }
示例#6
0
        public override async Task RefundAsync(
            Swap swap,
            CancellationToken cancellationToken = default)
        {
            var fa12 = Fa12;

            if (swap.StateFlags.HasFlag(SwapStateFlags.IsRefundBroadcast) &&
                swap.RefundTx != null &&
                swap.RefundTx.CreationTime != null &&
                swap.RefundTx.CreationTime.Value.ToUniversalTime() + TimeSpan.FromMinutes(5) > DateTime.UtcNow)
            {
                TrackTransactionConfirmationAsync(
                    swap: swap,
                    currency: fa12,
                    txId: swap.RefundTx.Id,
                    confirmationHandler: RefundConfirmedEventHandler,
                    cancellationToken: cancellationToken)
                .FireAndForget();

                return;
            }

            Log.Debug("Create refund for swap {@swap}", swap.Id);

            var walletAddress = (await Fa12Account
                                 .GetUnspentAddressesAsync(
                                     toAddress: null, // todo: 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 refundTx = new TezosTransaction   //todo: use estimated fee and storage limit
            {
                Currency      = fa12,
                CreationTime  = DateTime.UtcNow,
                From          = walletAddress.Address,
                To            = fa12.SwapContractAddress,
                Fee           = fa12.RefundFee + fa12.RevealFee,
                GasLimit      = fa12.RefundGasLimit,
                StorageLimit  = fa12.RefundStorageLimit,
                Params        = RefundParams(swap),
                UseDefaultFee = true,
                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: fa12,
                txId: refundTx.Id,
                confirmationHandler: RefundConfirmedEventHandler,
                cancellationToken: cancellationToken)
            .FireAndForget();
        }
示例#7
0
        public override async Task RedeemForPartyAsync(
            Swap swap,
            CancellationToken cancellationToken = default)
        {
            var fa12 = Fa12;

            if (swap.IsInitiator)
            {
                var partyRedeemDeadline = swap.TimeStamp.ToUniversalTime().AddSeconds(DefaultAcceptorLockTimeInSeconds) - PartyRedeemTimeReserve;

                if (DateTime.UtcNow > partyRedeemDeadline)
                {
                    Log.Error("Party redeem dedline reached for swap {@swap}", swap.Id);
                    return;
                }
            }

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

            var walletAddress = (await Fa12Account
                                 .GetUnspentAddressesAsync(
                                     toAddress: swap.PartyAddress, // todo: check it
                                     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 redeemTx = new TezosTransaction
            {
                Currency      = fa12,
                CreationTime  = DateTime.UtcNow,
                From          = walletAddress.Address,
                To            = fa12.SwapContractAddress,
                Amount        = 0,
                Fee           = fa12.RedeemFee + fa12.RevealFee,
                GasLimit      = fa12.RedeemGasLimit,
                StorageLimit  = fa12.RedeemStorageLimit,
                Params        = RedeemParams(swap),
                UseDefaultFee = true,
                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
        public override async Task RedeemAsync(
            Swap swap,
            CancellationToken cancellationToken = default)
        {
            var fa12 = Fa12;

            var secretResult = await FA12SwapRedeemedHelper
                               .IsRedeemedAsync(
                swap : swap,
                currency : fa12,
                tezos : Xtz,
                attempts : MaxRedeemCheckAttempts,
                attemptIntervalInSec : RedeemCheckAttemptIntervalInSec,
                cancellationToken : cancellationToken)
                               .ConfigureAwait(false);

            if (!secretResult.HasError && secretResult.Value != null)
            {
                RedeemConfirmedEventHandler(swap, null, cancellationToken);
                return;
            }

            if (swap.StateFlags.HasFlag(SwapStateFlags.IsRedeemBroadcast) &&
                swap.RedeemTx != null &&
                swap.RedeemTx.CreationTime != null &&
                swap.RedeemTx.CreationTime.Value.ToUniversalTime() + TimeSpan.FromMinutes(5) > DateTime.UtcNow)
            {
                // redeem already broadcast
                TrackTransactionConfirmationAsync(
                    swap: swap,
                    currency: fa12,
                    txId: swap.RedeemTx.Id,
                    confirmationHandler: RedeemConfirmedEventHandler,
                    cancellationToken: cancellationToken)
                .FireAndForget();

                return;
            }

            // check already refunded by initiator
            if (swap.IsAcceptor &&
                swap.TimeStamp.ToUniversalTime().AddSeconds(DefaultInitiatorLockTimeInSeconds) < DateTime.UtcNow)
            {
                var isRefundedByParty = await FA12SwapRefundedHelper
                                        .IsRefundedAsync(swap, fa12, Xtz, cancellationToken)
                                        .ConfigureAwait(false);

                if (isRefundedByParty != null &&
                    !isRefundedByParty.HasError &&
                    isRefundedByParty.Value)
                {
                    swap.StateFlags |= SwapStateFlags.IsUnsettled;
                    RaiseSwapUpdated(swap, SwapStateFlags.IsUnsettled);
                    return;
                }
            }

            if (swap.IsInitiator)
            {
                var redeemDeadline = swap.TimeStamp.ToUniversalTime().AddSeconds(DefaultAcceptorLockTimeInSeconds) - RedeemTimeReserve;

                if (DateTime.UtcNow > redeemDeadline)
                {
                    Log.Error("Redeem dedline reached for swap {@swap}", swap.Id);
                    return;
                }
            }

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

            var walletAddress = (await Fa12Account
                                 .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)
            {
                Log.Error("Insufficient funds for redeem");
                return;
            }

            var redeemTx = new TezosTransaction
            {
                Currency      = fa12,
                CreationTime  = DateTime.UtcNow,
                From          = walletAddress.Address,
                To            = fa12.SwapContractAddress,
                Amount        = 0,
                Fee           = fa12.RedeemFee + fa12.RevealFee,
                GasLimit      = fa12.RedeemGasLimit,
                StorageLimit  = fa12.RedeemStorageLimit,
                Params        = RedeemParams(swap),
                UseDefaultFee = true,
                Type          = BlockchainTransactionType.Output | BlockchainTransactionType.SwapRedeem
            };

            var signResult = await SignTransactionAsync(redeemTx, cancellationToken)
                             .ConfigureAwait(false);

            if (!signResult)
            {
                Log.Error("Transaction signing error");
                return;
            }

            swap.RedeemTx    = redeemTx;
            swap.StateFlags |= SwapStateFlags.IsRedeemSigned;
            RaiseSwapUpdated(swap, SwapStateFlags.IsRedeemSigned);

            await BroadcastTxAsync(swap, redeemTx, cancellationToken)
            .ConfigureAwait(false);

            swap.RedeemTx    = redeemTx;
            swap.StateFlags |= SwapStateFlags.IsRedeemBroadcast;
            RaiseSwapUpdated(swap, SwapStateFlags.IsRedeemBroadcast);

            TrackTransactionConfirmationAsync(
                swap: swap,
                currency: fa12,
                txId: redeemTx.Id,
                confirmationHandler: RedeemConfirmedEventHandler,
                cancellationToken: cancellationToken)
            .FireAndForget();
        }