From d3e2d5a095fdc2610eaa386aaf0b4e4ed164ef39 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 18 Nov 2022 07:25:35 +0100 Subject: [PATCH] Wallet object scripts --- BTCPayServer.Data/Data/WalletObjectData.cs | 2 + BTCPayServer.Tests/GreenfieldAPITests.cs | 2 +- ...GreenfieldStoreOnChainWalletsController.cs | 2 +- .../Controllers/UIWalletsController.cs | 15 +++- BTCPayServer/Data/WalletTransactionInfo.cs | 19 ++++- .../TransactionLabelMarkerHostedService.cs | 74 ++++++++++++++----- BTCPayServer/Services/WalletRepository.cs | 52 +++++++++++-- .../Services/Wallets/WalletReceiveService.cs | 7 +- 8 files changed, 140 insertions(+), 33 deletions(-) diff --git a/BTCPayServer.Data/Data/WalletObjectData.cs b/BTCPayServer.Data/Data/WalletObjectData.cs index a9c48720a..e73e087eb 100644 --- a/BTCPayServer.Data/Data/WalletObjectData.cs +++ b/BTCPayServer.Data/Data/WalletObjectData.cs @@ -21,6 +21,8 @@ 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 Utxo = "utxo"; } public string WalletId { get; set; } public string Type { get; set; } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 574b52863..828a69085 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -3194,7 +3194,7 @@ namespace BTCPayServer.Tests // Only the node `test` `test` is connected to `test1` var wid = new WalletId(admin.StoreId, "BTC"); var repo = tester.PayTester.GetService(); - var allObjects = await repo.GetWalletObjects((new(wid, null) { UseInefficientPath = useInefficient })); + var allObjects = await repo.GetWalletObjects(new(wid) { UseInefficientPath = useInefficient }); var allObjectsNoWallet = await repo.GetWalletObjects((new() { UseInefficientPath = useInefficient })); var allObjectsNoWalletAndType = await repo.GetWalletObjects((new() { Type = "test", UseInefficientPath = useInefficient })); var allTests = await repo.GetWalletObjects((new(wid, "test") { UseInefficientPath = useInefficient })); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs index 9b6a85363..022439b94 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs @@ -189,7 +189,7 @@ namespace BTCPayServer.Controllers.Greenfield var wallet = _btcPayWalletProvider.GetWallet(network); var walletId = new WalletId(storeId, cryptoCode); - var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId); + var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId, (string[] ) null); var preFiltering = true; if (statusFilter?.Any() is true || !string.IsNullOrWhiteSpace(labelFilter)) diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index 488f79076..4b77be0a6 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -578,17 +578,24 @@ namespace BTCPayServer.Controllers var utxos = await _walletProvider.GetWallet(network) .GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation); - var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).Distinct().ToArray()); + var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId, + utxos.SelectMany(u => GetWalletObjectsQuery.Get(u)).Distinct().ToArray()); vm.InputsAvailable = utxos.Select(coin => { walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info); - var labels = CreateTransactionTagModels(info).ToList(); + walletTransactionsInfoAsync.TryGetValue(coin.ScriptPubKey.ToHex(), out var info2); + + if (info is not null && info2 is not null) + { + info.Merge(info2); + } + info ??= info2; return new WalletSendModel.InputSelectionOption() { Outpoint = coin.OutPoint.ToString(), Amount = coin.Value.GetValue(network), Comment = info?.Comment, - Labels = labels, + Labels = CreateTransactionTagModels(info), Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString()), Confirmations = coin.Confirmations @@ -1291,7 +1298,7 @@ namespace BTCPayServer.Controllers return NotFound(); var wallet = _walletProvider.GetWallet(paymentMethod.Network); - var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId); + var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[] ) null); var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, null, null); var walletTransactionsInfo = await walletTransactionsInfoAsync; var export = new TransactionsExport(wallet, walletTransactionsInfo); diff --git a/BTCPayServer/Data/WalletTransactionInfo.cs b/BTCPayServer/Data/WalletTransactionInfo.cs index 779d89fc7..df55c8fb4 100644 --- a/BTCPayServer/Data/WalletTransactionInfo.cs +++ b/BTCPayServer/Data/WalletTransactionInfo.cs @@ -7,7 +7,6 @@ using BTCPayServer.Services; using BTCPayServer.Services.Labels; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; namespace BTCPayServer.Data { @@ -83,5 +82,23 @@ namespace BTCPayServer.Data } } + public string Type { get; set; } + + public void Merge(WalletTransactionInfo? value) + { + if (value is null) + return; + + foreach (var valueLabelColor in value.LabelColors) + { + LabelColors.TryAdd(valueLabelColor.Key, valueLabelColor.Value); + } + + foreach (var valueAttachment in value.Attachments.Where(valueAttachment => !Attachments.Any(attachment => + attachment.Id == valueAttachment.Id && attachment.Type == valueAttachment.Type))) + { + Attachments.Add(valueAttachment); + } + } } } diff --git a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs index 1704c04e5..a5583b0b1 100644 --- a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs +++ b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Logging; @@ -22,42 +23,77 @@ namespace BTCPayServer.HostedServices { public class TransactionLabelMarkerHostedService : EventHostedServiceBase { - private readonly EventAggregator _eventAggregator; private readonly WalletRepository _walletRepository; public TransactionLabelMarkerHostedService(EventAggregator eventAggregator, WalletRepository walletRepository, Logs logs) : base(eventAggregator, logs) { - _eventAggregator = eventAggregator; _walletRepository = walletRepository; } protected override void SubscribeToEvents() { Subscribe(); + Subscribe(); } protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) { - if (evt is InvoiceEvent invoiceEvent && invoiceEvent.Name == InvoiceEvent.ReceivedPayment && - invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance && - invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData) + switch (evt) { - var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode()); - var transactionId = bitcoinLikePaymentData.Outpoint.Hash; - var labels = new List + case NewOnChainTransactionEvent transactionEvent: { - Attachment.Invoice(invoiceEvent.Invoice.Id) - }; - foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice)) - { - labels.Add(Attachment.PaymentRequest(paymentId)); - } - foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice)) - { - labels.Add(Attachment.App(appId)); - } + 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(); + + // we are intentionally excluding wallet id filter so that we reduce db trips + var objs = await _walletRepository.GetWalletObjects(new GetWalletObjectsQuery(){TypesIds = matchedObjects}); - await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels); + 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: + { + var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode()); + var transactionId = bitcoinLikePaymentData.Outpoint.Hash; + var labels = new List + { + Attachment.Invoice(invoiceEvent.Invoice.Id) + }; + foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice)) + { + labels.Add(Attachment.PaymentRequest(paymentId)); + } + foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice)) + { + labels.Add(Attachment.App(appId)); + } + + await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels); + break; + } } } } diff --git a/BTCPayServer/Services/WalletRepository.cs b/BTCPayServer/Services/WalletRepository.cs index 94265278e..8689edd1f 100644 --- a/BTCPayServer/Services/WalletRepository.cs +++ b/BTCPayServer/Services/WalletRepository.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client.Models; using BTCPayServer.Data; +using BTCPayServer.Services.Wallets; using Microsoft.EntityFrameworkCore; using NBitcoin; using Newtonsoft.Json; @@ -37,8 +38,9 @@ namespace BTCPayServer.Services Type = type; Ids = ids; } - public GetWalletObjectsQuery(ObjectTypeId[]? typesIds) + public GetWalletObjectsQuery(WalletId? walletId,ObjectTypeId[]? typesIds) { + WalletId = walletId; TypesIds = typesIds; } @@ -50,6 +52,18 @@ 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.Utxo, coin.OutPoint.ToString()); + } } #nullable restore @@ -230,9 +244,28 @@ namespace BTCPayServer.Services } } #nullable restore - public async Task> GetWalletTransactionsInfo(WalletId walletId, string[] transactionIds = null) + + public async Task> GetWalletTransactionsInfo(WalletId walletId, + string[] transactionIds = null) { - var wos = await GetWalletObjects((GetWalletObjectsQuery)(new(walletId, WalletObjectData.Types.Tx, transactionIds))); + var wos = await GetWalletObjects( + new GetWalletObjectsQuery(walletId, WalletObjectData.Types.Tx, transactionIds)); + return await GetWalletTransactionsInfoCore(walletId, wos); + } + + public async Task> GetWalletTransactionsInfo(WalletId walletId, + ObjectTypeId[] transactionIds = null) + { + var wos = await GetWalletObjects( + new GetWalletObjectsQuery(walletId, transactionIds)); + + return await GetWalletTransactionsInfoCore(walletId, wos); + } + + private async Task> GetWalletTransactionsInfoCore(WalletId walletId, + Dictionary wos) + { + var result = new Dictionary(wos.Count); foreach (var obj in wos.Values) { @@ -421,13 +454,20 @@ namespace BTCPayServer.Services } public Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, Attachment attachment) { - return AddWalletTransactionAttachment(walletId, txId, new[] { attachment }); + return AddWalletTransactionAttachment(walletId, txId.ToString(), new []{attachment}, WalletObjectData.Types.Tx); } - public async Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, IEnumerable attachments) + + public Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, + IEnumerable attachments) + { + return AddWalletTransactionAttachment(walletId, txId.ToString(), attachments, WalletObjectData.Types.Tx); + } + + public async Task AddWalletTransactionAttachment(WalletId walletId, string txId, IEnumerable attachments, string type) { ArgumentNullException.ThrowIfNull(walletId); ArgumentNullException.ThrowIfNull(txId); - var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, txId.ToString()); + var txObjId = new WalletObjectId(walletId, type, txId.ToString()); await EnsureWalletObject(txObjId); foreach (var attachment in attachments) { diff --git a/BTCPayServer/Services/Wallets/WalletReceiveService.cs b/BTCPayServer/Services/Wallets/WalletReceiveService.cs index b184aefd5..eb2e6900d 100644 --- a/BTCPayServer/Services/Wallets/WalletReceiveService.cs +++ b/BTCPayServer/Services/Wallets/WalletReceiveService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Services.Stores; using Microsoft.Extensions.Hosting; @@ -22,19 +23,21 @@ namespace BTCPayServer.Services.Wallets private readonly BTCPayWalletProvider _btcPayWalletProvider; private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly StoreRepository _storeRepository; + private readonly WalletRepository _walletRepository; private readonly ConcurrentDictionary _walletReceiveState = new ConcurrentDictionary(); public WalletReceiveService(EventAggregator eventAggregator, ExplorerClientProvider explorerClientProvider, BTCPayWalletProvider btcPayWalletProvider, BTCPayNetworkProvider btcPayNetworkProvider, - StoreRepository storeRepository) + StoreRepository storeRepository, WalletRepository walletRepository ) { _eventAggregator = eventAggregator; _explorerClientProvider = explorerClientProvider; _btcPayWalletProvider = btcPayWalletProvider; _btcPayNetworkProvider = btcPayNetworkProvider; _storeRepository = storeRepository; + _walletRepository = walletRepository; } public async Task UnReserveAddress(WalletId walletId) @@ -73,6 +76,8 @@ namespace BTCPayServer.Services.Wallets } var reserve = (await wallet.ReserveAddressAsync(derivationScheme.AccountDerivation)); + await _walletRepository.AddWalletTransactionAttachment(walletId, reserve.ScriptPubKey.ToString(), new []{new Attachment("receive")}, + WalletObjectData.Types.Script); Set(walletId, reserve); return reserve; }