mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
xmr (#1044)
This commit is contained in:
committed by
Nicolas Dorier
parent
3366c86b16
commit
d66b111121
@@ -52,6 +52,7 @@ namespace BTCPayServer
|
|||||||
public string LightningImagePath { get; set; }
|
public string LightningImagePath { get; set; }
|
||||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||||
public KeyPath CoinType { get; internal set; }
|
public KeyPath CoinType { get; internal set; }
|
||||||
|
|
||||||
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
|
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
|
||||||
|
|
||||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||||
@@ -131,7 +132,7 @@ namespace BTCPayServer
|
|||||||
|
|
||||||
public virtual string ToString<T>(T obj)
|
public virtual string ToString<T>(T obj)
|
||||||
{
|
{
|
||||||
return JsonConvert.SerializeObject(obj);
|
return NBitcoin.JsonConverters.Serializer.ToString(obj, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ namespace BTCPayServer
|
|||||||
InitFeathercoin();
|
InitFeathercoin();
|
||||||
InitGroestlcoin();
|
InitGroestlcoin();
|
||||||
InitViacoin();
|
InitViacoin();
|
||||||
|
InitMonero();
|
||||||
|
|
||||||
// Assume that electrum mappings are same as BTC if not specified
|
// Assume that electrum mappings are same as BTC if not specified
|
||||||
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
|
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
|
||||||
{
|
{
|
||||||
|
|||||||
21
BTCPayServer.Common/Monero/BTCPayNetworkProvider.Monero.cs
Normal file
21
BTCPayServer.Common/Monero/BTCPayNetworkProvider.Monero.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public partial class BTCPayNetworkProvider
|
||||||
|
{
|
||||||
|
public void InitMonero()
|
||||||
|
{
|
||||||
|
Add(new MoneroLikeSpecificBtcPayNetwork()
|
||||||
|
{
|
||||||
|
CryptoCode = "XMR",
|
||||||
|
DisplayName = "Monero",
|
||||||
|
BlockExplorerLink =
|
||||||
|
NetworkType == NetworkType.Mainnet
|
||||||
|
? "https://www.exploremonero.com/transaction/{0}"
|
||||||
|
: "https://testnet.xmrchain.net/tx/{0}",
|
||||||
|
CryptoImagePath = "/imlegacy/monero.svg"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public class MoneroLikeSpecificBtcPayNetwork : BTCPayNetworkBase
|
||||||
|
{
|
||||||
|
public int MaxTrackedConfirmation = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Hosting;
|
using BTCPayServer.Hosting;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|||||||
28
BTCPayServer.Tests/docker-compose.monero.yml
Normal file
28
BTCPayServer.Tests/docker-compose.monero.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
monerod:
|
||||||
|
image: kukks/docker-monero:test
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: xmr_monerod
|
||||||
|
entrypoint: monerod --fixed-difficulty 100 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --non-interactive --block-notify="/scripts/notifier.sh https://127.0.0.1:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --no-sync --offline
|
||||||
|
volumes:
|
||||||
|
- "monero_data:/home/monero/.bitmonero"
|
||||||
|
ports:
|
||||||
|
- "18081:18081"
|
||||||
|
monero_wallet:
|
||||||
|
image: kukks/docker-monero:test
|
||||||
|
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=127.0.0.1:18081 --wallet-file=/wallet/wallet.keys --tx-notify="/scripts/notifier.sh https://127.0.0.1:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
|
||||||
|
ports:
|
||||||
|
- "18082:18082"
|
||||||
|
volumes:
|
||||||
|
- "monero_wallet:/wallet"
|
||||||
|
depends_on:
|
||||||
|
- monerod
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
monero_data:
|
||||||
|
monero_wallet:
|
||||||
@@ -179,7 +179,7 @@ namespace BTCPayServer.Controllers
|
|||||||
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
|
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
|
||||||
isDefaultPaymentId = true;
|
isDefaultPaymentId = true;
|
||||||
}
|
}
|
||||||
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
|
||||||
if (network == null && isDefaultPaymentId)
|
if (network == null && isDefaultPaymentId)
|
||||||
{
|
{
|
||||||
//TODO: need to look into a better way for this as it does not scale
|
//TODO: need to look into a better way for this as it does not scale
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ namespace BTCPayServer.Controllers
|
|||||||
.Where(c => c != null))
|
.Where(c => c != null))
|
||||||
{
|
{
|
||||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
|
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
|
||||||
|
//TODO: abstract
|
||||||
if (storeBlob.LightningMaxValue != null)
|
if (storeBlob.LightningMaxValue != null)
|
||||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
|
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
|
||||||
if (storeBlob.OnChainMinValue != null)
|
if (storeBlob.OnChainMinValue != null)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ using OpenIddict.EntityFrameworkCore.Models;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using BTCPayServer.Authentication;
|
using BTCPayServer.Authentication;
|
||||||
using BTCPayServer.Authentication.OpenId;
|
using BTCPayServer.Authentication.OpenId;
|
||||||
|
using BTCPayServer.Monero;
|
||||||
using BTCPayServer.PaymentRequest;
|
using BTCPayServer.PaymentRequest;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Storage;
|
using BTCPayServer.Storage;
|
||||||
@@ -48,6 +49,10 @@ namespace BTCPayServer.Hosting
|
|||||||
{
|
{
|
||||||
Logs.Configure(LoggerFactory);
|
Logs.Configure(LoggerFactory);
|
||||||
services.ConfigureBTCPayServer(Configuration);
|
services.ConfigureBTCPayServer(Configuration);
|
||||||
|
if (Configuration.AnyMoneroLikeCoinsConfigured())
|
||||||
|
{
|
||||||
|
services.AddMoneroLike();
|
||||||
|
}
|
||||||
services.AddMemoryCache();
|
services.AddMemoryCache();
|
||||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||||
|
|||||||
18
BTCPayServer/Monero/Configuration/MoneroLikeConfiguration.cs
Normal file
18
BTCPayServer/Monero/Configuration/MoneroLikeConfiguration.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
76
BTCPayServer/Monero/MoneroLikeExtensions.cs
Normal file
76
BTCPayServer/Monero/MoneroLikeExtensions.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using BTCPayServer.Configuration;
|
||||||
|
using BTCPayServer.Contracts;
|
||||||
|
using BTCPayServer.Monero.Configuration;
|
||||||
|
using BTCPayServer.Monero.Payments;
|
||||||
|
using BTCPayServer.Monero.Services;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero
|
||||||
|
{
|
||||||
|
public static class MoneroLikeExtensions
|
||||||
|
{
|
||||||
|
public static bool AnyMoneroLikeCoinsConfigured(this IConfiguration configuration)
|
||||||
|
{
|
||||||
|
return configuration.GetOrDefault<string>("chains", "")
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(t => t.ToUpperInvariant())
|
||||||
|
.Any(s => s == "XMR");
|
||||||
|
}
|
||||||
|
public static IServiceCollection AddMoneroLike(this IServiceCollection serviceCollection)
|
||||||
|
{
|
||||||
|
serviceCollection.AddSingleton(provider =>
|
||||||
|
provider.ConfigureMoneroLikeConfiguration());
|
||||||
|
serviceCollection.AddSingleton<MoneroRPCProvider>();
|
||||||
|
serviceCollection.AddHostedService<MoneroLikeSummaryUpdaterHostedService>();
|
||||||
|
serviceCollection.AddHostedService<MoneroListener>();
|
||||||
|
serviceCollection.AddSingleton<MoneroLikePaymentMethodHandler>();
|
||||||
|
serviceCollection.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<MoneroLikePaymentMethodHandler>());
|
||||||
|
serviceCollection.AddSingleton<IStoreNavExtension,MoneroStoreNavExtension>();
|
||||||
|
|
||||||
|
return serviceCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MoneroLikeConfiguration ConfigureMoneroLikeConfiguration(this IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var configuration = serviceProvider.GetService<IConfiguration>();
|
||||||
|
var btcPayNetworkProvider = serviceProvider.GetService<BTCPayNetworkProvider>();
|
||||||
|
var result = new MoneroLikeConfiguration();
|
||||||
|
|
||||||
|
var supportedChains = configuration.GetOrDefault<string>("chains", string.Empty)
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(t => t.ToUpperInvariant());
|
||||||
|
|
||||||
|
var supportedNetworks = btcPayNetworkProvider.Filter(supportedChains.ToArray()).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);
|
||||||
|
if (daemonUri == null || walletDaemonUri == null )
|
||||||
|
{
|
||||||
|
throw new ConfigException($"{moneroLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.MoneroLikeConfigurationItems.Add(moneroLikeSpecificBtcPayNetwork.CryptoCode, new MoneroLikeConfigurationItem()
|
||||||
|
{
|
||||||
|
DaemonRpcUri = daemonUri,
|
||||||
|
InternalWalletRpcUri = walletDaemonUri,
|
||||||
|
WalletDirectory = walletDaemonWalletDirectory
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
BTCPayServer/Monero/MoneroStoreNavExtension.cs
Normal file
9
BTCPayServer/Monero/MoneroStoreNavExtension.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using BTCPayServer.Contracts;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero
|
||||||
|
{
|
||||||
|
public class MoneroStoreNavExtension: IStoreNavExtension
|
||||||
|
{
|
||||||
|
public string Partial { get; } = "Monero/StoreNavMoneroExtension";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using BTCPayServer.Payments;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.Payments
|
||||||
|
{
|
||||||
|
public class MoneroLikeOnChainPaymentMethodDetails : IPaymentMethodDetails
|
||||||
|
{
|
||||||
|
public PaymentType GetPaymentType()
|
||||||
|
{
|
||||||
|
return MoneroPaymentType.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetPaymentDestination()
|
||||||
|
{
|
||||||
|
return DepositAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal GetNextNetworkFee()
|
||||||
|
{
|
||||||
|
return NextNetworkFee;
|
||||||
|
}
|
||||||
|
public void SetPaymentDestination(string newPaymentDestination)
|
||||||
|
{
|
||||||
|
DepositAddress = newPaymentDestination;
|
||||||
|
}
|
||||||
|
public long AccountIndex { get; set; }
|
||||||
|
public long AddressIndex { get; set; }
|
||||||
|
public string DepositAddress { get; set; }
|
||||||
|
public decimal NextNetworkFee { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
67
BTCPayServer/Monero/Payments/MoneroLikePaymentData.cs
Normal file
67
BTCPayServer/Monero/Payments/MoneroLikePaymentData.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Monero.Utils;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.Payments
|
||||||
|
{
|
||||||
|
public class MoneroLikePaymentData : CryptoPaymentData
|
||||||
|
{
|
||||||
|
public long Amount { get; set; }
|
||||||
|
public string Address { get; set; }
|
||||||
|
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 BTCPayNetworkBase Network { get; set; }
|
||||||
|
|
||||||
|
public string GetPaymentId()
|
||||||
|
{
|
||||||
|
return $"{TransactionId}#{SubaccountIndex}#{SubaddressIndex}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetSearchTerms()
|
||||||
|
{
|
||||||
|
return new[] {TransactionId};
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal GetValue()
|
||||||
|
{
|
||||||
|
return MoneroMoney.Convert(Amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PaymentCompleted(PaymentEntity entity)
|
||||||
|
{
|
||||||
|
return ConfirmationCount >= (Network as MoneroLikeSpecificBtcPayNetwork).MaxTrackedConfirmation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy)
|
||||||
|
{
|
||||||
|
switch (speedPolicy)
|
||||||
|
{
|
||||||
|
case SpeedPolicy.HighSpeed:
|
||||||
|
return ConfirmationCount >= 0;
|
||||||
|
case SpeedPolicy.MediumSpeed:
|
||||||
|
return ConfirmationCount >= 1;
|
||||||
|
case SpeedPolicy.LowMediumSpeed:
|
||||||
|
return ConfirmationCount >= 2;
|
||||||
|
case SpeedPolicy.LowSpeed:
|
||||||
|
return ConfirmationCount >= 6;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentType GetPaymentType()
|
||||||
|
{
|
||||||
|
return MoneroPaymentType.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDestination()
|
||||||
|
{
|
||||||
|
return Address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
BTCPayServer/Monero/Payments/MoneroLikePaymentMethodHandler.cs
Normal file
128
BTCPayServer/Monero/Payments/MoneroLikePaymentMethodHandler.cs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Models.InvoicingModels;
|
||||||
|
using BTCPayServer.Monero.RPC.Models;
|
||||||
|
using BTCPayServer.Monero.Services;
|
||||||
|
using BTCPayServer.Monero.Utils;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.Payments
|
||||||
|
{
|
||||||
|
public class MoneroLikePaymentMethodHandler : PaymentMethodHandlerBase<MoneroSupportedPaymentMethod, MoneroLikeSpecificBtcPayNetwork>
|
||||||
|
{
|
||||||
|
private readonly BTCPayNetworkProvider _networkProvider;
|
||||||
|
private readonly MoneroRPCProvider _moneroRpcProvider;
|
||||||
|
|
||||||
|
public MoneroLikePaymentMethodHandler(BTCPayNetworkProvider networkProvider, MoneroRPCProvider moneroRpcProvider)
|
||||||
|
{
|
||||||
|
_networkProvider = networkProvider;
|
||||||
|
_moneroRpcProvider = moneroRpcProvider;
|
||||||
|
}
|
||||||
|
public override PaymentType PaymentType => MoneroPaymentType.Instance;
|
||||||
|
|
||||||
|
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(MoneroSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod,
|
||||||
|
StoreData store, MoneroLikeSpecificBtcPayNetwork network, object preparePaymentObject)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!_moneroRpcProvider.IsAvailable(network.CryptoCode))
|
||||||
|
throw new PaymentMethodUnavailableException($"Node or wallet not available");
|
||||||
|
var invoice = paymentMethod.ParentEntity;
|
||||||
|
if (!(preparePaymentObject is Prepare moneroPrepare)) throw new ArgumentException();
|
||||||
|
var feeRatePerKb = await moneroPrepare.GetFeeRate;
|
||||||
|
var address = await moneroPrepare.ReserveAddress(invoice.Id);
|
||||||
|
|
||||||
|
var feeRatePerByte = feeRatePerKb.Fee / 1024;
|
||||||
|
return new MoneroLikeOnChainPaymentMethodDetails()
|
||||||
|
{
|
||||||
|
NextNetworkFee = MoneroMoney.Convert(feeRatePerByte * 100),
|
||||||
|
AccountIndex = supportedPaymentMethod.AccountIndex,
|
||||||
|
AddressIndex = address.AddressIndex,
|
||||||
|
DepositAddress = address.Address
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object PreparePayment(MoneroSupportedPaymentMethod supportedPaymentMethod, StoreData store,
|
||||||
|
BTCPayNetworkBase network)
|
||||||
|
{
|
||||||
|
|
||||||
|
var walletClient = _moneroRpcProvider.WalletRpcClients [supportedPaymentMethod.CryptoCode];
|
||||||
|
var daemonClient = _moneroRpcProvider.DaemonRpcClients [supportedPaymentMethod.CryptoCode];
|
||||||
|
return 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 })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Prepare
|
||||||
|
{
|
||||||
|
public Task<GetFeeEstimateResponse> GetFeeRate;
|
||||||
|
public Func<string, Task<CreateAddressResponse>> ReserveAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse, StoreBlob storeBlob)
|
||||||
|
{
|
||||||
|
var paymentMethodId = new PaymentMethodId(model.CryptoCode, PaymentType);
|
||||||
|
|
||||||
|
var client = _moneroRpcProvider.WalletRpcClients[model.CryptoCode];
|
||||||
|
|
||||||
|
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||||
|
var network = _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(model.CryptoCode);
|
||||||
|
model.IsLightning = false;
|
||||||
|
model.PaymentMethodName = GetPaymentMethodName(network);
|
||||||
|
model.CryptoImage = GetCryptoImage(network);
|
||||||
|
model.InvoiceBitcoinUrl = client.SendCommandAsync<MakeUriRequest, MakeUriResponse>("make_uri", new MakeUriRequest()
|
||||||
|
{
|
||||||
|
Address = cryptoInfo.Address,
|
||||||
|
Amount = LightMoney.Parse(cryptoInfo.Due).MilliSatoshi
|
||||||
|
}).GetAwaiter()
|
||||||
|
.GetResult().Uri;
|
||||||
|
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
|
||||||
|
}
|
||||||
|
public override string GetCryptoImage(PaymentMethodId paymentMethodId)
|
||||||
|
{
|
||||||
|
var network = _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(paymentMethodId.CryptoCode);
|
||||||
|
return GetCryptoImage(network);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetPaymentMethodName(PaymentMethodId paymentMethodId)
|
||||||
|
{
|
||||||
|
var network = _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(paymentMethodId.CryptoCode);
|
||||||
|
return GetPaymentMethodName(network);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob, Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount,
|
||||||
|
PaymentMethodId paymentMethodId)
|
||||||
|
{
|
||||||
|
return Task.FromResult<string>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
|
||||||
|
{
|
||||||
|
return _networkProvider.GetAll()
|
||||||
|
.Where(network => network is MoneroLikeSpecificBtcPayNetwork)
|
||||||
|
.Select(network => new PaymentMethodId(network.CryptoCode, PaymentType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCryptoImage(MoneroLikeSpecificBtcPayNetwork network)
|
||||||
|
{
|
||||||
|
return network.CryptoImagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string GetPaymentMethodName(MoneroLikeSpecificBtcPayNetwork network)
|
||||||
|
{
|
||||||
|
return $"{network.DisplayName}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
BTCPayServer/Monero/Payments/MoneroPaymentType.cs
Normal file
42
BTCPayServer/Monero/Payments/MoneroPaymentType.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.Payments
|
||||||
|
{
|
||||||
|
public class MoneroPaymentType: PaymentType
|
||||||
|
{
|
||||||
|
public static MoneroPaymentType Instance { get; } = new MoneroPaymentType();
|
||||||
|
public override string ToPrettyString() => "On-Chain";
|
||||||
|
|
||||||
|
public override string GetId()=> "MoneroLike";
|
||||||
|
|
||||||
|
|
||||||
|
public override CryptoPaymentData DeserializePaymentData(string str)
|
||||||
|
{
|
||||||
|
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
return JsonConvert.DeserializeObject<MoneroLikePaymentData>(str);
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str)
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<MoneroLikeOnChainPaymentMethodDetails>(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value)
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<MoneroSupportedPaymentMethod>(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetTransactionLink(BTCPayNetworkBase network, string txId)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string InvoiceViewPaymentPartialName { get; }= "Monero/ViewMoneroLikePaymentData";
|
||||||
|
}
|
||||||
|
}
|
||||||
12
BTCPayServer/Monero/Payments/MoneroSupportedPaymentMethod.cs
Normal file
12
BTCPayServer/Monero/Payments/MoneroSupportedPaymentMethod.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using BTCPayServer.Payments;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.Payments
|
||||||
|
{
|
||||||
|
public class MoneroSupportedPaymentMethod : ISupportedPaymentMethod
|
||||||
|
{
|
||||||
|
|
||||||
|
public string CryptoCode { get; set; }
|
||||||
|
public long AccountIndex { get; set; }
|
||||||
|
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, MoneroPaymentType.Instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
BTCPayServer/Monero/RPC/JsonRpcClient.cs
Normal file
121
BTCPayServer/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.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}")));
|
||||||
|
|
||||||
|
var rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
||||||
|
|
||||||
|
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
||||||
|
rawResult.EnsureSuccessStatusCode();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal 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
BTCPayServer/Monero/RPC/Models/CreateAccountRequest.cs
Normal file
9
BTCPayServer/Monero/RPC/Models/CreateAccountRequest.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class CreateAccountRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("label")] public string Label { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
BTCPayServer/Monero/RPC/Models/CreateAccountResponse.cs
Normal file
10
BTCPayServer/Monero/RPC/Models/CreateAccountResponse.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class CreateAccountResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||||
|
[JsonProperty("address")] public string Address { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
BTCPayServer/Monero/RPC/Models/CreateAddressRequest.cs
Normal file
10
BTCPayServer/Monero/RPC/Models/CreateAddressRequest.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class CreateAddressRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||||
|
[JsonProperty("label")] public string Label { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
BTCPayServer/Monero/RPC/Models/CreateAddressResponse.cs
Normal file
10
BTCPayServer/Monero/RPC/Models/CreateAddressResponse.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class CreateAddressResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("address")] public string Address { get; set; }
|
||||||
|
[JsonProperty("address_index")] public long AddressIndex { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
BTCPayServer/Monero/RPC/Models/GetAccountsRequest.cs
Normal file
9
BTCPayServer/Monero/RPC/Models/GetAccountsRequest.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class GetAccountsRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("tag")] public string Tag { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
BTCPayServer/Monero/RPC/Models/GetAccountsResponse.cs
Normal file
14
BTCPayServer/Monero/RPC/Models/GetAccountsResponse.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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
BTCPayServer/Monero/RPC/Models/GetFeeEstimateRequest.cs
Normal file
9
BTCPayServer/Monero/RPC/Models/GetFeeEstimateRequest.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public class GetFeeEstimateRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("grace_blocks")] public int? GraceBlocks { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
BTCPayServer/Monero/RPC/Models/GetFeeEstimateResponse.cs
Normal file
11
BTCPayServer/Monero/RPC/Models/GetFeeEstimateResponse.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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
BTCPayServer/Monero/RPC/Models/GetHeightResponse.cs
Normal file
9
BTCPayServer/Monero/RPC/Models/GetHeightResponse.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class GetHeightResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("height")] public long Height { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public class GetTransferByTransactionIdRequest
|
||||||
|
{
|
||||||
|
[JsonProperty("txid")] public string TransactionId { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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("fee")] public long Fee { 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
BTCPayServer/Monero/RPC/Models/GetTransfersRequest.cs
Normal file
19
BTCPayServer/Monero/RPC/Models/GetTransfersRequest.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
36
BTCPayServer/Monero/RPC/Models/GetTransfersResponse.cs
Normal file
36
BTCPayServer/Monero/RPC/Models/GetTransfersResponse.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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("fee")] public long Fee { 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
BTCPayServer/Monero/RPC/Models/Info.cs
Normal file
33
BTCPayServer/Monero/RPC/Models/Info.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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
BTCPayServer/Monero/RPC/Models/MakeUriRequest.cs
Normal file
13
BTCPayServer/Monero/RPC/Models/MakeUriRequest.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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
BTCPayServer/Monero/RPC/Models/MakeUriResponse.cs
Normal file
9
BTCPayServer/Monero/RPC/Models/MakeUriResponse.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class MakeUriResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("uri")] public string Uri { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
39
BTCPayServer/Monero/RPC/Models/ParseStringConverter.cs
Normal file
39
BTCPayServer/Monero/RPC/Models/ParseStringConverter.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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
BTCPayServer/Monero/RPC/Models/Peer.cs
Normal file
9
BTCPayServer/Monero/RPC/Models/Peer.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class Peer
|
||||||
|
{
|
||||||
|
[JsonProperty("info")] public Info Info { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
BTCPayServer/Monero/RPC/Models/SubaddrIndex.cs
Normal file
10
BTCPayServer/Monero/RPC/Models/SubaddrIndex.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class SubaddrIndex
|
||||||
|
{
|
||||||
|
[JsonProperty("major")] public long Major { get; set; }
|
||||||
|
[JsonProperty("minor")] public long Minor { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
BTCPayServer/Monero/RPC/Models/SubaddressAccount.cs
Normal file
14
BTCPayServer/Monero/RPC/Models/SubaddressAccount.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
BTCPayServer/Monero/RPC/Models/SyncInfoResponse.cs
Normal file
13
BTCPayServer/Monero/RPC/Models/SyncInfoResponse.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC.Models
|
||||||
|
{
|
||||||
|
public partial class SyncInfoResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("height")] public long Height { get; set; }
|
||||||
|
[JsonProperty("peers")] public List<Peer> Peers { get; set; }
|
||||||
|
[JsonProperty("status")] public string Status { get; set; }
|
||||||
|
[JsonProperty("target_height")] public long? TargetHeight { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
36
BTCPayServer/Monero/RPC/MoneroDaemonCallbackController.cs
Normal file
36
BTCPayServer/Monero/RPC/MoneroDaemonCallbackController.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.RPC
|
||||||
|
{
|
||||||
|
[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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
15
BTCPayServer/Monero/RPC/MoneroEvent.cs
Normal file
15
BTCPayServer/Monero/RPC/MoneroEvent.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace BTCPayServer.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,66 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.Monero.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.Services
|
||||||
|
{
|
||||||
|
public class MoneroLikeSummaryUpdaterHostedService: IHostedService
|
||||||
|
{
|
||||||
|
private readonly MoneroRPCProvider _MoneroRpcProvider;
|
||||||
|
private readonly MoneroLikeConfiguration _moneroLikeConfiguration;
|
||||||
|
private CancellationTokenSource _Cts;
|
||||||
|
public MoneroLikeSummaryUpdaterHostedService(MoneroRPCProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration)
|
||||||
|
{
|
||||||
|
_MoneroRpcProvider = moneroRpcProvider;
|
||||||
|
_moneroLikeConfiguration = moneroLikeConfiguration;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
360
BTCPayServer/Monero/Services/MoneroListener.cs
Normal file
360
BTCPayServer/Monero/Services/MoneroListener.cs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.Monero.Configuration;
|
||||||
|
using BTCPayServer.Monero.Payments;
|
||||||
|
using BTCPayServer.Monero.RPC;
|
||||||
|
using BTCPayServer.Monero.RPC.Models;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBXplorer;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.Services
|
||||||
|
{
|
||||||
|
public class MoneroListener : IHostedService
|
||||||
|
{
|
||||||
|
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 CompositeDisposable leases = new CompositeDisposable();
|
||||||
|
|
||||||
|
|
||||||
|
private CancellationTokenSource _Cts;
|
||||||
|
|
||||||
|
public MoneroListener(InvoiceRepository invoiceRepository,
|
||||||
|
EventAggregator eventAggregator,
|
||||||
|
MoneroRPCProvider moneroRpcProvider,
|
||||||
|
MoneroLikeConfiguration moneroLikeConfiguration,
|
||||||
|
BTCPayNetworkProvider networkProvider,
|
||||||
|
ILogger<MoneroListener> logger)
|
||||||
|
{
|
||||||
|
_invoiceRepository = invoiceRepository;
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
|
_moneroRpcProvider = moneroRpcProvider;
|
||||||
|
_MoneroLikeConfiguration = moneroLikeConfiguration;
|
||||||
|
_networkProvider = networkProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
|
||||||
|
leases.Add(_eventAggregator.Subscribe<MoneroEvent>(OnMoneroEvent));
|
||||||
|
leases.Add(_eventAggregator.Subscribe<MoneroRPCProvider.MoneroDaemonStateChange>(e =>
|
||||||
|
{
|
||||||
|
if (_moneroRpcProvider.IsAvailable(e.CryptoCode))
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"{e.CryptoCode} just became available");
|
||||||
|
_ = UpdateAnyPendingMoneroLikePayment(e.CryptoCode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"{e.CryptoCode} just became unavailable");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMoneroEvent(MoneroEvent obj)
|
||||||
|
{
|
||||||
|
if (!_moneroRpcProvider.IsAvailable(obj.CryptoCode))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(obj.BlockHash))
|
||||||
|
{
|
||||||
|
OnNewBlock(obj.CryptoCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(obj.TransactionHash))
|
||||||
|
{
|
||||||
|
_ = OnTransactionUpdated(obj.CryptoCode, obj.TransactionHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}");
|
||||||
|
var paymentData = (MoneroLikePaymentData)payment.GetCryptoPaymentData();
|
||||||
|
var paymentMethod = invoice.GetPaymentMethod(payment.Network, MoneroPaymentType.Instance);
|
||||||
|
if (paymentMethod != null &&
|
||||||
|
paymentMethod.GetPaymentMethodDetails() is MoneroLikeOnChainPaymentMethodDetails monero &&
|
||||||
|
monero.GetPaymentDestination() == paymentData.GetDestination() &&
|
||||||
|
paymentMethod.Calculate().Due > Money.Zero)
|
||||||
|
{
|
||||||
|
var walletClient = _moneroRpcProvider.WalletRpcClients[payment.GetCryptoCode()];
|
||||||
|
|
||||||
|
var address = await walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>(
|
||||||
|
"create_address",
|
||||||
|
new CreateAddressRequest()
|
||||||
|
{
|
||||||
|
Label = $"btcpay invoice #{invoice.Id}", AccountIndex = monero.AccountIndex
|
||||||
|
});
|
||||||
|
monero.DepositAddress = address.Address;
|
||||||
|
monero.AddressIndex = address.AddressIndex;
|
||||||
|
await _invoiceRepository.NewAddress(invoice.Id, monero, payment.Network);
|
||||||
|
_eventAggregator.Publish(
|
||||||
|
new InvoiceNewAddressEvent(invoice.Id, address.Address, payment.Network));
|
||||||
|
paymentMethod.SetPaymentMethodDetails(monero);
|
||||||
|
invoice.SetPaymentMethod(paymentMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventAggregator.Publish(
|
||||||
|
new InvoiceEvent(invoice, 1002, 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);
|
||||||
|
|
||||||
|
|
||||||
|
//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),
|
||||||
|
PaymentMethodDetails: entity.GetPaymentMethod(network, MoneroPaymentType.Instance)
|
||||||
|
.GetPaymentMethodDetails() as MoneroLikeOnChainPaymentMethodDetails))
|
||||||
|
.Select(tuple => (
|
||||||
|
tuple.Invoice,
|
||||||
|
tuple.PaymentMethodDetails,
|
||||||
|
ExistingPayments: tuple.ExistingPayments.Select(entity =>
|
||||||
|
(Payment: entity, PaymentData: (MoneroLikePaymentData)entity.GetCryptoPaymentData(),
|
||||||
|
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 BlockingCollection<(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.PaymentData.Address == transfer.Address &&
|
||||||
|
tuple.PaymentData.TransactionId == transfer.Txid);
|
||||||
|
|
||||||
|
if (existingMatch.Invoice != null)
|
||||||
|
{
|
||||||
|
invoice = existingMatch.Invoice;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var newMatch = expandedInvoices.SingleOrDefault(tuple =>
|
||||||
|
tuple.PaymentMethodDetails.GetPaymentDestination() == transfer.Address);
|
||||||
|
|
||||||
|
if (newMatch.Invoice == null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice = newMatch.Invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return HandlePaymnetData(cryptoCode, transfer.Address, transfer.Amount, transfer.SubaddrIndex.Major,
|
||||||
|
transfer.SubaddrIndex.Minor, transfer.Txid, transfer.Confirmations, transfer.Height, invoice,
|
||||||
|
updatedPaymentEntities);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
transferProcessingTasks.Add(
|
||||||
|
_invoiceRepository.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
leases.Dispose();
|
||||||
|
_Cts.Cancel();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNewBlock(string cryptoCode)
|
||||||
|
{
|
||||||
|
_ = UpdateAnyPendingMoneroLikePayment(cryptoCode);
|
||||||
|
_eventAggregator.Publish(new NewBlockEvent() {CryptoCode = cryptoCode});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnTransactionUpdated(string cryptoCode, string transactionHash)
|
||||||
|
{
|
||||||
|
var paymentMethodId = new PaymentMethodId(cryptoCode, MoneroPaymentType.Instance);
|
||||||
|
var transfer = await _moneroRpcProvider.WalletRpcClients[cryptoCode]
|
||||||
|
.SendCommandAsync<GetTransferByTransactionIdRequest, GetTransferByTransactionIdResponse>(
|
||||||
|
"get_transfer_by_txid",
|
||||||
|
new GetTransferByTransactionIdRequest() {TransactionId = transactionHash});
|
||||||
|
|
||||||
|
var paymentsToUpdate = new BlockingCollection<(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 address = destination.Key + "#" + paymentMethodId;
|
||||||
|
var invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] {address})).FirstOrDefault();
|
||||||
|
if (invoice == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = destination.First().SubaddrIndex;
|
||||||
|
|
||||||
|
await HandlePaymnetData(cryptoCode,
|
||||||
|
destination.Key,
|
||||||
|
destination.Sum(destination1 => destination1.Amount),
|
||||||
|
index.Major,
|
||||||
|
index.Minor,
|
||||||
|
transfer.Transfer.Txid,
|
||||||
|
transfer.Transfer.Confirmations,
|
||||||
|
transfer.Transfer.Height
|
||||||
|
, invoice, paymentsToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentsToUpdate.Any())
|
||||||
|
{
|
||||||
|
await _invoiceRepository.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 HandlePaymnetData(string cryptoCode, string address, long totalAmount, long subaccountIndex,
|
||||||
|
long subaddressIndex,
|
||||||
|
string txId, long confirmations, long blockHeight, InvoiceEntity invoice,
|
||||||
|
BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate)
|
||||||
|
{
|
||||||
|
//construct the payment data
|
||||||
|
var paymentData = new MoneroLikePaymentData()
|
||||||
|
{
|
||||||
|
Address = address,
|
||||||
|
SubaccountIndex = subaccountIndex,
|
||||||
|
SubaddressIndex = subaddressIndex,
|
||||||
|
TransactionId = txId,
|
||||||
|
ConfirmationCount = confirmations,
|
||||||
|
Amount = totalAmount,
|
||||||
|
BlockHeight = blockHeight,
|
||||||
|
Network = _networkProvider.GetNetwork(cryptoCode)
|
||||||
|
};
|
||||||
|
|
||||||
|
//check if this tx exists as a payment to this invoice already
|
||||||
|
var alreadyExistingPaymentThatMatches = GetAllMoneroLikePayments(invoice, cryptoCode)
|
||||||
|
.Select(entity => (Payment: entity, PaymentData: entity.GetCryptoPaymentData()))
|
||||||
|
.SingleOrDefault(c => c.PaymentData.GetPaymentId() == paymentData.GetPaymentId());
|
||||||
|
|
||||||
|
//if it doesnt, add it and assign a new monerolike address to the system if a balance is still due
|
||||||
|
if (alreadyExistingPaymentThatMatches.Payment == null)
|
||||||
|
{
|
||||||
|
var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow,
|
||||||
|
paymentData, _networkProvider.GetNetwork<MoneroLikeSpecificBtcPayNetwork>(cryptoCode), true);
|
||||||
|
if (payment != null)
|
||||||
|
await ReceivedPayment(invoice, payment);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//else update it with the new data
|
||||||
|
alreadyExistingPaymentThatMatches.PaymentData = paymentData;
|
||||||
|
alreadyExistingPaymentThatMatches.Payment.SetCryptoPaymentData(paymentData);
|
||||||
|
paymentsToUpdate.Add((alreadyExistingPaymentThatMatches.Payment, invoice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateAnyPendingMoneroLikePayment(string cryptoCode)
|
||||||
|
{
|
||||||
|
var invoiceIds =
|
||||||
|
await GetPendingInvoicesWithPaymentMethodOption(new PaymentMethodId(cryptoCode,
|
||||||
|
MoneroPaymentType.Instance));
|
||||||
|
if (!invoiceIds.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() {InvoiceId = invoiceIds});
|
||||||
|
_logger.LogInformation($"Updating pending payments for {cryptoCode} in {string.Join(',', invoiceIds)}");
|
||||||
|
await UpdatePaymentStates(cryptoCode, invoices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string[]> GetPendingInvoicesWithPaymentMethodOption(PaymentMethodId paymentMethodId)
|
||||||
|
{
|
||||||
|
return await _invoiceRepository.GetPendingInvoices(pendingInvoice =>
|
||||||
|
pendingInvoice.Where(data => data.InvoiceData.AddressInvoices.Any(invoiceData =>
|
||||||
|
invoiceData.GetpaymentMethodId() != null && invoiceData.GetpaymentMethodId() == paymentMethodId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<PaymentEntity> GetAllMoneroLikePayments(InvoiceEntity invoice, string cryptoCode)
|
||||||
|
{
|
||||||
|
return invoice.GetPayments()
|
||||||
|
.Where(p => p.GetPaymentMethodId() == new PaymentMethodId(cryptoCode, MoneroPaymentType.Instance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
BTCPayServer/Monero/Services/MoneroRPCProvider.cs
Normal file
118
BTCPayServer/Monero/Services/MoneroRPCProvider.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Monero.Configuration;
|
||||||
|
using BTCPayServer.Monero.RPC;
|
||||||
|
using BTCPayServer.Monero.RPC.Models;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.Services
|
||||||
|
{
|
||||||
|
public class MoneroRPCProvider
|
||||||
|
{
|
||||||
|
private readonly MoneroLikeConfiguration _moneroLikeConfiguration;
|
||||||
|
private readonly EventAggregator _eventAggregator;
|
||||||
|
public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients;
|
||||||
|
public ImmutableDictionary<string, JsonRpcClient> WalletRpcClients;
|
||||||
|
|
||||||
|
private ConcurrentDictionary<string, MoneroLikeSummary> _summaries =
|
||||||
|
new ConcurrentDictionary<string, MoneroLikeSummary>();
|
||||||
|
|
||||||
|
public ConcurrentDictionary<string, MoneroLikeSummary> Summaries => _summaries;
|
||||||
|
|
||||||
|
public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration, EventAggregator eventAggregator, IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
_moneroLikeConfiguration = moneroLikeConfiguration;
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
|
DaemonRpcClients =
|
||||||
|
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
|
||||||
|
pair => new JsonRpcClient(pair.Value.DaemonRpcUri, "", "", httpClientFactory.CreateClient()));
|
||||||
|
WalletRpcClients =
|
||||||
|
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
|
||||||
|
pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "", httpClientFactory.CreateClient()));
|
||||||
|
}
|
||||||
|
|
||||||
|
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, SyncInfoResponse>("sync_info",
|
||||||
|
JsonRpcClient.NoRequestModel.Instance);
|
||||||
|
summary.TargetHeight = daemonResult.TargetHeight ?? daemonResult.Height;
|
||||||
|
summary.Synced = !daemonResult.TargetHeight.HasValue ||
|
||||||
|
(daemonResult.Height >= daemonResult.TargetHeight && daemonResult.TargetHeight > 0);
|
||||||
|
summary.CurrentHeight = daemonResult.Height;
|
||||||
|
summary.UpdatedAt = DateTime.Now;
|
||||||
|
summary.DaemonAvailable = true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
summary.DaemonAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var walletResult =
|
||||||
|
await walletRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, GetHeightResponse>(
|
||||||
|
"get_height", JsonRpcClient.NoRequestModel.Instance);
|
||||||
|
|
||||||
|
summary.WalletHeight = walletResult.Height;
|
||||||
|
summary.WalletAvailable = true;
|
||||||
|
}
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
301
BTCPayServer/Monero/UI/MoneroLikeStoreController.cs
Normal file
301
BTCPayServer/Monero/UI/MoneroLikeStoreController.cs
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
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.Data;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Monero.Configuration;
|
||||||
|
using BTCPayServer.Monero.Payments;
|
||||||
|
using BTCPayServer.Monero.RPC.Models;
|
||||||
|
using BTCPayServer.Monero.Services;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.UI
|
||||||
|
{
|
||||||
|
[Route("stores/{storeId}/monerolike")]
|
||||||
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
|
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
|
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
|
public class MoneroLikeStoreController : Controller
|
||||||
|
{
|
||||||
|
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
|
||||||
|
private readonly StoreRepository _StoreRepository;
|
||||||
|
private readonly MoneroRPCProvider _MoneroRpcProvider;
|
||||||
|
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||||
|
|
||||||
|
public MoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration,
|
||||||
|
StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider,
|
||||||
|
BTCPayNetworkProvider btcPayNetworkProvider)
|
||||||
|
{
|
||||||
|
_MoneroLikeConfiguration = moneroLikeConfiguration;
|
||||||
|
_StoreRepository = storeRepository;
|
||||||
|
_MoneroRpcProvider = moneroRpcProvider;
|
||||||
|
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoreData StoreData => HttpContext.GetStoreData();
|
||||||
|
|
||||||
|
[HttpGet()]
|
||||||
|
public async Task<IActionResult> GetStoreMoneroLikePaymentMethods(string statusMessage)
|
||||||
|
{
|
||||||
|
var monero = StoreData.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
|
||||||
|
.OfType<MoneroSupportedPaymentMethod>();
|
||||||
|
|
||||||
|
var excludeFilters = StoreData.GetStoreBlob().GetExcludedPaymentMethods();
|
||||||
|
|
||||||
|
var accountsList = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.ToDictionary(pair => pair.Key,
|
||||||
|
pair => GetAccounts(pair.Key));
|
||||||
|
|
||||||
|
await Task.WhenAll(accountsList.Values);
|
||||||
|
|
||||||
|
return View(new MoneroLikePaymentMethodListViewModel()
|
||||||
|
{
|
||||||
|
StatusMessage = statusMessage,
|
||||||
|
Items = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.Select(pair =>
|
||||||
|
GetMoneroLikePaymentMethodViewModel(monero, 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(
|
||||||
|
IEnumerable<MoneroSupportedPaymentMethod> monero, string cryptoCode,
|
||||||
|
IPaymentFilter excludeFilters, GetAccountsResponse accountsResponse)
|
||||||
|
{
|
||||||
|
var settings = monero.SingleOrDefault(method => method.CryptoCode == cryptoCode);
|
||||||
|
_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)));
|
||||||
|
return new MoneroLikePaymentMethodViewModel()
|
||||||
|
{
|
||||||
|
WalletFileFound = System.IO.File.Exists(fileAddress),
|
||||||
|
Enabled =
|
||||||
|
settings != null &&
|
||||||
|
!excludeFilters.Match(new PaymentMethodId(cryptoCode, MoneroPaymentType.Instance)),
|
||||||
|
Summary = summary,
|
||||||
|
CryptoCode = cryptoCode,
|
||||||
|
AccountIndex = settings?.AccountIndex ?? accountsResponse?.SubaddressAccounts?.FirstOrDefault()?.AccountIndex?? (long)0,
|
||||||
|
Accounts = accounts == null? null : new SelectList(accounts, nameof(SelectListItem.Value),
|
||||||
|
nameof(SelectListItem.Text))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{cryptoCode}")]
|
||||||
|
public async Task<IActionResult> GetStoreMoneroLikePaymentMethod(string cryptoCode, string statusMessage = null)
|
||||||
|
{
|
||||||
|
cryptoCode = cryptoCode.ToUpperInvariant();
|
||||||
|
if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.ContainsKey(cryptoCode))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var vm = GetMoneroLikePaymentMethodViewModel(StoreData.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
|
||||||
|
.OfType<MoneroSupportedPaymentMethod>(), cryptoCode,
|
||||||
|
StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode));
|
||||||
|
vm.StatusMessage = statusMessage;
|
||||||
|
return View(nameof(GetStoreMoneroLikePaymentMethod), vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{cryptoCode}")]
|
||||||
|
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), "Could not create new account.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}else if (command == "upload-wallet")
|
||||||
|
{
|
||||||
|
var valid = true;
|
||||||
|
if (viewModel.WalletFile == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(viewModel.WalletFile), "Please select the wallet file");
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
if (viewModel.WalletKeysFile == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(viewModel.WalletKeysFile), "Please select the wallet.keys file");
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(valid)
|
||||||
|
{
|
||||||
|
if(_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary))
|
||||||
|
{
|
||||||
|
if (summary.WalletAvailable)
|
||||||
|
{
|
||||||
|
return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod),
|
||||||
|
new {cryptoCode, StatusMessage = new StatusMessageModel()
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
|
Message = $"There is already an active wallet configured for {cryptoCode}. Replacing it would break any existing invoices"
|
||||||
|
}.ToString()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), new
|
||||||
|
{
|
||||||
|
cryptoCode,
|
||||||
|
StatusMessage ="Wallet files uploaded. If it was valid, the wallet will become available soon"
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
|
||||||
|
var vm = GetMoneroLikePaymentMethodViewModel(StoreData
|
||||||
|
.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
|
||||||
|
.OfType<MoneroSupportedPaymentMethod>(), cryptoCode,
|
||||||
|
StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode));
|
||||||
|
|
||||||
|
vm.Enabled = viewModel.Enabled;
|
||||||
|
vm.NewAccountLabel = viewModel.NewAccountLabel;
|
||||||
|
vm.AccountIndex = viewModel.AccountIndex;
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
var storeData = StoreData;
|
||||||
|
var blob = storeData.GetStoreBlob();
|
||||||
|
storeData.SetSupportedPaymentMethod(new MoneroSupportedPaymentMethod()
|
||||||
|
{
|
||||||
|
AccountIndex = viewModel.AccountIndex,
|
||||||
|
CryptoCode = viewModel.CryptoCode
|
||||||
|
});
|
||||||
|
|
||||||
|
blob.SetExcluded(new PaymentMethodId(viewModel.CryptoCode, MoneroPaymentType.Instance), !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}\""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
process.WaitForExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MoneroLikePaymentMethodListViewModel
|
||||||
|
{
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
public IEnumerable<MoneroLikePaymentMethodViewModel> Items { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MoneroLikePaymentMethodViewModel
|
||||||
|
{
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
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; }
|
||||||
|
public IFormFile WalletKeysFile { get; set; }
|
||||||
|
public string WalletPassword { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
BTCPayServer/Monero/UI/MoneroPaymentViewModel.cs
Normal file
15
BTCPayServer/Monero/UI/MoneroPaymentViewModel.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.UI
|
||||||
|
{
|
||||||
|
public class MoneroPaymentViewModel
|
||||||
|
{
|
||||||
|
public string Crypto { 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
20
BTCPayServer/Monero/Utils/MoneroMoney.cs
Normal file
20
BTCPayServer/Monero/Utils/MoneroMoney.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Monero.Utils
|
||||||
|
{
|
||||||
|
public class MoneroMoney
|
||||||
|
{
|
||||||
|
public static decimal Convert(long atoms)
|
||||||
|
{
|
||||||
|
var amt = atoms.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Monero.Payments;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
@@ -33,6 +34,9 @@ namespace BTCPayServer.Payments
|
|||||||
case "offchain":
|
case "offchain":
|
||||||
type = PaymentTypes.LightningLike;
|
type = PaymentTypes.LightningLike;
|
||||||
break;
|
break;
|
||||||
|
case "monerolike":
|
||||||
|
type = MoneroPaymentType.Instance;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
type = null;
|
type = null;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -46,6 +46,35 @@
|
|||||||
"BTCPAY_SSHPASSWORD": "opD3i2282D"
|
"BTCPAY_SSHPASSWORD": "opD3i2282D"
|
||||||
},
|
},
|
||||||
"applicationUrl": "https://localhost:14142/"
|
"applicationUrl": "https://localhost:14142/"
|
||||||
|
},
|
||||||
|
"Docker-Regtest-https-monero": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"BTCPAY_NETWORK": "regtest",
|
||||||
|
"BTCPAY_LAUNCHSETTINGS": "true",
|
||||||
|
"BTCPAY_PORT": "14142",
|
||||||
|
"BTCPAY_HttpsUseDefaultCertificate": "true",
|
||||||
|
"BTCPAY_BUNDLEJSCSS": "false",
|
||||||
|
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||||
|
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||||
|
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||||
|
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true",
|
||||||
|
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
|
||||||
|
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
|
||||||
|
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||||
|
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
|
||||||
|
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"BTCPAY_CHAINS": "btc,ltc,xmr",
|
||||||
|
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
|
||||||
|
"BTCPAY_EXTERNALSERVICES": "totoservice:totolink;",
|
||||||
|
"BTCPAY_XMR_DAEMON_URI": "http://127.0.0.1:18081",
|
||||||
|
"BTCPAY_XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082",
|
||||||
|
"BTCPAY_XMR_WALLET_DAEMON_WALLETDIR": "C:/ProgramData/bitmonero/wallet"
|
||||||
|
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:14142/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ namespace BTCPayServer.Services.PaymentRequests
|
|||||||
return await context.PaymentRequests.Include(x => x.StoreData)
|
return await context.PaymentRequests.Include(x => x.StoreData)
|
||||||
.AnyAsync(data =>
|
.AnyAsync(data =>
|
||||||
data.Id == paymentRequestId &&
|
data.Id == paymentRequestId &&
|
||||||
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)));
|
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
@using BTCPayServer.Controllers
|
||||||
|
@using BTCPayServer.Views.Stores
|
||||||
|
@model BTCPayServer.Monero.UI.MoneroLikeStoreController.MoneroLikePaymentMethodViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
|
||||||
|
ViewData["NavPartialName"] = "../Stores/_Nav";
|
||||||
|
ViewData.SetActivePageAndTitle(StoreNavPages.ActivePage, $"{Model.CryptoCode} Settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage"/>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
@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(long))
|
||||||
|
{
|
||||||
|
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod" 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"></label>
|
||||||
|
<input class="form-control-file" asp-for="WalletFile" required>
|
||||||
|
<span asp-validation-for="WalletFile" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group p-2">
|
||||||
|
<label asp-for="WalletKeysFile"></label>
|
||||||
|
<input class="form-control-file" asp-for="WalletKeysFile" required>
|
||||||
|
<span asp-validation-for="WalletKeysFile" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group p-2">
|
||||||
|
<label asp-for="WalletPassword"></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" class="mt-4" enctype="multipart/form-data">
|
||||||
|
|
||||||
|
<input type="hidden" asp-for="CryptoCode"/>
|
||||||
|
@if (!Model.WalletFileFound || Model.Summary.WalletHeight == default(long))
|
||||||
|
{
|
||||||
|
<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="New account label" asp-for="NewAccountLabel">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button name="command" value="add-account" class="btn btn-secondary"type="submit">Add account</button>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
|
||||||
|
|
||||||
|
<a class="btn btn-secondary" asp-action="GetStoreMoneroLikePaymentMethods" asp-controller="MoneroLikeStore">Back to list</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
@using BTCPayServer.Views.Stores
|
||||||
|
@model BTCPayServer.Monero.UI.MoneroLikeStoreController.MoneroLikePaymentMethodListViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
|
||||||
|
ViewData.SetActivePageAndTitle(StoreNavPages.ActivePage, "Monero Settings");
|
||||||
|
|
||||||
|
ViewData["NavPartialName"] = "../Stores/_Nav";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="form-group">
|
||||||
|
<table class="table table-sm table-responsive-md">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Crypto</th>
|
||||||
|
<th>Account Index</th>
|
||||||
|
<th class="text-center">Enabled</th>
|
||||||
|
<th class="text-right">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)
|
||||||
|
{
|
||||||
|
<span class="fa fa-check"></span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="fa fa-times"></span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<a id="Modify" asp-action="GetStoreMoneroLikePaymentMethod" asp-route-cryptoCode="@item.CryptoCode">Modify</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@using BTCPayServer.Controllers
|
||||||
|
@using BTCPayServer.Monero.Configuration
|
||||||
|
@using BTCPayServer.Monero.UI
|
||||||
|
@inject SignInManager<ApplicationUser> SignInManager;
|
||||||
|
@inject MoneroLikeConfiguration MoneroLikeConfiguration;
|
||||||
|
@{
|
||||||
|
var controller = ViewContext.RouteData.Values["Controller"].ToString();
|
||||||
|
var isMonero = controller.Equals(nameof(MoneroLikeStoreController), StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
|
||||||
|
{
|
||||||
|
<a class="nav-link @(isMonero ? "active" : string.Empty)" asp-action="GetStoreMoneroLikePaymentMethods" asp-controller="MoneroLikeStore">Monero</a>
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
@using System.Globalization
|
||||||
|
@using BTCPayServer.Controllers
|
||||||
|
@using BTCPayServer.Monero.Payments
|
||||||
|
@using BTCPayServer.Monero.UI
|
||||||
|
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
|
||||||
|
|
||||||
|
@{
|
||||||
|
var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == MoneroPaymentType.Instance).Select(payment =>
|
||||||
|
{
|
||||||
|
var m = new MoneroPaymentViewModel();
|
||||||
|
var onChainPaymentData = payment.GetCryptoPaymentData() as MoneroLikePaymentData;
|
||||||
|
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||||
|
m.DepositAddress = onChainPaymentData.GetDestination();
|
||||||
|
|
||||||
|
var confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||||
|
var network = payment.Network as MoneroLikeSpecificBtcPayNetwork;
|
||||||
|
if (confirmationCount >= network.MaxTrackedConfirmation)
|
||||||
|
{
|
||||||
|
m.Confirmations = "At least " + (network.MaxTrackedConfirmation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
m.TransactionId = onChainPaymentData.TransactionId;
|
||||||
|
m.ReceivedTime = payment.ReceivedTime;
|
||||||
|
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (onchainPayments.Any())
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 invoice-payments">
|
||||||
|
<h3>Monero payments</h3>
|
||||||
|
<table class="table table-sm table-responsive-lg">
|
||||||
|
<thead class="thead-inverse">
|
||||||
|
<tr>
|
||||||
|
<th>Crypto</th>
|
||||||
|
<th>Deposit address</th>
|
||||||
|
<th>Transaction Id</th>
|
||||||
|
<th class="text-right">Confirmations</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var payment in onchainPayments)
|
||||||
|
{
|
||||||
|
<tr >
|
||||||
|
<td>@payment.Crypto</td>
|
||||||
|
<td>@payment.DepositAddress</td>
|
||||||
|
<td>
|
||||||
|
<div class="wraptextAuto">
|
||||||
|
<a href="@payment.TransactionLink" target="_blank">
|
||||||
|
@payment.TransactionId
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">@payment.Confirmations</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<div class="nav flex-column nav-pills">
|
<div class="nav flex-column nav-pills">
|
||||||
<a id="@(nameof(StoreNavPages.Index))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Index)" asp-action="UpdateStore">General settings</a>
|
<a id="@(nameof(StoreNavPages.Index))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Index)" asp-controller="Stores" asp-action="UpdateStore">General settings</a>
|
||||||
<a id="@(nameof(StoreNavPages.Rates))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-action="Rates">Rates</a>
|
<a id="@(nameof(StoreNavPages.Rates))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="Stores" asp-action="Rates">Rates</a>
|
||||||
<a id="@(nameof(StoreNavPages.Checkout))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Checkout)" asp-action="CheckoutExperience">Checkout experience</a>
|
<a id="@(nameof(StoreNavPages.Checkout))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Checkout)" asp-controller="Stores" asp-action="CheckoutExperience">Checkout experience</a>
|
||||||
<a id="@(nameof(StoreNavPages.Tokens))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-action="ListTokens">Access Tokens</a>
|
<a id="@(nameof(StoreNavPages.Tokens))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="Stores" asp-action="ListTokens">Access Tokens</a>
|
||||||
<a id="@(nameof(StoreNavPages.Users))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-action="StoreUsers">Users</a>
|
<a id="@(nameof(StoreNavPages.Users))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="Stores" asp-action="StoreUsers">Users</a>
|
||||||
<a id="@(nameof(StoreNavPages.PayButton))"class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-action="PayButton">Pay Button</a>
|
<a id="@(nameof(StoreNavPages.PayButton))"class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)"asp-controller="Stores" asp-action="PayButton">Pay Button</a>
|
||||||
@inject IEnumerable<BTCPayServer.Contracts.IStoreNavExtension> Extensions;
|
@inject IEnumerable<BTCPayServer.Contracts.IStoreNavExtension> Extensions;
|
||||||
@foreach (var extension in Extensions)
|
@foreach (var extension in Extensions)
|
||||||
{
|
{
|
||||||
|
|||||||
40
BTCPayServer/wwwroot/imlegacy/monero.svg
Normal file
40
BTCPayServer/wwwroot/imlegacy/monero.svg
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||||
|
<circle style="fill:#F0EFEB;" cx="256" cy="256" r="256"/>
|
||||||
|
<path style="fill:#4C4C4C;" d="M364.2,393.163h107.979c-45.411,71.439-125.262,118.836-216.178,118.836
|
||||||
|
S85.235,464.603,39.824,393.163h107.969V257.328l108.209,108.146L364.2,257.328V393.163z"/>
|
||||||
|
<path style="fill:#FF6600;" d="M512,256.001c0,28.599-4.692,56.1-13.343,81.784H421.21V122.537L256.002,286.062L90.794,122.537
|
||||||
|
v215.248H13.346C4.694,312.102,0.003,284.6,0.003,256.001c0-141.384,114.614-255.998,255.998-255.998S512,114.616,512,256.001z"/>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 995 B |
Reference in New Issue
Block a user