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 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); }