Improve debugging experience through cheatmode

This commit is contained in:
nicolas.dorier
2025-01-10 16:19:27 +09:00
parent a9899e5d0d
commit 73f9320112
14 changed files with 267 additions and 62 deletions

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@
.idea
Plugins/packed
.vs/
monero_wallet/
wallets/cashcow/
wallets/merchant/

View File

@@ -16,5 +16,7 @@ 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; }
}
}

View File

@@ -82,6 +82,9 @@ public class MoneroPlugin : BaseBTCPayServerPlugin
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroCheckoutModelExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutCheatModeExtension>(provider =>
(ICheckoutCheatModeExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroCheckoutCheatModeExtension), new object[] { network, pmi }));
services.AddUIExtension("store-nav", "/Views/Monero/StoreNavMoneroExtension.cshtml");
services.AddUIExtension("store-wallets-nav", "/Views/Monero/StoreWalletsNavMoneroExtension.cshtml");
@@ -119,9 +122,16 @@ public class MoneroPlugin : BaseBTCPayServerPlugin
var walletDaemonUri =
configuration.GetOrDefault<Uri>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_uri", null);
var cashCowWalletDaemonUri =
configuration.GetOrDefault<Uri>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_cashcow_wallet_daemon_uri", null);
var walletDaemonWalletDirectory =
configuration.GetOrDefault<string>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null);
// Only for regtest
var walletCashCowDaemonWalletDirectory =
configuration.GetOrDefault<string>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_cashcow_wallet_daemon_walletdir", null);
var daemonUsername =
configuration.GetOrDefault<string>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_username", null);
@@ -154,7 +164,9 @@ public class MoneroPlugin : BaseBTCPayServerPlugin
Username = daemonUsername,
Password = daemonPassword,
InternalWalletRpcUri = walletDaemonUri,
WalletDirectory = walletDaemonWalletDirectory
WalletDirectory = walletDaemonWalletDirectory,
CashCowWalletDirectory = walletCashCowDaemonWalletDirectory,
CashCowWalletRpcUri = cashCowWalletDaemonUri,
});
}
}

View File

@@ -0,0 +1,9 @@
using Newtonsoft.Json;
namespace BTCPayServer.Plugins.Monero.RPC.Models;
public class GenerateBlocks
{
[JsonProperty("wallet_address")]public string WalletAddress { get; set; }
[JsonProperty("amount_of_blocks")] public int AmountOfBlocks { get; set; }
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace BTCPayServer.Plugins.Monero.RPC.Models;
public class GetAddressRequest
{
[JsonProperty("account_index")] public int AccountIndex { get; set; }
}
public class GetAddressResponse
{
[JsonProperty("address")] public string Address { get; set; }
}

View File

@@ -0,0 +1,8 @@
using Newtonsoft.Json;
namespace BTCPayServer.Plugins.Monero.RPC.Models;
public class GetBalanceResponse
{
[JsonProperty("unlocked_balance")] public long UnlockedBalance { get; set; }
}

View File

@@ -3,6 +3,12 @@ using Newtonsoft.Json;
namespace BTCPayServer.Plugins.Monero.RPC.Models
{
public partial class TransferDestination
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("amount")] public long Amount { get; set; }
}
public partial class GetTransferByTransactionIdResponse
{
[JsonProperty("transfer")] public TransferItem Transfer { get; set; }
@@ -12,6 +18,7 @@ namespace BTCPayServer.Plugins.Monero.RPC.Models
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("destinations")] public TransferDestination[] Destinations { get; set; }
[JsonProperty("confirmations")] public long Confirmations { get; set; }
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
[JsonProperty("height")] public long Height { get; set; }

View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace BTCPayServer.Plugins.Monero.RPC.Models;
public class TransferRequest
{
[JsonProperty("destinations")] public TransferDestination[] Destinations { get; set; }
}
public class TransferResponse
{
[JsonProperty("tx_hash")] public string TransactionHash { get; set; }
}

View File

@@ -0,0 +1,65 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Plugins.Monero.RPC;
using BTCPayServer.Plugins.Monero.RPC.Models;
using NBitcoin;
namespace BTCPayServer.Plugins.Monero.Services;
public class MoneroCheckoutCheatModeExtension : ICheckoutCheatModeExtension
{
private readonly MoneroRPCProvider _rpcProvider;
private readonly MoneroLikeSpecificBtcPayNetwork _network;
private readonly PaymentMethodId _paymentMethodId;
public MoneroCheckoutCheatModeExtension(
MoneroRPCProvider rpcProvider,
MoneroLikeSpecificBtcPayNetwork network,
PaymentMethodId paymentMethodId)
{
_rpcProvider = rpcProvider;
_network = network;
_paymentMethodId = paymentMethodId;
}
public bool Handle(PaymentMethodId paymentMethodId) => _paymentMethodId == paymentMethodId;
public async Task<ICheckoutCheatModeExtension.PayInvoiceResult> PayInvoice(ICheckoutCheatModeExtension.PayInvoiceContext payInvoiceContext)
{
var amount = payInvoiceContext.Amount;
for (int i = 0; i < _network.Divisibility; i++)
{
amount *= 10;
}
var cashcow = _rpcProvider.CashCowWalletRpcClients[_network.CryptoCode];
var result = await cashcow.SendCommandAsync<TransferRequest, TransferResponse>("transfer",
new TransferRequest()
{
Destinations = new[] { new TransferDestination()
{
Amount = (long)amount,
Address = payInvoiceContext.PaymentPrompt.Destination
}
}});
return new ICheckoutCheatModeExtension.PayInvoiceResult(result.TransactionHash);
}
public async Task<ICheckoutCheatModeExtension.MineBlockResult> MineBlock(ICheckoutCheatModeExtension.MineBlockContext mineBlockContext)
{
var cashcow = _rpcProvider.CashCowWalletRpcClients[_network.CryptoCode];
var deamon = _rpcProvider.WalletRpcClients[_network.CryptoCode];
var address = (await cashcow.SendCommandAsync<GetAddressRequest, GetAddressResponse>("get_address", new()
{
AccountIndex = 0
})).Address;
await deamon.SendCommandAsync<GenerateBlocks, JsonRpcClient.NoRequestModel>("generateblocks", new GenerateBlocks()
{
WalletAddress = address,
AmountOfBlocks = mineBlockContext.BlockCount
});
return new ICheckoutCheatModeExtension.MineBlockResult();
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Amazon.Runtime;
@@ -8,6 +9,7 @@ using BTCPayServer.Plugins.Monero.Configuration;
using BTCPayServer.Plugins.Monero.RPC;
using BTCPayServer.Plugins.Monero.RPC.Models;
using BTCPayServer.Services;
using Microsoft.Extensions.Logging;
using NBitcoin;
namespace BTCPayServer.Plugins.Monero.Services
@@ -15,9 +17,10 @@ namespace BTCPayServer.Plugins.Monero.Services
public class MoneroRPCProvider
{
private readonly MoneroLikeConfiguration _moneroLikeConfiguration;
private readonly ILogger<MoneroRPCProvider> _logger;
private readonly EventAggregator _eventAggregator;
private readonly BTCPayServerEnvironment environment;
public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients;
private readonly BTCPayServerEnvironment environment;
public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients;
public ImmutableDictionary<string, JsonRpcClient> WalletRpcClients;
private readonly ConcurrentDictionary<string, MoneroLikeSummary> _summaries =
@@ -25,19 +28,35 @@ namespace BTCPayServer.Plugins.Monero.Services
public ConcurrentDictionary<string, MoneroLikeSummary> Summaries => _summaries;
public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration, EventAggregator eventAggregator, IHttpClientFactory httpClientFactory, BTCPayServerEnvironment environment)
public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration,
ILogger<MoneroRPCProvider> logger,
EventAggregator eventAggregator,
IHttpClientFactory httpClientFactory, BTCPayServerEnvironment environment)
{
_moneroLikeConfiguration = moneroLikeConfiguration;
_logger = logger;
_eventAggregator = eventAggregator;
this.environment = environment;
DaemonRpcClients =
this.environment = environment;
DaemonRpcClients =
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
pair => new JsonRpcClient(pair.Value.DaemonRpcUri, pair.Value.Username, pair.Value.Password, httpClientFactory.CreateClient($"{pair.Key}client")));
pair => new JsonRpcClient(pair.Value.DaemonRpcUri, pair.Value.Username, pair.Value.Password,
httpClientFactory.CreateClient($"{pair.Key}client")));
WalletRpcClients =
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "", httpClientFactory.CreateClient($"{pair.Key}client")));
pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "",
httpClientFactory.CreateClient($"{pair.Key}client")));
if (environment.CheatMode)
{
CashCowWalletRpcClients =
_moneroLikeConfiguration.MoneroLikeConfigurationItems
.Where(i => i.Value.CashCowWalletRpcUri is not null).ToImmutableDictionary(pair => pair.Key,
pair => new JsonRpcClient(pair.Value.CashCowWalletRpcUri, "", "",
httpClientFactory.CreateClient($"{pair.Key}cashcow-client")));
}
}
public ImmutableDictionary<string, JsonRpcClient> CashCowWalletRpcClients { get; set; }
public bool IsAvailable(string cryptoCode)
{
cryptoCode = cryptoCode.ToUpperInvariant();
@@ -77,8 +96,8 @@ namespace BTCPayServer.Plugins.Monero.Services
}
bool walletCreated = false;
retry:
try
retry:
try
{
var walletResult =
await walletRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, GetHeightResponse>(
@@ -88,21 +107,21 @@ namespace BTCPayServer.Plugins.Monero.Services
}
catch when (environment.CheatMode && !walletCreated)
{
await walletRpcClient.SendCommandAsync<CreateWalletRequest, JsonRpcClient.NoRequestModel>("create_wallet",
new()
{
Filename = "wallet",
Password = "",
Language = "English"
});
await CreateTestWallet(walletRpcClient);
walletCreated = true;
goto retry;
}
}
catch
{
summary.WalletAvailable = false;
}
if (environment.CheatMode &&
CashCowWalletRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var cashCow))
{
await MakeCashCowFat(cashCow, daemonRpcClient);
}
var changed = !_summaries.ContainsKey(cryptoCode) || IsAvailable(cryptoCode) != IsAvailable(summary);
_summaries.AddOrReplace(cryptoCode, summary);
@@ -114,6 +133,64 @@ namespace BTCPayServer.Plugins.Monero.Services
return summary;
}
private async Task MakeCashCowFat(JsonRpcClient cashcow, JsonRpcClient deamon)
{
try
{
var walletResult =
await cashcow.SendCommandAsync<JsonRpcClient.NoRequestModel, GetHeightResponse>(
"get_height", JsonRpcClient.NoRequestModel.Instance);
}
catch
{
_logger.LogInformation("Creating XMR cashcow wallet...");
await CreateTestWallet(cashcow);
}
var balance =
(await cashcow.SendCommandAsync<JsonRpcClient.NoRequestModel, GetBalanceResponse>("get_balance",
JsonRpcClient.NoRequestModel.Instance));
if (balance.UnlockedBalance != 0)
return;
_logger.LogInformation("Mining blocks for the cashcow...");
var address = (await cashcow.SendCommandAsync<GetAddressRequest, GetAddressResponse>("get_address", new()
{
AccountIndex = 0
})).Address;
await deamon.SendCommandAsync<GenerateBlocks, JsonRpcClient.NoRequestModel>("generateblocks", new GenerateBlocks()
{
WalletAddress = address,
AmountOfBlocks = 100
});
_logger.LogInformation("Mining succeed!");
}
private static async Task CreateTestWallet(JsonRpcClient walletRpcClient)
{
try
{
await walletRpcClient.SendCommandAsync<OpenWalletRequest, JsonRpcClient.NoRequestModel>(
"open_wallet",
new OpenWalletRequest()
{
Filename = "wallet",
Password = "password"
});
return;
}
catch
{
}
await walletRpcClient.SendCommandAsync<CreateWalletRequest, JsonRpcClient.NoRequestModel>("create_wallet",
new()
{
Filename = "wallet",
Password = "password",
Language = "English"
});
}
public class MoneroDaemonStateChange
{
@@ -132,4 +209,4 @@ namespace BTCPayServer.Plugins.Monero.Services
public bool WalletAvailable { get; set; }
}
}
}
}

View File

@@ -36,7 +36,9 @@ Then create the `appsettings.dev.json` file in `btcpayserver/BTCPayServer`, with
"DEBUG_PLUGINS": "C:\\Sources\\btcpayserver-monero-plugin\\Plugins\\Monero\\bin\\Debug\\net8.0\\BTCPayServer.Plugins.Monero.dll",
"XMR_DAEMON_URI": "http://127.0.0.1:18081",
"XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082",
"XMR_WALLET_DAEMON_WALLETDIR": "C:\\Sources\\btcpayserver-monero-plugin\\monero_wallet"
"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"
}
```
@@ -52,6 +54,9 @@ We recommend using [Rider](https://www.jetbrains.com/rider/) for plugin developm
Visual Studio does not support this feature.
When debugging in regtest, BTCPay Server will automatically create an configure two wallets. (cashcow and merchant)
You can trigger payments or mine blocks on the invoice's checkout page.
# Licence
[MIT](LICENSE.md)

View File

@@ -25,6 +25,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{C962
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Monero", "Plugins\Monero\BTCPayServer.Plugins.Monero.csproj", "{319C8C91-952F-4CF6-A251-058DFC66D70F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{BDB6EEEA-46CE-4E42-A1EC-A705518094A5}"
ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore
.gitignore = .gitignore
.gitmodules = .gitmodules
docker-compose.yml = docker-compose.yml
LICENSE.md = LICENSE.md
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

View File

@@ -5,36 +5,6 @@ version: "3"
# The Visual Studio launch setting `Docker-regtest` is configured to use this environment.
services:
tests:
build:
context: ..
dockerfile: BTCPayServer.Tests/Dockerfile
args:
CONFIGURATION_NAME: Release
environment:
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=btcpayserver
TESTS_EXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
TESTS_HOSTNAME: tests
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-"false"}
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TESTS_INCONTAINER: "true"
TESTS_SSHCONNECTION: "root@sshd:22"
TESTS_SSHPASSWORD: ""
TESTS_SSHKEYFILE: ""
TESTS_SOCKSENDPOINT: "tor:9050"
expose:
- "80"
depends_on:
- dev
extra_hosts:
- "tests:127.0.0.1"
networks:
default:
custom:
ipv4_address: 172.23.0.18
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: alpine:3.7
@@ -42,7 +12,8 @@ services:
depends_on:
- nbxplorer
- postgres
- monero_wallet
- monero_cashcow_wallet
- monero_merchant_wallet
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.16
@@ -98,27 +69,39 @@ services:
- "bitcoin_datadir:/data"
monerod:
image: btcpayserver/monero:0.18.3.3
image: btcpayserver/monero:0.18.3.4
restart: unless-stopped
container_name: xmr_monerod
entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline --non-interactive
container_name: tests_monerod
entrypoint: monerod --fixed-difficulty 1 --log-level=2 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --regtest --no-igd --hide-my-port --offline --non-interactive
volumes:
- "monero_data:/home/monero/.bitmonero"
ports:
- "18081:18081"
monero_wallet:
image: btcpayserver/monero:0.18.3.3
monero_merchant_wallet:
image: btcpayserver/monero:0.18.3.4
restart: unless-stopped
container_name: xmr_wallet_rpc
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
container_name: tests_monero_merchant_wallet
entrypoint: monero-wallet-rpc --log-level 2 --allow-mismatched-daemon-version --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
ports:
- "18082:18082"
volumes:
- "./monero_wallet:/wallet"
volumes:
- "./wallets/merchant:/wallet"
depends_on:
- monerod
monero_cashcow_wallet:
image: btcpayserver/monero:0.18.3.4
restart: unless-stopped
container_name: tests_monero_cashcow_wallet
entrypoint: monero-wallet-rpc --log-level 2 --allow-mismatched-daemon-version --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18092 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet
ports:
- "18092:18092"
volumes:
- "./wallets/cashcow:/wallet"
depends_on:
- monerod
postgres:
image: postgres:13.13
environment: