private async Task <(int Count, string Cursor)> ProcessTransactionsAsync(IHorizonService horizonService, ILog logger, string cursor) { var transactions = await horizonService.GetTransactions(_address, OrderDirection.ASC, cursor); var count = 0; cursor = null; foreach (var transaction in transactions) { try { if (_latestBlock.HasValue && (long)transaction.Ledger > _latestBlock) { cursor = null; break; } var sign = 1; cursor = transaction.PagingToken; count++; // skip outgoing transactions and transactions without memo //var memo = horizonService.GetMemo(transaction); if (_address.Equals(transaction.SourceAccount, StringComparison.OrdinalIgnoreCase)) { sign = -1; } var xdr = Convert.FromBase64String(transaction.EnvelopeXdr); var reader = new XdrDataInputStream(xdr); var txEnvelope = TransactionEnvelope.Decode(reader); var tx = txEnvelope.V0.Tx.Operations; for (short i = 0; i < tx.Length; i++) { var operation = tx[i]; var operationType = operation.Body.Discriminant.InnerValue; string toAddress = null; long amount = 0; // ReSharper disable once SwitchStatementMissingSomeCases switch (operationType) { case OperationType.OperationTypeEnum.PAYMENT: { var op = operation.Body.PaymentOp; if (op.Asset.Discriminant.InnerValue == AssetType.AssetTypeEnum.ASSET_TYPE_NATIVE) { var keyPair = KeyPair.FromPublicKey(op.Destination.Ed25519.InnerValue); toAddress = keyPair.Address; amount = op.Amount.InnerValue; } break; } case OperationType.OperationTypeEnum.ACCOUNT_MERGE: { var op = operation.Body; var keyPair = KeyPair.FromPublicKey(op.Destination.Ed25519.InnerValue); toAddress = keyPair.Address; amount = horizonService.GetAccountMergeAmount(transaction.ResultXdr, i); break; } case OperationType.OperationTypeEnum.PATH_PAYMENT_STRICT_RECEIVE: { var op = operation.Body.PathPaymentStrictReceiveOp; if (op.DestAsset.Discriminant.InnerValue == AssetType.AssetTypeEnum.ASSET_TYPE_NATIVE) { var keyPair = KeyPair.FromPublicKey(op.Destination.Ed25519.InnerValue); toAddress = keyPair.Address; amount = op.DestAmount.InnerValue; } break; } case OperationType.OperationTypeEnum.CREATE_ACCOUNT: { var op = operation.Body.CreateAccountOp; if (op != null) { var keyPair = KeyPair.FromXdrPublicKey(op.Destination.InnerValue); toAddress = keyPair.Address; amount = op.StartingBalance.InnerValue; } break; } default: continue; } //var addressWithExtension = $"{toAddress}{Constants.PublicAddressExtension.Separator}{memo.ToLower()}"; var amountChange = (sign * amount); _amountSoFar += amountChange; //logger.Info($"Balance changed to {_amountSoFar} ({amountChange}) on ledger {transaction.Ledger}"); } } catch (Exception ex) { throw new BusinessException($"Failed to process transaction. hash={transaction?.Hash}", ex); } } return(count, cursor); }
private async Task <int> QueryAndProcessTransactions(string address, TransactionContext context, Func <TxDirectionType, TxHistory, Task <bool> > process) { var transactions = await _horizonService.GetTransactions(address, OrderDirection.ASC, context.Cursor); var count = 0; context.Cursor = null; foreach (var transaction in transactions) { try { context.Cursor = transaction.PagingToken; count++; var xdr = Convert.FromBase64String(transaction.EnvelopeXdr); var reader = new XdrDataInputStream(xdr); var txEnvelope = TransactionEnvelope.Decode(reader); var tx = txEnvelope.V1; var operations = txEnvelope?.V1?.Tx?.Operations ?? txEnvelope.V0.Tx.Operations; for (short i = 0; i < operations.Length; i++) { var operation = operations[i]; var operationType = operation.Body.Discriminant.InnerValue; DateTime.TryParse(transaction.CreatedAt, out var createdAt); var history = new TxHistory { FromAddress = transaction.SourceAccount, AssetId = _blockchainAssetsService.GetNativeAsset().Id, Hash = transaction.Hash, OperationIndex = i, PagingToken = transaction.PagingToken, CreatedAt = createdAt, Memo = _horizonService.GetMemo(transaction) }; // ReSharper disable once SwitchStatementMissingSomeCases switch (operationType) { case OperationType.OperationTypeEnum.CREATE_ACCOUNT: { var op = operation.Body.CreateAccountOp; var keyPair = KeyPair.FromXdrPublicKey(op.Destination.InnerValue); history.ToAddress = keyPair.Address; history.Amount = op.StartingBalance.InnerValue; history.PaymentType = PaymentType.CreateAccount; break; } case OperationType.OperationTypeEnum.PAYMENT: { var op = operation.Body.PaymentOp; if (op.Asset.Discriminant.InnerValue == AssetType.AssetTypeEnum.ASSET_TYPE_NATIVE) { var keyPair = KeyPair.FromPublicKey(op.Destination.Ed25519.InnerValue); history.ToAddress = keyPair.Address; history.Amount = op.Amount.InnerValue; history.PaymentType = PaymentType.Payment; } break; } case OperationType.OperationTypeEnum.ACCOUNT_MERGE: { var op = operation.Body; var keyPair = KeyPair.FromPublicKey(op.Destination.Ed25519.InnerValue); history.ToAddress = keyPair.Address; history.Amount = _horizonService.GetAccountMergeAmount(transaction.ResultXdr, i); history.PaymentType = PaymentType.AccountMerge; break; } case OperationType.OperationTypeEnum.PATH_PAYMENT_STRICT_RECEIVE: { var op = operation.Body.PathPaymentStrictReceiveOp; if (op.DestAsset.Discriminant.InnerValue == AssetType.AssetTypeEnum.ASSET_TYPE_NATIVE) { var keyPair = KeyPair.FromPublicKey(op.Destination.Ed25519.InnerValue); history.ToAddress = keyPair.Address; history.Amount = op.DestAmount.InnerValue; history.PaymentType = PaymentType.PathPayment; } break; } default: continue; } if (!ForbiddenCharacterAzureStorageUtils.IsValidRowKey(history.Memo)) { await _log.WriteErrorAsync(nameof(TransactionHistoryService), nameof(QueryAndProcessTransactions), history.Memo, new Exception("Possible cashin skipped. It has forbiddden characters in memo.")); continue; } var cancel = false; if (address.Equals(history.ToAddress, StringComparison.OrdinalIgnoreCase)) { cancel = await process(TxDirectionType.Incoming, history); } if (address.Equals(history.FromAddress, StringComparison.OrdinalIgnoreCase)) { cancel = await process(TxDirectionType.Outgoing, history); } if (cancel) { return(count); } } } catch (Exception ex) { throw new BusinessException($"Failed to process transaction. hash={transaction?.Hash}", ex); } } return(count); }
private async Task <int> QueryAndProcessPayments(string address, PaymentContext context) { int count = 0; var payments = await _horizonService.GetPayments(address, StellarSdkConstants.OrderAsc, context.Cursor); if (payments == null) { await _log.WriteWarningAsync(nameof(TransactionHistoryService), nameof(QueryAndProcessPayments), $"Address not found: {address}"); context.Cursor = null; return(count); } context.Cursor = null; foreach (var payment in payments.Embedded.Records) { try { context.Cursor = payment.PagingToken; count++; // create_account, payment or account_merge if (payment.TypeI == (int)OperationType.OperationTypeEnum.CREATE_ACCOUNT || payment.TypeI == (int)OperationType.OperationTypeEnum.PAYMENT && Core.Domain.Asset.Stellar.TypeName.Equals(payment.AssetType, StringComparison.OrdinalIgnoreCase) || payment.TypeI == (int)OperationType.OperationTypeEnum.ACCOUNT_MERGE) { if (context.Transaction == null || !context.Transaction.Hash.Equals(payment.TransactionHash, StringComparison.OrdinalIgnoreCase)) { var tx = await _horizonService.GetTransactionDetails(payment.TransactionHash); context.Transaction = tx ?? throw new BusinessException($"Transaction not found. hash={payment.TransactionHash}"); context.AccountMerge = 0; } var history = new TxHistory { AssetId = Core.Domain.Asset.Stellar.Id, Hash = payment.TransactionHash, PaymentId = payment.Id, CreatedAt = payment.CreatedAt, Memo = GetMemo(context.Transaction) }; // create_account if (payment.TypeI == (int)OperationType.OperationTypeEnum.CREATE_ACCOUNT) { history.FromAddress = payment.Funder; history.ToAddress = payment.Account; history.PaymentType = PaymentType.CreateAccount; decimal amount = Decimal.Parse(payment.StartingBalance); history.Amount = Convert.ToInt64(amount * One.Value); } // payment else if (payment.TypeI == (int)OperationType.OperationTypeEnum.PAYMENT) { history.FromAddress = payment.From; history.ToAddress = payment.To; history.PaymentType = PaymentType.Payment; decimal amount = Decimal.Parse(payment.Amount); history.Amount = Convert.ToInt64(amount * One.Value); } // account_merge else if (payment.TypeI == (int)OperationType.OperationTypeEnum.ACCOUNT_MERGE) { history.FromAddress = payment.Account; history.ToAddress = payment.Into; history.PaymentType = PaymentType.AccountMerge; var resultXdrBase64 = context.Transaction.ResultXdr; history.Amount = _horizonService.GetAccountMergeAmount(resultXdrBase64, context.AccountMerge); context.AccountMerge++; } else { throw new BusinessException($"Invalid payment type. type=${payment.TypeI}"); } history.OperationId = await _txBroadcastRepository.GetOperationId(payment.TransactionHash); if (address.Equals(history.ToAddress, StringComparison.OrdinalIgnoreCase)) { await _txHistoryRepository.InsertOrReplaceAsync(context.TableId, TxDirectionType.Incoming, history); context.Sequence++; } if (address.Equals(history.FromAddress, StringComparison.OrdinalIgnoreCase)) { await _txHistoryRepository.InsertOrReplaceAsync(context.TableId, TxDirectionType.Outgoing, history); context.Sequence++; } } } catch (Exception ex) { throw new BusinessException($"Failed to process payment of transaction. payment={payment?.Id}, hash={context?.Transaction?.Hash}", ex); } } return(count); }
private async Task <(int Count, string Cursor)> ProcessDeposits(string cursor) { var transactions = await _horizonService.GetTransactions(_depositBaseAddress, OrderDirection.ASC, cursor); var count = 0; var walletsToRefresh = new HashSet <(string assetId, string address)>(); var asset = _blockchainAssetsService.GetNativeAsset(); cursor = null; foreach (var transaction in transactions) { try { cursor = transaction.PagingToken; count++; // skip outgoing transactions and transactions without memo var memo = _horizonService.GetMemo(transaction); if (_depositBaseAddress.Equals(transaction.SourceAccount, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(memo)) { continue; } // transaction XDR doesn't contain operation IDs, // make a dedicated request to get operations var operations = await _horizonService.GetTransactionOperations(transaction.Hash); if (operations == null) { continue; } foreach (var op in operations) { string toAddress = null; long amount = 0; switch (op.Type.ToLower()) { case "payment": var payment = (PaymentOperationResponse)op; if (payment.AssetType == "native") { toAddress = payment.To; amount = asset.ParseDecimal(payment.Amount); } break; case "account_merge": var accountMerge = (AccountMergeOperationResponse)op; toAddress = accountMerge.Into; amount = _horizonService.GetAccountMergeAmount(transaction.ResultMetaXdr, accountMerge.SourceAccount); break; case "path_payment": var pathPayment = (PathPaymentStrictReceiveOperationResponse)op; if (pathPayment.AssetType == "native") { toAddress = pathPayment.To; amount = asset.ParseDecimal(pathPayment.Amount); } break; default: continue; } var addressWithExtension = $"{toAddress}{Constants.PublicAddressExtension.Separator}{memo.ToLower()}"; if (!ForbiddenCharacterAzureStorageUtils.IsValidRowKey(memo)) { await _log.WriteErrorAsync(nameof(BalanceService), nameof(ProcessDeposits), addressWithExtension, new Exception("Possible cashin skipped. It has forbiddden characters in memo.")); continue; } var observation = await _observationRepository.GetAsync(addressWithExtension); if (observation == null) { continue; } await _walletBalanceRepository.RecordOperationAsync(asset.Id, addressWithExtension, transaction.Ledger * 10, op.Id, transaction.Hash, amount); walletsToRefresh.Add((asset.Id, addressWithExtension)); } } catch (Exception ex) { throw new BusinessException($"Failed to process transaction. hash={transaction?.Hash}", ex); } } await _walletBalanceRepository.RefreshBalance(walletsToRefresh); if (!string.IsNullOrEmpty(cursor)) { await _keyValueStoreRepository.SetAsync(GetPagingTokenKey, cursor); } return(count, cursor); }
private async Task ProcessBroadcastInProgress(Guid operationId) { TxBroadcast broadcast = null; try { broadcast = await _broadcastRepository.GetAsync(operationId); if (broadcast == null) { await _observationRepository.DeleteIfExistAsync(operationId.ToString()); throw new BusinessException($"Broadcast for observed operation not found. operationId={operationId}"); } var tx = await _horizonService.GetTransactionDetails(broadcast.Hash); if (tx == null) { // transaction still in progress return; } if (!broadcast.Hash.Equals(tx.Hash, StringComparison.OrdinalIgnoreCase)) { throw new BusinessException($"Transaction hash mismatch. actual={tx.Hash}, expected={broadcast.Hash}"); } var operation = _horizonService.GetFirstOperationFromTxEnvelopeXdr(tx.EnvelopeXdr); var operationType = operation.Discriminant.InnerValue; // ReSharper disable once SwitchStatementMissingSomeCases switch (operationType) { case OperationType.OperationTypeEnum.CREATE_ACCOUNT: { broadcast.Amount = operation.CreateAccountOp.StartingBalance.InnerValue; break; } case OperationType.OperationTypeEnum.PAYMENT: { broadcast.Amount = operation.PaymentOp.Amount.InnerValue; break; } case OperationType.OperationTypeEnum.ACCOUNT_MERGE: { broadcast.Amount = _horizonService.GetAccountMergeAmount(tx.ResultXdr, 0); break; } default: throw new BusinessException($"Unsupported operation type. type={operationType}"); } DateTime.TryParse(tx.CreatedAt, out var createdAt); broadcast.State = TxBroadcastState.Completed; broadcast.Fee = tx.FeeCharged; broadcast.CreatedAt = createdAt; broadcast.Ledger = tx.Ledger * 10; await _broadcastRepository.MergeAsync(broadcast); await _observationRepository.DeleteIfExistAsync(operationId.ToString()); } catch (Exception ex) { if (broadcast != null) { broadcast.State = TxBroadcastState.Failed; broadcast.Error = ex.Message; broadcast.ErrorCode = TxExecutionError.Unknown; await _broadcastRepository.MergeAsync(broadcast); await _observationRepository.DeleteIfExistAsync(operationId.ToString()); } throw new BusinessException($"Failed to process in progress broadcast. operationId={operationId}", ex); } }