diff --git a/NBXplorer.Client/ExplorerClient.cs b/NBXplorer.Client/ExplorerClient.cs index defcd741f..de6bd02f1 100644 --- a/NBXplorer.Client/ExplorerClient.cs +++ b/NBXplorer.Client/ExplorerClient.cs @@ -568,10 +568,6 @@ public Task AddGroupAddressAsync(string cryptoCode, string groupId, string[] add { return SendAsync(HttpMethod.Post, addresses, $"v1/cryptos/{cryptoCode}/groups/{groupId}/addresses", cancellationToken); } - public Task GetGroupAddressesAsync(string cryptoCode, string groupId, CancellationToken cancellationToken = default) - { - return SendAsync(HttpMethod.Get, null, $"v1/cryptos/{cryptoCode}/groups/{groupId}/addresses", cancellationToken); - } private static readonly HttpClient SharedClient = new HttpClient(); internal HttpClient Client = SharedClient; diff --git a/NBXplorer.Tests/UnitTest1.Groups.cs b/NBXplorer.Tests/UnitTest1.Groups.cs index 903c9fc78..5b7a1a376 100644 --- a/NBXplorer.Tests/UnitTest1.Groups.cs +++ b/NBXplorer.Tests/UnitTest1.Groups.cs @@ -1,7 +1,11 @@ -using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure; +using Dapper; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure; using NBitcoin; +using NBXplorer.Backends.Postgres; +using NBXplorer.Controllers; using NBXplorer.Models; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -56,7 +60,7 @@ void AssertG1Empty() async Task AssertAddresses(GroupInformation g) { - var groupAddresses = await tester.Client.GetGroupAddressesAsync("BTC", g.GroupId); + var groupAddresses = await GetGroupAddressesAsync(tester, "BTC", g.GroupId); Assert.Equal(groupAddresses.Length, addresses.Length); foreach (var a in addresses) { @@ -65,14 +69,27 @@ async Task AssertAddresses(GroupInformation g) } await AssertAddresses(g1); await AssertAddresses(g2); - var g3Addrs = await tester.Client.GetGroupAddressesAsync("BTC", g3.GroupId); + var g3Addrs = await GetGroupAddressesAsync(tester, "BTC", g3.GroupId); Assert.Empty(g3Addrs); // Removing g2 should remove all its addresses g1 = await tester.Client.RemoveGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild()]); await AssertAddresses(g2); - var g1Addrs = await tester.Client.GetGroupAddressesAsync("BTC", g1.GroupId); + var g1Addrs = await GetGroupAddressesAsync(tester, "BTC", g1.GroupId); Assert.Empty(g1Addrs); + + await AssertNBXplorerException(400, tester.Client.AddGroupChildrenAsync(g2.GroupId, [new GroupChild() { TrackedSource= "DERIVATIONSCHEME:tpubDC45vUDsFAAqwYKz5hSLi5yJLNduJzpmTw6QTMRPrwdXURoyL81H8oZAaL8EiwEgg92qgMa9h1bB4Y1BZpy9CTNPfjfxvFcWxeiKBHCqSdc" }])); + await AssertNBXplorerException(400, tester.Client.AddGroupChildrenAsync(g2.GroupId, [new GroupChild() { CryptoCode="BTC", TrackedSource = "DERIVATIONSCHEME:lol" }])); + } + + private async Task GetGroupAddressesAsync(ServerTester tester, string code, string groupId) + { + await using var conn = await tester.GetService().CreateConnection(); + return (await conn.QueryAsync("SELECT s.addr FROM wallets_scripts JOIN scripts s USING (code, script) WHERE code=@code AND wallet_id=@wid", new + { + code = code, + wid = PostgresRepository.GetWalletKey(new GroupTrackedSource(groupId)).wid + })).ToArray(); } [Fact] @@ -96,6 +113,17 @@ public async Task CanAliceAndBobShareWallet() var txs = await tester.Client.GetTransactionsAsync(gts); var tx = Assert.Single(txs.UnconfirmedTransactions.Transactions); Assert.Equal(txid, tx.TransactionId); + Assert.NotNull(tx.Outputs[0].Address); + + // Can we track manually added address? + await tester.Client.AddGroupAddressAsync("BTC", shared.GroupId, ["n3XyBWEKWLxm5EzrrvLCJyCQrRhVWQ8YGa"]); + txid = tester.SendToAddress(BitcoinAddress.Create("n3XyBWEKWLxm5EzrrvLCJyCQrRhVWQ8YGa", tester.Network), Money.Coins(1.2m)); + var txEvt = tester.Notifications.WaitForTransaction(gts, txid); + Assert.Single(txEvt.Outputs); + Assert.NotNull(tx.Outputs[0].Address); + + balance = await tester.Client.GetBalanceAsync(gts); + Assert.Equal(Money.Coins(1.0m + 1.2m), balance.Unconfirmed); } private async Task AssertNBXplorerException(int httpCode, Task task) diff --git a/NBXplorer/Backends/DBTrie/Repository.cs b/NBXplorer/Backends/DBTrie/Repository.cs index 6ff046310..648f1845e 100644 --- a/NBXplorer/Backends/DBTrie/Repository.cs +++ b/NBXplorer/Backends/DBTrie/Repository.cs @@ -1411,7 +1411,7 @@ public async Task GetMatches(IList t } foreach (var m in matches.Values) { - m.KnownKeyPathMappingUpdated(); + m.OwnedScriptsUpdated(); await AfterMatch(m, keyPathInformationsByTrackedTransaction[m]); } diff --git a/NBXplorer/Backends/Postgres/PostgresRepository.cs b/NBXplorer/Backends/Postgres/PostgresRepository.cs index 0453e838e..920afab00 100644 --- a/NBXplorer/Backends/Postgres/PostgresRepository.cs +++ b/NBXplorer/Backends/Postgres/PostgresRepository.cs @@ -773,7 +773,7 @@ SaveTransactionRecord CreateTransactionRecord(Transaction tx) => SaveTransaction } foreach (var m in matches.Values) { - m.KnownKeyPathMappingUpdated(); + m.OwnedScriptsUpdated(); if (elementContext is not null) await elementContext.Unblind(rpc, m); } @@ -841,9 +841,9 @@ public async Task GetTransactions(TrackedSource trackedSou var tip = await connection.GetTip(); var txIdCond = txId is null ? string.Empty : " AND tx_id=@tx_id"; var utxos = await - connection.Connection.QueryAsync<(string tx_id, long idx, string blk_id, long? blk_height, int? blk_idx, bool is_out, string spent_tx_id, long spent_idx, string script, long value, string asset_id, bool immature, string keypath, DateTime seen_at)>( - "SELECT tx_id, idx, blk_id, blk_height, blk_idx, is_out, spent_tx_id, spent_idx, script, value, asset_id, immature, keypath, seen_at " + - "FROM nbxv1_tracked_txs " + + connection.Connection.QueryAsync<(string tx_id, long idx, string blk_id, long? blk_height, int? blk_idx, bool is_out, string spent_tx_id, long spent_idx, string script, string addr, long value, string asset_id, bool immature, string keypath, DateTime seen_at)>( + "SELECT tx_id, idx, blk_id, blk_height, blk_idx, is_out, spent_tx_id, spent_idx, script, s.addr, value, asset_id, immature, keypath, seen_at " + + "FROM nbxv1_tracked_txs LEFT JOIN scripts s USING (code, script) " + $"WHERE code=@code AND wallet_id=@walletId{txIdCond}", new { code = Network.CryptoCode, walletId = GetWalletKey(trackedSource).wid, tx_id = txId?.ToString() }); utxos.TryGetNonEnumeratedCount(out int c); var trackedById = new Dictionary(c); @@ -854,6 +854,7 @@ public async Task GetTransactions(TrackedSource trackedSou { var txout = Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut(); txout.ScriptPubKey = Script.FromHex(utxo.script); + tracked.KnownAddresses.TryAdd(txout.ScriptPubKey, BitcoinAddress.Create(utxo.addr, this.Network.NBitcoinNetwork)); txout.Value = Money.Satoshis(utxo.value); var coin = new Coin(new OutPoint(tracked.Key.TxId, (uint)utxo.idx), txout); if (Network.IsElement) @@ -864,11 +865,13 @@ public async Task GetTransactions(TrackedSource trackedSou { tracked.ReceivedCoins.Add(coin); } + // TODO: IsCoinBase is actually not used anywhere. tracked.IsCoinBase = utxo.immature; tracked.Immature = utxo.immature; if (utxo.keypath is string) tracked.KnownKeyPathMapping.TryAdd(txout.ScriptPubKey, KeyPath.Parse(utxo.keypath)); + tracked.OwnedScripts.Add(txout.ScriptPubKey); } else { @@ -896,7 +899,7 @@ public async Task GetTransactions(TrackedSource trackedSou return trackedById.Values.Select(c => { - c.KnownKeyPathMappingUpdated(); + c.OwnedScriptsUpdated(); return c; }).ToArray(); } diff --git a/NBXplorer/Controllers/GroupsController.cs b/NBXplorer/Controllers/GroupsController.cs index 3fdb74486..6dff977f6 100644 --- a/NBXplorer/Controllers/GroupsController.cs +++ b/NBXplorer/Controllers/GroupsController.cs @@ -152,25 +152,15 @@ await conn.ExecuteAsync(PostgresRepository.InsertScriptsScript + return Ok(); } - [HttpGet($"{CommonRoutes.BaseCryptoEndpoint}/{CommonRoutes.GroupEndpoint}/addresses")] - public async Task GetGroupAddresses(TrackedSourceContext trackedSourceContext) - { - var group = (GroupTrackedSource)trackedSourceContext.TrackedSource; - await using var conn = await ConnectionFactory.CreateConnection(); - return Ok((await conn.QueryAsync("SELECT s.addr FROM wallets_scripts JOIN scripts s USING (code, script) WHERE code=@code AND wallet_id=@wid", new - { - code = trackedSourceContext.Network.CryptoCode, - wid = PostgresRepository.GetWalletKey(group).wid - })).ToArray()); - } - private string GetWid(GroupChild c) { if (c?.TrackedSource is null) return null; var net = c.CryptoCode is null ? null : NetworkProvider.GetFromCryptoCode(c.CryptoCode); + if (c.TrackedSource.StartsWith("ADDRESS:") || c.TrackedSource.StartsWith("DERIVATIONSCHEME:") && c.CryptoCode is null) + throw new NBXplorerException(new NBXplorerError(400, "invalid-group-child", "ADDRESS: and DERIVATIONSCHEME: tracked sources must also include a cryptoCode parameter")); if (!TrackedSource.TryParse(c.TrackedSource, out var ts, net)) - return null; + throw new NBXplorerException(new NBXplorerError(400, "invalid-group-child", "Invalid tracked source format")); return PostgresRepository.GetWalletKey(ts, net)?.wid; } diff --git a/NBXplorer/Controllers/MainController.cs b/NBXplorer/Controllers/MainController.cs index dfba0b7ac..2f0e42a7b 100644 --- a/NBXplorer/Controllers/MainController.cs +++ b/NBXplorer/Controllers/MainController.cs @@ -25,7 +25,6 @@ using NBXplorer.Backends; using NBitcoin.Scripting; using System.Globalization; -using NBXplorer.Backends.Postgres; namespace NBXplorer.Controllers { @@ -757,15 +756,18 @@ public async Task Rescan(TrackedSourceContext trackedSourceContex } } - [HttpPost] - [Route($"{CommonRoutes.DerivationEndpoint}/metadata/{{key}}")] + [HttpPost($"{CommonRoutes.DerivationEndpoint}/metadata/{{key}}")] + [HttpPost($"{CommonRoutes.AddressEndpoint}/metadata/{{key}}")] + [HttpPost($"{CommonRoutes.GroupEndpoint}/metadata/{{key}}")] public async Task SetMetadata(TrackedSourceContext trackedSourceContext, string key, [FromBody] JToken value = null) { await trackedSourceContext.Repository.SaveMetadata(trackedSourceContext.TrackedSource, key, value); return Ok(); } - [HttpGet] - [Route($"{CommonRoutes.DerivationEndpoint}/metadata/{{key}}")] + + [HttpGet($"{CommonRoutes.DerivationEndpoint}/metadata/{{key}}")] + [HttpGet($"{CommonRoutes.AddressEndpoint}/metadata/{{key}}")] + [HttpGet($"{CommonRoutes.GroupEndpoint}/metadata/{{key}}")] public async Task GetMetadata(TrackedSourceContext trackedSourceContext, string key) { var result = await trackedSourceContext.Repository.GetMetadata(trackedSourceContext.TrackedSource, key); diff --git a/NBXplorer/TrackedTransaction.cs b/NBXplorer/TrackedTransaction.cs index 4a3a2036c..86b54e50c 100644 --- a/NBXplorer/TrackedTransaction.cs +++ b/NBXplorer/TrackedTransaction.cs @@ -69,19 +69,23 @@ public TrackedTransaction(TrackedTransactionKey key, TrackedSource trackedSource Transaction = transaction; transaction.PrecomputeHash(false, true); KnownKeyPathMapping = knownScriptMapping; - - KnownKeyPathMappingUpdated(); + if ((TrackedSource as IDestination)?.ScriptPubKey is Script s) + OwnedScripts.Add(s); + foreach (var ss in knownScriptMapping.Keys) + OwnedScripts.Add(ss); + OwnedScriptsUpdated(); } - internal void KnownKeyPathMappingUpdated() + internal void OwnedScriptsUpdated() { if (Transaction == null) return; - var scriptPubKey = (TrackedSource as IDestination)?.ScriptPubKey; + ReceivedCoins.Clear(); + SpentOutpoints.Clear(); for (int i = 0; i < Transaction.Outputs.Count; i++) { var output = Transaction.Outputs[i]; - if (KnownKeyPathMapping.ContainsKey(output.ScriptPubKey) || scriptPubKey == output.ScriptPubKey) + if (OwnedScripts.Contains(output.ScriptPubKey)) ReceivedCoins.Add(new Coin(new OutPoint(Key.TxId, i), output)); } @@ -89,8 +93,9 @@ internal void KnownKeyPathMappingUpdated() SpentOutpoints.AddInputs(Transaction); } + public HashSet