public NYXSwap( NYXAccount account, TezosAccount tezosAccount, ISwapClient swapClient, ICurrencies currencies) : base(account, swapClient, currencies) { TezosAccount = tezosAccount ?? throw new ArgumentNullException(nameof(account)); }
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 NYXAccount .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 }
private async Task <bool> SignTransactionAsync( TezosTransaction tx, CancellationToken cancellationToken = default) { var walletAddress = await NYXAccount .GetAddressAsync( address : tx.From, cancellationToken : cancellationToken) .ConfigureAwait(false); return(await NYXAccount.Wallet .SignAsync( tx : tx, address : walletAddress, cancellationToken : cancellationToken) .ConfigureAwait(false)); }
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 NYXAccount .GetTransactionByIdAsync(txId) .ConfigureAwait(false); if (tx != null && tx.IsConfirmed) { return(true); } } return(false); }
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 nyx = NYX; var nyxApi = nyx.BlockchainApi as ITokenBlockchainApi; var requiredAmountInTokens = AmountHelper.QtyToAmount(swap.Side, swap.Qty, swap.Price, nyx.DigitsMultiplier); var refundTimeStampUtcInSec = new DateTimeOffset(swap.TimeStamp.ToUniversalTime().AddSeconds(lockTimeSeconds)).ToUnixTimeSeconds(); var rewardForRedeemInTokenDigits = swap.IsInitiator ? swap.PartyRewardForRedeem.ToTokenDigits(nyx.DigitsMultiplier) : 0; var unspentAddresses = (await NYXAccount .GetUnspentAddressesAsync(cancellationToken) .ConfigureAwait(false)) .ToList() .SortList(new AvailableBalanceAscending()); unspentAddresses.RemoveRange(0, unspentAddresses.Count - 1); 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 NYXAccount .GetAddressBalanceAsync( address: walletAddress.Address, cancellationToken: cancellationToken) .ConfigureAwait(false)) .Available; Log.Debug("Available balance: {@balance}", balanceInTokens); var balanceInMtz = balanceInTz.ToMicroTez(); var balanceInTokenDigits = balanceInTokens.ToTokenDigits(nyx.DigitsMultiplier); var isRevealed = await _account .IsRevealedSourceAsync(walletAddress.Address, cancellationToken) .ConfigureAwait(false); var feeAmountInMtz = nyx.ApproveFee + nyx.InitiateFee + (isRevealed ? 0 : nyx.RevealFee); var storageLimitInMtz = (nyx.ApproveStorageLimit + nyx.InitiateStorageLimit) * nyx.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, nyx.DigitsMultiplier, nyx.DustDigitsMultiplier) : 0; if (amountInTokens == 0) { break; } requiredAmountInTokens -= amountInTokens; using var callingAddressPublicKey = new SecureBytes((await NYXAccount.GetAddressAsync(walletAddress.Address) .ConfigureAwait(false)) .PublicKeyBytes()); //todo: get allowance //var allowanceResult = await nyxApi // .TryGetTokenAllowanceAsync( // holderAddress: walletAddress.Address, // spenderAddress: nyx.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 = nyx, // CreationTime = DateTime.UtcNow, // From = walletAddress.Address, // To = nyx.TokenContractAddress, // Fee = nyx.ApproveFee, // GasLimit = nyx.ApproveGasLimit, // StorageLimit = nyx.ApproveStorageLimit, // Params = ApproveParams(nyx.SwapContractAddress, 0), // UseDefaultFee = true, // Type = BlockchainTransactionType.TokenApprove // }); //} transactions.Add(new TezosTransaction { Currency = nyx, CreationTime = DateTime.UtcNow, From = walletAddress.Address, To = nyx.TokenContractAddress, Fee = nyx.ApproveFee, GasLimit = nyx.ApproveGasLimit, StorageLimit = nyx.ApproveStorageLimit, Params = ApproveParams(walletAddress.Address, nyx.SwapContractAddress, amountInTokens.ToTokenDigits(nyx.DigitsMultiplier)), UseDefaultFee = true, Type = BlockchainTransactionType.TokenApprove }); transactions.Add(new TezosTransaction { Currency = nyx, CreationTime = DateTime.UtcNow, From = walletAddress.Address, To = nyx.SwapContractAddress, Fee = feeAmountInMtz, GasLimit = nyx.InitiateGasLimit, StorageLimit = nyx.InitiateStorageLimit, Params = InitParams(swap, nyx.TokenContractAddress, amountInTokens.ToTokenDigits(nyx.DigitsMultiplier), refundTimeStampUtcInSec, (long)rewardForRedeemInTokenDigits), UseDefaultFee = true, Type = BlockchainTransactionType.Output | BlockchainTransactionType.SwapPayment }); if (requiredAmountInTokens <= 0) { break; } } if (requiredAmountInTokens > 0) { Log.Warning("Insufficient funds (left {@requredAmount}).", requiredAmountInTokens); return(Enumerable.Empty <TezosTransaction>()); } return(transactions); }
public override async Task RefundAsync( Swap swap, CancellationToken cancellationToken = default) { var nyx = NYX; 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: nyx, txId: swap.RefundTx.Id, confirmationHandler: RefundConfirmedEventHandler, cancellationToken: cancellationToken) .FireAndForget(); return; } Log.Debug("Create refund for swap {@swap}", swap.Id); var walletAddress = (await NYXAccount .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 = nyx, CreationTime = DateTime.UtcNow, From = walletAddress.Address, To = nyx.SwapContractAddress, Fee = nyx.RefundFee + nyx.RevealFee, GasLimit = nyx.RefundGasLimit, StorageLimit = nyx.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: nyx, txId: refundTx.Id, confirmationHandler: RefundConfirmedEventHandler, cancellationToken: cancellationToken) .FireAndForget(); }
public override async Task RedeemForPartyAsync( Swap swap, CancellationToken cancellationToken = default) { var nyx = NYX; 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 NYXAccount .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 = nyx, CreationTime = DateTime.UtcNow, From = walletAddress.Address, To = nyx.SwapContractAddress, Amount = 0, Fee = nyx.RedeemFee + nyx.RevealFee, GasLimit = nyx.RedeemGasLimit, StorageLimit = nyx.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); }
public override async Task RedeemAsync( Swap swap, CancellationToken cancellationToken = default) { var nyx = NYX; var secretResult = await NYXSwapRedeemedHelper .IsRedeemedAsync( swap : swap, currency : nyx, 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: nyx, 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 NYXSwapRefundedHelper .IsRefundedAsync(swap, nyx, 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 NYXAccount .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 = nyx, CreationTime = DateTime.UtcNow, From = walletAddress.Address, To = nyx.SwapContractAddress, Amount = 0, Fee = nyx.RedeemFee + nyx.RevealFee, GasLimit = nyx.RedeemGasLimit, StorageLimit = nyx.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: nyx, txId: redeemTx.Id, confirmationHandler: RedeemConfirmedEventHandler, cancellationToken: cancellationToken) .FireAndForget(); }