public async Task SaveAsync(CashinAggregate aggregate) { var entity = CashinEntity.FromDomain(aggregate); await _storage.ReplaceAsync(entity); _chaosKitty.Meow(aggregate.OperationId); }
private void ProcessBalance( WalletBalance depositWallet, IReadOnlyDictionary <string, EnrolledBalance> enrolledBalances, int batchSize) { if (!_assets.TryGetValue(depositWallet.AssetId, out var asset)) { if (!_warningAssets.Contains(depositWallet.AssetId)) { _log.Warning(nameof(ProcessBalance), "Lykke asset for the blockchain asset is not found", context: depositWallet); _warningAssets.Add(depositWallet.AssetId); } return; } enrolledBalances.TryGetValue(GetEnrolledBalancesDictionaryKey(depositWallet.Address, depositWallet.AssetId), out var enrolledBalance); var cashinCouldBeStarted = CashinAggregate.CouldBeStarted( depositWallet.Balance, depositWallet.Block, enrolledBalance?.Balance ?? 0, enrolledBalance?.Block ?? 0, asset.Accuracy); if (!cashinCouldBeStarted) { return; } _cqrsEngine.SendCommand ( new LockDepositWalletCommand { BlockchainType = _blockchainType, BlockchainAssetId = depositWallet.AssetId, DepositWalletAddress = depositWallet.Address, DepositWalletBalance = depositWallet.Balance, DepositWalletBlock = depositWallet.Block, AssetId = asset.Id, AssetAccuracy = asset.Accuracy, BlockchainAssetAccuracy = GetAssetAccuracy(asset.BlockchainIntegrationLayerAssetId, batchSize), CashinMinimalAmount = (decimal)asset.CashinMinimalAmount, HotWalletAddress = _hotWalletAddress }, BlockchainCashinDetectorBoundedContext.Name, BlockchainCashinDetectorBoundedContext.Name ); }
public static CashinEntity FromDomain(CashinAggregate aggregate) { return(new CashinEntity { AssetId = aggregate.AssetId, BlockchainAssetAccuracy = aggregate.BlockchainAssetAccuracy, AssetAccuracy = aggregate.AssetAccuracy, BalanceAmount = aggregate.BalanceAmount, BalanceBlock = aggregate.BalanceBlock, BlockchainAssetId = aggregate.BlockchainAssetId, BlockchainType = aggregate.BlockchainType, CashinMinimalAmount = aggregate.CashinMinimalAmount, ClientId = aggregate.ClientId, CreationMoment = aggregate.CreationMoment, DepositWalletAddress = aggregate.DepositWalletAddress, EnrolledBalanceAmount = aggregate.EnrolledBalanceAmount, EnrolledBalanceBlock = aggregate.EnrolledBalanceBlock, EnrolledBalanceResetMoment = aggregate.EnrolledBalanceResetMoment, EnrolledBalanceSetMoment = aggregate.EnrolledBalanceSetMoment, Error = aggregate.Error, Fee = aggregate.Fee, HotWalletAddress = aggregate.HotWalletAddress, MatchingEngineEnrollementMoment = aggregate.MatchingEngineEnrollementMoment, OperationFinishMoment = aggregate.OperationFinishMoment, DepositWalletLockReleasedMoment = aggregate.DepositWalletLockReleasedMoment, OperationAmount = aggregate.OperationAmount, MeAmount = aggregate.MeAmount, OperationId = aggregate.OperationId, Result = aggregate.Result, StartMoment = aggregate.StartMoment, BalanceOutdatingMoment = aggregate.BalanceOutdatingMoment, State = aggregate.State, TransactionAmount = aggregate.TransactionAmount, TransactionBlock = aggregate.TransactionBlock, TransactionHash = aggregate.TransactionHash, IsDustCashin = aggregate.IsDustCashin, OperationAcceptanceMoment = aggregate.OperationAcceptanceMoment, ETag = string.IsNullOrEmpty(aggregate.Version) ? "*" : aggregate.Version, PartitionKey = GetPartitionKey(aggregate.OperationId), RowKey = GetRowKey(aggregate.OperationId), ErrorCode = aggregate.ErrorCode }); }
public CashinAggregate ToDomain() { return(CashinAggregate.Restore ( clientId: ClientId, assetId: AssetId, blockchainAssetAccuracy: BlockchainAssetAccuracy, assetAccuracy: AssetAccuracy, balanceAmount: BalanceAmount, balanceBlock: BalanceBlock, blockchainAssetId: BlockchainAssetId, blockchainType: BlockchainType, cashinMinimalAmount: CashinMinimalAmount, creationMoment: CreationMoment, depositWalletAddress: DepositWalletAddress, enrolledBalanceAmount: EnrolledBalanceAmount, enrolledBalanceBlock: EnrolledBalanceBlock, enrolledBalanceResetMoment: EnrolledBalanceResetMoment, enrolledBalanceSetMoment: EnrolledBalanceSetMoment, error: Error, fee: Fee, hotWalletAddress: HotWalletAddress, matchingEngineEnrollementMoment: MatchingEngineEnrollementMoment, operationAmount: OperationAmount, meAmount: MeAmount, operationFinishMoment: OperationFinishMoment, depositWalletLockReleasedMoment: DepositWalletLockReleasedMoment, operationId: OperationId, result: Result, startMoment: StartMoment, balanceOutdatingMoment: BalanceOutdatingMoment, transactionAmount: TransactionAmount, transactionBlock: TransactionBlock, transactionHash: TransactionHash, state: State, isDustCashin: IsDustCashin, version: ETag, cashinErrorCode: ErrorCode, operationAcceptanceMoment: OperationAcceptanceMoment )); }
public async Task InvalidDepositAmounEnrolledToMeTest() { var blockchainType = "Stellar"; var hotWallet = "hot-wallet"; var depositWallet = "deposit-wallet"; var operationId = Guid.NewGuid(); Mock <ILogFactory> logFactory = new Mock <ILogFactory>(); var hotWalletProviderMock = new Mock <IHotWalletsProvider>(); var blockchainApiClientMock = new Mock <IBlockchainApiClient>(); var cqrsEngineMock = new Mock <ICqrsEngine>(); var enrolledBalanceRepositoryMock = new Mock <IEnrolledBalanceRepository>(); var cashinRepositoryMock = new Mock <ICashinRepository>(); var depositWalletLockRepository = new Mock <IDepositWalletLockRepository>(); var chaosKittyMock = new Mock <IChaosKitty>(); var xlmBlockchainAsset = new BlockchainAsset ( new AssetContract { AssetId = "XLM", Accuracy = 7, Name = "Stellar XLM" } ); var blockchainAssets = new Dictionary <string, BlockchainAsset> { { xlmBlockchainAsset.AssetId, xlmBlockchainAsset } }; var xlmAsset = new Asset { Id = "XLM-asset", BlockchainIntegrationLayerAssetId = xlmBlockchainAsset.AssetId, BlockchainIntegrationLayerId = blockchainType, CashinMinimalAmount = 1 }; var assets = new Dictionary <string, Asset> { { xlmAsset.BlockchainIntegrationLayerAssetId, xlmAsset } }; hotWalletProviderMock .Setup(x => x.GetHotWalletAddress(It.Is <string>(b => b == blockchainType))) .Returns(hotWallet); cashinRepositoryMock .Setup(x => x.GetOrAddAsync ( It.Is <string>(b => b == blockchainType), It.Is <string>(d => d == depositWallet), It.Is <string>(a => a == xlmBlockchainAsset.AssetId), It.Is <Guid>(o => o == operationId), It.IsAny <Func <CashinAggregate> >() )) .ReturnsAsync(() => CashinAggregate.StartWaitingForActualBalance ( operationId, xlmAsset.Id, xlmBlockchainAsset.Accuracy, xlmBlockchainAsset.Accuracy, xlmBlockchainAsset.AssetId, blockchainType, (decimal)xlmAsset.CashinMinimalAmount, depositWallet, hotWallet )); var balanceProcessor = new BalanceProcessor( blockchainType, EmptyLogFactory.Instance, hotWalletProviderMock.Object, blockchainApiClientMock.Object, cqrsEngineMock.Object, enrolledBalanceRepositoryMock.Object, assets, blockchainAssets); // 1. Deposit 100 is detected on DW at block 5000 // 2. Balance processor has detected this balance, published EnrollToMatchingEngineCommand with balance 100, // but failed to save aggregate state due to Azure Storage unavailability here - // https://github.com/LykkeCity/Lykke.Job.BlockchainCashinDetector/blob/895c9d879e59af5c1312ef5f09f8e76a97607679/src/Lykke.Job.BlockchainCashinDetector/Workflow/PeriodicalHandlers/BalanceProcessor.cs#L173 // In current implementation this turns to the publicshing LockDepositWalletCommand // Arrange depositWalletLockRepository .Setup(x => x.LockAsync ( It.Is <DepositWalletKey>(k => k.DepositWalletAddress == depositWallet && k.BlockchainType == blockchainType && k.BlockchainAssetId == xlmBlockchainAsset.AssetId), It.Is <decimal>(b => b == 100), It.Is <long>(d => d == 5000), It.IsAny <Func <Guid> >() )) .ReturnsAsync <DepositWalletKey, decimal, long, Func <Guid>, IDepositWalletLockRepository, DepositWalletLock> ( (key, balance, block, newOperationIdFactory) => DepositWalletLock.Create ( key, operationId, 100, 5000 ) ); blockchainApiClientMock .Setup(x => x.EnumerateWalletBalanceBatchesAsync ( It.IsAny <int>(), It.IsAny <Func <string, int> >(), It.IsAny <Func <IReadOnlyList <WalletBalance>, Task <bool> > >() )) .ReturnsAsync <int, Func <string, int>, Func <IReadOnlyList <WalletBalance>, Task <bool> >, IBlockchainApiClient, EnumerationStatistics> ( (batchSize, accuracyProvider, enumerationCallback) => { enumerationCallback(new List <WalletBalance> { new WalletBalance ( new WalletBalanceContract { Address = depositWallet, AssetId = xlmBlockchainAsset.AssetId, Balance = Conversions.CoinsToContract(100, xlmBlockchainAsset.Accuracy), Block = 5000 }, assetAccuracy: xlmBlockchainAsset.Accuracy ) }).GetAwaiter().GetResult(); return(new EnumerationStatistics(1, 1, TimeSpan.FromMilliseconds(1))); } ); cashinRepositoryMock .Setup(x => x.SaveAsync(It.IsAny <CashinAggregate>())) .Throws <CashinAggregatePersistingFailureTestException>(); // Act / Assert await Assert.ThrowsAsync <CashinAggregatePersistingFailureTestException>(async() => { await balanceProcessor.ProcessAsync(100); }); depositWalletLockRepository.Verify(x => x.LockAsync ( It.Is <DepositWalletKey>(k => k.DepositWalletAddress == depositWallet && k.BlockchainType == blockchainType && k.BlockchainAssetId == xlmBlockchainAsset.AssetId), It.Is <decimal>(b => b == 100), It.Is <long>(d => d == 5000), It.IsAny <Func <Guid> >() )); cqrsEngineMock.Verify( x => x.SendCommand ( It.Is <EnrollToMatchingEngineCommand>(c => c.DepositWalletAddress == depositWallet && c.AssetId == xlmAsset.Id && c.BlockchainType == blockchainType && c.BlockchainAssetId == xlmBlockchainAsset.AssetId && c.OperationId == operationId && // ReSharper disable once CompareOfFloatsByEqualityOperator c.MatchingEngineOperationAmount == 100.0d), It.Is <string>(c => c == BlockchainCashinDetectorBoundedContext.Name), It.Is <string>(c => c == BlockchainCashinDetectorBoundedContext.Name), It.IsAny <uint>() ), Times.Once); // The bug is: // 3. One more deposit has detected and DW balance is changed to 300 at block 5001 // 4. Balance processor has detected balance 300, publish EnrollToMatchingEngineCommand with balance 300 and saved aggregate. // 5. First enrollement command is processed and 100 is enrolled to ME // 6. Second enrollement command with amount = 300 is processed but it's deduplicated by ME // 7. Finally aggregate contains ME amount = 300, but actually only 100 is enrolled // Should be: // There should be the same balance 100 at block 5000 as in first iteration // Arrange depositWalletLockRepository .Setup(x => x.LockAsync ( It.Is <DepositWalletKey>(k => k.DepositWalletAddress == depositWallet && k.BlockchainType == blockchainType && k.BlockchainAssetId == xlmBlockchainAsset.AssetId), It.Is <decimal>(b => b == 300), It.Is <long>(d => d == 5001), It.IsAny <Func <Guid> >() )) .ReturnsAsync <DepositWalletKey, decimal, long, Func <Guid>, IDepositWalletLockRepository, DepositWalletLock> ( (key, balance, block, newOperationIdFactory) => DepositWalletLock.Create ( key, operationId, 100, 5000 ) ); blockchainApiClientMock .Setup(x => x.EnumerateWalletBalanceBatchesAsync ( It.IsAny <int>(), It.IsAny <Func <string, int> >(), It.IsAny <Func <IReadOnlyList <WalletBalance>, Task <bool> > >() )) .ReturnsAsync <int, Func <string, int>, Func <IReadOnlyList <WalletBalance>, Task <bool> >, IBlockchainApiClient, EnumerationStatistics> ( (batchSize, accuracyProvider, enumerationCallback) => { enumerationCallback(new List <WalletBalance> { new WalletBalance ( new WalletBalanceContract { Address = depositWallet, AssetId = xlmBlockchainAsset.AssetId, Balance = Conversions.CoinsToContract(300, xlmBlockchainAsset.Accuracy), Block = 5001 }, assetAccuracy: xlmBlockchainAsset.Accuracy ) }).GetAwaiter().GetResult(); return(new EnumerationStatistics(1, 1, TimeSpan.FromMilliseconds(1))); } ); cashinRepositoryMock .Setup(x => x.SaveAsync(It.IsAny <CashinAggregate>())) .Returns(Task.CompletedTask); depositWalletLockRepository.Invocations.Clear(); cqrsEngineMock.Invocations.Clear(); cashinRepositoryMock.Invocations.Clear(); // Act await balanceProcessor.ProcessAsync(100); // Verify depositWalletLockRepository.Verify( x => x.LockAsync ( It.Is <DepositWalletKey>(k => k.DepositWalletAddress == depositWallet && k.BlockchainType == blockchainType && k.BlockchainAssetId == xlmBlockchainAsset.AssetId), It.Is <decimal>(b => b == 300), It.Is <long>(d => d == 5001), It.IsAny <Func <Guid> >() ), Times.Once); cqrsEngineMock.Verify( x => x.SendCommand ( It.Is <EnrollToMatchingEngineCommand>(c => c.DepositWalletAddress == depositWallet && c.AssetId == xlmAsset.Id && c.BlockchainType == blockchainType && c.BlockchainAssetId == xlmBlockchainAsset.AssetId && c.OperationId == operationId && // ReSharper disable once CompareOfFloatsByEqualityOperator c.MatchingEngineOperationAmount == 100.0d), It.Is <string>(c => c == BlockchainCashinDetectorBoundedContext.Name), It.Is <string>(c => c == BlockchainCashinDetectorBoundedContext.Name), It.IsAny <uint>() ), Times.Once); cashinRepositoryMock.Verify(x => x.SaveAsync ( It.Is <CashinAggregate>(a => a.OperationAmount == 100 && a.MeAmount == 100 && a.BalanceAmount == 100 && a.BalanceBlock == 5000 && a.DepositWalletAddress == depositWallet && a.HotWalletAddress == hotWallet && a.OperationId == operationId && a.AssetAccuracy == xlmBlockchainAsset.Accuracy && a.AssetId == xlmAsset.Id && a.BlockchainAssetId == xlmBlockchainAsset.AssetId && a.BlockchainType == blockchainType && a.CashinMinimalAmount == (decimal)xlmAsset.CashinMinimalAmount && a.State == CashinState.Started ) )); }
public async Task DepositWalletLockReleasedEventSent__AggregateIsRejectedCashin__NotifyCashinFailedCommandSent() { var operationId = Guid.NewGuid(); var cashinAggregate = CashinAggregate.Restore(operationId, "ETC", 6, 6, 10, 250, "ETC", "EthereumClassic", 0, DateTime.UtcNow, "0x...", 150, 250, null, null, null, 0.05m, "0x...", DateTime.UtcNow, 10, 10, DateTime.UtcNow, null, operationId, CashinResult.Unknown, null, null, 10, 250, "0xHASH", CashinState.ClientRetrieved, true, "1.0.0", CashinErrorCode.Unknown, null); var cashinRepoMock = new Mock <ICashinRepository>(); cashinRepoMock.Setup(x => x.GetAsync(It.IsAny <Guid>())).ReturnsAsync(cashinAggregate); var repoModule = new RepoMockModule((builder) => { var depositWalletLockRepository = new Mock <IDepositWalletLockRepository>(); var matchingEngineCallsDeduplicationRepository = new Mock <IMatchingEngineCallsDeduplicationRepository>(); var enrolledBalanceRepository = new Mock <IEnrolledBalanceRepository>(); builder.RegisterInstance(cashinRepoMock.Object) .As <ICashinRepository>(); builder.RegisterInstance(matchingEngineCallsDeduplicationRepository.Object) .As <IMatchingEngineCallsDeduplicationRepository>(); builder.RegisterInstance(enrolledBalanceRepository.Object) .As <IEnrolledBalanceRepository>(); builder.RegisterInstance(depositWalletLockRepository.Object) .As <IDepositWalletLockRepository>(); }); var dependencies = GetIntegrationDependencies(); dependencies.Add(repoModule); var testContainer = ContainerCreator.CreateContainer( dependencies.ToArray() ); var cqrsEngine = testContainer.Resolve <ICqrsEngine>(); var @event = new BlockchainRiskControl.Contract.Events.OperationRejectedEvent() { OperationId = operationId, Message = "Test" }; cqrsEngine.StartSubscribers(); cqrsEngine.PublishEvent(@event, BlockchainRiskControl.Contract.BlockchainRiskControlBoundedContext.Name); await CqrsTestModule.CommandsInterceptor.WaitForCommandToBeHandledWithTimeoutAsync( typeof(ReleaseDepositWalletLockCommand), TimeSpan.FromMinutes(4)); await CqrsTestModule.EventsInterceptor.WaitForEventToBeHandledWithTimeoutAsync( typeof(DepositWalletLockReleasedEvent), TimeSpan.FromMinutes(4)); await CqrsTestModule.CommandsInterceptor.WaitForCommandToBeHandledWithTimeoutAsync( typeof(NotifyCashinFailedCommand), TimeSpan.FromMinutes(4)); }
private async Task Handle(DepositWalletLockedEvent evt, ICommandSender sender) { var aggregate = await _cashinRepository.GetOrAddAsync ( evt.BlockchainType, evt.DepositWalletAddress, evt.BlockchainAssetId, evt.OperationId, () => CashinAggregate.StartWaitingForActualBalance ( operationId: evt.OperationId, assetId: evt.AssetId, blockchainAssetAccuracy: evt.BlockchainAssetAccuracy, assetAccuracy: evt.AssetAccuracy, blockchainAssetId: evt.BlockchainAssetId, blockchainType: evt.BlockchainType, cashinMinimalAmount: evt.CashinMinimalAmount, depositWalletAddress: evt.DepositWalletAddress, hotWalletAddress: evt.HotWalletAddress ) ); var transitionResult = aggregate.Start ( balanceAmount: evt.LockedAtBalance, balanceBlock: evt.LockedAtBlock, enrolledBalanceAmount: evt.EnrolledBalance, enrolledBalanceBlock: evt.EnrolledBlock ); if (transitionResult.ShouldSaveAggregate()) { await _cashinRepository.SaveAsync(aggregate); } if (transitionResult.ShouldSendCommands()) { switch (aggregate.State) { case CashinState.Started when !aggregate.MeAmount.HasValue: throw new InvalidOperationException("ME operation amount should be not null here"); case CashinState.Started: sender.SendCommand ( new RetrieveClientCommand { BlockchainType = aggregate.BlockchainType, DepositWalletAddress = aggregate.DepositWalletAddress, OperationId = aggregate.OperationId }, Self ); break; case CashinState.OutdatedBalance: sender.SendCommand ( new ReleaseDepositWalletLockCommand { BlockchainAssetId = aggregate.BlockchainAssetId, BlockchainType = aggregate.BlockchainType, DepositWalletAddress = aggregate.DepositWalletAddress, OperationId = aggregate.OperationId }, Self ); break; default: throw new InvalidOperationException($"Unexpected aggregate state {aggregate.State}"); } _chaosKitty.Meow(aggregate.OperationId); } }