public async Task HandledDepositAsync(SignalBitGoTransfer transferId) { transferId.AddToActivityAsJsonTag("bitgo signal"); _logger.LogInformation("Request to handle transfer from BitGo: {transferJson}", JsonConvert.SerializeObject(transferId)); var(brokerId, assetSymbol) = _assetMapper.BitgoCoinToAsset(transferId.Coin, transferId.WalletId); if (string.IsNullOrEmpty(brokerId) || string.IsNullOrEmpty(assetSymbol)) { _logger.LogWarning("Cannot handle BitGo deposit, asset do not found {transferJson}", JsonConvert.SerializeObject(transferId)); return; } var coin = _bitgoCoinReader.Get(BitgoCoinEntity.GeneratePartitionKey(), BitgoCoinEntity.GenerateRowKey(transferId.Coin)); var transferResp = await _bitgoClient.Get(coin.IsMainNet) .GetTransferAsync(transferId.Coin, transferId.WalletId, transferId.TransferId); var transfer = transferResp.Data; if (transfer == null) { _logger.LogWarning("Cannot handle BitGo deposit, transfer do not found {transferJson}", JsonConvert.SerializeObject(transferId)); Activity.Current?.SetStatus(Status.Error); return; } transfer.AddToActivityAsJsonTag("bitgo-transfer"); _logger.LogInformation("Transfer from BitGo: {transferJson}", JsonConvert.SerializeObject(transfer)); var requirement = _assetMapper.GetRequiredConfirmations(transfer.Coin); if (transfer.Confirmations < requirement) { _logger.LogError( $"Transaction do not has enough conformations. Transaction has: {transfer.Confirmations}, requirement: {requirement}"); Activity.Current?.SetStatus(Status.Error); throw new Exception( $"Transaction do not has enough conformations. Transaction has: {transfer.Confirmations}, requirement: {requirement}"); } if (!_assetMapper.IsWalletEnabled(transfer.Coin, transfer.WalletId)) { _logger.LogError( "Transfer {transferIdString} from BitGo is skipped, Wallet do not include in enabled wallet list", transfer.TransferId); Activity.Current?.SetStatus(Status.Error); return; } foreach (var entryGroup in transfer.Entries .Where(e => e.Value > 0 && !string.IsNullOrEmpty(e.Label) && e.WalletId == transferId.WalletId && (e.Token == null || e.Token == transferId.Coin)) .GroupBy(e => e.Label)) { var label = entryGroup.Key; var wallet = _walletMapper.BitgoLabelToWallet(label); if (wallet == null) { _logger.LogWarning("Cannot found wallet for transfer entry with label={label} address={address}", label, entryGroup.First().Address); continue; } wallet.WalletId.AddToActivityAsTag("walletId"); wallet.ClientId.AddToActivityAsTag("clientId"); var bitgoAmount = entryGroup.Sum(e => e.Value); var meAmount = _assetMapper.ConvertAmountFromBitgo(transferId.Coin, bitgoAmount); var accuracy = _assetsDictionary.GetAssetById(new AssetIdentity() { Symbol = assetSymbol, BrokerId = wallet.BrokerId }).Accuracy; var roundedAmount = Math.Round(meAmount, accuracy, MidpointRounding.ToNegativeInfinity); try { await using var ctx = DatabaseContext.Create(_dbContextOptionsBuilder); await ctx.InsertAsync(new DepositEntity { BrokerId = wallet.BrokerId, WalletId = wallet.WalletId, ClientId = wallet.ClientId, TransactionId = $"{transfer.TransferId}:{wallet.WalletId}", Amount = (decimal)roundedAmount, AssetSymbol = assetSymbol, Comment = $"Bitgo transfer [{transferId.Coin}:{transferId.WalletId}] entry label='{label}', count entry={entryGroup.Count()}", Integration = "BitGo", Txid = transfer.TxId, Status = DepositStatus.New, EventDate = DateTime.UtcNow, UpdateDate = DateTime.UtcNow, FeeAmount = 0, FeeAssetSymbol = assetSymbol, CardLast4 = string.Empty }); } catch (Exception e) { Console.WriteLine(e); throw; } } _logger.LogInformation("Deposit request from BitGo {transferIdString} is handled", transfer.TransferId); }
public async Task <SendTransactionResponse> SignAndSendTransactionAsync(SendTransactionRequest request) { _logger.LogInformation("Transfer Request: {jsonText}", JsonConvert.SerializeObject(request)); try { var client = _bitGoClientService.GetByUser(request.BrokerId, BitGoUserNoSqlEntity.TechSignerId, request.BitgoCoin); if (client == null) { throw new Exception($"Tech account is not configured, id = {BitGoUserNoSqlEntity.TechSignerId}, coin = {request.BitgoCoin}"); } var wallet = _myNoSqlServerWalletDataReader.Get( BitGoWalletNoSqlEntity.GeneratePartitionKey(request.BrokerId), BitGoWalletNoSqlEntity.GenerateRowKey(request.BitgoWalletId)); if (string.IsNullOrEmpty(wallet?.Wallet?.ApiKey)) { _logger.LogError("Cannot find pass phase for wallet {bitgoWalletIdText}", request.BitgoWalletId); throw new Exception($"Cannot find pass phase for wallet {request.BitgoWalletId}"); } var walletPass = _encryptionService.Decrypt(wallet.Wallet.ApiKey); var result = await client.SendCoinsAsync(request.BitgoCoin, request.BitgoWalletId, walletPass, request.SequenceId, request.Amount, request.Address); if (!result.Success) { switch (result.Error.Code) { case "DuplicateSequenceIdError": { var transaction = await client.GetTransferBySequenceIdAsync(request.BitgoCoin, request.BitgoWalletId, request.SequenceId); if (!transaction.Success || transaction.Data == null) { _logger.LogError("Transfer is Duplicate, but cannot found transaction: {jsonText}", JsonConvert.SerializeObject(transaction.Error)); return(new SendTransactionResponse() { Error = result.Error }); } _logger.LogInformation("Transfer is Duplicate, Result: {jsonText}", JsonConvert.SerializeObject(transaction.Data)); return(new SendTransactionResponse() { DuplicateTransaction = transaction.Data }); } case "needs unlock": await _sessionPublisher.PublishAsync(new SignalBitGoSessionStateUpdate() { State = BitGoSessionState.Locked }); return(new SendTransactionResponse() { Error = { Code = "needs unlock", ErrorMessage = "Session is locked" } }); } } if (!result.Success) { _logger.LogError("Transfer Result: {jsonText}", JsonConvert.SerializeObject(result.Error)); return(new SendTransactionResponse() { Error = result.Error }); } await AddVolumeToApiKey(request.BrokerId, _encryptionService.GetSha256Hash(wallet.Wallet.ApiKey), request.BitgoCoin, _assetMapper.ConvertAmountFromBitgo(request.BitgoCoin, long.Parse(request.Amount))); _logger.LogInformation("Transfer Result: {jsonText}", JsonConvert.SerializeObject(result.Data)); return(new SendTransactionResponse() { Result = result.Data }); } catch (Exception ex) { _logger.LogError(ex, "Transfer Request ERROR: {jsonText}. Error: {message}", JsonConvert.SerializeObject(request), ex.Message); throw; } }
public async Task <GetBitGoWalletLimitsResponse> GetSpendingLimitsAsync(GetBitGoWalletLimitsRequest request) { try { var(coin, wallet) = _assetMapper.AssetToBitgoCoinAndWallet(request.BrokerId, request.AssetId); var client = _bitGoClientService.GetByUser(request.BrokerId, BitGoUserNoSqlEntity.TechSignerId, coin); if (client == null) { return(new GetBitGoWalletLimitsResponse { Success = false, Error = $"Tech account is not configured, id = {BitGoUserNoSqlEntity.TechSignerId}, coin = {coin}" }); } var result = await client.GetSpendingLimitsForWalletAsync(coin, wallet); if (!result.Success) { _logger.LogInformation( $"Unable to get spending limits: {JsonConvert.SerializeObject(result.Error)}"); return(new GetBitGoWalletLimitsResponse { Success = false, Error = result.Error.Message }); } var limit = new SpendingLimit { AssetId = request.AssetId, BitgoCoin = coin }; foreach (var spendingLimit in result.Data.Limits) { if (spendingLimit.Coin == coin) { switch (spendingLimit.TimeWindow) { case "0": limit.TransactionLimit = _assetMapper.ConvertAmountFromBitgo(coin, decimal.Parse(spendingLimit.LimitAmountString)); break; case "3600": limit.HourlyLimit = _assetMapper.ConvertAmountFromBitgo(coin, decimal.Parse(spendingLimit.LimitAmountString)); limit.HourlySpent = _assetMapper.ConvertAmountFromBitgo(coin, decimal.Parse(spendingLimit.AmountSpentString)); break; case "86400": limit.DailyLimit = _assetMapper.ConvertAmountFromBitgo(coin, decimal.Parse(spendingLimit.LimitAmountString)); limit.DailySpent = _assetMapper.ConvertAmountFromBitgo(coin, decimal.Parse(spendingLimit.AmountSpentString)); break; } } } return(new GetBitGoWalletLimitsResponse { Success = true, Limit = limit }); } catch (Exception ex) { _logger.LogError(ex, "GetSpendingLimitsAsync request error: {message}", ex.Message); throw; } }