public async Task SaveAsync(CashinAggregate aggregate)
        {
            var entity = CashinEntity.FromDomain(aggregate);

            await _storage.ReplaceAsync(entity);

            _chaosKitty.Meow(aggregate.OperationId);
        }
Beispiel #2
0
        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
            ));
 }
Beispiel #5
0
        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
                                                                    )
                                        ));
        }
Beispiel #6
0
        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));
        }
Beispiel #7
0
        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);
            }
        }