diff --git a/BTCPayServer.Common/BTCPayServer.Common.csproj b/BTCPayServer.Common/BTCPayServer.Common.csproj index 53ebecf8d..555b7ed40 100644 --- a/BTCPayServer.Common/BTCPayServer.Common.csproj +++ b/BTCPayServer.Common/BTCPayServer.Common.csproj @@ -4,7 +4,7 @@ - + diff --git a/BTCPayServer.Data/Data/WalletObjectData.cs b/BTCPayServer.Data/Data/WalletObjectData.cs index e73e087eb..1cc0ea954 100644 --- a/BTCPayServer.Data/Data/WalletObjectData.cs +++ b/BTCPayServer.Data/Data/WalletObjectData.cs @@ -21,7 +21,7 @@ namespace BTCPayServer.Data public const string PayjoinExposed = "pj-exposed"; public const string Payout = "payout"; public const string PullPayment = "pull-payment"; - public const string Script = "script"; + public const string Address = "address"; public const string Utxo = "utxo"; } public string WalletId { get; set; } diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 2a8378554..d1e506f08 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -678,7 +678,7 @@ namespace BTCPayServer.Tests parser = new DerivationSchemeParser(networkProvider.BTC); var od = "wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48"; (strategyBase, rootedKeyPath) = parser.ParseOutputDescriptor(od); - Assert.Equal(1, rootedKeyPath.Length); + Assert.Single(rootedKeyPath); Assert.IsType(strategyBase); Assert.True(((DirectDerivationStrategy)strategyBase).Segwit); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index f1f17bb2b..f09e1b8b3 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1936,7 +1936,7 @@ namespace BTCPayServer.Tests } }); var invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", newInvoice.Id), false); - Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "script"); + Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "address"); Assert.EndsWith($"/i/{newInvoice.Id}", newInvoice.CheckoutLink); var controller = tester.PayTester.GetController(user.UserId, user.StoreId); @@ -1972,7 +1972,7 @@ namespace BTCPayServer.Tests invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 1, Currency = "USD" }); invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false); - Assert.DoesNotContain(invoiceObject.Links.Select(l => l.Type), t => t == "script"); + Assert.DoesNotContain(invoiceObject.Links.Select(l => l.Type), t => t == "address"); paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id); @@ -1981,7 +1981,7 @@ namespace BTCPayServer.Tests await client.ActivateInvoicePaymentMethod(user.StoreId, invoice.Id, paymentMethods.First().PaymentMethod); invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false); - Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "script"); + Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "address"); paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id); Assert.Single(paymentMethods); @@ -2046,10 +2046,10 @@ namespace BTCPayServer.Tests var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)); Assert.Single(pm.Payments); Assert.Equal(-0.0001m, pm.Due); - }); - invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false); - Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "tx"); + invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false); + Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "tx"); + }); } [Fact(Timeout = 60 * 20 * 1000)] diff --git a/BTCPayServer.Tests/docker-compose.altcoins.yml b/BTCPayServer.Tests/docker-compose.altcoins.yml index 23916e7ad..c2476387d 100644 --- a/BTCPayServer.Tests/docker-compose.altcoins.yml +++ b/BTCPayServer.Tests/docker-compose.altcoins.yml @@ -90,7 +90,7 @@ services: expose: - "4444" nbxplorer: - image: nicolasdorier/nbxplorer:2.3.40 + image: nicolasdorier/nbxplorer:2.3.54 restart: unless-stopped ports: - "32838:32838" diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index a4d689fc7..133bc5650 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -87,7 +87,7 @@ services: expose: - "4444" nbxplorer: - image: nicolasdorier/nbxplorer:2.3.40 + image: nicolasdorier/nbxplorer:2.3.54 restart: unless-stopped ports: - "32838:32838" diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs index 51eb14c3a..87f986fad 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.cs @@ -391,8 +391,8 @@ namespace BTCPayServer.Controllers await _walletRepository.EnsureWalletObjectLink( new WalletObjectId( walletId, - WalletObjectData.Types.Script, - address.ScriptPubKey.ToHex()), + WalletObjectData.Types.Address, + address.ToString()), new WalletObjectId( walletId, WalletObjectData.Types.Invoice, diff --git a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs index 620cdd6c7..a6663e510 100644 --- a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs +++ b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs @@ -16,6 +16,7 @@ using BTCPayServer.Services.Apps; using BTCPayServer.Services.Labels; using BTCPayServer.Services.PaymentRequests; using NBitcoin; +using NBXplorer.DerivationStrategy; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -25,9 +26,12 @@ namespace BTCPayServer.HostedServices { private readonly WalletRepository _walletRepository; - public TransactionLabelMarkerHostedService(EventAggregator eventAggregator, WalletRepository walletRepository, Logs logs) : + public BTCPayNetworkProvider NetworkProvider { get; } + + public TransactionLabelMarkerHostedService(BTCPayNetworkProvider networkProvider, EventAggregator eventAggregator, WalletRepository walletRepository, Logs logs) : base(eventAggregator, logs) { + NetworkProvider = networkProvider; _walletRepository = walletRepository; } @@ -44,36 +48,40 @@ namespace BTCPayServer.HostedServices // any utxo or script object matching it. // If we find, then we create a link between them and the tx object. case NewOnChainTransactionEvent transactionEvent: - { - var txHash = transactionEvent.NewTransactionEvent.TransactionData.TransactionHash.ToString(); - - // find all wallet objects that fit this transaction - // that means see if there are any utxo objects that match in/outs and scripts/addresses that match outs - var matchedObjects = transactionEvent.NewTransactionEvent.TransactionData.Transaction.Inputs - .Select(txIn => new ObjectTypeId(WalletObjectData.Types.Utxo, txIn.PrevOut.ToString())) - .Concat(transactionEvent.NewTransactionEvent.TransactionData.Transaction.Outputs.AsIndexedOutputs().SelectMany(txOut => - - new[]{ - new ObjectTypeId(WalletObjectData.Types.Script,txOut.TxOut.ScriptPubKey.ToHex()), - new ObjectTypeId(WalletObjectData.Types.Utxo,txOut.ToCoin().Outpoint.ToString()) - - } )).Distinct().ToArray(); - - var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery(){TypesIds = matchedObjects}); - - foreach (var walletObjectDatas in objs.GroupBy(data => data.Key.WalletId)) { - var txWalletObject = new WalletObjectId(walletObjectDatas.Key, - WalletObjectData.Types.Tx, txHash); - await _walletRepository.EnsureWalletObject(txWalletObject); - foreach (var walletObjectData in walletObjectDatas) - { - await _walletRepository.EnsureWalletObjectLink(txWalletObject, walletObjectData.Key); - } - } + var network = NetworkProvider.GetNetwork(transactionEvent.CryptoCode); + var derivation = transactionEvent.NewTransactionEvent.DerivationStrategy; + if (network is null || derivation is null) + break; + var txHash = transactionEvent.NewTransactionEvent.TransactionData.TransactionHash.ToString(); - break; - } + // find all wallet objects that fit this transaction + // that means see if there are any utxo objects that match in/outs and scripts/addresses that match outs + var matchedObjects = transactionEvent.NewTransactionEvent.TransactionData.Transaction.Inputs + .Select(txIn => new ObjectTypeId(WalletObjectData.Types.Utxo, txIn.PrevOut.ToString())) + .Concat(transactionEvent.NewTransactionEvent.Outputs.SelectMany(txOut => + + new[]{ + new ObjectTypeId(WalletObjectData.Types.Address, GetAddress(derivation, txOut, network).ToString()), + new ObjectTypeId(WalletObjectData.Types.Utxo, new OutPoint(transactionEvent.NewTransactionEvent.TransactionData.TransactionHash, (uint)txOut.Index).ToString()) + + })).Distinct().ToArray(); + + var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery() { TypesIds = matchedObjects }); + + foreach (var walletObjectDatas in objs.GroupBy(data => data.Key.WalletId)) + { + var txWalletObject = new WalletObjectId(walletObjectDatas.Key, + WalletObjectData.Types.Tx, txHash); + await _walletRepository.EnsureWalletObject(txWalletObject); + foreach (var walletObjectData in walletObjectDatas) + { + await _walletRepository.EnsureWalletObjectLink(txWalletObject, walletObjectData.Key); + } + } + + break; + } case InvoiceEvent {Name: InvoiceEvent.ReceivedPayment} invoiceEvent when invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance && invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData: @@ -98,5 +106,11 @@ namespace BTCPayServer.HostedServices } } } + + private BitcoinAddress GetAddress(DerivationStrategyBase derivationStrategy, NBXplorer.Models.MatchedOutput txOut, BTCPayNetwork network) + { + // Old version of NBX doesn't give address in the event, so we need to guess + return (txOut.Address ?? network.NBXplorerNetwork.CreateAddress(derivationStrategy, txOut.KeyPath, txOut.ScriptPubKey)); + } } } diff --git a/BTCPayServer/Services/InvoiceActivator.cs b/BTCPayServer/Services/InvoiceActivator.cs index bc1692bad..e3643c8ff 100644 --- a/BTCPayServer/Services/InvoiceActivator.cs +++ b/BTCPayServer/Services/InvoiceActivator.cs @@ -66,8 +66,8 @@ namespace BTCPayServer.Services await _walletRepository.EnsureWalletObjectLink( new WalletObjectId( walletId, - WalletObjectData.Types.Script, - address.ScriptPubKey.ToHex()), + WalletObjectData.Types.Address, + address.ToString()), new WalletObjectId( walletId, WalletObjectData.Types.Invoice, diff --git a/BTCPayServer/Services/WalletRepository.cs b/BTCPayServer/Services/WalletRepository.cs index 8689edd1f..f33d973c0 100644 --- a/BTCPayServer/Services/WalletRepository.cs +++ b/BTCPayServer/Services/WalletRepository.cs @@ -52,16 +52,11 @@ namespace BTCPayServer.Services public string[]? Ids { get; set; } public bool IncludeNeighbours { get; set; } = true; public bool UseInefficientPath { get; set; } - - public static ObjectTypeId Get(Script script) - { - return new ObjectTypeId(WalletObjectData.Types.Script, script.ToHex()); - } public static IEnumerable Get(ReceivedCoin coin) { yield return new ObjectTypeId(WalletObjectData.Types.Tx, coin.OutPoint.Hash.ToString()); - yield return Get(coin.ScriptPubKey); + yield return new ObjectTypeId(WalletObjectData.Types.Address, coin.Address.ToString()); yield return new ObjectTypeId(WalletObjectData.Types.Utxo, coin.OutPoint.ToString()); } } @@ -250,7 +245,7 @@ namespace BTCPayServer.Services { var wos = await GetWalletObjects( new GetWalletObjectsQuery(walletId, WalletObjectData.Types.Tx, transactionIds)); - return await GetWalletTransactionsInfoCore(walletId, wos); + return GetWalletTransactionsInfoCore(walletId, wos); } public async Task> GetWalletTransactionsInfo(WalletId walletId, @@ -259,10 +254,10 @@ namespace BTCPayServer.Services var wos = await GetWalletObjects( new GetWalletObjectsQuery(walletId, transactionIds)); - return await GetWalletTransactionsInfoCore(walletId, wos); + return GetWalletTransactionsInfoCore(walletId, wos); } - private async Task> GetWalletTransactionsInfoCore(WalletId walletId, + private Dictionary GetWalletTransactionsInfoCore(WalletId walletId, Dictionary wos) { diff --git a/BTCPayServer/Services/Wallets/BTCPayWallet.cs b/BTCPayServer/Services/Wallets/BTCPayWallet.cs index 7e9a67028..e091d240b 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWallet.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWallet.cs @@ -27,6 +27,7 @@ namespace BTCPayServer.Services.Wallets public IMoney Value { get; set; } public Coin Coin { get; set; } public long Confirmations { get; set; } + public BitcoinAddress Address { get; set; } } public class NetworkCoins { @@ -91,7 +92,7 @@ namespace BTCPayServer.Services.Wallets if (storeId != null) { await WalletRepository.EnsureWalletObject( - new WalletObjectId(new WalletId(storeId, Network.CryptoCode), WalletObjectData.Types.Script, pathInfo.ScriptPubKey.ToHex()), + new WalletObjectId(new WalletId(storeId, Network.CryptoCode), WalletObjectData.Types.Address, pathInfo.Address.ToString()), new JObject() { ["generatedBy"] = generatedBy }); } return pathInfo; @@ -360,7 +361,9 @@ namespace BTCPayServer.Services.Wallets OutPoint = c.Outpoint, ScriptPubKey = c.ScriptPubKey, Coin = c.AsCoin(derivationStrategy), - Confirmations = c.Confirmations + Confirmations = c.Confirmations, + // Some old version of NBX doesn't have Address in this call + Address = c.Address ?? c.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork) }).ToArray(); }