mirror of
https://github.com/MAGICGrants/btcpayserver-monero-plugin.git
synced 2026-01-08 02:23:51 -05:00
Init commit
This commit is contained in:
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
**/bin/**/*
|
||||
**/obj
|
||||
.idea
|
||||
Plugins/packed
|
||||
.vs/
|
||||
monero_wallet/
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "btcpayserver"]
|
||||
path = btcpayserver
|
||||
url = https://github.com/btcpayserver/btcpayserver
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2025 btcpayserver
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
56
Plugins/Monero/BTCPayServer.Plugins.Monero.csproj
Normal file
56
Plugins/Monero/BTCPayServer.Plugins.Monero.csproj
Normal file
@@ -0,0 +1,56 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Plugin specific properties -->
|
||||
<PropertyGroup>
|
||||
<Product>BTCPay Server Plugin Template</Product>
|
||||
<Description>A template for your own BTCPay Server plugin.</Description>
|
||||
<Version>1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Plugin development properties -->
|
||||
<PropertyGroup>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<PreserveCompilationContext>false</PreserveCompilationContext>
|
||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
|
||||
<ItemDefinitionGroup>
|
||||
<ProjectReference>
|
||||
<Properties>StaticWebAssetsEnabled=false</Properties>
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
|
||||
<!-- If you need Entity Framework, you can uncomment this. This will make it usable in your project without publishing assemblies
|
||||
already referenced by BTCPay Server Core project -->
|
||||
<!--
|
||||
<ItemGroup Condition="$(Configuration) != 'Release'">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.9.2" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
<!-- If you reference another project, by default, the dlls won't be copied in the published plugin, you need <Private>true</Private> -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\submodules\some-client\src\Some.Client\Some.Client.csproj">
|
||||
<Private>true</Private>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
20
Plugins/Monero/Configuration/MoneroLikeConfiguration.cs
Normal file
20
Plugins/Monero/Configuration/MoneroLikeConfiguration.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Configuration
|
||||
{
|
||||
public class MoneroLikeConfiguration
|
||||
{
|
||||
public Dictionary<string, MoneroLikeConfigurationItem> MoneroLikeConfigurationItems { get; set; } =
|
||||
new Dictionary<string, MoneroLikeConfigurationItem>();
|
||||
}
|
||||
|
||||
public class MoneroLikeConfigurationItem
|
||||
{
|
||||
public Uri DaemonRpcUri { get; set; }
|
||||
public Uri InternalWalletRpcUri { get; set; }
|
||||
public string WalletDirectory { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
38
Plugins/Monero/Controllers/MoneroDaemonCallbackController.cs
Normal file
38
Plugins/Monero/Controllers/MoneroDaemonCallbackController.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Plugins.Monero.RPC;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Controllers
|
||||
{
|
||||
[Route("[controller]")]
|
||||
public class MoneroLikeDaemonCallbackController : Controller
|
||||
{
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
public MoneroLikeDaemonCallbackController(EventAggregator eventAggregator)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
[HttpGet("block")]
|
||||
public IActionResult OnBlockNotify(string hash, string cryptoCode)
|
||||
{
|
||||
_eventAggregator.Publish(new MoneroEvent()
|
||||
{
|
||||
BlockHash = hash,
|
||||
CryptoCode = cryptoCode.ToUpperInvariant()
|
||||
});
|
||||
return Ok();
|
||||
}
|
||||
[HttpGet("tx")]
|
||||
public IActionResult OnTransactionNotify(string hash, string cryptoCode)
|
||||
{
|
||||
_eventAggregator.Publish(new MoneroEvent()
|
||||
{
|
||||
TransactionHash = hash,
|
||||
CryptoCode = cryptoCode.ToUpperInvariant()
|
||||
});
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
392
Plugins/Monero/Controllers/MoneroLikeStoreController.cs
Normal file
392
Plugins/Monero/Controllers/MoneroLikeStoreController.cs
Normal file
@@ -0,0 +1,392 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.Monero.Configuration;
|
||||
using BTCPayServer.Plugins.Monero.Payments;
|
||||
using BTCPayServer.Plugins.Monero.RPC.Models;
|
||||
using BTCPayServer.Plugins.Monero.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Controllers
|
||||
{
|
||||
[Route("stores/{storeId}/monerolike")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public class UIMoneroLikeStoreController : Controller
|
||||
{
|
||||
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
private readonly MoneroRPCProvider _MoneroRpcProvider;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private IStringLocalizer StringLocalizer { get; }
|
||||
|
||||
public UIMoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration,
|
||||
StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider,
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
IStringLocalizer stringLocalizer)
|
||||
{
|
||||
_MoneroLikeConfiguration = moneroLikeConfiguration;
|
||||
_StoreRepository = storeRepository;
|
||||
_MoneroRpcProvider = moneroRpcProvider;
|
||||
_handlers = handlers;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
public StoreData StoreData => HttpContext.GetStoreData();
|
||||
|
||||
[HttpGet()]
|
||||
public async Task<IActionResult> GetStoreMoneroLikePaymentMethods()
|
||||
{
|
||||
return View("/Views/Monero/GetStoreMoneroLikePaymentMethods.cshtml", await GetVM(StoreData));
|
||||
}
|
||||
[NonAction]
|
||||
public async Task<MoneroLikePaymentMethodListViewModel> GetVM(StoreData storeData)
|
||||
{
|
||||
var excludeFilters = storeData.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
|
||||
var accountsList = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.ToDictionary(pair => pair.Key,
|
||||
pair => GetAccounts(pair.Key));
|
||||
|
||||
await Task.WhenAll(accountsList.Values);
|
||||
return new MoneroLikePaymentMethodListViewModel()
|
||||
{
|
||||
Items = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.Select(pair =>
|
||||
GetMoneroLikePaymentMethodViewModel(storeData, pair.Key, excludeFilters,
|
||||
accountsList[pair.Key].Result))
|
||||
};
|
||||
}
|
||||
|
||||
private Task<GetAccountsResponse> GetAccounts(string cryptoCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary) && summary.WalletAvailable)
|
||||
{
|
||||
|
||||
return _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<GetAccountsRequest, GetAccountsResponse>("get_accounts", new GetAccountsRequest());
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return Task.FromResult<GetAccountsResponse>(null);
|
||||
}
|
||||
|
||||
private MoneroLikePaymentMethodViewModel GetMoneroLikePaymentMethodViewModel(
|
||||
StoreData storeData, string cryptoCode,
|
||||
IPaymentFilter excludeFilters, GetAccountsResponse accountsResponse)
|
||||
{
|
||||
var monero = storeData.GetPaymentMethodConfigs(_handlers)
|
||||
.Where(s => s.Value is MoneroPaymentPromptDetails)
|
||||
.Select(s => (PaymentMethodId: s.Key, Details: (MoneroPaymentPromptDetails)s.Value));
|
||||
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
|
||||
var settings = monero.Where(method => method.PaymentMethodId == pmi).Select(m => m.Details).SingleOrDefault();
|
||||
_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)}",
|
||||
account.AccountIndex.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
var settlementThresholdChoice = MoneroLikeSettlementThresholdChoice.StoreSpeedPolicy;
|
||||
if (settings != null && settings.InvoiceSettledConfirmationThreshold is { } confirmations)
|
||||
{
|
||||
settlementThresholdChoice = confirmations switch
|
||||
{
|
||||
0 => MoneroLikeSettlementThresholdChoice.ZeroConfirmation,
|
||||
1 => MoneroLikeSettlementThresholdChoice.AtLeastOne,
|
||||
10 => MoneroLikeSettlementThresholdChoice.AtLeastTen,
|
||||
_ => MoneroLikeSettlementThresholdChoice.Custom
|
||||
};
|
||||
}
|
||||
|
||||
return new MoneroLikePaymentMethodViewModel()
|
||||
{
|
||||
WalletFileFound = System.IO.File.Exists(fileAddress),
|
||||
Enabled =
|
||||
settings != null &&
|
||||
!excludeFilters.Match(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)),
|
||||
Summary = summary,
|
||||
CryptoCode = cryptoCode,
|
||||
AccountIndex = settings?.AccountIndex ?? accountsResponse?.SubaddressAccounts?.FirstOrDefault()?.AccountIndex ?? 0,
|
||||
Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value),
|
||||
nameof(SelectListItem.Text)),
|
||||
SettlementConfirmationThresholdChoice = settlementThresholdChoice,
|
||||
CustomSettlementConfirmationThreshold =
|
||||
settings != null &&
|
||||
settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
|
||||
? settings.InvoiceSettledConfirmationThreshold
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("{cryptoCode}")]
|
||||
public async Task<IActionResult> GetStoreMoneroLikePaymentMethod(string cryptoCode)
|
||||
{
|
||||
cryptoCode = cryptoCode.ToUpperInvariant();
|
||||
if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.ContainsKey(cryptoCode))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var vm = GetMoneroLikePaymentMethodViewModel(StoreData, cryptoCode,
|
||||
StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode));
|
||||
return View("/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml", vm);
|
||||
}
|
||||
|
||||
[HttpPost("{cryptoCode}")]
|
||||
[DisableRequestSizeLimit]
|
||||
public async Task<IActionResult> GetStoreMoneroLikePaymentMethod(MoneroLikePaymentMethodViewModel viewModel, string command, string cryptoCode)
|
||||
{
|
||||
cryptoCode = cryptoCode.ToUpperInvariant();
|
||||
if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode,
|
||||
out var configurationItem))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (command == "add-account")
|
||||
{
|
||||
try
|
||||
{
|
||||
var newAccount = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<CreateAccountRequest, CreateAccountResponse>("create_account", new CreateAccountRequest()
|
||||
{
|
||||
Label = viewModel.NewAccountLabel
|
||||
});
|
||||
viewModel.AccountIndex = newAccount.AccountIndex;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not create a new account."]);
|
||||
}
|
||||
|
||||
}
|
||||
else if (command == "upload-wallet")
|
||||
{
|
||||
var valid = true;
|
||||
if (viewModel.WalletFile == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.WalletFile), StringLocalizer["Please select the view-only wallet file"]);
|
||||
valid = false;
|
||||
}
|
||||
if (viewModel.WalletKeysFile == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.WalletKeysFile), StringLocalizer["Please select the view-only wallet keys file"]);
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (valid)
|
||||
{
|
||||
if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary))
|
||||
{
|
||||
if (summary.WalletAvailable)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = StringLocalizer["There is already an active wallet configured for {0}. Replacing it would break any existing invoices!", cryptoCode].Value
|
||||
});
|
||||
return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod),
|
||||
new { cryptoCode });
|
||||
}
|
||||
}
|
||||
|
||||
var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet");
|
||||
using (var fileStream = new FileStream(fileAddress, FileMode.Create))
|
||||
{
|
||||
await viewModel.WalletFile.CopyToAsync(fileStream);
|
||||
try
|
||||
{
|
||||
Exec($"chmod 666 {fileAddress}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet.keys");
|
||||
using (var fileStream = new FileStream(fileAddress, FileMode.Create))
|
||||
{
|
||||
await viewModel.WalletKeysFile.CopyToAsync(fileStream);
|
||||
try
|
||||
{
|
||||
Exec($"chmod 666 {fileAddress}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileAddress = Path.Combine(configurationItem.WalletDirectory, "password");
|
||||
using (var fileStream = new StreamWriter(fileAddress, false))
|
||||
{
|
||||
await fileStream.WriteAsync(viewModel.WalletPassword);
|
||||
try
|
||||
{
|
||||
Exec($"chmod 666 {fileAddress}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<OpenWalletRequest, OpenWalletResponse>("open_wallet", new OpenWalletRequest
|
||||
{
|
||||
Filename = "wallet",
|
||||
Password = viewModel.WalletPassword
|
||||
});
|
||||
if (response?.Error != null)
|
||||
{
|
||||
throw new Exception(response.Error.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not open the wallet: {0}", ex.Message]);
|
||||
return View("/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml", viewModel);
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = StringLocalizer["View-only wallet files uploaded. The wallet will soon become available."].Value
|
||||
});
|
||||
return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), new { cryptoCode });
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
|
||||
var vm = GetMoneroLikePaymentMethodViewModel(StoreData, cryptoCode,
|
||||
StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode));
|
||||
|
||||
vm.Enabled = viewModel.Enabled;
|
||||
vm.NewAccountLabel = viewModel.NewAccountLabel;
|
||||
vm.AccountIndex = viewModel.AccountIndex;
|
||||
vm.SettlementConfirmationThresholdChoice = viewModel.SettlementConfirmationThresholdChoice;
|
||||
vm.CustomSettlementConfirmationThreshold = viewModel.CustomSettlementConfirmationThreshold;
|
||||
return View("/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml", vm);
|
||||
}
|
||||
|
||||
var storeData = StoreData;
|
||||
var blob = storeData.GetStoreBlob();
|
||||
storeData.SetPaymentMethodConfig(_handlers[PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)], new MoneroPaymentPromptDetails()
|
||||
{
|
||||
AccountIndex = viewModel.AccountIndex,
|
||||
InvoiceSettledConfirmationThreshold = viewModel.SettlementConfirmationThresholdChoice switch
|
||||
{
|
||||
MoneroLikeSettlementThresholdChoice.ZeroConfirmation => 0,
|
||||
MoneroLikeSettlementThresholdChoice.AtLeastOne => 1,
|
||||
MoneroLikeSettlementThresholdChoice.AtLeastTen => 10,
|
||||
MoneroLikeSettlementThresholdChoice.Custom when viewModel.CustomSettlementConfirmationThreshold is { } custom => custom,
|
||||
_ => null
|
||||
}
|
||||
});
|
||||
|
||||
blob.SetExcluded(PaymentTypes.CHAIN.GetPaymentMethodId(viewModel.CryptoCode), !viewModel.Enabled);
|
||||
storeData.SetStoreBlob(blob);
|
||||
await _StoreRepository.UpdateStore(storeData);
|
||||
return RedirectToAction("GetStoreMoneroLikePaymentMethods",
|
||||
new { StatusMessage = $"{cryptoCode} settings updated successfully", storeId = StoreData.Id });
|
||||
}
|
||||
|
||||
private void Exec(string cmd)
|
||||
{
|
||||
|
||||
var escapedArgs = cmd.Replace("\"", "\\\"", StringComparison.InvariantCulture);
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
FileName = "/bin/sh",
|
||||
Arguments = $"-c \"{escapedArgs}\""
|
||||
}
|
||||
};
|
||||
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
process.Start();
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
public class MoneroLikePaymentMethodListViewModel
|
||||
{
|
||||
public IEnumerable<MoneroLikePaymentMethodViewModel> Items { get; set; }
|
||||
}
|
||||
|
||||
public class MoneroLikePaymentMethodViewModel : IValidatableObject
|
||||
{
|
||||
public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string NewAccountLabel { get; set; }
|
||||
public long AccountIndex { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public IEnumerable<SelectListItem> Accounts { get; set; }
|
||||
public bool WalletFileFound { get; set; }
|
||||
[Display(Name = "View-Only Wallet File")]
|
||||
public IFormFile WalletFile { get; set; }
|
||||
[Display(Name = "Wallet Keys File")]
|
||||
public IFormFile WalletKeysFile { get; set; }
|
||||
[Display(Name = "Wallet Password")]
|
||||
public string WalletPassword { get; set; }
|
||||
[Display(Name = "Consider the invoice settled when the payment transaction …")]
|
||||
public MoneroLikeSettlementThresholdChoice SettlementConfirmationThresholdChoice { get; set; }
|
||||
[Display(Name = "Required Confirmations"), Range(0, 100)]
|
||||
public long? CustomSettlementConfirmationThreshold { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (SettlementConfirmationThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
|
||||
&& CustomSettlementConfirmationThreshold is null)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"You must specify the number of required confirmations when using a custom threshold.",
|
||||
new[] { nameof(CustomSettlementConfirmationThreshold) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MoneroLikeSettlementThresholdChoice
|
||||
{
|
||||
[Display(Name = "Store Speed Policy", Description = "Use the store's speed policy")]
|
||||
StoreSpeedPolicy,
|
||||
[Display(Name = "Zero Confirmation", Description = "Is unconfirmed")]
|
||||
ZeroConfirmation,
|
||||
[Display(Name = "At Least One", Description = "Has at least 1 confirmation")]
|
||||
AtLeastOne,
|
||||
[Display(Name = "At Least Ten", Description = "Has at least 10 confirmations")]
|
||||
AtLeastTen,
|
||||
[Display(Name = "Custom", Description = "Custom")]
|
||||
Custom
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Plugins/Monero/MoneroLikeSpecificBtcPayNetwork.cs
Normal file
8
Plugins/Monero/MoneroLikeSpecificBtcPayNetwork.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace BTCPayServer.Plugins.Altcoins;
|
||||
|
||||
public class MoneroLikeSpecificBtcPayNetwork : BTCPayNetworkBase
|
||||
{
|
||||
public int MaxTrackedConfirmation = 10;
|
||||
public string UriScheme { get; set; }
|
||||
}
|
||||
|
||||
163
Plugins/Monero/MoneroPlugin.cs
Normal file
163
Plugins/Monero/MoneroPlugin.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using System.Net.Http;
|
||||
using System.Net;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Configuration;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Plugins.Altcoins;
|
||||
using BTCPayServer.Plugins.Monero.Configuration;
|
||||
using BTCPayServer.Plugins.Monero.Payments;
|
||||
using BTCPayServer.Plugins.Monero.Services;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using NBXplorer;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero;
|
||||
|
||||
public class MoneroPlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
|
||||
{
|
||||
new IBTCPayServerPlugin.PluginDependency { Identifier = nameof(BTCPayServer), Condition = ">=2.0.5" }
|
||||
};
|
||||
public ChainName ChainName { get; private set; }
|
||||
public NBXplorerNetworkProvider NBXplorerNetworkProvider { get; private set; }
|
||||
public override void Execute(IServiceCollection services)
|
||||
{
|
||||
var network = new MoneroLikeSpecificBtcPayNetwork()
|
||||
{
|
||||
CryptoCode = "XMR",
|
||||
DisplayName = "Monero",
|
||||
Divisibility = 12,
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"XMR_X = XMR_BTC * BTC_X",
|
||||
"XMR_BTC = kraken(XMR_BTC)"
|
||||
},
|
||||
CryptoImagePath = "/imlegacy/monero.svg",
|
||||
UriScheme = "monero"
|
||||
};
|
||||
var blockExplorerLink = ChainName == ChainName.Mainnet
|
||||
? "https://www.exploremonero.com/transaction/{0}"
|
||||
: "https://testnet.xmrchain.net/tx/{0}";
|
||||
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("XMR");
|
||||
services.AddDefaultPrettyName(pmi, network.DisplayName);
|
||||
services.AddBTCPayNetwork(network)
|
||||
.AddTransactionLinkProvider(pmi, new SimpleTransactionLinkProvider(blockExplorerLink));
|
||||
|
||||
|
||||
services.AddSingleton(provider =>
|
||||
ConfigureMoneroLikeConfiguration(provider));
|
||||
services.AddHttpClient("XMRclient")
|
||||
.ConfigurePrimaryHttpMessageHandler(provider =>
|
||||
{
|
||||
var configuration = provider.GetRequiredService<MoneroLikeConfiguration>();
|
||||
if (!configuration.MoneroLikeConfigurationItems.TryGetValue("XMR", out var xmrConfig) || xmrConfig.Username is null || xmrConfig.Password is null)
|
||||
{
|
||||
return new HttpClientHandler();
|
||||
}
|
||||
return new HttpClientHandler
|
||||
{
|
||||
Credentials = new NetworkCredential(xmrConfig.Username, xmrConfig.Password),
|
||||
PreAuthenticate = true
|
||||
};
|
||||
});
|
||||
services.AddSingleton<MoneroRPCProvider>();
|
||||
services.AddHostedService<MoneroLikeSummaryUpdaterHostedService>();
|
||||
services.AddHostedService<MoneroListener>();
|
||||
services.AddSingleton<IPaymentMethodHandler>(provider =>
|
||||
(IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), new object[] { network }));
|
||||
services.AddSingleton<IPaymentLinkExtension>(provider =>
|
||||
(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.AddUIExtension("store-nav", "/Views/Monero/StoreNavMoneroExtension.cshtml");
|
||||
services.AddUIExtension("store-wallets-nav", "/Views/Monero/StoreWalletsNavMoneroExtension.cshtml");
|
||||
services.AddUIExtension("store-invoices-payments", "/Views/Monero/ViewMoneroLikePaymentData.cshtml");
|
||||
services.AddSingleton<ISyncSummaryProvider, MoneroSyncSummaryProvider>();
|
||||
}
|
||||
class SimpleTransactionLinkProvider : DefaultTransactionLinkProvider
|
||||
{
|
||||
public SimpleTransactionLinkProvider(string blockExplorerLink) : base(blockExplorerLink)
|
||||
{
|
||||
}
|
||||
|
||||
public override string GetTransactionLink(string paymentId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(BlockExplorerLink))
|
||||
return null;
|
||||
return string.Format(CultureInfo.InvariantCulture, BlockExplorerLink, paymentId);
|
||||
}
|
||||
}
|
||||
|
||||
private static MoneroLikeConfiguration ConfigureMoneroLikeConfiguration(IServiceProvider serviceProvider)
|
||||
{
|
||||
var configuration = serviceProvider.GetService<IConfiguration>();
|
||||
var btcPayNetworkProvider = serviceProvider.GetService<BTCPayNetworkProvider>();
|
||||
var result = new MoneroLikeConfiguration();
|
||||
|
||||
var supportedNetworks = btcPayNetworkProvider.GetAll()
|
||||
.OfType<MoneroLikeSpecificBtcPayNetwork>();
|
||||
|
||||
foreach (var moneroLikeSpecificBtcPayNetwork in supportedNetworks)
|
||||
{
|
||||
var daemonUri =
|
||||
configuration.GetOrDefault<Uri>($"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_uri",
|
||||
null);
|
||||
var walletDaemonUri =
|
||||
configuration.GetOrDefault<Uri>(
|
||||
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_uri", null);
|
||||
var walletDaemonWalletDirectory =
|
||||
configuration.GetOrDefault<string>(
|
||||
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null);
|
||||
var daemonUsername =
|
||||
configuration.GetOrDefault<string>(
|
||||
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_username", null);
|
||||
var daemonPassword =
|
||||
configuration.GetOrDefault<string>(
|
||||
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_password", null);
|
||||
if (daemonUri == null || walletDaemonUri == null || walletDaemonWalletDirectory == null)
|
||||
{
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<MoneroPlugin>>();
|
||||
var cryptoCode = moneroLikeSpecificBtcPayNetwork.CryptoCode.ToUpperInvariant();
|
||||
if (daemonUri is null)
|
||||
{
|
||||
logger.LogWarning($"BTCPAY_{cryptoCode}_DAEMON_URI is not configured");
|
||||
}
|
||||
if (walletDaemonUri is null)
|
||||
{
|
||||
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
|
||||
{
|
||||
result.MoneroLikeConfigurationItems.Add(moneroLikeSpecificBtcPayNetwork.CryptoCode, new MoneroLikeConfigurationItem()
|
||||
{
|
||||
DaemonRpcUri = daemonUri,
|
||||
Username = daemonUsername,
|
||||
Password = daemonPassword,
|
||||
InternalWalletRpcUri = walletDaemonUri,
|
||||
WalletDirectory = walletDaemonWalletDirectory
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
52
Plugins/Monero/Payments/MoneroCheckoutModelExtension.cs
Normal file
52
Plugins/Monero/Payments/MoneroCheckoutModelExtension.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Plugins.Monero.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Payments
|
||||
{
|
||||
public class MoneroCheckoutModelExtension : ICheckoutModelExtension
|
||||
{
|
||||
private readonly BTCPayNetworkBase _network;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly IPaymentLinkExtension paymentLinkExtension;
|
||||
|
||||
public MoneroCheckoutModelExtension(
|
||||
PaymentMethodId paymentMethodId,
|
||||
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
|
||||
BTCPayNetworkBase network,
|
||||
PaymentMethodHandlerDictionary handlers)
|
||||
{
|
||||
PaymentMethodId = paymentMethodId;
|
||||
_network = network;
|
||||
_handlers = handlers;
|
||||
paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
|
||||
}
|
||||
public PaymentMethodId PaymentMethodId { get; }
|
||||
|
||||
public string Image => _network.CryptoImagePath;
|
||||
public string Badge => "";
|
||||
|
||||
public void ModifyCheckoutModel(CheckoutModelContext context)
|
||||
{
|
||||
if (context is not { Handler: MoneroLikePaymentMethodHandler handler })
|
||||
return;
|
||||
context.Model.CheckoutBodyComponentName = BitcoinCheckoutModelExtension.CheckoutBodyComponentName;
|
||||
var details = context.InvoiceEntity.GetPayments(true)
|
||||
.Select(p => p.GetDetails<MoneroLikePaymentData>(handler))
|
||||
.Where(p => p is not null)
|
||||
.FirstOrDefault();
|
||||
if (details is not null)
|
||||
{
|
||||
context.Model.ReceivedConfirmations = details.ConfirmationCount;
|
||||
context.Model.RequiredConfirmations = (int)MoneroListener.ConfirmationsRequired(details, context.InvoiceEntity.SpeedPolicy);
|
||||
}
|
||||
|
||||
context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper);
|
||||
context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Payments
|
||||
{
|
||||
public class MoneroLikeOnChainPaymentMethodDetails
|
||||
{
|
||||
public long AccountIndex { get; set; }
|
||||
public long AddressIndex { get; set; }
|
||||
public long? InvoiceSettledConfirmationThreshold { get; set; }
|
||||
}
|
||||
}
|
||||
20
Plugins/Monero/Payments/MoneroLikePaymentData.cs
Normal file
20
Plugins/Monero/Payments/MoneroLikePaymentData.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.Altcoins;
|
||||
using BTCPayServer.Plugins.Monero.Utils;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Payments
|
||||
{
|
||||
public class MoneroLikePaymentData
|
||||
{
|
||||
public long SubaddressIndex { get; set; }
|
||||
public long SubaccountIndex { get; set; }
|
||||
public long BlockHeight { get; set; }
|
||||
public long ConfirmationCount { get; set; }
|
||||
public string TransactionId { get; set; }
|
||||
public long? InvoiceSettledConfirmationThreshold { get; set; }
|
||||
|
||||
public long LockTime { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
119
Plugins/Monero/Payments/MoneroLikePaymentMethodHandler.cs
Normal file
119
Plugins/Monero/Payments/MoneroLikePaymentMethodHandler.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.Altcoins;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Plugins.Monero.Services;
|
||||
using BTCPayServer.Plugins.Monero.Utils;
|
||||
using BTCPayServer.Plugins.Monero.RPC.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Payments
|
||||
{
|
||||
public class MoneroLikePaymentMethodHandler : IPaymentMethodHandler
|
||||
{
|
||||
private readonly MoneroLikeSpecificBtcPayNetwork _network;
|
||||
public MoneroLikeSpecificBtcPayNetwork Network => _network;
|
||||
public JsonSerializer Serializer { get; }
|
||||
private readonly MoneroRPCProvider _moneroRpcProvider;
|
||||
|
||||
public PaymentMethodId PaymentMethodId { get; }
|
||||
|
||||
public MoneroLikePaymentMethodHandler(MoneroLikeSpecificBtcPayNetwork network, MoneroRPCProvider moneroRpcProvider)
|
||||
{
|
||||
PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
|
||||
_network = network;
|
||||
Serializer = BlobSerializer.CreateSerializer().Serializer;
|
||||
_moneroRpcProvider = moneroRpcProvider;
|
||||
}
|
||||
|
||||
public Task BeforeFetchingRates(PaymentMethodContext context)
|
||||
{
|
||||
context.Prompt.Currency = _network.CryptoCode;
|
||||
context.Prompt.Divisibility = _network.Divisibility;
|
||||
if (context.Prompt.Activated)
|
||||
{
|
||||
var supportedPaymentMethod = ParsePaymentMethodConfig(context.PaymentMethodConfig);
|
||||
var walletClient = _moneroRpcProvider.WalletRpcClients[_network.CryptoCode];
|
||||
var daemonClient = _moneroRpcProvider.DaemonRpcClients[_network.CryptoCode];
|
||||
context.State = new Prepare()
|
||||
{
|
||||
GetFeeRate = daemonClient.SendCommandAsync<GetFeeEstimateRequest, GetFeeEstimateResponse>("get_fee_estimate", new GetFeeEstimateRequest()),
|
||||
ReserveAddress = s => walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>("create_address", new CreateAddressRequest() { Label = $"btcpay invoice #{s}", AccountIndex = supportedPaymentMethod.AccountIndex }),
|
||||
AccountIndex = supportedPaymentMethod.AccountIndex
|
||||
};
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task ConfigurePrompt(PaymentMethodContext context)
|
||||
{
|
||||
if (!_moneroRpcProvider.IsAvailable(_network.CryptoCode))
|
||||
throw new PaymentMethodUnavailableException($"Node or wallet not available");
|
||||
var invoice = context.InvoiceEntity;
|
||||
Prepare moneroPrepare = (Prepare)context.State;
|
||||
var feeRatePerKb = await moneroPrepare.GetFeeRate;
|
||||
var address = await moneroPrepare.ReserveAddress(invoice.Id);
|
||||
|
||||
var feeRatePerByte = feeRatePerKb.Fee / 1024;
|
||||
var details = new MoneroLikeOnChainPaymentMethodDetails()
|
||||
{
|
||||
AccountIndex = moneroPrepare.AccountIndex,
|
||||
AddressIndex = address.AddressIndex,
|
||||
InvoiceSettledConfirmationThreshold = ParsePaymentMethodConfig(context.PaymentMethodConfig).InvoiceSettledConfirmationThreshold
|
||||
};
|
||||
context.Prompt.Destination = address.Address;
|
||||
context.Prompt.PaymentMethodFee = MoneroMoney.Convert(feeRatePerByte * 100);
|
||||
context.Prompt.Details = JObject.FromObject(details, Serializer);
|
||||
context.TrackedDestinations.Add(address.Address);
|
||||
}
|
||||
private MoneroPaymentPromptDetails ParsePaymentMethodConfig(JToken config)
|
||||
{
|
||||
return config.ToObject<MoneroPaymentPromptDetails>(Serializer) ?? throw new FormatException($"Invalid {nameof(MoneroLikePaymentMethodHandler)}");
|
||||
}
|
||||
object IPaymentMethodHandler.ParsePaymentMethodConfig(JToken config)
|
||||
{
|
||||
return ParsePaymentMethodConfig(config);
|
||||
}
|
||||
|
||||
class Prepare
|
||||
{
|
||||
public Task<GetFeeEstimateResponse> GetFeeRate;
|
||||
public Func<string, Task<CreateAddressResponse>> ReserveAddress;
|
||||
|
||||
public long AccountIndex { get; internal set; }
|
||||
}
|
||||
|
||||
public MoneroLikeOnChainPaymentMethodDetails ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details)
|
||||
{
|
||||
return details.ToObject<MoneroLikeOnChainPaymentMethodDetails>(Serializer);
|
||||
}
|
||||
object IPaymentMethodHandler.ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details)
|
||||
{
|
||||
return ParsePaymentPromptDetails(details);
|
||||
}
|
||||
|
||||
public MoneroLikePaymentData ParsePaymentDetails(JToken details)
|
||||
{
|
||||
return details.ToObject<MoneroLikePaymentData>(Serializer) ?? throw new FormatException($"Invalid {nameof(MoneroLikePaymentMethodHandler)}");
|
||||
}
|
||||
object IPaymentMethodHandler.ParsePaymentDetails(JToken details)
|
||||
{
|
||||
return ParsePaymentDetails(details);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Plugins/Monero/Payments/MoneroPaymentLinkExtension.cs
Normal file
27
Plugins/Monero/Payments/MoneroPaymentLinkExtension.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
#nullable enable
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.Altcoins;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Payments
|
||||
{
|
||||
public class MoneroPaymentLinkExtension : IPaymentLinkExtension
|
||||
{
|
||||
private readonly MoneroLikeSpecificBtcPayNetwork _network;
|
||||
|
||||
public MoneroPaymentLinkExtension(PaymentMethodId paymentMethodId, MoneroLikeSpecificBtcPayNetwork network)
|
||||
{
|
||||
PaymentMethodId = paymentMethodId;
|
||||
_network = network;
|
||||
}
|
||||
public PaymentMethodId PaymentMethodId { get; }
|
||||
|
||||
public string? GetPaymentLink(PaymentPrompt prompt, IUrlHelper? urlHelper)
|
||||
{
|
||||
var due = prompt.Calculate().Due;
|
||||
return $"{_network.UriScheme}:{prompt.Destination}?tx_amount={due.ToString(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Plugins/Monero/Payments/MoneroPaymentPromptDetails.cs
Normal file
11
Plugins/Monero/Payments/MoneroPaymentPromptDetails.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using BTCPayServer.Payments;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Payments
|
||||
{
|
||||
public class MoneroPaymentPromptDetails
|
||||
{
|
||||
public long AccountIndex { get; set; }
|
||||
public long? InvoiceSettledConfirmationThreshold { get; set; }
|
||||
}
|
||||
}
|
||||
121
Plugins/Monero/RPC/JsonRpcClient.cs
Normal file
121
Plugins/Monero/RPC/JsonRpcClient.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC
|
||||
{
|
||||
public class JsonRpcClient
|
||||
{
|
||||
private readonly Uri _address;
|
||||
private readonly string _username;
|
||||
private readonly string _password;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public JsonRpcClient(Uri address, string username, string password, HttpClient client = null)
|
||||
{
|
||||
_address = address;
|
||||
_username = username;
|
||||
_password = password;
|
||||
_httpClient = client ?? new HttpClient();
|
||||
}
|
||||
|
||||
|
||||
public async Task<TResponse> SendCommandAsync<TRequest, TResponse>(string method, TRequest data,
|
||||
CancellationToken cts = default(CancellationToken))
|
||||
{
|
||||
var jsonSerializer = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
var httpRequest = new HttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(_address, "json_rpc"),
|
||||
Content = new StringContent(
|
||||
JsonConvert.SerializeObject(new JsonRpcCommand<TRequest>(method, data), jsonSerializer),
|
||||
Encoding.UTF8, "application/json")
|
||||
};
|
||||
httpRequest.Headers.Accept.Clear();
|
||||
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
|
||||
Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
|
||||
|
||||
HttpResponseMessage rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
||||
rawResult.EnsureSuccessStatusCode();
|
||||
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
||||
|
||||
JsonRpcResult<TResponse> response;
|
||||
try
|
||||
{
|
||||
response = JsonConvert.DeserializeObject<JsonRpcResult<TResponse>>(rawJson, jsonSerializer);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.Message);
|
||||
Console.WriteLine(rawJson);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (response.Error != null)
|
||||
{
|
||||
throw new JsonRpcApiException()
|
||||
{
|
||||
Error = response.Error
|
||||
};
|
||||
}
|
||||
|
||||
return response.Result;
|
||||
}
|
||||
|
||||
public class NoRequestModel
|
||||
{
|
||||
public static NoRequestModel Instance = new NoRequestModel();
|
||||
}
|
||||
|
||||
internal class JsonRpcApiException : Exception
|
||||
{
|
||||
public JsonRpcResultError Error { get; set; }
|
||||
|
||||
public override string Message => Error?.Message;
|
||||
}
|
||||
|
||||
public class JsonRpcResultError
|
||||
{
|
||||
[JsonProperty("code")] public int Code { get; set; }
|
||||
[JsonProperty("message")] public string Message { get; set; }
|
||||
[JsonProperty("data")] dynamic Data { get; set; }
|
||||
}
|
||||
internal class JsonRpcResult<T>
|
||||
{
|
||||
|
||||
|
||||
[JsonProperty("result")] public T Result { get; set; }
|
||||
[JsonProperty("error")] public JsonRpcResultError Error { get; set; }
|
||||
[JsonProperty("id")] public string Id { get; set; }
|
||||
}
|
||||
|
||||
internal class JsonRpcCommand<T>
|
||||
{
|
||||
[JsonProperty("jsonRpc")] public string JsonRpc { get; set; } = "2.0";
|
||||
[JsonProperty("id")] public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
[JsonProperty("method")] public string Method { get; set; }
|
||||
|
||||
[JsonProperty("params")] public T Parameters { get; set; }
|
||||
|
||||
public JsonRpcCommand()
|
||||
{
|
||||
}
|
||||
|
||||
public JsonRpcCommand(string method, T parameters)
|
||||
{
|
||||
Method = method;
|
||||
Parameters = parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Plugins/Monero/RPC/Models/CreateAccountRequest.cs
Normal file
9
Plugins/Monero/RPC/Models/CreateAccountRequest.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class CreateAccountRequest
|
||||
{
|
||||
[JsonProperty("label")] public string Label { get; set; }
|
||||
}
|
||||
}
|
||||
10
Plugins/Monero/RPC/Models/CreateAccountResponse.cs
Normal file
10
Plugins/Monero/RPC/Models/CreateAccountResponse.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class CreateAccountResponse
|
||||
{
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
}
|
||||
}
|
||||
10
Plugins/Monero/RPC/Models/CreateAddressRequest.cs
Normal file
10
Plugins/Monero/RPC/Models/CreateAddressRequest.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class CreateAddressRequest
|
||||
{
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
[JsonProperty("label")] public string Label { get; set; }
|
||||
}
|
||||
}
|
||||
10
Plugins/Monero/RPC/Models/CreateAddressResponse.cs
Normal file
10
Plugins/Monero/RPC/Models/CreateAddressResponse.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class CreateAddressResponse
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("address_index")] public long AddressIndex { get; set; }
|
||||
}
|
||||
}
|
||||
11
Plugins/Monero/RPC/Models/CreateWalletRequest.cs
Normal file
11
Plugins/Monero/RPC/Models/CreateWalletRequest.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class CreateWalletRequest
|
||||
{
|
||||
[JsonProperty("filename")] public string Filename { get; set; }
|
||||
[JsonProperty("password")] public string Password { get; set; }
|
||||
[JsonProperty("language")] public string Language { get; set; }
|
||||
}
|
||||
}
|
||||
9
Plugins/Monero/RPC/Models/GetAccountsRequest.cs
Normal file
9
Plugins/Monero/RPC/Models/GetAccountsRequest.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class GetAccountsRequest
|
||||
{
|
||||
[JsonProperty("tag")] public string Tag { get; set; }
|
||||
}
|
||||
}
|
||||
14
Plugins/Monero/RPC/Models/GetAccountsResponse.cs
Normal file
14
Plugins/Monero/RPC/Models/GetAccountsResponse.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class GetAccountsResponse
|
||||
{
|
||||
[JsonProperty("subaddress_accounts")] public List<SubaddressAccount> SubaddressAccounts { get; set; }
|
||||
[JsonProperty("total_balance")] public decimal TotalBalance { get; set; }
|
||||
|
||||
[JsonProperty("total_unlocked_balance")]
|
||||
public decimal TotalUnlockedBalance { get; set; }
|
||||
}
|
||||
}
|
||||
9
Plugins/Monero/RPC/Models/GetFeeEstimateRequest.cs
Normal file
9
Plugins/Monero/RPC/Models/GetFeeEstimateRequest.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public class GetFeeEstimateRequest
|
||||
{
|
||||
[JsonProperty("grace_blocks")] public int? GraceBlocks { get; set; }
|
||||
}
|
||||
}
|
||||
11
Plugins/Monero/RPC/Models/GetFeeEstimateResponse.cs
Normal file
11
Plugins/Monero/RPC/Models/GetFeeEstimateResponse.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public class GetFeeEstimateResponse
|
||||
{
|
||||
[JsonProperty("fee")] public long Fee { get; set; }
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
[JsonProperty("untrusted")] public bool Untrusted { get; set; }
|
||||
}
|
||||
}
|
||||
9
Plugins/Monero/RPC/Models/GetHeightResponse.cs
Normal file
9
Plugins/Monero/RPC/Models/GetHeightResponse.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class GetHeightResponse
|
||||
{
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
}
|
||||
}
|
||||
13
Plugins/Monero/RPC/Models/GetInfoResponse.cs
Normal file
13
Plugins/Monero/RPC/Models/GetInfoResponse.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class GetInfoResponse
|
||||
{
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
[JsonProperty("busy_syncing")] public bool BusySyncing { get; set; }
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
[JsonProperty("target_height")] public long? TargetHeight { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public class GetTransferByTransactionIdRequest
|
||||
{
|
||||
[JsonProperty("txid")] public string TransactionId { get; set; }
|
||||
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class GetTransferByTransactionIdResponse
|
||||
{
|
||||
[JsonProperty("transfer")] public TransferItem Transfer { get; set; }
|
||||
[JsonProperty("transfers")] public IEnumerable<TransferItem> Transfers { get; set; }
|
||||
|
||||
public partial class TransferItem
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("amount")] public long Amount { 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; }
|
||||
[JsonProperty("note")] public string Note { get; set; }
|
||||
[JsonProperty("payment_id")] public string PaymentId { get; set; }
|
||||
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
|
||||
|
||||
[JsonProperty("suggested_confirmations_threshold")]
|
||||
public long SuggestedConfirmationsThreshold { get; set; }
|
||||
|
||||
[JsonProperty("timestamp")] public long Timestamp { get; set; }
|
||||
[JsonProperty("txid")] public string Txid { get; set; }
|
||||
[JsonProperty("type")] public string Type { get; set; }
|
||||
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Plugins/Monero/RPC/Models/GetTransfersRequest.cs
Normal file
19
Plugins/Monero/RPC/Models/GetTransfersRequest.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class GetTransfersRequest
|
||||
{
|
||||
[JsonProperty("in")] public bool In { get; set; }
|
||||
[JsonProperty("out")] public bool Out { get; set; }
|
||||
[JsonProperty("pending")] public bool Pending { get; set; }
|
||||
[JsonProperty("failed")] public bool Failed { get; set; }
|
||||
[JsonProperty("pool")] public bool Pool { get; set; }
|
||||
[JsonProperty("filter_by_height ")] public bool FilterByHeight { get; set; }
|
||||
[JsonProperty("min_height")] public long MinHeight { get; set; }
|
||||
[JsonProperty("max_height")] public long MaxHeight { get; set; }
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
[JsonProperty("subaddr_indices")] public List<long> SubaddrIndices { get; set; }
|
||||
}
|
||||
}
|
||||
35
Plugins/Monero/RPC/Models/GetTransfersResponse.cs
Normal file
35
Plugins/Monero/RPC/Models/GetTransfersResponse.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class GetTransfersResponse
|
||||
{
|
||||
[JsonProperty("in")] public List<GetTransfersResponseItem> In { get; set; }
|
||||
[JsonProperty("out")] public List<GetTransfersResponseItem> Out { get; set; }
|
||||
[JsonProperty("pending")] public List<GetTransfersResponseItem> Pending { get; set; }
|
||||
[JsonProperty("failed")] public List<GetTransfersResponseItem> Failed { get; set; }
|
||||
[JsonProperty("pool")] public List<GetTransfersResponseItem> Pool { get; set; }
|
||||
|
||||
public partial class GetTransfersResponseItem
|
||||
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("amount")] public long Amount { 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; }
|
||||
[JsonProperty("note")] public string Note { get; set; }
|
||||
[JsonProperty("payment_id")] public string PaymentId { get; set; }
|
||||
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
|
||||
|
||||
[JsonProperty("suggested_confirmations_threshold")]
|
||||
public long SuggestedConfirmationsThreshold { get; set; }
|
||||
|
||||
[JsonProperty("timestamp")] public long Timestamp { get; set; }
|
||||
[JsonProperty("txid")] public string Txid { get; set; }
|
||||
[JsonProperty("type")] public string Type { get; set; }
|
||||
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Plugins/Monero/RPC/Models/Info.cs
Normal file
33
Plugins/Monero/RPC/Models/Info.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class Info
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("avg_download")] public long AvgDownload { get; set; }
|
||||
[JsonProperty("avg_upload")] public long AvgUpload { get; set; }
|
||||
[JsonProperty("connection_id")] public string ConnectionId { get; set; }
|
||||
[JsonProperty("current_download")] public long CurrentDownload { get; set; }
|
||||
[JsonProperty("current_upload")] public long CurrentUpload { get; set; }
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
[JsonProperty("host")] public string Host { get; set; }
|
||||
[JsonProperty("incoming")] public bool Incoming { get; set; }
|
||||
[JsonProperty("ip")] public string Ip { get; set; }
|
||||
[JsonProperty("live_time")] public long LiveTime { get; set; }
|
||||
[JsonProperty("local_ip")] public bool LocalIp { get; set; }
|
||||
[JsonProperty("localhost")] public bool Localhost { get; set; }
|
||||
[JsonProperty("peer_id")] public string PeerId { get; set; }
|
||||
|
||||
[JsonProperty("port")]
|
||||
[JsonConverter(typeof(ParseStringConverter))]
|
||||
public long Port { get; set; }
|
||||
|
||||
[JsonProperty("recv_count")] public long RecvCount { get; set; }
|
||||
[JsonProperty("recv_idle_time")] public long RecvIdleTime { get; set; }
|
||||
[JsonProperty("send_count")] public long SendCount { get; set; }
|
||||
[JsonProperty("send_idle_time")] public long SendIdleTime { get; set; }
|
||||
[JsonProperty("state")] public string State { get; set; }
|
||||
[JsonProperty("support_flags")] public long SupportFlags { get; set; }
|
||||
}
|
||||
}
|
||||
13
Plugins/Monero/RPC/Models/MakeUriRequest.cs
Normal file
13
Plugins/Monero/RPC/Models/MakeUriRequest.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class MakeUriRequest
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("amount")] public long Amount { get; set; }
|
||||
[JsonProperty("payment_id")] public string PaymentId { get; set; }
|
||||
[JsonProperty("tx_description")] public string TxDescription { get; set; }
|
||||
[JsonProperty("recipient_name")] public string RecipientName { get; set; }
|
||||
}
|
||||
}
|
||||
9
Plugins/Monero/RPC/Models/MakeUriResponse.cs
Normal file
9
Plugins/Monero/RPC/Models/MakeUriResponse.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class MakeUriResponse
|
||||
{
|
||||
[JsonProperty("uri")] public string Uri { get; set; }
|
||||
}
|
||||
}
|
||||
10
Plugins/Monero/RPC/Models/OpenWallerErrorResponse.cs
Normal file
10
Plugins/Monero/RPC/Models/OpenWallerErrorResponse.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class OpenWalletErrorResponse
|
||||
{
|
||||
[JsonProperty("code")] public int Code { get; set; }
|
||||
[JsonProperty("message")] public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
10
Plugins/Monero/RPC/Models/OpenWalletRequest.cs
Normal file
10
Plugins/Monero/RPC/Models/OpenWalletRequest.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class OpenWalletRequest
|
||||
{
|
||||
[JsonProperty("filename")] public string Filename { get; set; }
|
||||
[JsonProperty("password")] public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
12
Plugins/Monero/RPC/Models/OpenWalletResponse.cs
Normal file
12
Plugins/Monero/RPC/Models/OpenWalletResponse.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class OpenWalletResponse
|
||||
{
|
||||
[JsonProperty("id")] public string Id { get; set; }
|
||||
[JsonProperty("jsonrpc")] public string Jsonrpc { get; set; }
|
||||
[JsonProperty("result")] public object Result { get; set; }
|
||||
[JsonProperty("error")] public OpenWalletErrorResponse Error { get; set; }
|
||||
}
|
||||
}
|
||||
40
Plugins/Monero/RPC/Models/ParseStringConverter.cs
Normal file
40
Plugins/Monero/RPC/Models/ParseStringConverter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
internal class ParseStringConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
var value = serializer.Deserialize<string>(reader);
|
||||
long l;
|
||||
if (Int64.TryParse(value, out l))
|
||||
{
|
||||
return l;
|
||||
}
|
||||
|
||||
throw new Exception("Cannot unmarshal type long");
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
|
||||
{
|
||||
if (untypedValue == null)
|
||||
{
|
||||
serializer.Serialize(writer, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var value = (long)untypedValue;
|
||||
serializer.Serialize(writer, value.ToString(CultureInfo.InvariantCulture));
|
||||
return;
|
||||
}
|
||||
|
||||
public static readonly ParseStringConverter Singleton = new ParseStringConverter();
|
||||
}
|
||||
}
|
||||
9
Plugins/Monero/RPC/Models/Peer.cs
Normal file
9
Plugins/Monero/RPC/Models/Peer.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class Peer
|
||||
{
|
||||
[JsonProperty("info")] public Info Info { get; set; }
|
||||
}
|
||||
}
|
||||
10
Plugins/Monero/RPC/Models/SubaddrIndex.cs
Normal file
10
Plugins/Monero/RPC/Models/SubaddrIndex.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class SubaddrIndex
|
||||
{
|
||||
[JsonProperty("major")] public long Major { get; set; }
|
||||
[JsonProperty("minor")] public long Minor { get; set; }
|
||||
}
|
||||
}
|
||||
14
Plugins/Monero/RPC/Models/SubaddressAccount.cs
Normal file
14
Plugins/Monero/RPC/Models/SubaddressAccount.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.RPC.Models
|
||||
{
|
||||
public partial class SubaddressAccount
|
||||
{
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
[JsonProperty("balance")] public decimal Balance { get; set; }
|
||||
[JsonProperty("base_address")] public string BaseAddress { get; set; }
|
||||
[JsonProperty("label")] public string Label { get; set; }
|
||||
[JsonProperty("tag")] public string Tag { get; set; }
|
||||
[JsonProperty("unlocked_balance")] public decimal UnlockedBalance { get; set; }
|
||||
}
|
||||
}
|
||||
15
Plugins/Monero/RPC/MoneroEvent.cs
Normal file
15
Plugins/Monero/RPC/MoneroEvent.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace BTCPayServer.Plugins.Monero.RPC
|
||||
{
|
||||
public class MoneroEvent
|
||||
{
|
||||
public string BlockHash { get; set; }
|
||||
public string TransactionHash { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"{CryptoCode}: {(string.IsNullOrEmpty(TransactionHash) ? string.Empty : "Tx Update")}{(string.IsNullOrEmpty(BlockHash) ? string.Empty : "New Block")} ({TransactionHash ?? string.Empty}{BlockHash ?? string.Empty})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Plugins.Monero.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Services
|
||||
{
|
||||
public class MoneroLikeSummaryUpdaterHostedService : IHostedService
|
||||
{
|
||||
private readonly MoneroRPCProvider _MoneroRpcProvider;
|
||||
private readonly MoneroLikeConfiguration _moneroLikeConfiguration;
|
||||
|
||||
public Logs Logs { get; }
|
||||
|
||||
private CancellationTokenSource _Cts;
|
||||
public MoneroLikeSummaryUpdaterHostedService(MoneroRPCProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration, Logs logs)
|
||||
{
|
||||
_MoneroRpcProvider = moneroRpcProvider;
|
||||
_moneroLikeConfiguration = moneroLikeConfiguration;
|
||||
Logs = logs;
|
||||
}
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
foreach (var moneroLikeConfigurationItem in _moneroLikeConfiguration.MoneroLikeConfigurationItems)
|
||||
{
|
||||
_ = StartLoop(_Cts.Token, moneroLikeConfigurationItem.Key);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task StartLoop(CancellationToken cancellation, string cryptoCode)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Starting listening Monero-like daemons ({cryptoCode})");
|
||||
try
|
||||
{
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _MoneroRpcProvider.UpdateSummary(cryptoCode);
|
||||
if (_MoneroRpcProvider.IsAvailable(cryptoCode))
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), cancellation);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Unhandled exception in Summary updater ({cryptoCode})");
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested) { }
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts?.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
361
Plugins/Monero/Services/MoneroListener.cs
Normal file
361
Plugins/Monero/Services/MoneroListener.cs
Normal file
@@ -0,0 +1,361 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.Altcoins;
|
||||
using BTCPayServer.Plugins.Monero.Configuration;
|
||||
using BTCPayServer.Plugins.Monero.Payments;
|
||||
using BTCPayServer.Plugins.Monero.RPC.Models;
|
||||
using BTCPayServer.Plugins.Monero.Utils;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Plugins.Monero.RPC;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Services
|
||||
{
|
||||
public class MoneroListener : EventHostedServiceBase
|
||||
{
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly MoneroRPCProvider _moneroRpcProvider;
|
||||
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly ILogger<MoneroListener> _logger;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly InvoiceActivator _invoiceActivator;
|
||||
private readonly PaymentService _paymentService;
|
||||
|
||||
public MoneroListener(InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator,
|
||||
MoneroRPCProvider moneroRpcProvider,
|
||||
MoneroLikeConfiguration moneroLikeConfiguration,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ILogger<MoneroListener> logger,
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
InvoiceActivator invoiceActivator,
|
||||
PaymentService paymentService) : base(eventAggregator, logger)
|
||||
{
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_moneroRpcProvider = moneroRpcProvider;
|
||||
_MoneroLikeConfiguration = moneroLikeConfiguration;
|
||||
_networkProvider = networkProvider;
|
||||
_logger = logger;
|
||||
_handlers = handlers;
|
||||
_invoiceActivator = invoiceActivator;
|
||||
_paymentService = paymentService;
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
base.SubscribeToEvents();
|
||||
Subscribe<MoneroEvent>();
|
||||
Subscribe<MoneroRPCProvider.MoneroDaemonStateChange>();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is MoneroRPCProvider.MoneroDaemonStateChange stateChange)
|
||||
{
|
||||
if (_moneroRpcProvider.IsAvailable(stateChange.CryptoCode))
|
||||
{
|
||||
_logger.LogInformation($"{stateChange.CryptoCode} just became available");
|
||||
_ = UpdateAnyPendingMoneroLikePayment(stateChange.CryptoCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"{stateChange.CryptoCode} just became unavailable");
|
||||
}
|
||||
}
|
||||
else if (evt is MoneroEvent moneroEvent)
|
||||
{
|
||||
if (!_moneroRpcProvider.IsAvailable(moneroEvent.CryptoCode))
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(moneroEvent.BlockHash))
|
||||
{
|
||||
await OnNewBlock(moneroEvent.CryptoCode);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(moneroEvent.TransactionHash))
|
||||
{
|
||||
await OnTransactionUpdated(moneroEvent.CryptoCode, moneroEvent.TransactionHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
$"Invoice {invoice.Id} received payment {payment.Value} {payment.Currency} {payment.Id}");
|
||||
|
||||
var prompt = invoice.GetPaymentPrompt(payment.PaymentMethodId);
|
||||
|
||||
if (prompt != null &&
|
||||
prompt.Activated &&
|
||||
prompt.Destination == payment.Destination &&
|
||||
prompt.Calculate().Due > 0.0m)
|
||||
{
|
||||
await _invoiceActivator.ActivateInvoicePaymentMethod(invoice.Id, payment.PaymentMethodId, true);
|
||||
invoice = await _invoiceRepository.GetInvoice(invoice.Id);
|
||||
}
|
||||
|
||||
_eventAggregator.Publish(
|
||||
new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
}
|
||||
|
||||
private async Task UpdatePaymentStates(string cryptoCode, InvoiceEntity[] invoices)
|
||||
{
|
||||
if (!invoices.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var moneroWalletRpcClient = _moneroRpcProvider.WalletRpcClients[cryptoCode];
|
||||
var network = _networkProvider.GetNetwork(cryptoCode);
|
||||
var paymentId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
|
||||
var handler = (MoneroLikePaymentMethodHandler)_handlers[paymentId];
|
||||
|
||||
//get all the required data in one list (invoice, its existing payments and the current payment method details)
|
||||
var expandedInvoices = invoices.Select(entity => (Invoice: entity,
|
||||
ExistingPayments: GetAllMoneroLikePayments(entity, cryptoCode),
|
||||
Prompt: entity.GetPaymentPrompt(paymentId),
|
||||
PaymentMethodDetails: handler.ParsePaymentPromptDetails(entity.GetPaymentPrompt(paymentId).Details)))
|
||||
.Select(tuple => (
|
||||
tuple.Invoice,
|
||||
tuple.PaymentMethodDetails,
|
||||
tuple.Prompt,
|
||||
ExistingPayments: tuple.ExistingPayments.Select(entity =>
|
||||
(Payment: entity, PaymentData: handler.ParsePaymentDetails(entity.Details),
|
||||
tuple.Invoice))
|
||||
));
|
||||
|
||||
var existingPaymentData = expandedInvoices.SelectMany(tuple => tuple.ExistingPayments);
|
||||
|
||||
var accountToAddressQuery = new Dictionary<long, List<long>>();
|
||||
//create list of subaddresses to account to query the monero wallet
|
||||
foreach (var expandedInvoice in expandedInvoices)
|
||||
{
|
||||
var addressIndexList =
|
||||
accountToAddressQuery.GetValueOrDefault(expandedInvoice.PaymentMethodDetails.AccountIndex,
|
||||
new List<long>());
|
||||
|
||||
addressIndexList.AddRange(
|
||||
expandedInvoice.ExistingPayments.Select(tuple => tuple.PaymentData.SubaddressIndex));
|
||||
addressIndexList.Add(expandedInvoice.PaymentMethodDetails.AddressIndex);
|
||||
accountToAddressQuery.AddOrReplace(expandedInvoice.PaymentMethodDetails.AccountIndex, addressIndexList);
|
||||
}
|
||||
|
||||
var tasks = accountToAddressQuery.ToDictionary(datas => datas.Key,
|
||||
datas => moneroWalletRpcClient.SendCommandAsync<GetTransfersRequest, GetTransfersResponse>(
|
||||
"get_transfers",
|
||||
new GetTransfersRequest()
|
||||
{
|
||||
AccountIndex = datas.Key,
|
||||
In = true,
|
||||
SubaddrIndices = datas.Value.Distinct().ToList()
|
||||
}));
|
||||
|
||||
await Task.WhenAll(tasks.Values);
|
||||
|
||||
|
||||
var transferProcessingTasks = new List<Task>();
|
||||
|
||||
var updatedPaymentEntities = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
|
||||
foreach (var keyValuePair in tasks)
|
||||
{
|
||||
var transfers = keyValuePair.Value.Result.In;
|
||||
if (transfers == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
transferProcessingTasks.AddRange(transfers.Select(transfer =>
|
||||
{
|
||||
InvoiceEntity invoice = null;
|
||||
var existingMatch = existingPaymentData.SingleOrDefault(tuple =>
|
||||
tuple.Payment.Destination == transfer.Address &&
|
||||
tuple.PaymentData.TransactionId == transfer.Txid);
|
||||
|
||||
if (existingMatch.Invoice != null)
|
||||
{
|
||||
invoice = existingMatch.Invoice;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newMatch = expandedInvoices.SingleOrDefault(tuple =>
|
||||
tuple.Prompt.Destination == transfer.Address);
|
||||
|
||||
if (newMatch.Invoice == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
invoice = newMatch.Invoice;
|
||||
}
|
||||
|
||||
|
||||
return HandlePaymentData(cryptoCode, transfer.Address, transfer.Amount, transfer.SubaddrIndex.Major,
|
||||
transfer.SubaddrIndex.Minor, transfer.Txid, transfer.Confirmations, transfer.Height, transfer.UnlockTime,invoice,
|
||||
updatedPaymentEntities);
|
||||
}));
|
||||
}
|
||||
|
||||
transferProcessingTasks.Add(
|
||||
_paymentService.UpdatePayments(updatedPaymentEntities.Select(tuple => tuple.Item1).ToList()));
|
||||
await Task.WhenAll(transferProcessingTasks);
|
||||
foreach (var valueTuples in updatedPaymentEntities.GroupBy(entity => entity.Item2))
|
||||
{
|
||||
if (valueTuples.Any())
|
||||
{
|
||||
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnNewBlock(string cryptoCode)
|
||||
{
|
||||
await UpdateAnyPendingMoneroLikePayment(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<GetTransferByTransactionIdRequest, GetTransferByTransactionIdResponse>(
|
||||
"get_transfer_by_txid",
|
||||
new GetTransferByTransactionIdRequest() { TransactionId = transactionHash });
|
||||
|
||||
var paymentsToUpdate = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
|
||||
|
||||
//group all destinations of the tx together and loop through the sets
|
||||
foreach (var destination in transfer.Transfers.GroupBy(destination => destination.Address))
|
||||
{
|
||||
//find the invoice corresponding to this address, else skip
|
||||
var invoice = await _invoiceRepository.GetInvoiceFromAddress(paymentMethodId, destination.Key);
|
||||
if (invoice == null)
|
||||
continue;
|
||||
|
||||
var index = destination.First().SubaddrIndex;
|
||||
|
||||
await HandlePaymentData(cryptoCode,
|
||||
destination.Key,
|
||||
destination.Sum(destination1 => destination1.Amount),
|
||||
index.Major,
|
||||
index.Minor,
|
||||
transfer.Transfer.Txid,
|
||||
transfer.Transfer.Confirmations,
|
||||
transfer.Transfer.Height
|
||||
, transfer.Transfer.UnlockTime,invoice, paymentsToUpdate);
|
||||
}
|
||||
|
||||
if (paymentsToUpdate.Any())
|
||||
{
|
||||
await _paymentService.UpdatePayments(paymentsToUpdate.Select(tuple => tuple.Payment).ToList());
|
||||
foreach (var valueTuples in paymentsToUpdate.GroupBy(entity => entity.invoice))
|
||||
{
|
||||
if (valueTuples.Any())
|
||||
{
|
||||
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandlePaymentData(string cryptoCode, string address, long totalAmount, long subaccountIndex,
|
||||
long subaddressIndex,
|
||||
string txId, long confirmations, long blockHeight, long locktime, InvoiceEntity invoice,
|
||||
List<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork(cryptoCode);
|
||||
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
|
||||
var handler = (MoneroLikePaymentMethodHandler)_handlers[pmi];
|
||||
var promptDetails = handler.ParsePaymentPromptDetails(invoice.GetPaymentPrompt(pmi).Details);
|
||||
var details = new MoneroLikePaymentData()
|
||||
{
|
||||
SubaccountIndex = subaccountIndex,
|
||||
SubaddressIndex = subaddressIndex,
|
||||
TransactionId = txId,
|
||||
ConfirmationCount = confirmations,
|
||||
BlockHeight = blockHeight,
|
||||
LockTime = locktime,
|
||||
InvoiceSettledConfirmationThreshold = promptDetails.InvoiceSettledConfirmationThreshold
|
||||
};
|
||||
var status = GetStatus(details, invoice.SpeedPolicy) ? PaymentStatus.Settled : PaymentStatus.Processing;
|
||||
var paymentData = new Data.PaymentData()
|
||||
{
|
||||
Status = status,
|
||||
Amount = MoneroMoney.Convert(totalAmount),
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
Id = $"{txId}#{subaccountIndex}#{subaddressIndex}",
|
||||
Currency = network.CryptoCode,
|
||||
InvoiceDataId = invoice.Id,
|
||||
}.Set(invoice, handler, details);
|
||||
|
||||
|
||||
//check if this tx exists as a payment to this invoice already
|
||||
var alreadyExistingPaymentThatMatches = GetAllMoneroLikePayments(invoice, cryptoCode)
|
||||
.SingleOrDefault(c => c.Id == paymentData.Id && c.PaymentMethodId == pmi);
|
||||
|
||||
//if it doesnt, add it and assign a new monerolike address to the system if a balance is still due
|
||||
if (alreadyExistingPaymentThatMatches == null)
|
||||
{
|
||||
var payment = await _paymentService.AddPayment(paymentData, [txId]);
|
||||
if (payment != null)
|
||||
await ReceivedPayment(invoice, payment);
|
||||
}
|
||||
else
|
||||
{
|
||||
//else update it with the new data
|
||||
alreadyExistingPaymentThatMatches.Status = status;
|
||||
alreadyExistingPaymentThatMatches.Details = JToken.FromObject(details, handler.Serializer);
|
||||
paymentsToUpdate.Add((alreadyExistingPaymentThatMatches, invoice));
|
||||
}
|
||||
}
|
||||
|
||||
private bool GetStatus(MoneroLikePaymentData details, SpeedPolicy speedPolicy)
|
||||
=> 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,
|
||||
};
|
||||
|
||||
|
||||
private async Task UpdateAnyPendingMoneroLikePayment(string cryptoCode)
|
||||
{
|
||||
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
|
||||
var invoices = await _invoiceRepository.GetMonitoredInvoices(paymentMethodId);
|
||||
if (!invoices.Any())
|
||||
return;
|
||||
invoices = invoices.Where(entity => entity.GetPaymentPrompt(paymentMethodId)?.Activated is true).ToArray();
|
||||
await UpdatePaymentStates(cryptoCode, invoices);
|
||||
}
|
||||
|
||||
private IEnumerable<PaymentEntity> GetAllMoneroLikePayments(InvoiceEntity invoice, string cryptoCode)
|
||||
{
|
||||
return invoice.GetPayments(false)
|
||||
.Where(p => p.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
135
Plugins/Monero/Services/MoneroRPCProvider.cs
Normal file
135
Plugins/Monero/Services/MoneroRPCProvider.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.Runtime;
|
||||
using BTCPayServer.Plugins.Monero.Configuration;
|
||||
using BTCPayServer.Plugins.Monero.RPC;
|
||||
using BTCPayServer.Plugins.Monero.RPC.Models;
|
||||
using BTCPayServer.Services;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Services
|
||||
{
|
||||
public class MoneroRPCProvider
|
||||
{
|
||||
private readonly MoneroLikeConfiguration _moneroLikeConfiguration;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly BTCPayServerEnvironment environment;
|
||||
public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients;
|
||||
public ImmutableDictionary<string, JsonRpcClient> WalletRpcClients;
|
||||
|
||||
private readonly ConcurrentDictionary<string, MoneroLikeSummary> _summaries =
|
||||
new ConcurrentDictionary<string, MoneroLikeSummary>();
|
||||
|
||||
public ConcurrentDictionary<string, MoneroLikeSummary> Summaries => _summaries;
|
||||
|
||||
public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration, EventAggregator eventAggregator, IHttpClientFactory httpClientFactory, BTCPayServerEnvironment environment)
|
||||
{
|
||||
_moneroLikeConfiguration = moneroLikeConfiguration;
|
||||
_eventAggregator = eventAggregator;
|
||||
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")));
|
||||
WalletRpcClients =
|
||||
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
|
||||
pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "", httpClientFactory.CreateClient($"{pair.Key}client")));
|
||||
}
|
||||
|
||||
public bool IsAvailable(string cryptoCode)
|
||||
{
|
||||
cryptoCode = cryptoCode.ToUpperInvariant();
|
||||
return _summaries.ContainsKey(cryptoCode) && IsAvailable(_summaries[cryptoCode]);
|
||||
}
|
||||
|
||||
private bool IsAvailable(MoneroLikeSummary summary)
|
||||
{
|
||||
return summary.Synced &&
|
||||
summary.WalletAvailable;
|
||||
}
|
||||
|
||||
public async Task<MoneroLikeSummary> UpdateSummary(string cryptoCode)
|
||||
{
|
||||
if (!DaemonRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var daemonRpcClient) ||
|
||||
!WalletRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var walletRpcClient))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var summary = new MoneroLikeSummary();
|
||||
try
|
||||
{
|
||||
var daemonResult =
|
||||
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, GetInfoResponse>("get_info",
|
||||
JsonRpcClient.NoRequestModel.Instance);
|
||||
summary.TargetHeight = daemonResult.TargetHeight.GetValueOrDefault(0);
|
||||
summary.CurrentHeight = daemonResult.Height;
|
||||
summary.TargetHeight = summary.TargetHeight == 0 ? summary.CurrentHeight : summary.TargetHeight;
|
||||
summary.Synced = !daemonResult.BusySyncing;
|
||||
summary.UpdatedAt = DateTime.UtcNow;
|
||||
summary.DaemonAvailable = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
summary.DaemonAvailable = false;
|
||||
}
|
||||
|
||||
bool walletCreated = false;
|
||||
retry:
|
||||
try
|
||||
{
|
||||
var walletResult =
|
||||
await walletRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, GetHeightResponse>(
|
||||
"get_height", JsonRpcClient.NoRequestModel.Instance);
|
||||
summary.WalletHeight = walletResult.Height;
|
||||
summary.WalletAvailable = true;
|
||||
}
|
||||
catch when (environment.CheatMode && !walletCreated)
|
||||
{
|
||||
await walletRpcClient.SendCommandAsync<CreateWalletRequest, JsonRpcClient.NoRequestModel>("create_wallet",
|
||||
new()
|
||||
{
|
||||
Filename = "wallet",
|
||||
Password = "",
|
||||
Language = "English"
|
||||
});
|
||||
walletCreated = true;
|
||||
goto retry;
|
||||
}
|
||||
catch
|
||||
{
|
||||
summary.WalletAvailable = false;
|
||||
}
|
||||
|
||||
var changed = !_summaries.ContainsKey(cryptoCode) || IsAvailable(cryptoCode) != IsAvailable(summary);
|
||||
|
||||
_summaries.AddOrReplace(cryptoCode, summary);
|
||||
if (changed)
|
||||
{
|
||||
_eventAggregator.Publish(new MoneroDaemonStateChange() { Summary = summary, CryptoCode = cryptoCode });
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
|
||||
public class MoneroDaemonStateChange
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public MoneroLikeSummary Summary { get; set; }
|
||||
}
|
||||
|
||||
public class MoneroLikeSummary
|
||||
{
|
||||
public bool Synced { get; set; }
|
||||
public long CurrentHeight { get; set; }
|
||||
public long WalletHeight { get; set; }
|
||||
public long TargetHeight { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public bool DaemonAvailable { get; set; }
|
||||
public bool WalletAvailable { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Plugins/Monero/Services/MoneroSyncSummaryProvider.cs
Normal file
50
Plugins/Monero/Services/MoneroSyncSummaryProvider.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Services
|
||||
{
|
||||
public class MoneroSyncSummaryProvider : ISyncSummaryProvider
|
||||
{
|
||||
private readonly MoneroRPCProvider _moneroRpcProvider;
|
||||
|
||||
public MoneroSyncSummaryProvider(MoneroRPCProvider moneroRpcProvider)
|
||||
{
|
||||
_moneroRpcProvider = moneroRpcProvider;
|
||||
}
|
||||
|
||||
public bool AllAvailable()
|
||||
{
|
||||
return _moneroRpcProvider.Summaries.All(pair => pair.Value.WalletAvailable);
|
||||
}
|
||||
|
||||
public string Partial { get; } = "/Views/Monero/MoneroSyncSummary.cshtml";
|
||||
public IEnumerable<ISyncStatus> GetStatuses()
|
||||
{
|
||||
return _moneroRpcProvider.Summaries.Select(pair => new MoneroSyncStatus()
|
||||
{
|
||||
Summary = pair.Value, PaymentMethodId = PaymentMethodId.Parse(pair.Key)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class MoneroSyncStatus: SyncStatus, ISyncStatus
|
||||
{
|
||||
public new PaymentMethodId PaymentMethodId
|
||||
{
|
||||
get => PaymentMethodId.Parse(base.PaymentMethodId);
|
||||
set => base.PaymentMethodId = value.ToString();
|
||||
}
|
||||
public override bool Available
|
||||
{
|
||||
get
|
||||
{
|
||||
return Summary?.WalletAvailable ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; }
|
||||
}
|
||||
}
|
||||
20
Plugins/Monero/Utils/MoneroMoney.cs
Normal file
20
Plugins/Monero/Utils/MoneroMoney.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.Utils
|
||||
{
|
||||
public class MoneroMoney
|
||||
{
|
||||
public static decimal Convert(long piconero)
|
||||
{
|
||||
var amt = piconero.ToString(CultureInfo.InvariantCulture).PadLeft(12, '0');
|
||||
amt = amt.Length == 12 ? $"0.{amt}" : amt.Insert(amt.Length - 12, ".");
|
||||
|
||||
return decimal.Parse(amt, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static long Convert(decimal monero)
|
||||
{
|
||||
return System.Convert.ToInt64(monero * 1000000000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Plugins/Monero/ViewModels/MoneroPaymentViewModel.cs
Normal file
17
Plugins/Monero/ViewModels/MoneroPaymentViewModel.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Plugins.Monero.ViewModels
|
||||
{
|
||||
public class MoneroPaymentViewModel
|
||||
{
|
||||
public PaymentMethodId PaymentMethodId { get; set; }
|
||||
public string Confirmations { get; set; }
|
||||
public string DepositAddress { get; set; }
|
||||
public string Amount { get; set; }
|
||||
public string TransactionId { get; set; }
|
||||
public DateTimeOffset ReceivedTime { get; set; }
|
||||
public string TransactionLink { get; set; }
|
||||
public string Currency { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
@using MoneroLikePaymentMethodViewModel = BTCPayServer.Plugins.Monero.Controllers.UIMoneroLikeStoreController.MoneroLikePaymentMethodViewModel
|
||||
@using MoneroLikeSettlementThresholdChoice = BTCPayServer.Plugins.Monero.Controllers.UIMoneroLikeStoreController.MoneroLikeSettlementThresholdChoice;
|
||||
@model MoneroLikePaymentMethodViewModel
|
||||
|
||||
@{
|
||||
ViewData.SetActivePage(Model.CryptoCode, StringLocalizer["{0} Settings", Model.CryptoCode], Model.CryptoCode);
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All"></div>
|
||||
}
|
||||
@if (Model.Summary != null)
|
||||
{
|
||||
<div class="card">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">Node available: @Model.Summary.DaemonAvailable</li>
|
||||
<li class="list-group-item">Wallet available: @Model.Summary.WalletAvailable (@(Model.WalletFileFound ? "Wallet file present" : "Wallet file not found"))</li>
|
||||
<li class="list-group-item">Last updated: @Model.Summary.UpdatedAt</li>
|
||||
<li class="list-group-item">Synced: @Model.Summary.Synced (@Model.Summary.CurrentHeight / @Model.Summary.TargetHeight)</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!Model.WalletFileFound || Model.Summary.WalletHeight == default)
|
||||
{
|
||||
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod"
|
||||
asp-route-storeId="@Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
|
||||
class="mt-4" enctype="multipart/form-data">
|
||||
|
||||
<div class="card my-2">
|
||||
<h3 class="card-title p-2">Upload Wallet</h3>
|
||||
<div class="form-group p-2">
|
||||
<label asp-for="WalletFile" class="form-label"></label>
|
||||
<input class="form-control" asp-for="WalletFile" required>
|
||||
<span asp-validation-for="WalletFile" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group p-2">
|
||||
<label asp-for="WalletKeysFile" class="form-label"></label>
|
||||
<input class="form-control" asp-for="WalletKeysFile" required>
|
||||
<span asp-validation-for="WalletKeysFile" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group p-2">
|
||||
<label asp-for="WalletPassword" class="form-label"></label>
|
||||
<input class="form-control" asp-for="WalletPassword">
|
||||
<span asp-validation-for="WalletPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="card-footer text-right">
|
||||
<button name="command" value="upload-wallet" class="btn btn-secondary" type="submit">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod"
|
||||
asp-route-storeId="@Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
|
||||
class="mt-4" enctype="multipart/form-data">
|
||||
|
||||
<input type="hidden" asp-for="CryptoCode"/>
|
||||
@if (!Model.WalletFileFound || Model.Summary.WalletHeight == default)
|
||||
{
|
||||
<input type="hidden" asp-for="AccountIndex"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="AccountIndex" class="control-label"></label>
|
||||
@if (@Model.Accounts != null && Model.Accounts.Any())
|
||||
{
|
||||
<select asp-for="AccountIndex" asp-items="Model.Accounts" class="form-control"></select>
|
||||
<span asp-validation-for="AccountIndex" class="text-danger"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>No accounts available on the current wallet</span>
|
||||
<input type="hidden" asp-for="AccountIndex"/>
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group my-3">
|
||||
<input type="text" class="form-control" placeholder="@StringLocalizer["New account label"]" asp-for="NewAccountLabel">
|
||||
<button name="command" value="add-account" class="input-group-text btn btn-secondary" type="submit">Add account</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Enabled"></label>
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="Enabled" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="SettlementConfirmationThresholdChoice" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/FAQ/Stores/#consider-the-invoice-confirmed-when-the-payment-transaction" target="_blank" rel="noreferrer noopener" title="@StringLocalizer["More information..."]">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<select
|
||||
asp-for="SettlementConfirmationThresholdChoice"
|
||||
asp-items="Html.GetEnumSelectList<MoneroLikeSettlementThresholdChoice>()"
|
||||
class="form-select w-auto"
|
||||
onchange="
|
||||
document.getElementById('unconfirmed-warning').hidden = this.value !== '@((int)MoneroLikeSettlementThresholdChoice.ZeroConfirmation)';
|
||||
document.getElementById('custom-confirmation-value').hidden = this.value !== '@((int)MoneroLikeSettlementThresholdChoice.Custom)';">
|
||||
</select>
|
||||
<span asp-validation-for="SettlementConfirmationThresholdChoice" class="text-danger"></span>
|
||||
<p class="info-note my-3 text-warning" id="unconfirmed-warning" role="alert" hidden="@(Model.SettlementConfirmationThresholdChoice is not MoneroLikeSettlementThresholdChoice.ZeroConfirmation)">
|
||||
<vc:icon symbol="warning" />
|
||||
<span text-translate="true">Choosing to accept an unconfirmed invoice can lead to double-spending and is strongly discouraged.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="custom-confirmation-value" hidden="@(Model.SettlementConfirmationThresholdChoice is not MoneroLikeSettlementThresholdChoice.Custom)">
|
||||
<label asp-for="CustomSettlementConfirmationThreshold" class="form-label"></label>
|
||||
<input
|
||||
asp-for="CustomSettlementConfirmationThreshold"
|
||||
type="number"
|
||||
value="@(Model.CustomSettlementConfirmationThreshold)"
|
||||
class="form-control w-auto"
|
||||
min="0"
|
||||
max="100"
|
||||
pattern="\d+"
|
||||
/>
|
||||
<span asp-validation-for="CustomSettlementConfirmationThreshold" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
|
||||
|
||||
<a class="btn btn-secondary" asp-action="GetStoreMoneroLikePaymentMethods"
|
||||
asp-route-storeId="@Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
|
||||
asp-controller="UIMoneroLikeStore">
|
||||
Back to list
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section PageFootContent {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
@model BTCPayServer.Plugins.Monero.Controllers.UIMoneroLikeStoreController.MoneroLikePaymentMethodListViewModel
|
||||
|
||||
@{
|
||||
ViewData.SetActivePage("Monero Settings", StringLocalizer["{0} Settings", "Monero"], "Monero Settings");
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All"></div>
|
||||
}
|
||||
<div class="table-responsive-md">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th text-translate="true">Crypto</th>
|
||||
<th text-translate="true">Account Index</th>
|
||||
<th class="text-center" text-translate="true">Enabled</th>
|
||||
<th class="text-right" text-translate="true">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.CryptoCode</td>
|
||||
<td>@item.AccountIndex</td>
|
||||
<td class="text-center">
|
||||
@if (item.Enabled)
|
||||
{
|
||||
<vc:icon symbol="checkmark" css-class="text-success" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<vc:icon symbol="cross" css-class="text-danger" />
|
||||
}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a id="Modify" asp-action="GetStoreMoneroLikePaymentMethod"
|
||||
asp-route-storeId="@Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@item.CryptoCode"
|
||||
text-translate="true">
|
||||
Modify
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section PageFootContent {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
29
Plugins/Monero/Views/Monero/MoneroSyncSummary.cshtml
Normal file
29
Plugins/Monero/Views/Monero/MoneroSyncSummary.cshtml
Normal file
@@ -0,0 +1,29 @@
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Data
|
||||
@using BTCPayServer.Plugins.Monero.Services
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@inject MoneroRPCProvider MoneroRpcProvider
|
||||
@inject SignInManager<ApplicationUser> SignInManager;
|
||||
|
||||
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroRpcProvider.Summaries.Any())
|
||||
{
|
||||
@foreach (var summary in MoneroRpcProvider.Summaries)
|
||||
{
|
||||
@if (summary.Value != null)
|
||||
{
|
||||
var status = summary.Value.DaemonAvailable
|
||||
? summary.Value.Synced ? "enabled" : "pending"
|
||||
: "disabled";
|
||||
<h5 class="d-flex align-items-center fw-semibold">
|
||||
<span class="me-2 btcpay-status btcpay-status--@status"></span>
|
||||
@summary.Key
|
||||
</h5>
|
||||
<ul>
|
||||
<li>Node available: @summary.Value.DaemonAvailable</li>
|
||||
<li>Wallet available: @summary.Value.WalletAvailable</li>
|
||||
<li>Last updated: @summary.Value.UpdatedAt</li>
|
||||
<li>Synced: @summary.Value.Synced (@summary.Value.CurrentHeight / @summary.Value.TargetHeight)</li>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Plugins/Monero/Views/Monero/StoreNavMoneroExtension.cshtml
Normal file
19
Plugins/Monero/Views/Monero/StoreNavMoneroExtension.cshtml
Normal file
@@ -0,0 +1,19 @@
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Plugins.Monero.Configuration
|
||||
@using BTCPayServer.Plugins.Monero.Controllers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Data
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@inject SignInManager<ApplicationUser> SignInManager;
|
||||
@inject MoneroLikeConfiguration MoneroLikeConfiguration;
|
||||
@inject IScopeProvider ScopeProvider
|
||||
@{
|
||||
var storeId = ScopeProvider.GetCurrentStoreId();
|
||||
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
|
||||
nameof(UIMoneroLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
|
||||
{
|
||||
<a class="nav-link @(isActive ? "active" : string.Empty)" asp-route-storeId="@storeId" asp-action="GetStoreMoneroLikePaymentMethods" asp-controller="UIMoneroLikeStore">Monero</a>
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
@using BTCPayServer.Plugins.Monero.Configuration
|
||||
@using BTCPayServer.Plugins.Monero.Controllers
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@inject SignInManager<ApplicationUser> SignInManager;
|
||||
@inject MoneroLikeConfiguration MoneroLikeConfiguration;
|
||||
@inject IScopeProvider ScopeProvider
|
||||
@inject UIMoneroLikeStoreController UIMoneroLikeStore;
|
||||
@{
|
||||
var storeId = ScopeProvider.GetCurrentStoreId();
|
||||
|
||||
}
|
||||
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
|
||||
{
|
||||
var store = Context.GetStoreData();
|
||||
var result = await UIMoneroLikeStore.GetVM(store);
|
||||
|
||||
foreach (var item in result.Items)
|
||||
{
|
||||
|
||||
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
|
||||
nameof(UIMoneroLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase) &&
|
||||
ViewContext.RouteData.Values.TryGetValue("cryptoCode", out var cryptoCode) && cryptoCode is not null && cryptoCode.ToString() == item.CryptoCode;
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(isActive? "active" : "")"
|
||||
asp-route-cryptoCode="@item.CryptoCode"
|
||||
asp-route-storeId="@storeId"
|
||||
asp-action="GetStoreMoneroLikePaymentMethod"
|
||||
asp-controller="UIMoneroLikeStore">
|
||||
<span class="me-2 btcpay-status btcpay-status--@(item.Enabled ? "enabled" : "pending")"></span>
|
||||
<span>@item.CryptoCode Wallet</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
67
Plugins/Monero/Views/Monero/ViewMoneroLikePaymentData.cshtml
Normal file
67
Plugins/Monero/Views/Monero/ViewMoneroLikePaymentData.cshtml
Normal file
@@ -0,0 +1,67 @@
|
||||
@using System.Globalization
|
||||
@using BTCPayServer.Plugins.Monero.Payments
|
||||
@using BTCPayServer.Plugins.Monero.Services
|
||||
@using BTCPayServer.Plugins.Monero.ViewModels
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Models.InvoicingModels.InvoiceDetailsModel
|
||||
@inject TransactionLinkProviders TransactionLinkProviders
|
||||
@inject PaymentMethodHandlerDictionary handlers
|
||||
|
||||
@{
|
||||
var payments = Model.Payments.Select(payment =>
|
||||
{
|
||||
if (!handlers.TryGetValue(payment.PaymentMethodId, out var h) || h is not MoneroLikePaymentMethodHandler handler)
|
||||
return null;
|
||||
var m = new MoneroPaymentViewModel();
|
||||
var onChainPaymentData = handler.ParsePaymentDetails(payment.Details);
|
||||
m.PaymentMethodId = handler.PaymentMethodId;
|
||||
m.DepositAddress = payment.Destination;
|
||||
m.Amount = payment.Value.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
var confReq = MoneroListener.ConfirmationsRequired(onChainPaymentData, payment.InvoiceEntity.SpeedPolicy);
|
||||
var confCount = onChainPaymentData.ConfirmationCount;
|
||||
confCount = Math.Min(confReq, confCount);
|
||||
m.Confirmations = $"{confCount} / {confReq}";
|
||||
|
||||
m.TransactionId = onChainPaymentData.TransactionId;
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
if (onChainPaymentData.TransactionId != null)
|
||||
m.TransactionLink = TransactionLinkProviders.GetTransactionLink(m.PaymentMethodId, onChainPaymentData.TransactionId);
|
||||
m.Currency = payment.Currency;
|
||||
return m;
|
||||
}).Where(c => c != null).ToList();
|
||||
}
|
||||
|
||||
@if (payments.Any())
|
||||
{
|
||||
<section>
|
||||
<h5>Monero Payments</h5>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-75px">Payment Method</th>
|
||||
<th class="w-175px">Destination</th>
|
||||
<th class="text-nowrap">Payment Proof</th>
|
||||
<th class="text-end">Confirmations</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in payments)
|
||||
{
|
||||
<tr >
|
||||
<td>@payment.PaymentMethodId</td>
|
||||
<td><vc:truncate-center text="@payment.DepositAddress" classes="truncate-center-id" /></td>
|
||||
<td><vc:truncate-center text="@payment.TransactionId" link="@payment.TransactionLink" classes="truncate-center-id" /></td>
|
||||
<td class="text-end">@payment.Confirmations</td>
|
||||
<td class="payment-value text-end text-nowrap">
|
||||
<span data-sensitive class="text-success">@DisplayFormatter.Currency(payment.Amount, payment.Currency)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
}
|
||||
18
Plugins/Monero/Views/Monero/_ViewImports.cshtml
Normal file
18
Plugins/Monero/Views/Monero/_ViewImports.cshtml
Normal file
@@ -0,0 +1,18 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Abstractions.Services
|
||||
@using BTCPayServer.Views
|
||||
@using BTCPayServer.Models
|
||||
@using BTCPayServer.Models.AccountViewModels
|
||||
@using BTCPayServer.Models.InvoicingModels
|
||||
@using BTCPayServer.Models.ManageViewModels
|
||||
@using BTCPayServer.Models.StoreViewModels
|
||||
@using BTCPayServer.Data
|
||||
@using Microsoft.AspNetCore.Routing;
|
||||
@using BTCPayServer.Abstractions.Extensions;
|
||||
@inject Microsoft.AspNetCore.Mvc.Localization.ViewLocalizer ViewLocalizer
|
||||
@inject Microsoft.Extensions.Localization.IStringLocalizer StringLocalizer
|
||||
@inject Safe Safe
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, BTCPayServer
|
||||
@addTagHelper *, BTCPayServer.Abstractions
|
||||
57
README.md
Normal file
57
README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Monero support plugin
|
||||
|
||||
This plugin extends BTCPay Server to enable users to receive payments via Monero.
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
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)). |
|
||||
|
||||
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).
|
||||
|
||||
# For maintainers
|
||||
|
||||
If you are a developer maintaining this plugin, in order to maintain this plugin, you need to clone this repository with `--recurse-submodules`:
|
||||
```bash
|
||||
git clone --recurse-submodules
|
||||
```
|
||||
Then run the tests dependencies
|
||||
```bash
|
||||
docker-compose up -d dev
|
||||
```
|
||||
|
||||
Then create the `appsettings.dev.json` file in `btcpayserver/BTCPayServer`, with the following content:
|
||||
|
||||
```json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
```
|
||||
|
||||
Please replace `C:\\Sources\\btcpayserver-monero-plugin` with the absolute path of your repository.
|
||||
|
||||
This will ensure that BTCPay Server loads the plugin when it starts.
|
||||
|
||||
Finally, set up BTCPay Server as the startup project in [Rider](https://www.jetbrains.com/rider/) or Visual Studio.
|
||||
|
||||
Note: Running or compiling the BTCPay Server project will not automatically recompile the plugin project. Therefore, if you make any changes to the project, do not forget to build it before running BTCPay Server in debug mode.
|
||||
|
||||
We recommend using [Rider](https://www.jetbrains.com/rider/) for plugin development, as it supports hot reload with plugins. You can edit `.cshtml` files, save, and refresh the page to see the changes.
|
||||
|
||||
Visual Studio does not support this feature.
|
||||
|
||||
# Licence
|
||||
|
||||
[MIT](LICENSE.md)
|
||||
85
btcpay-monero-plugin.sln
Normal file
85
btcpay-monero-plugin.sln
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "btcpayserver", "btcpayserver", "{891F21E0-262C-4430-90C5-7A540AD7C9AD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer", "btcpayserver\BTCPayServer\BTCPayServer.csproj", "{049FC011-1952-4140-9652-12921C106B02}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Abstractions", "btcpayserver\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj", "{3ACB5270-BA91-4326-A7CC-5EBEFB8FB511}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Client", "btcpayserver\BTCPayServer.Client\BTCPayServer.Client.csproj", "{157B3D22-F859-482C-B387-2C326A3ECB52}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Common", "btcpayserver\BTCPayServer.Common\BTCPayServer.Common.csproj", "{0A4AAC1F-513C-493C-B173-AA9D28FF5E60}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Data", "btcpayserver\BTCPayServer.Data\BTCPayServer.Data.csproj", "{CB161BDA-5350-4B54-AA94-9540189BAE81}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.PluginPacker", "btcpayserver\BTCPayServer.PluginPacker\BTCPayServer.PluginPacker.csproj", "{E8FBC53B-768F-4454-B7AF-A4BD104B7F0D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Rating", "btcpayserver\BTCPayServer.Rating\BTCPayServer.Rating.csproj", "{67171233-EBD1-4086-9074-57D0F3A74ADC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Tests", "btcpayserver\BTCPayServer.Tests\BTCPayServer.Tests.csproj", "{B481573C-744D-433F-B4DA-442E3E19562E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{C9628212-0A00-4BF2-AF84-21797124579F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Monero", "Plugins\Monero\BTCPayServer.Plugins.Monero.csproj", "{319C8C91-952F-4CF6-A251-058DFC66D70F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{049FC011-1952-4140-9652-12921C106B02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{049FC011-1952-4140-9652-12921C106B02}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{049FC011-1952-4140-9652-12921C106B02}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{049FC011-1952-4140-9652-12921C106B02}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3ACB5270-BA91-4326-A7CC-5EBEFB8FB511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3ACB5270-BA91-4326-A7CC-5EBEFB8FB511}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3ACB5270-BA91-4326-A7CC-5EBEFB8FB511}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3ACB5270-BA91-4326-A7CC-5EBEFB8FB511}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{157B3D22-F859-482C-B387-2C326A3ECB52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{157B3D22-F859-482C-B387-2C326A3ECB52}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{157B3D22-F859-482C-B387-2C326A3ECB52}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{157B3D22-F859-482C-B387-2C326A3ECB52}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0A4AAC1F-513C-493C-B173-AA9D28FF5E60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0A4AAC1F-513C-493C-B173-AA9D28FF5E60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0A4AAC1F-513C-493C-B173-AA9D28FF5E60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0A4AAC1F-513C-493C-B173-AA9D28FF5E60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CB161BDA-5350-4B54-AA94-9540189BAE81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CB161BDA-5350-4B54-AA94-9540189BAE81}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB161BDA-5350-4B54-AA94-9540189BAE81}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CB161BDA-5350-4B54-AA94-9540189BAE81}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E8FBC53B-768F-4454-B7AF-A4BD104B7F0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E8FBC53B-768F-4454-B7AF-A4BD104B7F0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E8FBC53B-768F-4454-B7AF-A4BD104B7F0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E8FBC53B-768F-4454-B7AF-A4BD104B7F0D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{67171233-EBD1-4086-9074-57D0F3A74ADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{67171233-EBD1-4086-9074-57D0F3A74ADC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{67171233-EBD1-4086-9074-57D0F3A74ADC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{67171233-EBD1-4086-9074-57D0F3A74ADC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B481573C-744D-433F-B4DA-442E3E19562E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B481573C-744D-433F-B4DA-442E3E19562E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B481573C-744D-433F-B4DA-442E3E19562E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B481573C-744D-433F-B4DA-442E3E19562E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{319C8C91-952F-4CF6-A251-058DFC66D70F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{319C8C91-952F-4CF6-A251-058DFC66D70F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{319C8C91-952F-4CF6-A251-058DFC66D70F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{319C8C91-952F-4CF6-A251-058DFC66D70F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{049FC011-1952-4140-9652-12921C106B02} = {891F21E0-262C-4430-90C5-7A540AD7C9AD}
|
||||
{3ACB5270-BA91-4326-A7CC-5EBEFB8FB511} = {891F21E0-262C-4430-90C5-7A540AD7C9AD}
|
||||
{157B3D22-F859-482C-B387-2C326A3ECB52} = {891F21E0-262C-4430-90C5-7A540AD7C9AD}
|
||||
{0A4AAC1F-513C-493C-B173-AA9D28FF5E60} = {891F21E0-262C-4430-90C5-7A540AD7C9AD}
|
||||
{CB161BDA-5350-4B54-AA94-9540189BAE81} = {891F21E0-262C-4430-90C5-7A540AD7C9AD}
|
||||
{E8FBC53B-768F-4454-B7AF-A4BD104B7F0D} = {891F21E0-262C-4430-90C5-7A540AD7C9AD}
|
||||
{67171233-EBD1-4086-9074-57D0F3A74ADC} = {891F21E0-262C-4430-90C5-7A540AD7C9AD}
|
||||
{B481573C-744D-433F-B4DA-442E3E19562E} = {891F21E0-262C-4430-90C5-7A540AD7C9AD}
|
||||
{319C8C91-952F-4CF6-A251-058DFC66D70F} = {C9628212-0A00-4BF2-AF84-21797124579F}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
1
btcpayserver
Submodule
1
btcpayserver
Submodule
Submodule btcpayserver added at 29d602b937
142
docker-compose.yml
Normal file
142
docker-compose.yml
Normal file
@@ -0,0 +1,142 @@
|
||||
version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# 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
|
||||
command: [ "/bin/sh", "-c", "trap : TERM INT; while :; do echo Ready to code and debug like a rockstar!!!; sleep 2073600; done & wait" ]
|
||||
depends_on:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- monero_wallet
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.16
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
- "32838"
|
||||
environment:
|
||||
NBXPLORER_NETWORK: regtest
|
||||
NBXPLORER_CHAINS: "btc"
|
||||
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_BIND: 0.0.0.0:32838
|
||||
NBXPLORER_MINGAPSIZE: 5
|
||||
NBXPLORER_MAXGAPSIZE: 10
|
||||
NBXPLORER_VERBOSE: 1
|
||||
NBXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
|
||||
NBXPLORER_EXPOSERPC: 1
|
||||
NBXPLORER_NOAUTH: 1
|
||||
depends_on:
|
||||
- bitcoind
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:26.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
rpcport=43782
|
||||
rpcbind=0.0.0.0:43782
|
||||
rpcallowip=0.0.0.0/0
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtx=tcp://0.0.0.0:28333
|
||||
deprecatedrpc=signrawtransaction
|
||||
fallbackfee=0.0002
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "39388:39388"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
- "28332" # ZMQ
|
||||
- "28333" # ZMQ
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
monerod:
|
||||
image: btcpayserver/monero:0.18.3.3
|
||||
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
|
||||
volumes:
|
||||
- "monero_data:/home/monero/.bitmonero"
|
||||
ports:
|
||||
- "18081:18081"
|
||||
|
||||
monero_wallet:
|
||||
image: btcpayserver/monero:0.18.3.3
|
||||
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"
|
||||
ports:
|
||||
- "18082:18082"
|
||||
volumes:
|
||||
- "./monero_wallet:/wallet"
|
||||
depends_on:
|
||||
- monerod
|
||||
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
environment:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- "39372:5432"
|
||||
expose:
|
||||
- "5432"
|
||||
|
||||
volumes:
|
||||
bitcoin_datadir:
|
||||
monero_data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
custom:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.23.0.0/16
|
||||
BIN
img/Checkout.png
Normal file
BIN
img/Checkout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Reference in New Issue
Block a user