diff --git a/.gitignore b/.gitignore index 1956435..2e6b938 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,3 @@ .idea Plugins/packed .vs/ -wallets/cashcow/ -wallets/merchant/ diff --git a/Plugins/Monero/Configuration/MoneroLikeConfiguration.cs b/Plugins/Monero/Configuration/MoneroLikeConfiguration.cs index fc6bc5f..e19890c 100644 --- a/Plugins/Monero/Configuration/MoneroLikeConfiguration.cs +++ b/Plugins/Monero/Configuration/MoneroLikeConfiguration.cs @@ -16,7 +16,6 @@ namespace BTCPayServer.Plugins.Monero.Configuration public string WalletDirectory { get; set; } public string Username { get; set; } public string Password { get; set; } - public string CashCowWalletDirectory { get; set; } public Uri CashCowWalletRpcUri { get; set; } } } diff --git a/Plugins/Monero/Controllers/MoneroLikeStoreController.cs b/Plugins/Monero/Controllers/MoneroLikeStoreController.cs index 97df722..4f676b3 100644 --- a/Plugins/Monero/Controllers/MoneroLikeStoreController.cs +++ b/Plugins/Monero/Controllers/MoneroLikeStoreController.cs @@ -101,7 +101,6 @@ namespace BTCPayServer.Plugins.Monero.Controllers _MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary); _MoneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode, out var configurationItem); - var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet"); var accounts = accountsResponse?.SubaddressAccounts?.Select(account => new SelectListItem( $"{account.AccountIndex} - {(string.IsNullOrEmpty(account.Label) ? "No label" : account.Label)}", @@ -121,7 +120,6 @@ namespace BTCPayServer.Plugins.Monero.Controllers return new MoneroLikePaymentMethodViewModel() { - WalletFileFound = System.IO.File.Exists(fileAddress), Enabled = settings != null && !excludeFilters.Match(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)), @@ -193,7 +191,11 @@ namespace BTCPayServer.Plugins.Monero.Controllers ModelState.AddModelError(nameof(viewModel.WalletKeysFile), StringLocalizer["Please select the view-only wallet keys file"]); valid = false; } - + if (configurationItem.WalletDirectory == null) + { + ModelState.AddModelError(nameof(viewModel.WalletFile), StringLocalizer["This installation doesn't support wallet import (BTCPAY_XMR_WALLET_DAEMON_WALLETDIR is not set)"]); + valid = false; + } if (valid) { if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary)) @@ -288,6 +290,7 @@ namespace BTCPayServer.Plugins.Monero.Controllers vm.AccountIndex = viewModel.AccountIndex; vm.SettlementConfirmationThresholdChoice = viewModel.SettlementConfirmationThresholdChoice; vm.CustomSettlementConfirmationThreshold = viewModel.CustomSettlementConfirmationThreshold; + vm.SupportWalletExport = configurationItem.WalletDirectory is not null; return View("/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml", vm); } @@ -345,6 +348,7 @@ namespace BTCPayServer.Plugins.Monero.Controllers public class MoneroLikePaymentMethodViewModel : IValidatableObject { public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; } + public bool SupportWalletExport { get; set; } public string CryptoCode { get; set; } public string NewAccountLabel { get; set; } public long AccountIndex { get; set; } diff --git a/Plugins/Monero/MoneroPlugin.cs b/Plugins/Monero/MoneroPlugin.cs index 1ed60f7..8535d7b 100644 --- a/Plugins/Monero/MoneroPlugin.cs +++ b/Plugins/Monero/MoneroPlugin.cs @@ -128,17 +128,13 @@ public class MoneroPlugin : BaseBTCPayServerPlugin var walletDaemonWalletDirectory = configuration.GetOrDefault( $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null); - // Only for regtest - var walletCashCowDaemonWalletDirectory = - configuration.GetOrDefault( - $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_cashcow_wallet_daemon_walletdir", null); var daemonUsername = configuration.GetOrDefault( $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_username", null); var daemonPassword = configuration.GetOrDefault( $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_password", null); - if (daemonUri == null || walletDaemonUri == null || walletDaemonWalletDirectory == null) + if (daemonUri == null || walletDaemonUri == null) { var logger = serviceProvider.GetRequiredService>(); var cryptoCode = moneroLikeSpecificBtcPayNetwork.CryptoCode.ToUpperInvariant(); @@ -150,10 +146,6 @@ public class MoneroPlugin : BaseBTCPayServerPlugin { logger.LogWarning($"BTCPAY_{cryptoCode}_WALLET_DAEMON_URI is not configured"); } - if (walletDaemonWalletDirectory is null) - { - logger.LogWarning($"BTCPAY_{cryptoCode}_WALLET_DAEMON_WALLETDIR is not configured"); - } logger.LogWarning($"{cryptoCode} got disabled as it is not fully configured."); } else @@ -165,7 +157,6 @@ public class MoneroPlugin : BaseBTCPayServerPlugin Password = daemonPassword, InternalWalletRpcUri = walletDaemonUri, WalletDirectory = walletDaemonWalletDirectory, - CashCowWalletDirectory = walletCashCowDaemonWalletDirectory, CashCowWalletRpcUri = cashCowWalletDaemonUri, }); } diff --git a/Plugins/Monero/RPC/Models/GetTransferByTransactionIdRequest.cs b/Plugins/Monero/RPC/Models/GetTransferByTransactionIdRequest.cs index 23bb39b..d2419be 100644 --- a/Plugins/Monero/RPC/Models/GetTransferByTransactionIdRequest.cs +++ b/Plugins/Monero/RPC/Models/GetTransferByTransactionIdRequest.cs @@ -6,6 +6,6 @@ namespace BTCPayServer.Plugins.Monero.RPC.Models { [JsonProperty("txid")] public string TransactionId { get; set; } - [JsonProperty("account_index")] public long AccountIndex { get; set; } + [JsonProperty("account_index", DefaultValueHandling = DefaultValueHandling.Ignore)] public long? AccountIndex { get; set; } } } diff --git a/Plugins/Monero/Services/MoneroCheckoutCheatModeExtension.cs b/Plugins/Monero/Services/MoneroCheckoutCheatModeExtension.cs index f195a85..72590ed 100644 --- a/Plugins/Monero/Services/MoneroCheckoutCheatModeExtension.cs +++ b/Plugins/Monero/Services/MoneroCheckoutCheatModeExtension.cs @@ -50,7 +50,7 @@ public class MoneroCheckoutCheatModeExtension : ICheckoutCheatModeExtension public async Task MineBlock(ICheckoutCheatModeExtension.MineBlockContext mineBlockContext) { var cashcow = _rpcProvider.CashCowWalletRpcClients[_network.CryptoCode]; - var deamon = _rpcProvider.WalletRpcClients[_network.CryptoCode]; + var deamon = _rpcProvider.DaemonRpcClients[_network.CryptoCode]; var address = (await cashcow.SendCommandAsync("get_address", new() { AccountIndex = 0 diff --git a/Plugins/Monero/Services/MoneroListener.cs b/Plugins/Monero/Services/MoneroListener.cs index aede9ac..30a71cd 100644 --- a/Plugins/Monero/Services/MoneroListener.cs +++ b/Plugins/Monero/Services/MoneroListener.cs @@ -89,6 +89,7 @@ namespace BTCPayServer.Plugins.Monero.Services { await OnNewBlock(moneroEvent.CryptoCode); } + if (!string.IsNullOrEmpty(moneroEvent.TransactionHash)) { await OnTransactionUpdated(moneroEvent.CryptoCode, moneroEvent.TransactionHash); @@ -132,7 +133,8 @@ namespace BTCPayServer.Plugins.Monero.Services var expandedInvoices = invoices.Select(entity => (Invoice: entity, ExistingPayments: GetAllMoneroLikePayments(entity, cryptoCode), Prompt: entity.GetPaymentPrompt(paymentId), - PaymentMethodDetails: handler.ParsePaymentPromptDetails(entity.GetPaymentPrompt(paymentId).Details))) + PaymentMethodDetails: handler.ParsePaymentPromptDetails(entity.GetPaymentPrompt(paymentId) + .Details))) .Select(tuple => ( tuple.Invoice, tuple.PaymentMethodDetails, @@ -208,7 +210,8 @@ namespace BTCPayServer.Plugins.Monero.Services return HandlePaymentData(cryptoCode, transfer.Address, transfer.Amount, transfer.SubaddrIndex.Major, - transfer.SubaddrIndex.Minor, transfer.Txid, transfer.Confirmations, transfer.Height, transfer.UnlockTime,invoice, + transfer.SubaddrIndex.Minor, transfer.Txid, transfer.Confirmations, transfer.Height, + transfer.UnlockTime, invoice, updatedPaymentEntities); })); } @@ -228,17 +231,16 @@ namespace BTCPayServer.Plugins.Monero.Services private async Task OnNewBlock(string cryptoCode) { await UpdateAnyPendingMoneroLikePayment(cryptoCode); - _eventAggregator.Publish(new NewBlockEvent() { PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode) }); + _eventAggregator.Publish(new NewBlockEvent() + { PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode) }); } private async Task OnTransactionUpdated(string cryptoCode, string transactionHash) { var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); - var transfer = await _moneroRpcProvider.WalletRpcClients[cryptoCode] - .SendCommandAsync( - "get_transfer_by_txid", - new GetTransferByTransactionIdRequest() { TransactionId = transactionHash }); - + var transfer = await GetTransferByTxId(cryptoCode, transactionHash, this.CancellationToken); + if (transfer is null) + return; var paymentsToUpdate = new List<(PaymentEntity Payment, InvoiceEntity invoice)>(); //group all destinations of the tx together and loop through the sets @@ -259,7 +261,7 @@ namespace BTCPayServer.Plugins.Monero.Services transfer.Transfer.Txid, transfer.Transfer.Confirmations, transfer.Transfer.Height - , transfer.Transfer.UnlockTime,invoice, paymentsToUpdate); + , transfer.Transfer.UnlockTime, invoice, paymentsToUpdate); } if (paymentsToUpdate.Any()) @@ -275,6 +277,49 @@ namespace BTCPayServer.Plugins.Monero.Services } } + private async Task GetTransferByTxId(string cryptoCode, + string transactionHash, CancellationToken cancellationToken) + { + var accounts = await _moneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("get_accounts", new GetAccountsRequest(), cancellationToken); + var accountIndexes = accounts + .SubaddressAccounts + .Select(a => new long?(a.AccountIndex)) + .ToList(); + if (accountIndexes.Count is 0) + accountIndexes.Add(null); + var req = accountIndexes + .Select(i => GetTransferByTxId(cryptoCode, transactionHash, i)) + .ToArray(); + foreach (var task in req) + { + var result = await task; + if (result != null) + return result; + } + + return null; + } + + private async Task GetTransferByTxId(string cryptoCode, string transactionHash, long? accountIndex) + { + try + { + var result = await _moneroRpcProvider.WalletRpcClients[cryptoCode] + .SendCommandAsync( + "get_transfer_by_txid", + new GetTransferByTransactionIdRequest() + { + TransactionId = transactionHash, + AccountIndex = accountIndex + }); + return result; + } + catch (JsonRpcClient.JsonRpcApiException e) + { + return null; + } + } + private async Task HandlePaymentData(string cryptoCode, string address, long totalAmount, long subaccountIndex, long subaddressIndex, string txId, long confirmations, long blockHeight, long locktime, InvoiceEntity invoice, @@ -330,16 +375,17 @@ namespace BTCPayServer.Plugins.Monero.Services => ConfirmationsRequired(details, speedPolicy) <= details.ConfirmationCount; public static long ConfirmationsRequired(MoneroLikePaymentData details, SpeedPolicy speedPolicy) - => (details, speedPolicy) switch - { - (_, _) when details.ConfirmationCount < details.LockTime => details.LockTime - details.ConfirmationCount, - ({ InvoiceSettledConfirmationThreshold: long v }, _) => v, - (_, SpeedPolicy.HighSpeed) => 0, - (_, SpeedPolicy.MediumSpeed) => 1, - (_, SpeedPolicy.LowMediumSpeed) => 2, - (_, SpeedPolicy.LowSpeed) => 6, - _ => 6, - }; + => (details, speedPolicy) switch + { + (_, _) when details.ConfirmationCount < details.LockTime => + details.LockTime - details.ConfirmationCount, + ({ InvoiceSettledConfirmationThreshold: long v }, _) => v, + (_, SpeedPolicy.HighSpeed) => 0, + (_, SpeedPolicy.MediumSpeed) => 1, + (_, SpeedPolicy.LowMediumSpeed) => 2, + (_, SpeedPolicy.LowSpeed) => 6, + _ => 6, + }; private async Task UpdateAnyPendingMoneroLikePayment(string cryptoCode) @@ -358,4 +404,4 @@ namespace BTCPayServer.Plugins.Monero.Services .Where(p => p.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)); } } -} +} \ No newline at end of file diff --git a/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml b/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml index d33e9be..6f660e0 100644 --- a/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml +++ b/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml @@ -20,14 +20,13 @@
  • Node available: @Model.Summary.DaemonAvailable
  • -
  • Wallet available: @Model.Summary.WalletAvailable (@(Model.WalletFileFound ? "Wallet file present" : "Wallet file not found"))
  • Last updated: @Model.Summary.UpdatedAt
  • Synced: @Model.Summary.Synced (@Model.Summary.CurrentHeight / @Model.Summary.TargetHeight)
} - @if (!Model.WalletFileFound || Model.Summary.WalletHeight == default) + @if (Model.SupportWalletExport && Model.Summary?.WalletHeight is null or 0) {
- @if (!Model.WalletFileFound || Model.Summary.WalletHeight == default) + @if (Model.Summary?.WalletHeight is null or 0) { } diff --git a/README.md b/README.md index 5ecbe94..3a6afb3 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ This plugin extends BTCPay Server to enable users to receive payments via Monero Configure this plugin using the following environment variables: -| Environment variable | Description | Example | -| --- | --- | --- | -**BTCPAY_XMR_DAEMON_URI** | **Required**. The URI of the [monerod](https://github.com/monero-project/monero) RPC interface. | http://127.0.0.1:18081 | -**BTCPAY_XMR_DAEMON_USERNAME** | **Optional**. The username for authenticating with the daemon. | john | -**BTCPAY_XMR_DAEMON_PASSWORD** | **Optional**. The password for authenticating with the daemon. | secret | -**BTCPAY_XMR_WALLET_DAEMON_URI** | **Required**. The URI of the [monero-wallet-rpc](https://getmonero.dev/interacting/monero-wallet-rpc.html) RPC interface. | http://127.0.0.1:18082 | -**BTCPAY_XMR_WALLET_DAEMON_WALLETDIR** | **Required**. The directory where BTCPay Server saves wallet files uploaded via the UI ([See this blog post for more details](https://sethforprivacy.com/guides/accepting-monero-via-btcpay-server/#configure-the-bitcoin-wallet-of-choice)). | /home/cypherpunk/Monero/wallets/ | +| Environment variable | Description | Example | +| --- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | +**BTCPAY_XMR_DAEMON_URI** | **Required**. The URI of the [monerod](https://github.com/monero-project/monero) RPC interface. | http://127.0.0.1:18081 | +**BTCPAY_XMR_DAEMON_USERNAME** | **Optional**. The username for authenticating with the daemon. | john | +**BTCPAY_XMR_DAEMON_PASSWORD** | **Optional**. The password for authenticating with the daemon. | secret | +**BTCPAY_XMR_WALLET_DAEMON_URI** | **Required**. The URI of the [monero-wallet-rpc](https://getmonero.dev/interacting/monero-wallet-rpc.html) RPC interface. | http://127.0.0.1:18082 | +**BTCPAY_XMR_WALLET_DAEMON_WALLETDIR** | **Optional**. The directory where BTCPay Server saves wallet files uploaded via the UI ([See this blog post for more details](https://sethforprivacy.com/guides/accepting-monero-via-btcpay-server/#configure-the-bitcoin-wallet-of-choice)). | /home/cypherpunk/Monero/wallets/ | BTCPay Server's Docker deployment simplifies the setup by automatically configuring these variables. For further details, refer to this [blog post](https://sethforprivacy.com/guides/accepting-monero-via-btcpay-server). @@ -37,8 +37,6 @@ Then create the `appsettings.dev.json` file in `btcpayserver/BTCPayServer`, with "XMR_DAEMON_URI": "http://127.0.0.1:18081", "XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082", "XMR_CASHCOW_WALLET_DAEMON_URI": "http://127.0.0.1:18092", - "XMR_WALLET_DAEMON_WALLETDIR": "C:\\Sources\\btcpayserver-monero-plugin\\wallets\\merchant", - "XMR_CASHCOW_WALLET_DAEMON_WALLETDIR": "C:\\Sources\\btcpayserver-monero-plugin\\wallets\\cashcow" } ``` diff --git a/btcpayserver b/btcpayserver index 995d5a0..6b49544 160000 --- a/btcpayserver +++ b/btcpayserver @@ -1 +1 @@ -Subproject commit 995d5a0a193444f08a4ae8023e0726996356b5b4 +Subproject commit 6b495444700378cf3c4e1286b355df530e90c494 diff --git a/docker-compose.yml b/docker-compose.yml index 1a5e12b..eeb0674 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -86,7 +86,7 @@ services: ports: - "18082:18082" volumes: - - "./wallets/merchant:/wallet" + - "tests_monero_merchant_wallet:/wallet" depends_on: - monerod @@ -98,7 +98,7 @@ services: ports: - "18092:18092" volumes: - - "./wallets/cashcow:/wallet" + - "tests_monero_cashcow_wallet:/wallet" depends_on: - monerod @@ -114,6 +114,8 @@ services: volumes: bitcoin_datadir: monero_data: + tests_monero_merchant_wallet: + tests_monero_cashcow_wallet: networks: default: