diff --git a/NBXplorer.Tests/UnitTest1.cs b/NBXplorer.Tests/UnitTest1.cs index 62df57fed..94877e53a 100644 --- a/NBXplorer.Tests/UnitTest1.cs +++ b/NBXplorer.Tests/UnitTest1.cs @@ -4476,7 +4476,9 @@ private async Task Eventually(Func tsk) [Theory] [InlineData(Backend.Postgres)] +#if SUPPORT_DBTRIE [InlineData(Backend.DBTrie)] +#endif public async Task CanAssociateIndependentScripts(Backend backend) { using var tester = ServerTester.Create(backend); diff --git a/NBXplorer/Controllers/CommonRoutes.cs b/NBXplorer/Controllers/CommonRoutes.cs new file mode 100644 index 000000000..c33773fd6 --- /dev/null +++ b/NBXplorer/Controllers/CommonRoutes.cs @@ -0,0 +1,12 @@ +namespace NBXplorer.Controllers; + +public static class CommonRoutes +{ + public const string BaseCryptoEndpoint = "cryptos/{cryptoCode}"; + public const string BaseDerivationEndpoint = $"{BaseCryptoEndpoint}/derivations"; + public const string DerivationEndpoint = $"{BaseCryptoEndpoint}/derivations/{{derivationScheme}}"; + public const string AddressEndpoint = $"{BaseCryptoEndpoint}/addresses/{{address}}"; + public const string WalletEndpoint = $"{BaseCryptoEndpoint}/wallets/{{walletId}}"; + public const string TrackedSourceEndpoint = $"{BaseCryptoEndpoint}/tracked-sources/{{trackedSource}}"; + public const string TransactionsPath = "transactions/{txId?}"; +} \ No newline at end of file diff --git a/NBXplorer/Controllers/ControllerBase.cs b/NBXplorer/Controllers/ControllerBase.cs index 4ba7e7ec8..f6c52348b 100644 --- a/NBXplorer/Controllers/ControllerBase.cs +++ b/NBXplorer/Controllers/ControllerBase.cs @@ -27,17 +27,7 @@ public ControllerBase( public IRepositoryProvider RepositoryProvider { get; } public IIndexers Indexers { get; } - internal static TrackedSource GetTrackedSource(DerivationStrategyBase derivationScheme, BitcoinAddress address, string walletId) - { - TrackedSource trackedSource = null; - if (address != null) - trackedSource = new AddressTrackedSource(address); - if (derivationScheme != null) - trackedSource = new DerivationSchemeTrackedSource(derivationScheme); - if (walletId != null) - trackedSource = new WalletTrackedSource(walletId); - return trackedSource; - } + internal NBXplorerNetwork GetNetwork(string cryptoCode, bool checkRPC) { if (cryptoCode == null) diff --git a/NBXplorer/Controllers/MainController.PSBT.cs b/NBXplorer/Controllers/MainController.PSBT.cs index 6704b2ef4..5c934d105 100644 --- a/NBXplorer/Controllers/MainController.PSBT.cs +++ b/NBXplorer/Controllers/MainController.PSBT.cs @@ -17,12 +17,10 @@ namespace NBXplorer.Controllers public partial class MainController { [HttpPost] - [Route("cryptos/{network}/derivations/{strategy}/psbt/create")] + [Route($"{CommonRoutes.DerivationEndpoint}/psbt/create")] + [TrackedSourceContext.TrackedSourceContextRequirement(allowedTrackedSourceTypes:typeof(DerivationSchemeTrackedSource))] public async Task CreatePSBT( - [ModelBinder(BinderType = typeof(NetworkModelBinder))] - NBXplorerNetwork network, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase strategy, + TrackedSourceContext trackedSourceContext, [FromBody] JObject body, [FromServices] @@ -30,17 +28,15 @@ public async Task CreatePSBT( { if (body == null) throw new ArgumentNullException(nameof(body)); - CreatePSBTRequest request = ParseJObject(body, network); - if (strategy == null) - throw new ArgumentNullException(nameof(strategy)); - - var repo = RepositoryProvider.GetRepository(network); - var txBuilder = request.Seed is int s ? network.NBitcoinNetwork.CreateTransactionBuilder(s) - : network.NBitcoinNetwork.CreateTransactionBuilder(); + CreatePSBTRequest request = ParseJObject(body, trackedSourceContext.Network); + var repo = RepositoryProvider.GetRepository(trackedSourceContext.Network); + var txBuilder = request.Seed is int s ? trackedSourceContext.Network.NBitcoinNetwork.CreateTransactionBuilder(s) + : trackedSourceContext.Network.NBitcoinNetwork.CreateTransactionBuilder(); + var strategy = ((DerivationSchemeTrackedSource) trackedSourceContext.TrackedSource).DerivationStrategy; CreatePSBTSuggestions suggestions = null; if (!(request.DisableFingerprintRandomization is true) && - fingerprintService.GetDistribution(network) is FingerprintDistribution distribution) + fingerprintService.GetDistribution(trackedSourceContext.Network) is FingerprintDistribution distribution) { suggestions ??= new CreatePSBTSuggestions(); var known = new List<(Fingerprint feature, bool value)>(); @@ -97,8 +93,7 @@ public async Task CreatePSBT( suggestions.ShouldEnforceLowR = fingerprint.HasFlag(Fingerprint.LowR); } - var indexer = Indexers.GetIndexer(network); - if (indexer.NetworkInfo?.GetRelayFee() is FeeRate feeRate) + if (trackedSourceContext.Indexer.NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } @@ -135,7 +130,7 @@ public async Task CreatePSBT( // nLockTime that preclude a fix later. else if (!(request.DiscourageFeeSniping is false)) { - if (indexer.State is BitcoinDWaiterState.Ready) + if (trackedSourceContext.Indexer.State is BitcoinDWaiterState.Ready) { int blockHeight = (await repo.GetTip()).Height; // Secondly occasionally randomly pick a nLockTime even further back, so @@ -153,7 +148,7 @@ public async Task CreatePSBT( txBuilder.SetLockTime(new LockTime(0)); } } - var utxoChanges = (await utxoService.GetUTXOs(network.CryptoCode, strategy)).As(); + var utxoChanges = (await utxoService.GetUTXOs(trackedSourceContext)).As(); var utxos = utxoChanges.GetUnspentUTXOs(request.MinConfirmations); var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) @@ -194,10 +189,10 @@ public async Task CreatePSBT( // We remove unconf utxos with too many ancestors, as it will result in a transaction // that can't be broadcasted. // We do only for BTC, as this isn't a shitcoin issue. - if (network.CryptoCode == "BTC" && unconfUtxos.Count > 0 && request.MinConfirmations == 0) + if (trackedSourceContext.Network.CryptoCode == "BTC" && unconfUtxos.Count > 0 && request.MinConfirmations == 0) { HashSet requestedTxs = new HashSet(); - var rpc = RPCClients.Get(network); + var rpc = trackedSourceContext.RpcClient; rpc = rpc.PrepareBatch(); var mempoolEntries = unconfUtxos @@ -270,7 +265,7 @@ public async Task CreatePSBT( bool hasChange = false; if (request.ExplicitChangeAddress == null) { - var keyInfo = (await GetUnusedAddress(network.CryptoCode, strategy, DerivationFeature.Change, autoTrack: true)).As(); + var keyInfo = (await GetUnusedAddress(trackedSourceContext, DerivationFeature.Change, autoTrack: true)).As(); change = (keyInfo.ScriptPubKey, keyInfo.KeyPath); } else @@ -296,7 +291,7 @@ public async Task CreatePSBT( { try { - var rate = await GetFeeRate(blockTarget, network.CryptoCode); + var rate = await GetFeeRate(blockTarget, trackedSourceContext.Network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when (e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) @@ -312,7 +307,7 @@ public async Task CreatePSBT( { try { - var rate = await GetFeeRate(1, network.CryptoCode); + var rate = await GetFeeRate(1, trackedSourceContext.Network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when (e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) @@ -351,7 +346,7 @@ public async Task CreatePSBT( // We made sure we can build the PSBT, so now we can reserve the change address if we need to if (hasChange && request.ExplicitChangeAddress == null && request.ReserveChangeAddress) { - var derivation = (await GetUnusedAddress(network.CryptoCode, strategy, DerivationFeature.Change, reserve: true, autoTrack: true)).As(); + var derivation = (await GetUnusedAddress(trackedSourceContext, DerivationFeature.Change, reserve: true, autoTrack: true)).As(); // In most of the time, this is the same as previously, so no need to rebuild PSBT if (derivation.ScriptPubKey != change.ScriptPubKey) { @@ -375,14 +370,14 @@ public async Task CreatePSBT( AlwaysIncludeNonWitnessUTXO = request.AlwaysIncludeNonWitnessUTXO, IncludeGlobalXPub = request.IncludeGlobalXPub }; - await UpdatePSBTCore(update, network); + await UpdatePSBTCore(update, trackedSourceContext.Network); var resp = new CreatePSBTResponse() { PSBT = update.PSBT, - ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null, + ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(trackedSourceContext.Network.NBitcoinNetwork) : null, Suggestions = suggestions }; - return Json(resp, network.JsonSerializerSettings); + return Json(resp, trackedSourceContext.Network.JsonSerializerSettings); } [HttpPost] diff --git a/NBXplorer/Controllers/MainController.cs b/NBXplorer/Controllers/MainController.cs index f0ba44a5f..1de9478e7 100644 --- a/NBXplorer/Controllers/MainController.cs +++ b/NBXplorer/Controllers/MainController.cs @@ -86,7 +86,7 @@ private Exception JsonRPCNotExposed() } [HttpPost] - [Route("cryptos/{cryptoCode}/rpc")] + [Route($"{CommonRoutes.BaseCryptoEndpoint}/rpc")] [Consumes("application/json", "application/json-rpc")] public async Task RPCProxy(string cryptoCode) { @@ -125,7 +125,7 @@ public async Task RPCProxy(string cryptoCode) } [HttpGet] - [Route("cryptos/{cryptoCode}/fees/{blockCount}")] + [Route($"{CommonRoutes.BaseCryptoEndpoint}/fees/{{blockCount}}")] public async Task GetFeeRate(int blockCount, string cryptoCode) { var network = GetNetwork(cryptoCode, true); @@ -144,30 +144,28 @@ public async Task GetFeeRate(int blockCount, string cryptoCode } [HttpGet] - [Route("cryptos/{cryptoCode}/derivations/{strategy}/addresses/unused")] + [Route($"{CommonRoutes.DerivationEndpoint}/addresses/unused")] + [TrackedSourceContext.TrackedSourceContextRequirement(allowedTrackedSourceTypes: typeof(DerivationSchemeTrackedSource))] public async Task GetUnusedAddress( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase strategy, DerivationFeature feature = DerivationFeature.Deposit, int skip = 0, bool reserve = false, bool autoTrack = false) + TrackedSourceContext trackedSourceContext, DerivationFeature feature = DerivationFeature.Deposit, int skip = 0, bool reserve = false, bool autoTrack = false) { - if (strategy == null) - throw new ArgumentNullException(nameof(strategy)); - var network = GetNetwork(cryptoCode, false); - var repository = RepositoryProvider.GetRepository(network); + var derivationScheme = ((DerivationSchemeTrackedSource ) trackedSourceContext.TrackedSource).DerivationStrategy; + var network = trackedSourceContext.Network; + var repository = trackedSourceContext.Repository; if (skip >= repository.MinPoolSize) throw new NBXplorerError(404, "strategy-not-found", $"This strategy is not tracked, or you tried to skip too much unused addresses").AsException(); try { - var result = await repository.GetUnused(strategy, feature, skip, reserve); + var result = await repository.GetUnused(derivationScheme, feature, skip, reserve); if (reserve || autoTrack) { while (result == null) { - await AddressPoolService.GenerateAddresses(network, strategy, feature, new GenerateAddressQuery(1, null)); - result = await repository.GetUnused(strategy, feature, skip, reserve); + await AddressPoolService.GenerateAddresses(network, derivationScheme, feature, new GenerateAddressQuery(1, null)); + result = await repository.GetUnused(derivationScheme, feature, skip, reserve); } if (reserve) - _ = AddressPoolService.GenerateAddresses(network, strategy, feature); + _ = AddressPoolService.GenerateAddresses(network, derivationScheme, feature); } return Json(result, network.Serializer.Settings); } @@ -178,19 +176,17 @@ public async Task GetUnusedAddress( } [HttpPost] - [Route("cryptos/{cryptoCode}/derivations/{strategy}/addresses/cancelreservation")] - public async Task CancelReservation(string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase strategy, [FromBody] KeyPath[] keyPaths) + [Route($"{CommonRoutes.DerivationEndpoint}/addresses/cancelreservation")] + [TrackedSourceContext.TrackedSourceContextRequirement( allowedTrackedSourceTypes:typeof(DerivationSchemeTrackedSource))] + public async Task CancelReservation(TrackedSourceContext trackedSourceContext, [FromBody] KeyPath[] keyPaths) { - var network = GetNetwork(cryptoCode, false); - var repo = RepositoryProvider.GetRepository(network); - await repo.CancelReservation(strategy, keyPaths); + var derivationScheme = ((DerivationSchemeTrackedSource ) trackedSourceContext.TrackedSource).DerivationStrategy; + await trackedSourceContext.Repository.CancelReservation(derivationScheme, keyPaths); return Ok(); } [HttpGet] - [Route("cryptos/{cryptoCode}/scripts/{script}")] + [Route($"{CommonRoutes.BaseCryptoEndpoint}/scripts/{{script}}")] public async Task GetKeyInformations(string cryptoCode, [ModelBinder(BinderType = typeof(ScriptModelBinder))] Script script) { @@ -203,25 +199,21 @@ public async Task GetKeyInformations(string cryptoCode, } [HttpGet] - [Route("cryptos/{cryptoCode}/derivations/{strategy}/scripts/{script}")] - public async Task GetKeyInformations(string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase strategy, - [ModelBinder(BinderType = typeof(ScriptModelBinder))] Script script) + [Route($"{CommonRoutes.DerivationEndpoint}/scripts/{{script}}")] + [TrackedSourceContext.TrackedSourceContextRequirement(allowedTrackedSourceTypes:typeof(DerivationSchemeTrackedSource))] + public async Task GetKeyInformations(TrackedSourceContext trackedSourceContext, [ModelBinder(BinderType = typeof(ScriptModelBinder))] Script script) { - var network = GetNetwork(cryptoCode, false); - var repo = RepositoryProvider.GetRepository(network); - var result = (await repo.GetKeyInformations(new[] { script })) - .SelectMany(k => k.Value) - .Where(k => k.DerivationStrategy == strategy) - .FirstOrDefault(); + var derivationScheme = ((DerivationSchemeTrackedSource ) trackedSourceContext.TrackedSource).DerivationStrategy; + var result = (await trackedSourceContext.Repository.GetKeyInformations(new[] { script })) + .SelectMany(k => k.Value) + .FirstOrDefault(k => k.DerivationStrategy == derivationScheme); if (result == null) throw new NBXplorerError(404, "script-not-found", "The script does not seem to be tracked").AsException(); - return Json(result, network.Serializer.Settings); + return Json(result, trackedSourceContext.Network.Serializer.Settings); } [HttpGet] - [Route("cryptos/{cryptoCode}/status")] + [Route($"{CommonRoutes.BaseCryptoEndpoint}/status")] public async Task GetStatus(string cryptoCode) { var network = GetNetwork(cryptoCode, false); @@ -299,7 +291,7 @@ public async Task GetStatus(string cryptoCode) } [HttpGet] - [Route("cryptos/{cryptoCode}/connect")] + [Route($"{CommonRoutes.BaseCryptoEndpoint}/connect")] public async Task ConnectWebSocket( string cryptoCode, bool includeTransaction = true, @@ -419,7 +411,7 @@ private JsonSerializerSettings GetSerializerSettings(string cryptoCode) return this.GetNetwork(cryptoCode, false).JsonSerializerSettings; } - [Route("cryptos/{cryptoCode}/events")] + [Route($"{CommonRoutes.BaseCryptoEndpoint}/events")] public async Task GetEvents(string cryptoCode, int lastEventId = 0, int? limit = null, bool longPolling = false, CancellationToken cancellationToken = default) { if (limit != null && limit.Value < 1) @@ -455,7 +447,7 @@ public async Task GetEvents(string cryptoCode, int lastEventId = 0, int? } - [Route("cryptos/{cryptoCode}/events/latest")] + [Route($"{CommonRoutes.BaseCryptoEndpoint}/events/latest")] public async Task GetLatestEvents(string cryptoCode, int limit = 10) { if (limit < 1) @@ -468,7 +460,7 @@ public async Task GetLatestEvents(string cryptoCode, int limit = 10) [HttpGet] - [Route("cryptos/{cryptoCode}/transactions/{txId}")] + [Route($"{CommonRoutes.BaseCryptoEndpoint}/transactions/{{txId}}")] public async Task GetTransaction( [ModelBinder(BinderType = typeof(UInt256ModelBinding))] uint256 txId, @@ -510,57 +502,51 @@ private bool HasTxIndex(string cryptoCode) } [HttpPost] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}")] - [Route("cryptos/{cryptoCode}/addresses/{address}")] - [Route("cryptos/{cryptoCode}/wallets/{walletId}")] + + [Route($"{CommonRoutes.DerivationEndpoint}")] + [Route($"{CommonRoutes.AddressEndpoint}")] + [Route($"{CommonRoutes.WalletEndpoint}")] + [Route($"{CommonRoutes.TrackedSourceEndpoint}")] public async Task TrackWallet( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, - [ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))] - BitcoinAddress address, - string walletId, + TrackedSourceContext trackedSourceContext, [FromBody] JObject rawRequest = null) { - var request = ParseJObject(rawRequest ?? new JObject(), GetNetwork(cryptoCode, false)); - TrackedSource trackedSource = GetTrackedSource(derivationScheme, address, walletId); - if (trackedSource == null) - return NotFound(); - var network = GetNetwork(cryptoCode, false); - var repo = RepositoryProvider.GetRepository(network); + var request = ParseJObject(rawRequest ?? new JObject(), trackedSourceContext.Network); + + var repo = RepositoryProvider.GetRepository(trackedSourceContext.Network); if (repo is PostgresRepository postgresRepository && - (trackedSource is WalletTrackedSource || + (trackedSourceContext.TrackedSource is WalletTrackedSource || request?.ParentWallet is not null)) { - await postgresRepository.EnsureWalletCreated(trackedSource, request?.ParentWallet is null? null: new []{request?.ParentWallet }); + await postgresRepository.EnsureWalletCreated(trackedSourceContext.TrackedSource, request?.ParentWallet is null? null: new []{request?.ParentWallet }); } if (repo is not PostgresRepository && request.ParentWallet is not null) throw new NBXplorerException(new NBXplorerError(400, "parent-wallet-not-supported", "Parent wallet is only supported with Postgres")); - if (trackedSource is DerivationSchemeTrackedSource dts) + if (trackedSourceContext.TrackedSource is DerivationSchemeTrackedSource dts) { if (request.Wait) { foreach (var feature in keyPathTemplates.GetSupportedDerivationFeatures()) { - await RepositoryProvider.GetRepository(network).GenerateAddresses(dts.DerivationStrategy, feature, GenerateAddressQuery(request, feature)); + await RepositoryProvider.GetRepository(trackedSourceContext.Network).GenerateAddresses(dts.DerivationStrategy, feature, GenerateAddressQuery(request, feature)); } } else { foreach (var feature in keyPathTemplates.GetSupportedDerivationFeatures()) { - await RepositoryProvider.GetRepository(network).GenerateAddresses(dts.DerivationStrategy, feature, new GenerateAddressQuery(minAddresses: 3, null)); + await repo.GenerateAddresses(dts.DerivationStrategy, feature, new GenerateAddressQuery(minAddresses: 3, null)); } foreach (var feature in keyPathTemplates.GetSupportedDerivationFeatures()) { - _ = AddressPoolService.GenerateAddresses(network, dts.DerivationStrategy, feature, GenerateAddressQuery(request, feature)); + _ = AddressPoolService.GenerateAddresses(trackedSourceContext.Network, dts.DerivationStrategy, feature, GenerateAddressQuery(request, feature)); } } } - else if (trackedSource is IDestination ats) + else if (trackedSourceContext.TrackedSource is IDestination ats) { - await RepositoryProvider.GetRepository(network).Track(ats); + await repo.Track(ats); } return Ok(); } @@ -580,33 +566,27 @@ private GenerateAddressQuery GenerateAddressQuery(TrackWalletRequest request, De } [HttpGet] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/transactions/{txId?}")] - [Route("cryptos/{cryptoCode}/addresses/{address}/transactions/{txId?}")] - [Route("cryptos/{cryptoCode}/wallets/{walletId}/transactions/{txId?}")] + + + [Route($"{CommonRoutes.DerivationEndpoint}/{CommonRoutes.TransactionsPath}")] + [Route($"{CommonRoutes.AddressEndpoint}/{CommonRoutes.TransactionsPath}")] + [Route($"{CommonRoutes.WalletEndpoint}/{CommonRoutes.TransactionsPath}")] + [Route($"{CommonRoutes.TrackedSourceEndpoint}/{CommonRoutes.TransactionsPath}")] public async Task GetTransactions( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, - [ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))] - BitcoinAddress address, - string walletId, + TrackedSourceContext trackedSourceContext, [ModelBinder(BinderType = typeof(UInt256ModelBinding))] uint256 txId = null, bool includeTransaction = true) { - var trackedSource = GetTrackedSource(derivationScheme, address, walletId); - if (trackedSource == null) - throw new ArgumentNullException(nameof(trackedSource)); TransactionInformation fetchedTransactionInfo = null; - var network = GetNetwork(cryptoCode, false); - var repo = RepositoryProvider.GetRepository(network); + var repo = RepositoryProvider.GetRepository(trackedSourceContext.Network); var response = new GetTransactionsResponse(); int currentHeight = (await repo.GetTip()).Height; response.Height = currentHeight; - var txs = await GetAnnotatedTransactions(repo, trackedSource, includeTransaction, txId); + var txs = await GetAnnotatedTransactions(repo, trackedSourceContext.TrackedSource, includeTransaction, txId); foreach (var item in new[] { new @@ -654,7 +634,7 @@ public async Task GetTransactions( if (txId != null && txId == txInfo.TransactionId) fetchedTransactionInfo = txInfo; - if (network.NBitcoinNetwork.NetworkSet == NBitcoin.Altcoins.Liquid.Instance) + if (trackedSourceContext.Network.NBitcoinNetwork.NetworkSet == NBitcoin.Altcoins.Liquid.Instance) { txInfo.BalanceChange = new MoneyBag(txInfo.Outputs.Select(o => o.Value).OfType().ToArray()) - new MoneyBag(txInfo.Inputs.Select(o => o.Value).OfType().ToArray()); @@ -684,26 +664,28 @@ public async Task GetTransactions( } [HttpPost] - [Route("cryptos/{cryptoCode}/rescan")] - public async Task Rescan(string cryptoCode, [FromBody] JObject body) + [Route($"{CommonRoutes.BaseCryptoEndpoint}/rescan")] + [TrackedSourceContext.TrackedSourceContextRequirement(false, false, true)] + public async Task Rescan(TrackedSourceContext trackedSourceContext, [FromBody] JObject body) { if (body == null) throw new ArgumentNullException(nameof(body)); - var rescanRequest = ParseJObject(body, GetNetwork(cryptoCode, false)); + var rescanRequest = ParseJObject(body, trackedSourceContext.Network); if (rescanRequest == null) throw new ArgumentNullException(nameof(rescanRequest)); if (rescanRequest?.Transactions == null) throw new NBXplorerException(new NBXplorerError(400, "transactions-missing", "You must specify 'transactions'")); bool willFetchTransactions = rescanRequest.Transactions.Any(t => t.Transaction == null); - bool needTxIndex = rescanRequest.Transactions.Any(t => t.Transaction == null && t.BlockId == null); - var network = GetNetwork(cryptoCode, willFetchTransactions); + if (willFetchTransactions && trackedSourceContext.RpcClient is null) + { + TrackedSourceContext.TrackedSourceContextModelBinder.ThrowRpcUnavailableException(); + } - var rpc = RPCClients.Get(network).PrepareBatch(); - var repo = RepositoryProvider.GetRepository(network); + var rpc = trackedSourceContext.RpcClient!.PrepareBatch(); var fetchingTransactions = rescanRequest .Transactions - .Select(t => FetchTransaction(rpc, HasTxIndex(network), t)) + .Select(t => FetchTransaction(rpc, HasTxIndex(trackedSourceContext.Network), t)) .ToArray(); await rpc.SendBatchAsync(); @@ -723,17 +705,17 @@ public async Task Rescan(string cryptoCode, [FromBody] JObject bo } } await batch.SendBatchAsync(); - await repo.SaveBlocks(blocks.Select(b => b.Value.Result).ToList()); + await trackedSourceContext.Repository.SaveBlocks(blocks.Select(b => b.Value.Result).ToList()); foreach (var txs in transactions.GroupBy(t => t.BlockId, t => (t.Transaction, t.BlockTime)) .OrderBy(t => t.First().BlockTime)) { blocks.TryGetValue(txs.Key, out var slimBlock); - await repo.SaveTransactions(txs.First().BlockTime, txs.Select(t => t.Transaction).ToArray(), slimBlock.Result); + await trackedSourceContext.Repository.SaveTransactions(txs.First().BlockTime, txs.Select(t => t.Transaction).ToArray(), slimBlock.Result); foreach (var tx in txs) { - var matches = await repo.GetMatches(tx.Transaction, slimBlock.Result, tx.BlockTime, false); - await repo.SaveMatches(matches); - _ = AddressPoolService.GenerateAddresses(network, matches); + var matches = await trackedSourceContext.Repository.GetMatches(tx.Transaction, slimBlock.Result, tx.BlockTime, false); + await trackedSourceContext.Repository.SaveMatches(matches); + _ = AddressPoolService.GenerateAddresses(trackedSourceContext.Network, matches); } } return Ok(); @@ -787,59 +769,37 @@ public async Task Rescan(string cryptoCode, [FromBody] JObject bo } [HttpPost] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/metadata/{key}")] - public async Task SetMetadata(string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, string key, - [FromBody] - JToken value = null) + [Route($"{CommonRoutes.DerivationEndpoint}/metadata/{{key}}")] + public async Task SetMetadata(TrackedSourceContext trackedSourceContext, string key, [FromBody] JToken value = null) { - var network = this.GetNetwork(cryptoCode, true); - var trackedSource = new DerivationSchemeTrackedSource(derivationScheme); - var repo = this.RepositoryProvider.GetRepository(network); - await repo.SaveMetadata(trackedSource, key, value); + await trackedSourceContext.Repository.SaveMetadata(trackedSourceContext.TrackedSource, key, value); return Ok(); } [HttpGet] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/metadata/{key}")] - public async Task GetMetadata(string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, string key) + [Route($"{CommonRoutes.DerivationEndpoint}/metadata/{{key}}")] + public async Task GetMetadata(TrackedSourceContext trackedSourceContext, string key) { - var network = this.GetNetwork(cryptoCode, false); - var trackedSource = new DerivationSchemeTrackedSource(derivationScheme); - var repo = this.RepositoryProvider.GetRepository(network); - var result = await repo.GetMetadata(trackedSource, key); - return result == null ? (IActionResult)NotFound() : Json(result, repo.Serializer.Settings); + var result = await trackedSourceContext.Repository.GetMetadata(trackedSourceContext.TrackedSource, key); + return result == null ? NotFound() : Json(result, trackedSourceContext.Repository.Serializer.Settings); } [HttpPost] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/utxos/wipe")] - public async Task Wipe( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme) + [Route($"{CommonRoutes.DerivationEndpoint}/utxos/wipe")] + public async Task Wipe(TrackedSourceContext trackedSourceContext) { - var network = this.GetNetwork(cryptoCode, true); - var repo = RepositoryProvider.GetRepository(network); - var ts = new DerivationSchemeTrackedSource(derivationScheme); - var txs = await repo.GetTransactions(ts); - await repo.Prune(ts, txs); + var txs = await trackedSourceContext.Repository.GetTransactions(trackedSourceContext.TrackedSource); + await trackedSourceContext.Repository.Prune(trackedSourceContext.TrackedSource, txs); return Ok(); } [HttpPost] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/utxos/scan")] - public IActionResult ScanUTXOSet( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, int? batchSize = null, int? gapLimit = null, int? from = null) + [Route($"{CommonRoutes.DerivationEndpoint}/utxos/scan")] + [TrackedSourceContext.TrackedSourceContextRequirement(requireRPC:true,allowedTrackedSourceTypes: new []{typeof(DerivationSchemeTrackedSource)})] + public IActionResult ScanUTXOSet(TrackedSourceContext trackedSourceContext, int? batchSize = null, int? gapLimit = null, int? from = null) { - var network = this.GetNetwork(cryptoCode, true); - var rpc = GetAvailableRPC(network); - if (!rpc.Capabilities.SupportScanUTXOSet) + if (!trackedSourceContext.RpcClient.Capabilities.SupportScanUTXOSet) throw new NBXplorerError(405, "scanutxoset-not-suported", "ScanUTXOSet is not supported for this currency").AsException(); ScanUTXOSetOptions options = new ScanUTXOSetOptions(); @@ -849,47 +809,40 @@ public IActionResult ScanUTXOSet( options.GapLimit = gapLimit.Value; if (from != null) options.From = from.Value; - if (!ScanUTXOSetService.EnqueueScan(network, derivationScheme, options)) + if (!ScanUTXOSetService.EnqueueScan(trackedSourceContext.Network, ((DerivationSchemeTrackedSource) trackedSourceContext.TrackedSource).DerivationStrategy, options)) throw new NBXplorerError(409, "scanutxoset-in-progress", "ScanUTXOSet has already been called for this derivationScheme").AsException(); return Ok(); } [HttpGet] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/utxos/scan")] - public IActionResult GetScanUTXOSetInfromation( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme) + [Route($"{CommonRoutes.DerivationEndpoint}/utxos/scan")] + [TrackedSourceContext.TrackedSourceContextRequirement(allowedTrackedSourceTypes: new []{typeof(DerivationSchemeTrackedSource)})] + public IActionResult GetScanUTXOSetInfromation(TrackedSourceContext trackedSourceContext) { - var network = this.GetNetwork(cryptoCode, false); - var info = ScanUTXOSetService.GetInformation(network, derivationScheme); + var info = ScanUTXOSetService.GetInformation(trackedSourceContext.Network, ((DerivationSchemeTrackedSource) trackedSourceContext.TrackedSource).DerivationStrategy); if (info == null) throw new NBXplorerError(404, "scanutxoset-info-not-found", "ScanUTXOSet has not been called with this derivationScheme of the result has expired").AsException(); - return Json(info, network.Serializer.Settings); + return Json(info, trackedSourceContext.Network.Serializer.Settings); } #if SUPPORT_DBTRIE [HttpGet] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/balance")] - [Route("cryptos/{cryptoCode}/addresses/{address}/balance")] + [Route($"{CommonRoutes.DerivationEndpoint}/balance")] + [Route($"{CommonRoutes.AddressEndpoint}/balance")] + [TrackedSourceContext.TrackedSourceContextRequirement(allowedTrackedSourceTypes: new []{typeof(DerivationSchemeTrackedSource),typeof(AddressTrackedSource)})] [PostgresImplementationActionConstraint(false)] - public async Task GetBalance(string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, - [ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))] - BitcoinAddress address) + public async Task GetBalance(TrackedSourceContext trackedSourceContext) { - var getTransactionsResult = await GetTransactions(cryptoCode, derivationScheme, address, null, includeTransaction: false); + var getTransactionsResult = await GetTransactions(trackedSourceContext, includeTransaction: false); var jsonResult = getTransactionsResult as JsonResult; var transactions = jsonResult?.Value as GetTransactionsResponse; if (transactions == null) return getTransactionsResult; - var network = this.GetNetwork(cryptoCode, false); var balance = new GetBalanceResponse() { - Confirmed = CalculateBalance(network, transactions.ConfirmedTransactions), - Unconfirmed = CalculateBalance(network, transactions.UnconfirmedTransactions), - Immature = CalculateBalance(network, transactions.ImmatureTransactions) + Confirmed = CalculateBalance(trackedSourceContext.Network, transactions.ConfirmedTransactions), + Unconfirmed = CalculateBalance(trackedSourceContext.Network, transactions.UnconfirmedTransactions), + Immature = CalculateBalance(trackedSourceContext.Network, transactions.ImmatureTransactions) }; balance.Total = balance.Confirmed.Add(balance.Unconfirmed); balance.Available = balance.Total.Sub(balance.Immature); @@ -909,31 +862,19 @@ private IMoney CalculateBalance(NBXplorerNetwork network, TransactionInformation } [HttpGet] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/utxos")] - [Route("cryptos/{cryptoCode}/addresses/{address}/utxos")] - [Route("cryptos/{cryptoCode}/wallets/{walletId}/utxos")] - + [Route($"{CommonRoutes.DerivationEndpoint}/utxos")] + [Route($"{CommonRoutes.AddressEndpoint}/utxos")] [PostgresImplementationActionConstraint(false)] - public async Task GetUTXOs( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, - [ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))] - BitcoinAddress address, - string walletId) + [TrackedSourceContext.TrackedSourceContextRequirement(allowedTrackedSourceTypes: new []{typeof(DerivationSchemeTrackedSource),typeof(AddressTrackedSource)})] + public async Task GetUTXOs(TrackedSourceContext trackedSourceContext) { - var trackedSource = GetTrackedSource(derivationScheme, address, walletId); UTXOChanges changes = null; - if (trackedSource == null) - throw new ArgumentNullException(nameof(trackedSource)); - - var network = GetNetwork(cryptoCode, false); - var repo = RepositoryProvider.GetRepository(network); + var repo = RepositoryProvider.GetRepository(trackedSourceContext.Network); changes = new UTXOChanges(); changes.CurrentHeight = (await repo.GetTip()).Height; - var transactions = await GetAnnotatedTransactions(repo, trackedSource, false); + var transactions = await GetAnnotatedTransactions(repo, trackedSourceContext.TrackedSource, false); changes.Confirmed = ToUTXOChange(transactions.ConfirmedState); changes.Confirmed.SpentOutpoints.Clear(); @@ -942,8 +883,8 @@ public async Task GetUTXOs( FillUTXOsInformation(changes.Confirmed.UTXOs, transactions, changes.CurrentHeight); FillUTXOsInformation(changes.Unconfirmed.UTXOs, transactions, changes.CurrentHeight); - changes.TrackedSource = trackedSource; - changes.DerivationStrategy = (trackedSource as DerivationSchemeTrackedSource)?.DerivationStrategy; + changes.TrackedSource = trackedSourceContext.TrackedSource; + changes.DerivationStrategy = (trackedSourceContext.TrackedSource as DerivationSchemeTrackedSource)?.DerivationStrategy; return Json(changes, repo.Serializer.Settings); } @@ -994,37 +935,28 @@ private async Task GetAnnotatedTransactions(IRep } [HttpPost] - [Route("cryptos/{cryptoCode}/transactions")] + [Route($"{CommonRoutes.BaseCryptoEndpoint}/transactions")] + [TrackedSourceContext.TrackedSourceContextRequirement(true, false)] public async Task Broadcast( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase extPubKey, // For back compat - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, - [ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))] - BitcoinAddress address, - string walletId, + TrackedSourceContext trackedSourceContext, bool testMempoolAccept = false) { - var network = GetNetwork(cryptoCode, true); - var trackedSource = GetTrackedSource(derivationScheme ?? extPubKey, address, walletId); - var tx = network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTransaction(); + + var tx = trackedSourceContext.Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTransaction(); var buffer = new MemoryStream(); await Request.Body.CopyToAsync(buffer); buffer.Position = 0; tx.FromBytes(buffer.ToArrayEfficient()); - var rpc = GetAvailableRPC(network); - if (testMempoolAccept && !rpc.Capabilities.SupportTestMempoolAccept) + if (testMempoolAccept && !trackedSourceContext.RpcClient.Capabilities.SupportTestMempoolAccept) throw new NBXplorerException(new NBXplorerError(400, "not-supported", "This feature is not supported for this crypto currency")); - var repo = RepositoryProvider.GetRepository(network); - var indexer = Indexers.GetIndexer(network); + var repo = RepositoryProvider.GetRepository(trackedSourceContext.Network); RPCException rpcEx = null; try { if (testMempoolAccept) { - var mempoolAccept = await rpc.TestMempoolAcceptAsync(tx, default); + var mempoolAccept = await trackedSourceContext.RpcClient.TestMempoolAcceptAsync(tx, default); if (mempoolAccept.IsAllowed) return new BroadcastResult(true); var rpcCode = GetRPCCodeFromReason(mempoolAccept.RejectReason); @@ -1035,18 +967,18 @@ public async Task Broadcast( RPCCodeMessage = mempoolAccept.RejectReason, }; } - await rpc.SendRawTransactionAsync(tx); - await indexer.SaveMatches(tx); + await trackedSourceContext.RpcClient.SendRawTransactionAsync(tx); + await trackedSourceContext.Indexer.SaveMatches(tx); return new BroadcastResult(true); } catch (RPCException ex) when (!testMempoolAccept) { rpcEx = ex; - Logs.Explorer.LogInformation($"{network.CryptoCode}: Transaction {tx.GetHash()} failed to broadcast (Code: {ex.RPCCode}, Message: {ex.RPCCodeMessage}, Details: {ex.Message} )"); - if (trackedSource != null && ex.Message.StartsWith("Missing inputs", StringComparison.OrdinalIgnoreCase)) + Logs.Explorer.LogInformation($"{trackedSourceContext.Network.CryptoCode}: Transaction {tx.GetHash()} failed to broadcast (Code: {ex.RPCCode}, Message: {ex.RPCCodeMessage}, Details: {ex.Message} )"); + if (trackedSourceContext.TrackedSource != null && ex.Message.StartsWith("Missing inputs", StringComparison.OrdinalIgnoreCase)) { - Logs.Explorer.LogInformation($"{network.CryptoCode}: Trying to broadcast unconfirmed of the wallet"); - var transactions = await GetAnnotatedTransactions(repo, trackedSource, true); + Logs.Explorer.LogInformation($"{trackedSourceContext.Network.CryptoCode}: Trying to broadcast unconfirmed of the wallet"); + var transactions = await GetAnnotatedTransactions(repo, trackedSourceContext.TrackedSource, true); foreach (var existing in transactions.UnconfirmedTransactions) { var t = existing.Record.Transaction ?? (await repo.GetSavedTransactions(existing.Record.TransactionHash)).Select(c => c.Transaction).FirstOrDefault(); @@ -1054,21 +986,21 @@ public async Task Broadcast( continue; try { - await rpc.SendRawTransactionAsync(t); + await trackedSourceContext.RpcClient.SendRawTransactionAsync(t); } catch { } } try { - await rpc.SendRawTransactionAsync(tx); - Logs.Explorer.LogInformation($"{network.CryptoCode}: Broadcast success"); - await indexer.SaveMatches(tx); + await trackedSourceContext.RpcClient.SendRawTransactionAsync(tx); + Logs.Explorer.LogInformation($"{trackedSourceContext.Network.CryptoCode}: Broadcast success"); + await trackedSourceContext.Indexer.SaveMatches(tx); return new BroadcastResult(true); } catch (RPCException) { - Logs.Explorer.LogInformation($"{network.CryptoCode}: Transaction {tx.GetHash()} failed to broadcast (Code: {ex.RPCCode}, Message: {ex.RPCCodeMessage}, Details: {ex.Message} )"); + Logs.Explorer.LogInformation($"{trackedSourceContext.Network.CryptoCode}: Transaction {tx.GetHash()} failed to broadcast (Code: {ex.RPCCode}, Message: {ex.RPCCodeMessage}, Details: {ex.Message} )"); } } return new BroadcastResult(false) @@ -1093,15 +1025,17 @@ public async Task Broadcast( } [HttpPost] - [Route("cryptos/{cryptoCode}/derivations")] - public async Task GenerateWallet(string cryptoCode, [FromBody] JObject rawRequest = null) + + [Route($"{CommonRoutes.BaseDerivationEndpoint}")] + [TrackedSourceContext.TrackedSourceContextRequirement(false, false)] + public async Task GenerateWallet(TrackedSourceContext trackedSourceContext, [FromBody] JObject rawRequest = null) { - var request = ParseJObject(rawRequest, GetNetwork(cryptoCode, false)); + var request = ParseJObject(rawRequest, trackedSourceContext.Network); if (request == null) request = new GenerateWalletRequest(); - var network = GetNetwork(cryptoCode, request.ImportKeysToRPC); - if (network.CoinType == null) + GetNetwork(trackedSourceContext.Network.CryptoCode, request.ImportKeysToRPC); + if (trackedSourceContext.Network.CoinType == null) // Don't document, only shitcoins nobody use goes into this throw new NBXplorerException(new NBXplorerError(400, "not-supported", "This feature is not supported for this coin because we don't have CoinType information")); request.WordList ??= Wordlist.English; @@ -1109,12 +1043,12 @@ public async Task GenerateWallet(string cryptoCode, [FromBody] JO request.ScriptPubKeyType ??= ScriptPubKeyType.Segwit; if (request.ScriptPubKeyType is null) { - request.ScriptPubKeyType = network.NBitcoinNetwork.Consensus.SupportSegwit ? ScriptPubKeyType.Segwit : ScriptPubKeyType.Legacy; + request.ScriptPubKeyType = trackedSourceContext.Network.NBitcoinNetwork.Consensus.SupportSegwit ? ScriptPubKeyType.Segwit : ScriptPubKeyType.Legacy; } - if (!network.NBitcoinNetwork.Consensus.SupportSegwit && request.ScriptPubKeyType != ScriptPubKeyType.Legacy) + if (!trackedSourceContext.Network.NBitcoinNetwork.Consensus.SupportSegwit && request.ScriptPubKeyType != ScriptPubKeyType.Legacy) throw new NBXplorerException(new NBXplorerError(400, "segwit-not-supported", "Segwit is not supported, please explicitely set scriptPubKeyType to Legacy")); - var repo = RepositoryProvider.GetRepository(network); + var repo = RepositoryProvider.GetRepository(trackedSourceContext.Network); if (repo is not PostgresRepository && request.ParentWallet is not null) throw new NBXplorerException(new NBXplorerError(400, "parent-wallet-not-supported", "Parent wallet is only supported with Postgres")); @@ -1134,10 +1068,10 @@ public async Task GenerateWallet(string cryptoCode, [FromBody] JO { mnemonic = new Mnemonic(request.WordList, request.WordCount.Value); } - var masterKey = mnemonic.DeriveExtKey(request.Passphrase).GetWif(network.NBitcoinNetwork); - var keyPath = GetDerivationKeyPath(request.ScriptPubKeyType.Value, request.AccountNumber, network); + var masterKey = mnemonic.DeriveExtKey(request.Passphrase).GetWif(trackedSourceContext.Network.NBitcoinNetwork); + var keyPath = GetDerivationKeyPath(request.ScriptPubKeyType.Value, request.AccountNumber, trackedSourceContext.Network); var accountKey = masterKey.Derive(keyPath); - DerivationStrategyBase derivation = network.DerivationStrategyFactory.CreateDirectDerivationStrategy(accountKey.Neuter(), new DerivationStrategyOptions() + DerivationStrategyBase derivation = trackedSourceContext.Network.DerivationStrategyFactory.CreateDirectDerivationStrategy(accountKey.Neuter(), new DerivationStrategyOptions() { ScriptPubKeyType = request.ScriptPubKeyType.Value, AdditionalOptions = request.AdditionalOptions is not null ? new System.Collections.ObjectModel.ReadOnlyDictionary(request.AdditionalOptions) : null @@ -1164,13 +1098,20 @@ public async Task GenerateWallet(string cryptoCode, [FromBody] JO } var accountKeyPath = new RootedKeyPath(masterKey.GetPublicKey().GetHDFingerPrint(), keyPath); saveMetadata.Add(repo.SaveMetadata(derivationTrackedSource, WellknownMetadataKeys.AccountKeyPath, accountKeyPath)); - var importAddressToRPC = await GetImportAddressToRPC(request, network); + var importAddressToRPC = await GetImportAddressToRPC(request, trackedSourceContext.Network); saveMetadata.Add(repo.SaveMetadata(derivationTrackedSource, WellknownMetadataKeys.ImportAddressToRPC, (importAddressToRPC?.ToString() ?? "False"))); var descriptor = GetDescriptor(accountKeyPath, accountKey.Neuter(), request.ScriptPubKeyType.Value); saveMetadata.Add(repo.SaveMetadata(derivationTrackedSource, WellknownMetadataKeys.AccountDescriptor, descriptor)); await Task.WhenAll(saveMetadata.ToArray()); - await TrackWallet(cryptoCode, derivation,null, null); + + await TrackWallet(new TrackedSourceContext() + { + Indexer = trackedSourceContext.Indexer, + Network = trackedSourceContext.Network, + RpcClient = trackedSourceContext.RpcClient, + TrackedSource = new DerivationSchemeTrackedSource(derivation) + }); return Json(new GenerateWalletResponse() { MasterHDKey = masterKey, @@ -1182,7 +1123,7 @@ public async Task GenerateWallet(string cryptoCode, [FromBody] JO Passphrase = request.Passphrase ?? string.Empty, WordCount = request.WordCount.Value, WordList = request.WordList - }, network.Serializer.Settings); + }, trackedSourceContext.Network.Serializer.Settings); } private async Task GetImportAddressToRPC(GenerateWalletRequest request, NBXplorerNetwork network) @@ -1258,23 +1199,18 @@ private KeyPath GetDerivationKeyPath(ScriptPubKeyType scriptPubKeyType, int acco } [HttpPost] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/prune")] - public async Task Prune( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, [FromBody] PruneRequest request) + [Route($"{CommonRoutes.DerivationEndpoint}/prune")] + [TrackedSourceContext.TrackedSourceContextRequirement(allowedTrackedSourceTypes: new []{typeof(DerivationSchemeTrackedSource)})] + public async Task Prune(TrackedSourceContext trackedSourceContext ,[FromBody] PruneRequest request) { request ??= new PruneRequest(); request.DaysToKeep ??= 1.0; - var trackedSource = new DerivationSchemeTrackedSource(derivationScheme); - var network = GetNetwork(cryptoCode, false); - var repo = RepositoryProvider.GetRepository(network); - var transactions = await GetAnnotatedTransactions(repo, trackedSource, false); + var transactions = await GetAnnotatedTransactions(trackedSourceContext.Repository, trackedSourceContext.TrackedSource, false); var state = transactions.ConfirmedState; var prunableIds = new HashSet(); - var keepConfMax = network.NBitcoinNetwork.Consensus.GetExpectedBlocksFor(TimeSpan.FromDays(request.DaysToKeep.Value)); - var tip = (await repo.GetTip()).Height; + var keepConfMax = trackedSourceContext.Network.NBitcoinNetwork.Consensus.GetExpectedBlocksFor(TimeSpan.FromDays(request.DaysToKeep.Value)); + var tip = (await trackedSourceContext.Repository.GetTip()).Height; // Step 1. We can prune if all UTXOs are spent foreach (var tx in transactions.ConfirmedTransactions) { @@ -1314,22 +1250,17 @@ public async Task Prune( if (prunableIds.Count != 0) { - await repo.Prune(trackedSource, prunableIds + await trackedSourceContext.Repository.Prune(trackedSourceContext.TrackedSource, prunableIds .Select(id => transactions.GetByTxId(id).Record) .ToList()); - Logs.Explorer.LogInformation($"{network.CryptoCode}: Pruned {prunableIds.Count} transactions"); + Logs.Explorer.LogInformation($"{trackedSourceContext.Network.CryptoCode}: Pruned {prunableIds.Count} transactions"); } return new PruneResponse() { TotalPruned = prunableIds.Count }; } -#if SUPPORT_DBTRIE - public Task GetUTXOs(string cryptoCode, DerivationStrategyBase derivationStrategy) - { - return this.GetUTXOs(cryptoCode, derivationStrategy, null, null); - } -#else - public Task GetUTXOs(string cryptoCode, DerivationStrategyBase derivationStrategy) +#if !SUPPORT_DBTRIE + public async Task GetUTXOs(TrackedSourceContext trackedSourceContext) { - throw new NotSupportedException("This should never be called"); + throw new NotImplementedException(); } #endif } diff --git a/NBXplorer/Controllers/PostgresMainController.cs b/NBXplorer/Controllers/PostgresMainController.cs index 568d6d803..d8fa4f4a5 100644 --- a/NBXplorer/Controllers/PostgresMainController.cs +++ b/NBXplorer/Controllers/PostgresMainController.cs @@ -1,36 +1,32 @@ -using Dapper; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Dapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NBitcoin; -using NBXplorer.Backends; +using NBitcoin.RPC; using NBXplorer.Backends.Postgres; using NBXplorer.DerivationStrategy; -using NBXplorer.ModelBinders; using NBXplorer.Models; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using NBitcoin.DataEncoders; -using NBitcoin.RPC; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace NBXplorer.Controllers { - [Route("v1")] + [PostgresImplementationActionConstraint(true)] + [Route($"v1/{CommonRoutes.DerivationEndpoint}")] + [Route($"v1/{CommonRoutes.AddressEndpoint}")] + [Route($"v1/{CommonRoutes.WalletEndpoint}")] + [Route($"v1/{CommonRoutes.TrackedSourceEndpoint}")] [Authorize] - public class PostgresMainController : ControllerBase, IUTXOService + public class PostgresMainController :Controller, IUTXOService { public PostgresMainController( DbConnectionFactory connectionFactory, - NBXplorerNetworkProvider networkProvider, - IRPCClients rpcClients, - IIndexers indexers, - KeyPathTemplates keyPathTemplates, - IRepositoryProvider repositoryProvider) : base(networkProvider, rpcClients, repositoryProvider, indexers) + KeyPathTemplates keyPathTemplates) { ConnectionFactory = connectionFactory; KeyPathTemplates = keyPathTemplates; @@ -39,25 +35,12 @@ public PostgresMainController( public DbConnectionFactory ConnectionFactory { get; } public KeyPathTemplates KeyPathTemplates { get; } - [HttpGet] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/balance")] - [Route("cryptos/{cryptoCode}/addresses/{address}/balance")] - [Route("cryptos/{cryptoCode}/wallets/{walletId}/balance")] - [PostgresImplementationActionConstraint(true)] - public async Task GetBalance(string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, - [ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))] - BitcoinAddress address, - string walletId) + [HttpGet("balance")] + public async Task GetBalance( TrackedSourceContext trackedSourceContext) { - var trackedSource = GetTrackedSource(derivationScheme, address, walletId); - if (trackedSource == null) - throw new ArgumentNullException(nameof(trackedSource)); - var network = GetNetwork(cryptoCode, false); - var repo = (PostgresRepository)RepositoryProvider.GetRepository(cryptoCode); + var repo = (PostgresRepository)trackedSourceContext.Repository; await using var conn = await ConnectionFactory.CreateConnection(); - var b = await conn.QueryAsync("SELECT * FROM wallets_balances WHERE code=@code AND wallet_id=@walletId", new { code = network.CryptoCode, walletId = repo.GetWalletKey(trackedSource).wid }); + var b = await conn.QueryAsync("SELECT * FROM wallets_balances WHERE code=@code AND wallet_id=@walletId", new { code = trackedSourceContext.Network.CryptoCode, walletId = repo.GetWalletKey(trackedSourceContext.TrackedSource).wid }); MoneyBag available = new MoneyBag(), confirmed = new MoneyBag(), @@ -87,68 +70,39 @@ public async Task GetBalance(string cryptoCode, var balance = new GetBalanceResponse() { - Confirmed = Format(network, confirmed), - Unconfirmed = Format(network, unconfirmed), - Available = Format(network, available), - Total = Format(network, total), - Immature = Format(network, immature) + Confirmed = Format(trackedSourceContext.Network, confirmed), + Unconfirmed = Format(trackedSourceContext.Network, unconfirmed), + Available = Format(trackedSourceContext.Network, available), + Total = Format(trackedSourceContext.Network, total), + Immature = Format(trackedSourceContext.Network, immature) }; balance.Total = balance.Confirmed.Add(balance.Unconfirmed); - return Json(balance, network.JsonSerializerSettings); + return Json(balance, trackedSourceContext.Network.JsonSerializerSettings); } - [HttpPost] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/associate")] - [Route("cryptos/{cryptoCode}/addresses/{address}/associate")] - [Route("cryptos/{cryptoCode}/wallets/{walletId}/associate")] + [HttpPost("associate")] [PostgresImplementationActionConstraint(true)] - public async Task AssociateScripts(string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, - [ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))] - BitcoinAddress address, - string walletId, + public async Task AssociateScripts( TrackedSourceContext trackedSourceContext, [FromBody] Dictionary scripts) { - var trackedSource = GetTrackedSource(derivationScheme, address, walletId); - if (trackedSource == null) - throw new ArgumentNullException(nameof(trackedSource)); - var network = GetNetwork(cryptoCode, false); - var repo = (PostgresRepository)RepositoryProvider.GetRepository(cryptoCode); - - await repo.AssociateScriptsToWalletExplicitly(trackedSource, - scripts.ToDictionary(pair => (IDestination) BitcoinAddress.Create(pair.Key, network.NBitcoinNetwork), + var repo = (PostgresRepository)trackedSourceContext.Repository; + await repo.AssociateScriptsToWalletExplicitly(trackedSourceContext.TrackedSource, + scripts.ToDictionary(pair => (IDestination) BitcoinAddress.Create(pair.Key, trackedSourceContext.Network.NBitcoinNetwork), pair => pair.Value)); return Ok(); } - - - [HttpPost] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/import-utxos")] - [Route("cryptos/{cryptoCode}/addresses/{address}/import-utxos")] - [Route("cryptos/{cryptoCode}/wallets/{walletId}/import-utxos")] - [PostgresImplementationActionConstraint(true)] - public async Task ImportUTXOs(string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, - [ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))] - BitcoinAddress address, - string walletId, - [FromBody] JArray rawRequest) + [HttpPost("import-utxos")] + [TrackedSourceContext.TrackedSourceContextRequirement(true)] + public async Task ImportUTXOs( TrackedSourceContext trackedSourceContext, [FromBody] JArray rawRequest) { - var network = GetNetwork(cryptoCode, true); - var jsonSerializer = JsonSerializer.Create(network.JsonSerializerSettings); + var jsonSerializer = JsonSerializer.Create(trackedSourceContext.Network.JsonSerializerSettings); var coins = rawRequest.ToObject(jsonSerializer)?.Where(c => c.Coin != null).ToArray(); if (coins?.Any() is not true) throw new ArgumentNullException(nameof(coins)); - var trackedSource = GetTrackedSource(derivationScheme, address, walletId); - if (trackedSource == null) - throw new ArgumentNullException(nameof(trackedSource)); - var repo = (PostgresRepository) RepositoryProvider.GetRepository(cryptoCode); - - var rpc = RPCClients.Get(network); + var repo = (PostgresRepository)trackedSourceContext.Repository; + var rpc = trackedSourceContext.RpcClient; var clientBatch = rpc.PrepareBatch(); var coinToTxOut = new ConcurrentDictionary(); @@ -193,7 +147,7 @@ await Task.WhenAll(coins.SelectMany(o => await repo.SaveMatches(coinToTxOut.Select(pair => { coinToBlock.TryGetValue(pair.Key, out var blockHeader); - var ttx = repo.CreateTrackedTransaction(trackedSource, + var ttx = repo.CreateTrackedTransaction(trackedSourceContext.TrackedSource, new TrackedTransactionKey(pair.Key.Outpoint.Hash, blockHeader?.GetHash(), true){}, new[] {pair.Key}, null); ttx.Inserted = now; @@ -222,32 +176,17 @@ private static MoneyBag RemoveZeros(MoneyBag bag) return new MoneyBag(bag.Where(a => !a.Negate().Equals(a)).ToArray()); } - [HttpGet] - [Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/utxos")] - [Route("cryptos/{cryptoCode}/addresses/{address}/utxos")] - [Route("cryptos/{cryptoCode}/wallets/{walletId}/utxos")] - [PostgresImplementationActionConstraint(true)] - public async Task GetUTXOs( - string cryptoCode, - [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] - DerivationStrategyBase derivationScheme, - [ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))] - BitcoinAddress address, - string walletId) + [HttpGet("utxos")] + public async Task GetUTXOs( TrackedSourceContext trackedSourceContext) { - var trackedSource = GetTrackedSource(derivationScheme, address, walletId); - if (trackedSource == null) - throw new ArgumentNullException(nameof(trackedSource)); - var network = GetNetwork(cryptoCode, false); - var repo = (PostgresRepository)RepositoryProvider.GetRepository(cryptoCode); - + var repo = (PostgresRepository)trackedSourceContext.Repository; await using var conn = await ConnectionFactory.CreateConnection(); - var height = await conn.ExecuteScalarAsync("SELECT height FROM get_tip(@code)", new { code = network.CryptoCode }); - - + var height = await conn.ExecuteScalarAsync("SELECT height FROM get_tip(@code)", new { code = trackedSourceContext.Network.CryptoCode }); // On elements, we can't get blinded address from the scriptPubKey, so we need to fetch it rather than compute it string addrColumns = "NULL as address"; - if (network.IsElement && !derivationScheme.Unblinded()) + var derivationScheme = (trackedSourceContext.TrackedSource as DerivationSchemeTrackedSource) + ?.DerivationStrategy; + if (trackedSourceContext.Network.IsElement && derivationScheme?.Unblinded() is true) { addrColumns = "ds.metadata->>'blindedAddress' as address"; } @@ -274,11 +213,11 @@ public async Task GetUTXOs( bool input_mempool, DateTime tx_seen_at)>( $"SELECT blk_height, tx_id, wu.idx, value, script, {addrColumns}, {descriptorColumns}, mempool, input_mempool, seen_at " + - $"FROM wallets_utxos wu{descriptorJoin} WHERE code=@code AND wallet_id=@walletId AND immature IS FALSE", new { code = network.CryptoCode, walletId = repo.GetWalletKey(trackedSource).wid })); + $"FROM wallets_utxos wu{descriptorJoin} WHERE code=@code AND wallet_id=@walletId AND immature IS FALSE", new { code =trackedSourceContext.Network.CryptoCode, walletId = repo.GetWalletKey(trackedSourceContext.TrackedSource).wid })); UTXOChanges changes = new UTXOChanges() { CurrentHeight = (int)height, - TrackedSource = trackedSource, + TrackedSource = trackedSourceContext.TrackedSource, DerivationStrategy = derivationScheme }; foreach (var utxo in utxos.OrderBy(u => u.tx_seen_at)) @@ -303,7 +242,7 @@ public async Task GetUTXOs( u.KeyPath = KeyPath.Parse(utxo.keypath); u.Feature = Enum.Parse(utxo.feature); } - u.Address = utxo.address is null ? u.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : BitcoinAddress.Create(utxo.address, network.NBitcoinNetwork); + u.Address = utxo.address is null ? u.ScriptPubKey.GetDestinationAddress(trackedSourceContext.Network.NBitcoinNetwork) : BitcoinAddress.Create(utxo.address, trackedSourceContext.Network.NBitcoinNetwork); if (!utxo.mempool) { changes.Confirmed.UTXOs.Add(u); @@ -315,12 +254,7 @@ public async Task GetUTXOs( else // (utxo.mempool && utxo.input_mempool) changes.SpentUnconfirmed.Add(u); } - return Json(changes, network.JsonSerializerSettings); - } - - public Task GetUTXOs(string cryptoCode, DerivationStrategyBase derivationStrategy) - { - return this.GetUTXOs(cryptoCode, derivationStrategy, null, null); + return Json(changes, trackedSourceContext.Network.JsonSerializerSettings); } } } diff --git a/NBXplorer/Controllers/TrackedSourceContext.cs b/NBXplorer/Controllers/TrackedSourceContext.cs new file mode 100644 index 000000000..502dc99ee --- /dev/null +++ b/NBXplorer/Controllers/TrackedSourceContext.cs @@ -0,0 +1,135 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using NBitcoin; +using NBitcoin.RPC; +using NBXplorer.Backends; +using NBXplorer.Models; + +namespace NBXplorer.Controllers; + +[ModelBinder] +public class TrackedSourceContext +{ + public TrackedSource TrackedSource { get; set; } + public NBXplorerNetwork Network { get; set; } + public RPCClient RpcClient { get; set; } + public IIndexer Indexer { get; set; } + public IRepository Repository { get; set; } + + public class TrackedSourceContextRequirementAttribute: Attribute + { + public bool RequireRpc { get; } + public bool RequireTrackedSource { get; } + public bool DisallowTrackedSource { get; } + public Type[] AllowedTrackedSourceTypes { get; } + + public TrackedSourceContextRequirementAttribute(bool requireRPC = false, bool requireTrackedSource = true, bool disallowTrackedSource = false, params Type[] allowedTrackedSourceTypes) + { + RequireRpc = requireRPC; + RequireTrackedSource = requireTrackedSource; + DisallowTrackedSource = disallowTrackedSource; + AllowedTrackedSourceTypes = allowedTrackedSourceTypes; + + } + } + + public class TrackedSourceContextModelBinder : IModelBinder + { + public Task BindModelAsync(ModelBindingContext bindingContext) + { + var cryptoCode = bindingContext.ValueProvider.GetValue("cryptoCode").FirstValue?.ToUpperInvariant(); + + if (cryptoCode == null) + throw new ArgumentNullException(nameof(cryptoCode)); + + var addressValue = bindingContext.ValueProvider.GetValue("address").FirstValue; + var derivationSchemeValue = bindingContext.ValueProvider.GetValue("derivationScheme").FirstValue; + derivationSchemeValue??= bindingContext.ValueProvider.GetValue("extPubKey").FirstValue; + var walletIdValue = bindingContext.ValueProvider.GetValue("walletId").FirstValue; + var trackedSourceValue = bindingContext.ValueProvider.GetValue("trackedSource").FirstValue; + + var networkProvider = bindingContext.HttpContext.RequestServices.GetService(); + var indexers = bindingContext.HttpContext.RequestServices.GetService(); + var repositoryProvider = bindingContext.HttpContext.RequestServices.GetService(); + + var network = networkProvider.GetFromCryptoCode(cryptoCode); + + var indexer = network is null ? null : indexers.GetIndexer(network); + if (network is null || indexer is null) + { + throw new NBXplorerException(new NBXplorerError(404, "cryptoCode-not-supported", + $"{cryptoCode} is not supported")); + } + + var requirements = ((ControllerActionDescriptor) bindingContext.ActionContext.ActionDescriptor) + .MethodInfo.GetCustomAttributes().FirstOrDefault(); + + + var rpcClient = indexer.GetConnectedClient(); + if (rpcClient?.Capabilities == null) + { + rpcClient = null; + } + + if (requirements?.RequireRpc is true && rpcClient is null) + { + ThrowRpcUnavailableException(); + } + + var ts = GetTrackedSource(derivationSchemeValue, addressValue, walletIdValue, + trackedSourceValue, + network); + if (ts is null && requirements?.RequireTrackedSource is true) + { + + throw new NBXplorerException(new NBXplorerError(400, "tracked-source-required", + $"A tracked source is required for this endpoint.")); + } + if ( ts is not null && requirements?.DisallowTrackedSource is true) + { + throw new NBXplorerException(new NBXplorerError(400, "tracked-source-unwanted", + $"This endpoint does not tracked sources..")); + } + if(ts is not null && requirements?.AllowedTrackedSourceTypes?.Any() is true && !requirements.AllowedTrackedSourceTypes.Any(t => t.IsInstanceOfType(ts))) + { + throw new NBXplorerException(new NBXplorerError(400, "tracked-source-invalid", + $"The tracked source provided is not valid for this endpoint.")); + } + + bindingContext.Result = ModelBindingResult.Success(new TrackedSourceContext() + { + Indexer = indexer, + Network = network, + TrackedSource = ts , + RpcClient = rpcClient, + Repository = repositoryProvider.GetRepository(network) + }); + return Task.CompletedTask; + } + public static void ThrowRpcUnavailableException() + { + throw new NBXplorerError(400, "rpc-unavailable", $"The RPC interface is currently not available.").AsException(); + } + + public static TrackedSource GetTrackedSource(string derivationScheme, string address, string walletId, + string trackedSource, NBXplorerNetwork network) + { + if (trackedSource != null) + return TrackedSource.Parse(trackedSource, network); + if (address != null) + return new AddressTrackedSource(BitcoinAddress.Create(address, network.NBitcoinNetwork)); + if (derivationScheme != null) + return new DerivationSchemeTrackedSource(network.DerivationStrategyFactory.Parse(derivationScheme)); + if (walletId != null) + return new WalletTrackedSource(walletId); + return null; + } + } + +} \ No newline at end of file diff --git a/NBXplorer/IUTXOService.cs b/NBXplorer/IUTXOService.cs index ba78cadfe..fdfd881a1 100644 --- a/NBXplorer/IUTXOService.cs +++ b/NBXplorer/IUTXOService.cs @@ -1,11 +1,13 @@ using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; +using NBXplorer.Controllers; +using NBXplorer.DerivationStrategy; namespace NBXplorer { // Big hack to make CreatePSBT of MainController pick PostgresController as implementation for getting utxos. public interface IUTXOService { - Task GetUTXOs(string cryptoCode, DerivationStrategy.DerivationStrategyBase derivationStrategy); + Task GetUTXOs(TrackedSourceContext trackedSourceContext); } } diff --git a/NBXplorer/NBXplorer.csproj b/NBXplorer/NBXplorer.csproj index edef92adc..38421260b 100644 --- a/NBXplorer/NBXplorer.csproj +++ b/NBXplorer/NBXplorer.csproj @@ -6,7 +6,7 @@ 2.3.67 bin\$(Configuration)\$(TargetFramework)\NBXplorer.xml 1701;1702;1705;1591;CS1591 - 10.0 + 11 true $(DefineConstants);SUPPORT_DBTRIE diff --git a/global.json b/global.json new file mode 100644 index 000000000..dad2db5ef --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file