private static void ValidateAssetIsNotNull(BlockchainAsset asset) { if (asset == null) { throw new ArgumentNullException("Asset is required", nameof(asset)); } }
public async Task AddBalanceAsync( DateTime at, string blockchainType, string addressName, string address, BlockchainAsset asset, decimal balance, string explorerUrl) { if (_flushed) { throw new InvalidOperationException("Report already flushed"); } var item = new ReportItem { At = at, BlockchainType = blockchainType, AddressName = addressName, Address = address, Asset = asset, Balance = balance, ExplorerUrl = explorerUrl }; var tasks = _reportRepositories.Select(x => x.AddBalanceAsync(item)); await Task.WhenAll(tasks); }
public async Task CanCalculateBalanceAtPointOfTime() { var balanceProvider = new NeoBalanceProvider("https://neoscan.io/api/main_net/v1/"); var neoAsset = new BlockchainAsset("NEO", "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", "ac2e579f-187b-4429-8d60-bea6e4f65f76"); var gasAsset = new BlockchainAsset("GAS", "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7", "f1ccf1dd-9008-4999-adc8-2cb587717083"); var seasAsset = new BlockchainAsset("SEAS", "de7be47c4c93f1483a0a3fff556a885a68413d97", null); var expectations = new List <(string address, DateTime dateTime, IReadOnlyDictionary <BlockchainAsset, decimal> result)> { ("AYCFkFWhpxXgGzFjnMofYcJMUJ9Z8eneV3", DateTime.Parse("2019-07-01T19:00:00+0000"), new Dictionary <BlockchainAsset, decimal> { { neoAsset, 1289 }, { gasAsset, 4.30532041m }, { seasAsset, 12 } }), ("AYCFkFWhpxXgGzFjnMofYcJMUJ9Z8eneV3", DateTime.Parse("2019-07-01T12:23:50+0000"), new Dictionary <BlockchainAsset, decimal> { { neoAsset, 1259 }, { gasAsset, 0 }, { seasAsset, 12 } }), }; foreach (var assert in expectations) { var result = await balanceProvider.GetBalancesAsync(assert.address, assert.dateTime); Assert.Equal(result, assert.result); } }
public async Task CanCalculateBalanceAtPointOfTime() { var logFactory = LogFactory.Create().AddUnbufferedConsole(); var balanceProvider = new DecredBalanceProvider ( logFactory, "https://explorer.dcrdata.org/insight/api/" ); var decredAsset = new BlockchainAsset("DCR", "DCR", "02154b48-7ed9-4211-b614-e87679fd4f5a"); var expectations = new List <(string address, DateTime dateTime, IReadOnlyDictionary <BlockchainAsset, decimal> result)> { ("DscTMvxiYJvUsC9VFFTLTFj9MPWo2Togc33", DateTime.Parse("2019-07-01T19:00:00+0000"), new Dictionary <BlockchainAsset, decimal> { { decredAsset, 100.51172068m } }), }; foreach (var assert in expectations) { var result = await balanceProvider.GetBalancesAsync(assert.address, assert.dateTime); Assert.Equal(result, assert.result); } }
public string Format(string address, BlockchainAsset asset) { if (asset.BlockchainId != "ETH") { return($"https://etherscan.io/token/{asset.BlockchainId}?a={address}"); } return($"https://etherscan.io/address/{address}"); }
public SamuraiBalanceProvider( string url, BlockchainAsset nativeAsset, IAssetsServiceWithCache assetsServiceClient) { _nativeAsset = nativeAsset; _client = new SamuraiClient(url); _tokensCache = new Dictionary <string, SamuraiErc20TokenResponse>(); AsyncInitialization = InitializeAsync(assetsServiceClient); }
public Coin(CoinId id, BlockchainAsset asset, decimal value, string address, string redeem) { Id = id; Asset = asset; Value = value; Address = address; Redeem = redeem; }
public NeoBalanceProvider(string baseUrl) { _baseUrl = baseUrl; _neoAsset = BuildAsset(NeoBlockchainAssetId); _gasAsset = BuildAsset(GasBlockchainAssetId); _assetNames = new Dictionary <string, string> { { "de7be47c4c93f1483a0a3fff556a885a68413d97", "SEAS" } }; }
public async Task CanCalculateBalanceAtPointOfTime() { var balanceProvider = new SteemBalanceProvider("https://api.steemit.com/"); var asset = new BlockchainAsset("STEEM", "STEEM", "72da9464-49d0-4f95-983d-635c04e39f3c"); var expectations = new List <(string address, DateTime dateTime, IReadOnlyDictionary <BlockchainAsset, decimal> result)> { ("lykke-exchange", DateTime.Parse("2019-07-10T19:00:00+0000"), new Dictionary <BlockchainAsset, decimal> { { asset, 2751.324m } }) }; foreach (var assert in expectations) { var result = await balanceProvider.GetBalancesAsync(assert.address, assert.dateTime); Assert.Equal(assert.result[asset], result[asset]); } }
public async Task CanCalculateBalanceAtPointOfTime() { var balanceProvider = new BitsharesBalanceProvider("https://explorer.bitshares-kibana.info/"); var bitshresAsset = new BlockchainAsset("BTS", "1.3.0", "20ce0468-917e-4097-abba-edf7c8600cfb"); var expectations = new List <(string address, DateTime dateTime, IReadOnlyDictionary <BlockchainAsset, decimal> result)> { ("1.2.1038643", DateTime.Parse("2019-07-03T19:00:00+0000"), new Dictionary <BlockchainAsset, decimal> { { bitshresAsset, 35441.40289m } }) }; foreach (var assert in expectations) { var result = await balanceProvider.GetBalancesAsync(assert.address, assert.dateTime); Assert.Equal(assert.result[bitshresAsset], result[bitshresAsset]); } }
public async Task CanCalculateBalanceAtPointOfTime() { var balanceProvider = new SolarCoinBalanceProvider("https://chainz.cryptoid.info"); var baseAsset = new BlockchainAsset("SLR", "SLR", "SLR"); var expectations = new List <(string address, DateTime dateTime, IReadOnlyDictionary <BlockchainAsset, decimal> result)> { ("8cSBrj3d9Hc2KZ6dfNCMHG4BqLwjMjNULP", DateTime.Parse("2019-07-14T22:00+0000"), new Dictionary <BlockchainAsset, decimal> { { baseAsset, 7741781.94727671m } }) }; foreach (var assert in expectations) { var result = await balanceProvider.GetBalancesAsync(assert.address, assert.dateTime); Assert.Equal(assert.result[baseAsset], result[baseAsset]); } }
public async Task CanCalculateBalanceAtPointOfTime() { var balanceProvider = new NemBalanceProvider("http://explorer.nemtool.com/"); var nemAsset = new BlockchainAsset("XEM", "XEM", "903eafbd-cc29-4d60-8d7d-907695d9caae"); var expectations = new List <(string address, DateTime dateTime, IReadOnlyDictionary <BlockchainAsset, decimal> result)> { ("NAFSSJLNTIEI5ISMWKEY2BJFH5LAUSHP7JVQLWGT", DateTime.Parse("2019-07-04T19:00:00+0000"), new Dictionary <BlockchainAsset, decimal> { { nemAsset, 4868.073614m } }) }; foreach (var assert in expectations) { var result = await balanceProvider.GetBalancesAsync(assert.address, assert.dateTime); Assert.Equal(assert.result[nemAsset], result[nemAsset]); } }
public string Format(string address, BlockchainAsset asset) { return($"https://blockchair.com/bitcoin-cash/address/{address}"); }
public string Format(string address, BlockchainAsset asset) { return($"https://chainz.cryptoid.info/slr/address.dws?{address}.htm"); }
public SolarCoinBalanceProvider(string baseUrl) { _baseUrl = baseUrl; _baseAsset = new BlockchainAsset("SLR", "SLR", "SLR"); }
public string Format(string address, BlockchainAsset asset) { return($"https://bitshares-explorer.io/#/accounts/{address}"); }
public string Format(string address, BlockchainAsset asset) { return($"https://steemblockexplorer.com/@{address}"); }
/// <inheritdoc /> public async Task <TransactionBuildingResult> BuildTransactionAsync(Guid operationId, string fromAddress, string toAddress, BlockchainAsset asset, decimal amount, bool includeFee) { ValidateOperationIdIsNotEmpty(operationId); ValidateFromAddresIsNotEmpty(fromAddress); ValidateToAddressIsNotEmpty(toAddress); ValidateAssetIsNotNull(asset); ValidateAmountRange(amount); BuildTransactionResponse apiResponse; try { apiResponse = await _runner.RunWithRetriesAsync(() => _api.BuildTransactionAsync( new BuildTransactionRequest { OperationId = operationId, FromAddress = fromAddress, ToAddress = toAddress, AssetId = asset.AssetId, Amount = Conversions.CoinsToContract(amount, asset.Accuracy), IncludeFee = includeFee })); } catch (ErrorResponseException ex) when(ex.StatusCode == HttpStatusCode.NotAcceptable) { throw new NonAcceptableAmountException($"Transaction amount {amount} is non acceptable", ex); } return(new TransactionBuildingResult(apiResponse)); }
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 <BroadcastedTransaction> GetBroadcastedTransactionAsync(Guid operationId, BlockchainAsset asset) { ValidateOperationIdIsNotEmpty(operationId); ValidateAssetIsNotNull(asset); var apiResponse = await _runner.RunWithRetriesAsync(() => _api.GetBroadcastedTransactionAsync(operationId)); return(new BroadcastedTransaction(apiResponse, asset.Accuracy)); }
/// <inheritdoc /> public async Task <BroadcastedTransactionWithManyOutputs> GetBroadcastedTransactionWithManyOutputsAsync(Guid operationId, BlockchainAsset asset) { var result = await TryGetBroadcastedTransactionWithManyOutputsAsync(operationId, asset); if (result == null) { throw new ResultValidationException("Transaction is not found"); } return(result); }
/// <inheritdoc /> public async Task <TransactionBuildingResult> BuildTransactionWithManyOutputsAsync(Guid operationId, string fromAddress, string fromAddressContext, IEnumerable <BuildingTransactionOutput> outputs, BlockchainAsset asset) { ValidateOperationIdIsNotEmpty(operationId); ValidateToAddressIsNotEmpty(fromAddress); // ReSharper disable once PossibleMultipleEnumeration ValidateOutputsNotNull(outputs); ValidateAssetIsNotNull(asset); try { var apiResponse = await _runner.RunWithRetriesAsync(() => _api.BuildTransactionWithManyOutputsAsync( new BuildTransactionWithManyOutputsRequest { OperationId = operationId, FromAddress = fromAddress, FromAddressContext = fromAddressContext, // ReSharper disable once PossibleMultipleEnumeration Outputs = outputs .Select(o => o.ToContract(asset.Accuracy)) .ToArray(), AssetId = asset.AssetId })); return(new TransactionBuildingResult(apiResponse)); } catch (ErrorResponseException ex) when(ex.StatusCode == HttpStatusCode.NotImplemented) { throw new NotSupportedException("Operation is not supported by the blockchain. See GetCapabilitiesAsync", ex); } catch (ErrorResponseException ex) when(ex.StatusCode == HttpStatusCode.Conflict) { throw new TransactionAlreadyBroadcastedException(ex); } }
/// <inheritdoc /> public async Task <TransactionBuildingResult> BuildSingleTransactionAsync(Guid operationId, string fromAddress, string fromAddressContext, string toAddress, BlockchainAsset asset, decimal amount, bool includeFee) { ValidateOperationIdIsNotEmpty(operationId); ValidateFromAddresIsNotEmpty(fromAddress); ValidateToAddressIsNotEmpty(toAddress); ValidateAssetIsNotNull(asset); ValidateAmountRange(amount); try { var apiResponse = await _runner.RunWithRetriesAsync(() => _api.BuildSingleTransactionAsync( new BuildSingleTransactionRequest { OperationId = operationId, FromAddress = fromAddress, FromAddressContext = fromAddressContext, ToAddress = toAddress, AssetId = asset.AssetId, Amount = Conversions.CoinsToContract(amount, asset.Accuracy), IncludeFee = includeFee })); return(new TransactionBuildingResult(apiResponse)); } catch (ErrorResponseException ex) when(ex.StatusCode == HttpStatusCode.Conflict) { throw new TransactionAlreadyBroadcastedException(ex); } }
public NemBalanceProvider(string baseUrl) { _baseUrl = baseUrl; _nemAsset = new BlockchainAsset("XEM", "XEM", "903eafbd-cc29-4d60-8d7d-907695d9caae"); }
public string Format(string address, BlockchainAsset asset) { return($"https://gastracker.io/addr/{address}"); }
public string Format(string address, BlockchainAsset asset) { return($"https://explorer.dcrdata.org/address/{address}"); }
/// <inheritdoc /> public async Task <BroadcastedTransaction> TryGetBroadcastedTransactionAsync(Guid operationId, BlockchainAsset asset) { try { return(await GetBroadcastedTransactionAsync(operationId, asset)); } catch (ErrorResponseException ex) when(ex.StatusCode == HttpStatusCode.NoContent) { return(null); } }
public SteemBalanceProvider(string baseUrl) { _baseUrl = baseUrl; _steemAsset = new BlockchainAsset("STEEM", "STEEM", "72da9464-49d0-4f95-983d-635c04e39f3c"); }
public string Format(string address, BlockchainAsset asset) { return($"https://explorer.bitcoingold.org/insight/address/{address}"); }
public string Format(string address, BlockchainAsset asset) { return($"http://explorer.nemchina.com/#/s_account?account={address}"); }