From cfedff079d950d917c5a29febc0c59e1e61c2863 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 5 Dec 2023 17:46:28 +0900 Subject: [PATCH] Add WalletTrackedSource to backend --- NBXplorer.Client/ExplorerClient.cs | 84 ++++++------ NBXplorer.Client/Models/TrackedSource.cs | 40 ++++++ NBXplorer.Tests/UnitTest1.cs | 1 - NBXplorer.Tests/xunit.runner.json | 3 +- .../Backends/Postgres/PostgresRepository.cs | 129 ++++++++++++++---- NBXplorer/BlockHeaders.cs | 2 +- NBXplorer/Controllers/MainController.PSBT.cs | 11 +- NBXplorer/Controllers/MainController.cs | 11 +- .../Controllers/PostgresMainController.cs | 8 +- NBXplorer/Controllers/TrackedSourceContext.cs | 28 ++-- .../021.KeyPathInfoReturnsWalletId.sql | 13 ++ NBXplorer/DBScripts/FullSchema.sql | 4 +- NBXplorer/Extensions.cs | 7 + NBXplorer/RPCClientExtensions.cs | 6 +- NBXplorer/TrackedTransaction.cs | 2 +- 15 files changed, 249 insertions(+), 100 deletions(-) create mode 100644 NBXplorer/DBScripts/021.KeyPathInfoReturnsWalletId.sql diff --git a/NBXplorer.Client/ExplorerClient.cs b/NBXplorer.Client/ExplorerClient.cs index ddc2d9f07..26b309723 100644 --- a/NBXplorer.Client/ExplorerClient.cs +++ b/NBXplorer.Client/ExplorerClient.cs @@ -172,7 +172,8 @@ public PruneResponse Prune(DerivationStrategyBase extKey, PruneRequest pruneRequ return PruneAsync(extKey, pruneRequest, cancellation).GetAwaiter().GetResult(); } - internal class RawStr { + internal class RawStr + { private string str; public RawStr(string str) { @@ -269,7 +270,7 @@ public void Track(DerivationStrategyBase strategy, CancellationToken cancellatio } public Task TrackAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default) { - return TrackAsync(TrackedSource.Create(strategy), cancellation); + return TrackAsync(TrackedSource.Create(strategy), cancellation: cancellation); } public void Track(DerivationStrategyBase strategy, TrackWalletRequest trackDerivationRequest, CancellationToken cancellation = default) @@ -285,14 +286,13 @@ public async Task TrackAsync(DerivationStrategyBase strategy, TrackWalletRequest public void Track(TrackedSource trackedSource, CancellationToken cancellation = default) { - TrackAsync(trackedSource, cancellation).GetAwaiter().GetResult(); + TrackAsync(trackedSource, cancellation: cancellation).GetAwaiter().GetResult(); } - public Task TrackAsync(TrackedSource trackedSource, CancellationToken cancellation = default) + public Task TrackAsync(TrackedSource trackedSource, TrackWalletRequest trackDerivationRequest = null, CancellationToken cancellation = default) { if (trackedSource == null) throw new ArgumentNullException(nameof(trackedSource)); - - return SendAsync(HttpMethod.Post, null, GetBasePath(trackedSource), cancellation); + return SendAsync(HttpMethod.Post, trackDerivationRequest, GetBasePath(trackedSource), cancellation); } private Exception UnSupported(TrackedSource trackedSource) @@ -311,7 +311,7 @@ public GetBalanceResponse GetBalance(DerivationStrategyBase userDerivationScheme } public Task GetBalanceAsync(DerivationStrategyBase userDerivationScheme, CancellationToken cancellation = default) { - return SendAsync(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{userDerivationScheme}/balance", cancellation); + return GetBalanceAsync(TrackedSource.Create(userDerivationScheme), cancellation); } @@ -321,9 +321,12 @@ public GetBalanceResponse GetBalance(BitcoinAddress address, CancellationToken c } public Task GetBalanceAsync(BitcoinAddress address, CancellationToken cancellation = default) { - return SendAsync(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{address}/balance", cancellation); + return GetBalanceAsync(TrackedSource.Create(address), cancellation); + } + public Task GetBalanceAsync(TrackedSource trackedSource, CancellationToken cancellation = default) + { + return SendAsync(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/balance", cancellation); } - public Task CancelReservationAsync(DerivationStrategyBase strategy, KeyPath[] keyPaths, CancellationToken cancellation = default) { return SendAsync(HttpMethod.Post, keyPaths, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/cancelreservation", cancellation); @@ -363,18 +366,7 @@ public Task GetTransactionsAsync(DerivationStrategyBase } public Task GetTransactionsAsync(TrackedSource trackedSource, CancellationToken cancellation = default) { - if (trackedSource == null) - throw new ArgumentNullException(nameof(trackedSource)); - if (trackedSource is DerivationSchemeTrackedSource dsts) - { - return SendAsync(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}/transactions", cancellation); - } - else if (trackedSource is AddressTrackedSource asts) - { - return SendAsync(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}/transactions", cancellation); - } - else - throw UnSupported(trackedSource); + return SendAsync(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions", cancellation); } @@ -399,16 +391,7 @@ public Task GetTransactionAsync(TrackedSource trackedSou throw new ArgumentNullException(nameof(txId)); if (trackedSource == null) throw new ArgumentNullException(nameof(trackedSource)); - if (trackedSource is DerivationSchemeTrackedSource dsts) - { - return SendAsync(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}/transactions/{txId}", cancellation); - } - else if (trackedSource is AddressTrackedSource asts) - { - return SendAsync(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}/transactions/{txId}", cancellation); - } - else - throw UnSupported(trackedSource); + return SendAsync(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions/{txId}", cancellation); } public Task RescanAsync(RescanRequest rescanRequest, CancellationToken cancellation = default) @@ -447,16 +430,17 @@ public KeyPathInformation GetKeyInformation(DerivationStrategyBase strategy, Scr public async Task GetKeyInformationAsync(DerivationStrategyBase strategy, Script script, CancellationToken cancellation = default) { - return await SendAsync(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false); + return await GetKeyInformationAsync(new DerivationSchemeTrackedSource(strategy), script, cancellation).ConfigureAwait(false); + } + public async Task GetKeyInformationAsync(TrackedSource trackedSource, Script script, CancellationToken cancellation = default) + { + return await SendAsync(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false); } - - [Obsolete("Use GetKeyInformationAsync(DerivationStrategyBase strategy, Script script) instead")] public async Task GetKeyInformationsAsync(Script script, CancellationToken cancellation = default) { return await SendAsync(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false); } - [Obsolete("Use GetKeyInformation(DerivationStrategyBase strategy, Script script) instead")] public KeyPathInformation[] GetKeyInformations(Script script, CancellationToken cancellation = default) { return GetKeyInformationsAsync(script, cancellation).GetAwaiter().GetResult(); @@ -611,8 +595,8 @@ static FormattableString EncodeUrlParameters(FormattableString url) return FormattableStringFactory.Create( url.Format, url.GetArguments() - .Select(a => - a is RawStr ? a : + .Select(a => + a is RawStr ? a : a is FormattableString o ? EncodeUrlParameters(o) : Uri.EscapeDataString(a?.ToString() ?? "")) .ToArray()); @@ -653,11 +637,30 @@ internal async Task SendAsync(HttpMethod method, object body, FormattableS if (Auth.RefreshCache()) { message = CreateMessage(method, body, relativePath); - result = await Client.SendAsync(message).ConfigureAwait(false); + result = await Client.SendAsync(message, cancellation).ConfigureAwait(false); } } return await ParseResponse(result).ConfigureAwait(false); } + internal async Task SendAsync(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation) + { + var message = CreateMessage(method, body, relativePath); + var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false); + + if (result.StatusCode == HttpStatusCode.GatewayTimeout || result.StatusCode == HttpStatusCode.RequestTimeout) + { + throw new HttpRequestException($"HTTP error {(int)result.StatusCode}", new TimeoutException()); + } + if ((int)result.StatusCode == 401) + { + if (Auth.RefreshCache()) + { + message = CreateMessage(method, body, relativePath); + result = await Client.SendAsync(message, cancellation).ConfigureAwait(false); + } + } + await ParseResponse(result).ConfigureAwait(false); + } internal HttpRequestMessage CreateMessage(HttpMethod method, object body, FormattableString relativePath) { @@ -717,11 +720,14 @@ private async Task ParseResponse(HttpResponseMessage response) private FormattableString GetBasePath(TrackedSource trackedSource) { + if (trackedSource is null) + throw new ArgumentNullException(nameof(trackedSource)); return trackedSource switch { DerivationSchemeTrackedSource dsts => $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}", AddressTrackedSource asts => $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}", - _ => throw UnSupported(trackedSource) + WalletTrackedSource wts => $"v1/cryptos/{CryptoCode}/wallets/{wts.WalletId}", + _ => $"v1/cryptos/{CryptoCode}/tracked-sources/{trackedSource}" }; } } diff --git a/NBXplorer.Client/Models/TrackedSource.cs b/NBXplorer.Client/Models/TrackedSource.cs index c170a95df..5dcebe50b 100644 --- a/NBXplorer.Client/Models/TrackedSource.cs +++ b/NBXplorer.Client/Models/TrackedSource.cs @@ -26,6 +26,12 @@ public static bool TryParse(string str, out TrackedSource trackedSource, NBXplor return false; trackedSource = addressTrackedSource; } + else if (strSpan.StartsWith("WALLET:".AsSpan(), StringComparison.Ordinal)) + { + if (!WalletTrackedSource.TryParse(strSpan, out var walletTrackedSource)) + return false; + trackedSource = walletTrackedSource; + } else { return false; @@ -97,6 +103,40 @@ public static TrackedSource Parse(string str, NBXplorerNetwork network) } } + public class WalletTrackedSource : TrackedSource + { + public string WalletId { get; } + + public WalletTrackedSource(string walletId) + { + WalletId = walletId; + } + + public static bool TryParse(ReadOnlySpan strSpan, out WalletTrackedSource walletTrackedSource) + { + if (strSpan == null) + throw new ArgumentNullException(nameof(strSpan)); + walletTrackedSource = null; + if (!strSpan.StartsWith("WALLET:".AsSpan(), StringComparison.Ordinal)) + return false; + try + { + walletTrackedSource = new WalletTrackedSource(strSpan.Slice("WALLET:".Length).ToString()); + return true; + } + catch { return false; } + } + + public override string ToString() + { + return "WALLET:" + WalletId; + } + public override string ToPrettyString() + { + return WalletId; + } + } + public class AddressTrackedSource : TrackedSource, IDestination { // Note that we should in theory access BitcoinAddress. But parsing BitcoinAddress is very expensive, so we keep storing plain strings diff --git a/NBXplorer.Tests/UnitTest1.cs b/NBXplorer.Tests/UnitTest1.cs index e1fca3cb1..f0e861038 100644 --- a/NBXplorer.Tests/UnitTest1.cs +++ b/NBXplorer.Tests/UnitTest1.cs @@ -27,7 +27,6 @@ using System.Globalization; using System.Net; using NBXplorer.HostedServices; -using System.Reflection; namespace NBXplorer.Tests { diff --git a/NBXplorer.Tests/xunit.runner.json b/NBXplorer.Tests/xunit.runner.json index dc14f82b1..6dae53c9c 100644 --- a/NBXplorer.Tests/xunit.runner.json +++ b/NBXplorer.Tests/xunit.runner.json @@ -1,3 +1,4 @@ { - "parallelizeTestCollections": false + "parallelizeTestCollections": false, + "methodDisplay": "method" } \ No newline at end of file diff --git a/NBXplorer/Backends/Postgres/PostgresRepository.cs b/NBXplorer/Backends/Postgres/PostgresRepository.cs index fdca826d0..70cabec57 100644 --- a/NBXplorer/Backends/Postgres/PostgresRepository.cs +++ b/NBXplorer/Backends/Postgres/PostgresRepository.cs @@ -207,7 +207,12 @@ internal WalletKey GetWalletKey(DerivationStrategyBase strategy) m.Add(new JProperty("derivation", new JValue(strategy.ToString()))); return new WalletKey(hash, m.ToString(Formatting.None)); } - + WalletKey GetWalletKey(WalletTrackedSource walletTrackedSource) + { + var m = new JObject { new JProperty("type", new JValue("Wallet")) }; + var res = new WalletKey(walletTrackedSource.WalletId, m.ToString(Formatting.None)); + return res; + } WalletKey GetWalletKey(IDestination destination) { var address = destination.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork); @@ -226,10 +231,41 @@ internal WalletKey GetWalletKey(TrackedSource source) { DerivationSchemeTrackedSource derivation => GetWalletKey(derivation.DerivationStrategy), AddressTrackedSource addr => GetWalletKey(addr.Address), + WalletTrackedSource wallet => GetWalletKey(wallet), _ => throw new NotSupportedException(source.GetType().ToString()) }; } + internal TrackedSource GetTrackedSource(WalletKey walletKey) + { + var metadata = JObject.Parse(walletKey.metadata); + if (metadata.TryGetValue("type", StringComparison.OrdinalIgnoreCase, out JToken typeJToken) && + typeJToken.Value() is { } type) + { + if ((metadata.TryGetValue("code", StringComparison.OrdinalIgnoreCase, out JToken codeJToken) && + codeJToken.Value() is { } code) && !code.Equals(Network.CryptoCode, + StringComparison.InvariantCultureIgnoreCase)) + { + return null; + } + + switch (type) + { + case "NBXv1-Derivation": + var derivation = metadata["derivation"].Value(); + return new DerivationSchemeTrackedSource(Network.DerivationStrategyFactory.Parse(derivation)); + case "NBXv1-Address": + var address = metadata["address"].Value(); + return new AddressTrackedSource(BitcoinAddress.Create(address, Network.NBitcoinNetwork)); + case "Wallet": + return new WalletTrackedSource(walletKey.wid); + } + } + + return null; + } + + internal record ScriptInsert(string code, string wallet_id, string script, string addr, bool used); internal record DescriptorScriptInsert(string descriptor, int idx, string script, string metadata, string addr, bool used); public async Task GenerateAddresses(DerivationStrategyBase strategy, DerivationFeature derivationFeature, GenerateAddressQuery query = null) { @@ -450,12 +486,22 @@ async Task> GetKeyInformations( if (scripts.Count == 0) return result; string additionalColumn = Network.IsElement ? ", ts.blinded_addr" : ""; - var rows = await connection.QueryAsync($"SELECT ts.script, ts.addr, ts.derivation, ts.keypath, ts.redeem {additionalColumn} FROM " + - "unnest(@records) AS r (script)," + - " LATERAL (" + - " SELECT script, addr, descriptor_metadata->>'derivation' derivation, keypath, descriptors_scripts_metadata->>'redeem' redeem, descriptors_scripts_metadata->>'blindedAddress' blinded_addr " + - " FROM nbxv1_keypath_info ki " + - " WHERE ki.code=@code AND ki.script=r.script) ts;", new { code = Network.CryptoCode, records = scripts.Select(s => s.ToHex()).ToArray() }); + var rows = await connection.QueryAsync($@" + SELECT ts.code, ts.script, ts.addr, ts.derivation, ts.keypath, ts.redeem{additionalColumn}, + ts.wallet_id, + w.metadata->>'type' AS wallet_type + FROM unnest(@records) AS r (script), + LATERAL ( + SELECT code, script, wallet_id, addr, descriptor_metadata->>'derivation' derivation, + keypath, descriptors_scripts_metadata->>'redeem' redeem, + descriptors_scripts_metadata->>'blindedAddress' blinded_addr, + descriptors_scripts_metadata->>'blindingKey' blindingKey, + descriptor_metadata->>'descriptor' descriptor + FROM nbxv1_keypath_info ki + WHERE ki.code=@code AND ki.script=r.script + ) ts + JOIN wallets w USING(wallet_id)", + new { code = Network.CryptoCode, records = scripts.Select(s => s.ToHex()).ToArray() }); foreach (var r in rows) { // This might be the case for a derivation added by a different indexer @@ -465,24 +511,40 @@ async Task> GetKeyInformations( bool isExplicit = r.derivation is null; bool isDescriptor = !isExplicit; var script = Script.FromHex(r.script); - var derivationStrategy = isDescriptor ? Network.DerivationStrategyFactory.Parse(r.derivation) : null; - var keypath = isDescriptor ? KeyPath.Parse(r.keypath) : null; + var derivationStrategy = r.derivation is not null ? Network.DerivationStrategyFactory.Parse(r.derivation) : null; + var keypath = r.keypath is not null ? KeyPath.Parse(r.keypath) : null; var redeem = (string)r.redeem; - result.Add(script, new KeyPathInformation() - { - Address = addr, - DerivationStrategy = isDescriptor ? derivationStrategy : null, - KeyPath = isDescriptor ? keypath : null, - ScriptPubKey = script, - TrackedSource = isDescriptor && derivationStrategy is not null ? new DerivationSchemeTrackedSource(derivationStrategy) : - isExplicit ? new AddressTrackedSource(addr) : null, - Feature = keypath is null ? DerivationFeature.Deposit : KeyPathTemplates.GetDerivationFeature(keypath), - Redeem = redeem is null ? null : Script.FromHex(redeem) - }); + string walletType = r.wallet_type; + string walletId = r.wallet_id; + + var trackedSource = derivationStrategy is not null + ? new DerivationSchemeTrackedSource(derivationStrategy) + : walletType == "Wallet" + ? walletId is null ? (TrackedSource)null : new WalletTrackedSource(walletId) + : new AddressTrackedSource(addr); + var ki = Network.IsElement && r.blindingKey is not null + ? new LiquidKeyPathInformation() + { + BlindingKey = Key.Parse(r.blindingKey, Network.NBitcoinNetwork) + } + : new KeyPathInformation(); + ki.Address = addr; + ki.DerivationStrategy = r.derivation is not null ? derivationStrategy : null; + ki.KeyPath = keypath; + ki.ScriptPubKey = script; + ki.TrackedSource = trackedSource; + ki.Feature = keypath is null ? DerivationFeature.Deposit : KeyPathTemplates.GetDerivationFeature(keypath); + ki.Redeem = redeem is null ? null : Script.FromHex(redeem); + result.Add(script, ki); } return result; } + public class LiquidKeyPathInformation : KeyPathInformation + { + public Key BlindingKey { get; set; } + } + private BitcoinAddress GetAddress(dynamic r) { if (Network.IsElement && r.blinded_addr is not null) @@ -952,11 +1014,14 @@ await connection.ExecuteAsync( "INSERT INTO scripts VALUES (@code, @script, @address) ON CONFLICT DO NOTHING;" + "INSERT INTO wallets_scripts VALUES (@code, @script, @walletid) ON CONFLICT DO NOTHING;", inserts); } - + private async Task GetImportRPCMode(DbConnectionHelper connection, WalletKey walletKey) + { + return ImportRPCMode.Parse((await connection.GetMetadata(walletKey.wid, WellknownMetadataKeys.ImportAddressToRPC))); + } private async Task ImportAddressToRPC(DbConnectionHelper connection, TrackedSource trackedSource, BitcoinAddress address, KeyPath keyPath) { var k = GetWalletKey(trackedSource); - var shouldImportRPC = ImportRPCMode.Parse((await connection.GetMetadata(k.wid, WellknownMetadataKeys.ImportAddressToRPC))); + var shouldImportRPC = await GetImportRPCMode(connection, k); if (shouldImportRPC != ImportRPCMode.Legacy) return; var accountKey = await connection.GetMetadata(k.wid, WellknownMetadataKeys.AccountHDKey); @@ -1175,7 +1240,7 @@ public async Task Track(IDestination address) await using var conn = await GetConnection(); var walletKey = GetWalletKey(address); await conn.Connection.ExecuteAsync( - "INSERT INTO wallets VALUES (@wid, @metadata::JSONB) ON CONFLICT DO NOTHING;" + + WalletInsertQuery + "INSERT INTO scripts VALUES (@code, @script, @addr) ON CONFLICT DO NOTHING;" + "INSERT INTO wallets_scripts VALUES (@code, @script, @wid) ON CONFLICT DO NOTHING" , new { code = Network.CryptoCode, script = address.ScriptPubKey.ToHex(), addr = address.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork).ToString(), walletKey.wid, walletKey.metadata }); @@ -1263,9 +1328,23 @@ public async Task SaveBlocks(IList slimBlocks) public async Task EnsureWalletCreated(DerivationStrategyBase strategy) { - using var connection = await ConnectionFactory.CreateConnection(); - await connection.ExecuteAsync("INSERT INTO wallets VALUES (@wid, @metadata::JSONB) ON CONFLICT DO NOTHING", GetWalletKey(strategy)); + await EnsureWalletCreated(GetWalletKey(strategy)); + } + + public async Task EnsureWalletCreated(TrackedSource trackedSource, params TrackedSource[] parentTrackedSource) + { + parentTrackedSource = parentTrackedSource.Where(source => source is not null).ToArray(); + await EnsureWalletCreated(GetWalletKey(trackedSource)); } + + record WalletHierarchyInsert(string child, string parent); + public async Task EnsureWalletCreated(WalletKey walletKey) + { + await using var connection = await ConnectionFactory.CreateConnection(); + await connection.ExecuteAsync(WalletInsertQuery, walletKey); + } + + private readonly string WalletInsertQuery = "INSERT INTO wallets (wallet_id, metadata) VALUES (@wid, @metadata::JSONB) ON CONFLICT DO NOTHING;"; } public class LegacyDescriptorMetadata diff --git a/NBXplorer/BlockHeaders.cs b/NBXplorer/BlockHeaders.cs index 169f99351..0fc9c737a 100644 --- a/NBXplorer/BlockHeaders.cs +++ b/NBXplorer/BlockHeaders.cs @@ -6,7 +6,7 @@ namespace NBXplorer; -public record RPCBlockHeader(uint256 Hash, uint256? Previous, int Height, DateTimeOffset Time) +public record RPCBlockHeader(uint256 Hash, uint256? Previous, int Height, DateTimeOffset Time, uint256 MerkleRoot) { public SlimChainedBlock ToSlimChainedBlock() => new(Hash, Previous, Height); } diff --git a/NBXplorer/Controllers/MainController.PSBT.cs b/NBXplorer/Controllers/MainController.PSBT.cs index beab9945d..4314bf1fd 100644 --- a/NBXplorer/Controllers/MainController.PSBT.cs +++ b/NBXplorer/Controllers/MainController.PSBT.cs @@ -29,7 +29,7 @@ public async Task CreatePSBT( if (body == null) throw new ArgumentNullException(nameof(body)); var network = trackedSourceContext.Network; - CreatePSBTRequest request = ParseJObject(body, network); + CreatePSBTRequest request = network.ParseJObject(body); var repo = RepositoryProvider.GetRepository(network); var txBuilder = request.Seed is int s ? network.NBitcoinNetwork.CreateTransactionBuilder(s) @@ -390,7 +390,7 @@ public async Task UpdatePSBT( [FromBody] JObject body) { - var update = ParseJObject(body, network); + var update = network.ParseJObject(body); if (update.PSBT == null) throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "'psbt' is missing")); await UpdatePSBTCore(update, network); @@ -587,12 +587,5 @@ await Task.WhenAll(update.PSBT.Inputs await getTransactions; } } - - protected T ParseJObject(JObject requestObj, NBXplorerNetwork network) - { - if (requestObj == null) - return default; - return network.Serializer.ToObject(requestObj); - } } } diff --git a/NBXplorer/Controllers/MainController.cs b/NBXplorer/Controllers/MainController.cs index 23b2c76f1..0bb861a54 100644 --- a/NBXplorer/Controllers/MainController.cs +++ b/NBXplorer/Controllers/MainController.cs @@ -25,6 +25,7 @@ using NBXplorer.Backends; using NBitcoin.Scripting; using System.Globalization; +using NBXplorer.Backends.Postgres; namespace NBXplorer.Controllers { @@ -512,7 +513,8 @@ public async Task TrackWallet( { var network = trackedSourceContext.Network; var trackedSource = trackedSourceContext.TrackedSource; - var request = ParseJObject(rawRequest ?? new JObject(), network); + var request = network.ParseJObject(rawRequest ?? new JObject()); + if (trackedSource is DerivationSchemeTrackedSource dts) { if (request.Wait) @@ -554,7 +556,8 @@ private GenerateAddressQuery GenerateAddressQuery(TrackWalletRequest request, De } return null; } - + + [HttpGet] [Route($"{CommonRoutes.DerivationEndpoint}/{CommonRoutes.TransactionsPath}")] [Route($"{CommonRoutes.AddressEndpoint}/{CommonRoutes.TransactionsPath}")] [Route($"{CommonRoutes.WalletEndpoint}/{CommonRoutes.TransactionsPath}")] @@ -658,7 +661,7 @@ public async Task Rescan(TrackedSourceContext trackedSourceContex if (body == null) throw new ArgumentNullException(nameof(body)); var network = trackedSourceContext.Network; - var rescanRequest = ParseJObject(body, network); + var rescanRequest = network.ParseJObject(body); if (rescanRequest == null) throw new ArgumentNullException(nameof(rescanRequest)); if (rescanRequest?.Transactions == null) @@ -1024,7 +1027,7 @@ public async Task Broadcast( public async Task GenerateWallet(TrackedSourceContext trackedSourceContext, [FromBody] JObject rawRequest = null) { var network = trackedSourceContext.Network; - var request = ParseJObject(rawRequest, network) ?? new GenerateWalletRequest(); + var request = network.ParseJObject(rawRequest) ?? new GenerateWalletRequest(); if (request.ImportKeysToRPC && trackedSourceContext.RpcClient is null) { diff --git a/NBXplorer/Controllers/PostgresMainController.cs b/NBXplorer/Controllers/PostgresMainController.cs index 70417fc18..ed1322b97 100644 --- a/NBXplorer/Controllers/PostgresMainController.cs +++ b/NBXplorer/Controllers/PostgresMainController.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NBitcoin; +using NBitcoin.Altcoins.HashX11; +using NBitcoin.DataEncoders; using NBitcoin.RPC; using NBXplorer.Backends.Postgres; using NBXplorer.DerivationStrategy; @@ -23,11 +25,11 @@ namespace NBXplorer.Controllers [Route($"v1/{CommonRoutes.WalletEndpoint}")] [Route($"v1/{CommonRoutes.TrackedSourceEndpoint}")] [Authorize] - public class PostgresMainController :Controller, IUTXOService + public class PostgresMainController : Controller, IUTXOService { public PostgresMainController( DbConnectionFactory connectionFactory, - KeyPathTemplates keyPathTemplates) + KeyPathTemplates keyPathTemplates) { ConnectionFactory = connectionFactory; KeyPathTemplates = keyPathTemplates; @@ -103,7 +105,7 @@ private static MoneyBag RemoveZeros(MoneyBag bag) [HttpGet("utxos")] - public async Task GetUTXOs( TrackedSourceContext trackedSourceContext) + public async Task GetUTXOs(TrackedSourceContext trackedSourceContext) { var trackedSource = trackedSourceContext.TrackedSource; var repo = (PostgresRepository)trackedSourceContext.Repository; diff --git a/NBXplorer/Controllers/TrackedSourceContext.cs b/NBXplorer/Controllers/TrackedSourceContext.cs index fa757adad..035a06883 100644 --- a/NBXplorer/Controllers/TrackedSourceContext.cs +++ b/NBXplorer/Controllers/TrackedSourceContext.cs @@ -13,7 +13,7 @@ namespace NBXplorer.Controllers; -[ModelBinder] +[ModelBinder] public class TrackedSourceContext { public TrackedSource TrackedSource { get; set; } @@ -21,21 +21,21 @@ public class TrackedSourceContext public RPCClient RpcClient { get; set; } public IIndexer Indexer { get; set; } public IRepository Repository { get; set; } - - public class TrackedSourceContextRequirementAttribute: Attribute + + 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) + public TrackedSourceContextRequirementAttribute(bool requireRPC = false, bool requireTrackedSource = true, bool disallowTrackedSource = false, params Type[] allowedTrackedSourceTypes) { RequireRpc = requireRPC; RequireTrackedSource = requireTrackedSource; DisallowTrackedSource = disallowTrackedSource; AllowedTrackedSourceTypes = allowedTrackedSourceTypes; - + } } @@ -50,7 +50,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext) var addressValue = bindingContext.ValueProvider.GetValue("address").FirstValue; var derivationSchemeValue = bindingContext.ValueProvider.GetValue("derivationScheme").FirstValue; - derivationSchemeValue??= bindingContext.ValueProvider.GetValue("extPubKey").FirstValue; + derivationSchemeValue ??= bindingContext.ValueProvider.GetValue("extPubKey").FirstValue; var walletIdValue = bindingContext.ValueProvider.GetValue("walletId").FirstValue; var trackedSourceValue = bindingContext.ValueProvider.GetValue("trackedSource").FirstValue; @@ -67,10 +67,10 @@ public Task BindModelAsync(ModelBindingContext bindingContext) $"{cryptoCode} is not supported")); } - var requirements = ((ControllerActionDescriptor) bindingContext.ActionContext.ActionDescriptor) + var requirements = ((ControllerActionDescriptor)bindingContext.ActionContext.ActionDescriptor) .MethodInfo.GetCustomAttributes().FirstOrDefault(); - - + + var rpcClient = indexer.GetConnectedClient(); if (rpcClient?.Capabilities == null) { @@ -87,16 +87,16 @@ public Task BindModelAsync(ModelBindingContext bindingContext) 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) + 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))) + 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.")); @@ -106,7 +106,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext) { Indexer = indexer, Network = network, - TrackedSource = ts , + TrackedSource = ts, RpcClient = rpcClient, Repository = repositoryProvider.GetRepository(network) }); @@ -126,6 +126,8 @@ public static TrackedSource GetTrackedSource(string derivationScheme, string add 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; } } diff --git a/NBXplorer/DBScripts/021.KeyPathInfoReturnsWalletId.sql b/NBXplorer/DBScripts/021.KeyPathInfoReturnsWalletId.sql new file mode 100644 index 000000000..f8d6563a2 --- /dev/null +++ b/NBXplorer/DBScripts/021.KeyPathInfoReturnsWalletId.sql @@ -0,0 +1,13 @@ +CREATE OR REPLACE VIEW nbxv1_keypath_info AS + SELECT ws.code, + ws.script, + s.addr, + d.metadata AS descriptor_metadata, + nbxv1_get_keypath(d.metadata, ds.idx) AS keypath, + ds.metadata AS descriptors_scripts_metadata, + ws.wallet_id + FROM ((wallets_scripts ws + JOIN scripts s ON (((s.code = ws.code) AND (s.script = ws.script)))) + LEFT JOIN ((wallets_descriptors wd + JOIN descriptors_scripts ds ON (((ds.code = wd.code) AND (ds.descriptor = wd.descriptor)))) + JOIN descriptors d ON (((d.code = ds.code) AND (d.descriptor = ds.descriptor)))) ON (((wd.wallet_id = ws.wallet_id) AND (wd.code = ws.code) AND (ds.script = ws.script)))); \ No newline at end of file diff --git a/NBXplorer/DBScripts/FullSchema.sql b/NBXplorer/DBScripts/FullSchema.sql index 040bfd826..c2c736fb0 100644 --- a/NBXplorer/DBScripts/FullSchema.sql +++ b/NBXplorer/DBScripts/FullSchema.sql @@ -958,7 +958,8 @@ CREATE VIEW nbxv1_keypath_info AS s.addr, d.metadata AS descriptor_metadata, nbxv1_get_keypath(d.metadata, ds.idx) AS keypath, - ds.metadata AS descriptors_scripts_metadata + ds.metadata AS descriptors_scripts_metadata, + ws.wallet_id FROM ((wallets_scripts ws JOIN scripts s ON (((s.code = ws.code) AND (s.script = ws.script)))) LEFT JOIN ((wallets_descriptors wd @@ -1362,6 +1363,7 @@ INSERT INTO nbxv1_migrations VALUES ('017.FixDoubleSpendDetection'); INSERT INTO nbxv1_migrations VALUES ('018.FastWalletRecent'); INSERT INTO nbxv1_migrations VALUES ('019.FixDoubleSpendDetection2'); INSERT INTO nbxv1_migrations VALUES ('020.ReplacingShouldBeIdempotent'); +INSERT INTO nbxv1_migrations VALUES ('021.KeyPathInfoReturnsWalletId'); ALTER TABLE ONLY nbxv1_migrations ADD CONSTRAINT nbxv1_migrations_pkey PRIMARY KEY (script_name); \ No newline at end of file diff --git a/NBXplorer/Extensions.cs b/NBXplorer/Extensions.cs index dca02252a..69e4301be 100644 --- a/NBXplorer/Extensions.cs +++ b/NBXplorer/Extensions.cs @@ -31,11 +31,18 @@ using Npgsql; using NBitcoin.Altcoins; using System.Threading; +using Newtonsoft.Json.Linq; namespace NBXplorer { public static class Extensions { + public static T ParseJObject(this NBXplorerNetwork network, JObject requestObj) + { + if (requestObj == null) + return default; + return network.Serializer.ToObject(requestObj); + } public static async Task ReliableOpenConnectionAsync(this NpgsqlDataSource ds, CancellationToken cancellationToken = default) { int maxRetries = 10; diff --git a/NBXplorer/RPCClientExtensions.cs b/NBXplorer/RPCClientExtensions.cs index 0c5d2add8..b5a5384c1 100644 --- a/NBXplorer/RPCClientExtensions.cs +++ b/NBXplorer/RPCClientExtensions.cs @@ -14,6 +14,8 @@ using System.Net; using System.Net.Http.Headers; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using NBitcoin.Crypto; namespace NBXplorer { @@ -246,7 +248,6 @@ public static async Task UTXOUpdatePSBT(this RPCClient rpcClient, PSBT psb throw new Exception("This should never happen"); } - public static async Task GetBlockHeadersAsync(this RPCClient rpc, IList blockHeights) { var batch = rpc.PrepareBatch(); @@ -278,7 +279,8 @@ public static async Task GetBlockHeaderAsyncEx(this RPCClient rp blk, prev is null ? null : new uint256(prev), response["height"].Value(), - NBitcoin.Utils.UnixTimeToDateTime(response["time"].Value())); + NBitcoin.Utils.UnixTimeToDateTime(response["time"].Value()), + new uint256(response["merkleroot"]?.Value())); } public static async Task TryGetRawTransaction(this RPCClient client, uint256 txId) diff --git a/NBXplorer/TrackedTransaction.cs b/NBXplorer/TrackedTransaction.cs index 9a4589c1d..a079747ba 100644 --- a/NBXplorer/TrackedTransaction.cs +++ b/NBXplorer/TrackedTransaction.cs @@ -151,7 +151,7 @@ public IEnumerable GetReceivedOutputs() Output: o, KeyPath: KnownKeyPathMapping.TryGet(o.TxOut.ScriptPubKey), Address: KnownKeyPathInformation.TryGet(o.TxOut.ScriptPubKey)?.Address)) - .Where(o => o.KeyPath != null || o.Output.TxOut.ScriptPubKey == (TrackedSource as IDestination)?.ScriptPubKey) + .Where(o => o.KeyPath != null || o.Output.TxOut.ScriptPubKey == (TrackedSource as IDestination)?.ScriptPubKey || TrackedSource is WalletTrackedSource) .Select(o => new MatchedOutput() { Index = o.Index,