Remove ZCash and Monero from core code

This commit is contained in:
nicolas.dorier
2025-01-06 22:23:16 +09:00
parent 580518e5aa
commit 51c2b9f243
100 changed files with 2 additions and 4523 deletions

View File

@@ -58,7 +58,6 @@ services:
- merchant_lnd
- sshd
- tor
- monero_wallet
sshd:
build:
@@ -317,28 +316,6 @@ services:
- "torrcdir:/usr/local/etc/tor"
- "tor_servicesdir:/var/lib/tor/hidden_services"
monerod:
image: btcpayserver/monero:0.18.3.3
restart: unless-stopped
container_name: xmr_monerod
entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline --non-interactive
volumes:
- "monero_data:/home/monero/.bitmonero"
ports:
- "18081:18081"
monero_wallet:
image: btcpayserver/monero:0.18.3.3
restart: unless-stopped
container_name: xmr_wallet_rpc
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
ports:
- "18082:18082"
volumes:
- "./monero_wallet:/wallet"
depends_on:
- monerod
litecoind:
restart: unless-stopped
image: btcpayserver/litecoin:0.18.1
@@ -400,7 +377,6 @@ volumes:
tor_datadir:
torrcdir:
tor_servicesdir:
monero_data:
networks:
default:

View File

@@ -1,31 +0,0 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Filters
{
public class OnlyIfSupportAttribute : Attribute, IAsyncActionFilter
{
private readonly string _paymentMethodId;
public OnlyIfSupportAttribute(string paymentMethodId)
{
_paymentMethodId = paymentMethodId;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var handlers = context.HttpContext.RequestServices.GetService<PaymentMethodHandlerDictionary>();
if (!handlers.Support(PaymentMethodId.Parse(_paymentMethodId)))
{
context.Result = new NotFoundResult();
return;
}
await next();
}
}
}

View File

@@ -52,10 +52,6 @@ namespace BTCPayServer.Plugins.Altcoins
InitDash(services);
if (selectedChains.Contains("GRS"))
InitGroestlcoin(services);
if (selectedChains.Contains("XMR"))
InitMonero(services);
if (selectedChains.Contains("ZEC"))
InitZcash(services);
}
}
}

View File

@@ -1,122 +0,0 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Services;
using System.Net.Http;
using System.Net;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Altcoins.Monero.Configuration;
using BTCPayServer.Services.Altcoins.Monero.Payments;
using BTCPayServer.Services.Altcoins.Monero.Services;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using BTCPayServer.Configuration;
using System.Linq;
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace BTCPayServer.Plugins.Altcoins;
public partial class AltcoinsPlugin
{
public void InitMonero(IServiceCollection services)
{
var network = new MoneroLikeSpecificBtcPayNetwork()
{
CryptoCode = "XMR",
DisplayName = "Monero",
Divisibility = 12,
DefaultRateRules = new[]
{
"XMR_X = XMR_BTC * BTC_X",
"XMR_BTC = kraken(XMR_BTC)"
},
CryptoImagePath = "/imlegacy/monero.svg",
UriScheme = "monero"
};
var blockExplorerLink = ChainName == ChainName.Mainnet
? "https://www.exploremonero.com/transaction/{0}"
: "https://testnet.xmrchain.net/tx/{0}";
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("XMR");
services.AddDefaultPrettyName(pmi, network.DisplayName);
services.AddBTCPayNetwork(network)
.AddTransactionLinkProvider(pmi, new SimpleTransactionLinkProvider(blockExplorerLink));
services.AddSingleton(provider =>
ConfigureMoneroLikeConfiguration(provider));
services.AddHttpClient("XMRclient")
.ConfigurePrimaryHttpMessageHandler(provider =>
{
var configuration = provider.GetRequiredService<MoneroLikeConfiguration>();
if (!configuration.MoneroLikeConfigurationItems.TryGetValue("XMR", out var xmrConfig) || xmrConfig.Username is null || xmrConfig.Password is null)
{
return new HttpClientHandler();
}
return new HttpClientHandler
{
Credentials = new NetworkCredential(xmrConfig.Username, xmrConfig.Password),
PreAuthenticate = true
};
});
services.AddSingleton<MoneroRPCProvider>();
services.AddHostedService<MoneroLikeSummaryUpdaterHostedService>();
services.AddHostedService<MoneroListener>();
services.AddSingleton<IPaymentMethodHandler>(provider =>
(IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), new object[] { network }));
services.AddSingleton<IPaymentLinkExtension>(provider =>
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroCheckoutModelExtension), new object[] { network, pmi }));
services.AddUIExtension("store-nav", "Monero/StoreNavMoneroExtension");
services.AddUIExtension("store-wallets-nav", "Monero/StoreWalletsNavMoneroExtension");
services.AddUIExtension("store-invoices-payments", "Monero/ViewMoneroLikePaymentData");
services.AddSingleton<ISyncSummaryProvider, MoneroSyncSummaryProvider>();
}
private static MoneroLikeConfiguration ConfigureMoneroLikeConfiguration(IServiceProvider serviceProvider)
{
var configuration = serviceProvider.GetService<IConfiguration>();
var btcPayNetworkProvider = serviceProvider.GetService<BTCPayNetworkProvider>();
var result = new MoneroLikeConfiguration();
var supportedNetworks = btcPayNetworkProvider.GetAll()
.OfType<MoneroLikeSpecificBtcPayNetwork>();
foreach (var moneroLikeSpecificBtcPayNetwork in supportedNetworks)
{
var daemonUri =
configuration.GetOrDefault<Uri>($"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_uri",
null);
var walletDaemonUri =
configuration.GetOrDefault<Uri>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_uri", null);
var walletDaemonWalletDirectory =
configuration.GetOrDefault<string>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null);
var daemonUsername =
configuration.GetOrDefault<string>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_username", null);
var daemonPassword =
configuration.GetOrDefault<string>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_password", null);
if (daemonUri == null || walletDaemonUri == null || walletDaemonWalletDirectory == null)
{
throw new ConfigException($"{moneroLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured");
}
result.MoneroLikeConfigurationItems.Add(moneroLikeSpecificBtcPayNetwork.CryptoCode, new MoneroLikeConfigurationItem()
{
DaemonRpcUri = daemonUri,
Username = daemonUsername,
Password = daemonPassword,
InternalWalletRpcUri = walletDaemonUri,
WalletDirectory = walletDaemonWalletDirectory
});
}
return result;
}
}

View File

@@ -1,8 +0,0 @@
namespace BTCPayServer.Plugins.Altcoins;
public class MoneroLikeSpecificBtcPayNetwork : BTCPayNetworkBase
{
public int MaxTrackedConfirmation = 10;
public string UriScheme { get; set; }
}

View File

@@ -1,121 +0,0 @@
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.Services.Altcoins.Monero.RPC
{
public class JsonRpcClient
{
private readonly Uri _address;
private readonly string _username;
private readonly string _password;
private readonly HttpClient _httpClient;
public JsonRpcClient(Uri address, string username, string password, HttpClient client = null)
{
_address = address;
_username = username;
_password = password;
_httpClient = client ?? new HttpClient();
}
public async Task<TResponse> SendCommandAsync<TRequest, TResponse>(string method, TRequest data,
CancellationToken cts = default(CancellationToken))
{
var jsonSerializer = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var httpRequest = new HttpRequestMessage()
{
Method = HttpMethod.Post,
RequestUri = new Uri(_address, "json_rpc"),
Content = new StringContent(
JsonConvert.SerializeObject(new JsonRpcCommand<TRequest>(method, data), jsonSerializer),
Encoding.UTF8, "application/json")
};
httpRequest.Headers.Accept.Clear();
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
HttpResponseMessage rawResult = await _httpClient.SendAsync(httpRequest, cts);
rawResult.EnsureSuccessStatusCode();
var rawJson = await rawResult.Content.ReadAsStringAsync();
JsonRpcResult<TResponse> response;
try
{
response = JsonConvert.DeserializeObject<JsonRpcResult<TResponse>>(rawJson, jsonSerializer);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Console.WriteLine(rawJson);
throw;
}
if (response.Error != null)
{
throw new JsonRpcApiException()
{
Error = response.Error
};
}
return response.Result;
}
public class NoRequestModel
{
public static NoRequestModel Instance = new NoRequestModel();
}
internal class JsonRpcApiException : Exception
{
public JsonRpcResultError Error { get; set; }
public override string Message => Error?.Message;
}
public class JsonRpcResultError
{
[JsonProperty("code")] public int Code { get; set; }
[JsonProperty("message")] public string Message { get; set; }
[JsonProperty("data")] dynamic Data { get; set; }
}
internal class JsonRpcResult<T>
{
[JsonProperty("result")] public T Result { get; set; }
[JsonProperty("error")] public JsonRpcResultError Error { get; set; }
[JsonProperty("id")] public string Id { get; set; }
}
internal class JsonRpcCommand<T>
{
[JsonProperty("jsonRpc")] public string JsonRpc { get; set; } = "2.0";
[JsonProperty("id")] public string Id { get; set; } = Guid.NewGuid().ToString();
[JsonProperty("method")] public string Method { get; set; }
[JsonProperty("params")] public T Parameters { get; set; }
public JsonRpcCommand()
{
}
public JsonRpcCommand(string method, T parameters)
{
Method = method;
Parameters = parameters;
}
}
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class CreateAccountRequest
{
[JsonProperty("label")] public string Label { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class CreateAccountResponse
{
[JsonProperty("account_index")] public long AccountIndex { get; set; }
[JsonProperty("address")] public string Address { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class CreateAddressRequest
{
[JsonProperty("account_index")] public long AccountIndex { get; set; }
[JsonProperty("label")] public string Label { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class CreateAddressResponse
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("address_index")] public long AddressIndex { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class GetAccountsRequest
{
[JsonProperty("tag")] public string Tag { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.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; }
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public class GetFeeEstimateRequest
{
[JsonProperty("grace_blocks")] public int? GraceBlocks { get; set; }
}
}

View File

@@ -1,11 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.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; }
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class GetHeightResponse
{
[JsonProperty("height")] public long Height { get; set; }
}
}

View File

@@ -1,13 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class GetInfoResponse
{
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("busy_syncing")] public bool BusySyncing { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("target_height")] public long? TargetHeight { get; set; }
}
}

View File

@@ -1,11 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public class GetTransferByTransactionIdRequest
{
[JsonProperty("txid")] public string TransactionId { get; set; }
[JsonProperty("account_index", DefaultValueHandling = DefaultValueHandling.Ignore)] public long? AccountIndex { get; set; }
}
}

View File

@@ -1,31 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class GetTransferByTransactionIdResponse
{
[JsonProperty("transfer")] public TransferItem Transfer { get; set; }
[JsonProperty("transfers")] public IEnumerable<TransferItem> Transfers { get; set; }
public partial class TransferItem
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("confirmations")] public long Confirmations { get; set; }
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("note")] public string Note { get; set; }
[JsonProperty("payment_id")] public string PaymentId { get; set; }
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
[JsonProperty("suggested_confirmations_threshold")]
public long SuggestedConfirmationsThreshold { get; set; }
[JsonProperty("timestamp")] public long Timestamp { get; set; }
[JsonProperty("txid")] public string Txid { get; set; }
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
}
}
}

View File

@@ -1,19 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.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; }
}
}

View File

@@ -1,35 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class GetTransfersResponse
{
[JsonProperty("in")] public List<GetTransfersResponseItem> In { get; set; }
[JsonProperty("out")] public List<GetTransfersResponseItem> Out { get; set; }
[JsonProperty("pending")] public List<GetTransfersResponseItem> Pending { get; set; }
[JsonProperty("failed")] public List<GetTransfersResponseItem> Failed { get; set; }
[JsonProperty("pool")] public List<GetTransfersResponseItem> Pool { get; set; }
public partial class GetTransfersResponseItem
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("confirmations")] public long Confirmations { get; set; }
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("note")] public string Note { get; set; }
[JsonProperty("payment_id")] public string PaymentId { get; set; }
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
[JsonProperty("suggested_confirmations_threshold")]
public long SuggestedConfirmationsThreshold { get; set; }
[JsonProperty("timestamp")] public long Timestamp { get; set; }
[JsonProperty("txid")] public string Txid { get; set; }
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
}
}
}

View File

@@ -1,33 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.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; }
}
}

View File

@@ -1,13 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.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; }
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class MakeUriResponse
{
[JsonProperty("uri")] public string Uri { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class OpenWalletErrorResponse
{
[JsonProperty("code")] public int Code { get; set; }
[JsonProperty("message")] public string Message { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class OpenWalletRequest
{
[JsonProperty("filename")] public string Filename { get; set; }
[JsonProperty("password")] public string Password { get; set; }
}
}

View File

@@ -1,12 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class OpenWalletResponse
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("jsonrpc")] public string Jsonrpc { get; set; }
[JsonProperty("result")] public object Result { get; set; }
[JsonProperty("error")] public OpenWalletErrorResponse Error { get; set; }
}
}

View File

@@ -1,40 +0,0 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.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();
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class Peer
{
[JsonProperty("info")] public Info Info { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
{
public partial class SubaddrIndex
{
[JsonProperty("major")] public long Major { get; set; }
[JsonProperty("minor")] public long Minor { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.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; }
}
}

View File

@@ -1,20 +0,0 @@
using System.Globalization;
namespace BTCPayServer.Services.Altcoins.Monero.Utils
{
public class MoneroMoney
{
public static decimal Convert(long piconero)
{
var amt = piconero.ToString(CultureInfo.InvariantCulture).PadLeft(12, '0');
amt = amt.Length == 12 ? $"0.{amt}" : amt.Insert(amt.Length - 12, ".");
return decimal.Parse(amt, CultureInfo.InvariantCulture);
}
public static long Convert(decimal monero)
{
return System.Convert.ToInt64(monero * 1000000000000);
}
}
}

View File

@@ -1,118 +0,0 @@
#nullable enable
using BTCPayServer.Services;
using System.Globalization;
using System.Linq;
using NBitcoin;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Services;
using BTCPayServer.Services.Altcoins.Zcash.Payments;
using BTCPayServer.Services.Altcoins.Zcash.Services;
using BTCPayServer.Configuration;
using BTCPayServer.Services.Altcoins.Zcash.Configuration;
using System;
using Microsoft.Extensions.Configuration;
namespace BTCPayServer.Plugins.Altcoins;
public partial class AltcoinsPlugin
{
// Change this if you want another zcash coin
public void InitZcash(IServiceCollection services)
{
var network = new ZcashLikeSpecificBtcPayNetwork()
{
CryptoCode = "ZEC",
DisplayName = "Zcash",
Divisibility = 8,
DefaultRateRules = new[]
{
"ZEC_X = ZEC_BTC * BTC_X",
"ZEC_BTC = kraken(ZEC_BTC)"
},
CryptoImagePath = "/imlegacy/zcash.png",
UriScheme = "zcash"
};
var blockExplorerLink = ChainName == ChainName.Mainnet
? "https://www.exploreZcash.com/transaction/{0}"
: "https://testnet.xmrchain.net/tx/{0}";
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("ZEC");
services.AddDefaultPrettyName(pmi, network.DisplayName);
services.AddBTCPayNetwork(network)
.AddTransactionLinkProvider(pmi, new SimpleTransactionLinkProvider(blockExplorerLink));
services.AddSingleton(provider =>
ConfigureZcashLikeConfiguration(provider));
services.AddSingleton<ZcashRPCProvider>();
services.AddHostedService<ZcashLikeSummaryUpdaterHostedService>();
services.AddHostedService<ZcashListener>();
services.AddSingleton<IPaymentMethodHandler>(provider =>
(IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(ZcashLikePaymentMethodHandler), new object[] { network }));
services.AddSingleton<IPaymentLinkExtension>(provider =>
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(ZcashPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(ZcashCheckoutModelExtension), new object[] { network, pmi }));
services.AddSingleton<ZcashLikePaymentMethodHandler>();
services.AddSingleton<IPaymentMethodHandler>(provider => provider.GetRequiredService<ZcashLikePaymentMethodHandler>());
services.AddUIExtension("store-nav", "Zcash/StoreNavZcashExtension");
services.AddUIExtension("store-invoices-payments", "Zcash/ViewZcashLikePaymentData");
services.AddSingleton<ISyncSummaryProvider, ZcashSyncSummaryProvider>();
}
static ZcashLikeConfiguration ConfigureZcashLikeConfiguration(IServiceProvider serviceProvider)
{
var configuration = serviceProvider.GetService<IConfiguration>();
var btcPayNetworkProvider = serviceProvider.GetRequiredService<BTCPayNetworkProvider>();
var result = new ZcashLikeConfiguration();
var supportedNetworks = btcPayNetworkProvider.GetAll()
.OfType<ZcashLikeSpecificBtcPayNetwork>();
foreach (var ZcashLikeSpecificBtcPayNetwork in supportedNetworks)
{
var daemonUri =
configuration.GetOrDefault<Uri?>($"{ZcashLikeSpecificBtcPayNetwork.CryptoCode}_daemon_uri",
null);
var walletDaemonUri =
configuration.GetOrDefault<Uri?>(
$"{ZcashLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_uri", null);
var walletDaemonWalletDirectory =
configuration.GetOrDefault<string?>(
$"{ZcashLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null);
if (daemonUri == null || walletDaemonUri == null || walletDaemonWalletDirectory == null)
{
throw new ConfigException($"{ZcashLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured");
}
result.ZcashLikeConfigurationItems.Add(ZcashLikeSpecificBtcPayNetwork.CryptoCode, new ZcashLikeConfigurationItem()
{
DaemonRpcUri = daemonUri,
InternalWalletRpcUri = walletDaemonUri,
WalletDirectory = walletDaemonWalletDirectory
});
}
return result;
}
class SimpleTransactionLinkProvider : DefaultTransactionLinkProvider
{
public SimpleTransactionLinkProvider(string blockExplorerLink) : base(blockExplorerLink)
{
}
public override string? GetTransactionLink(string paymentId)
{
if (string.IsNullOrEmpty(BlockExplorerLink))
return null;
return string.Format(CultureInfo.InvariantCulture, BlockExplorerLink, paymentId);
}
}
}

View File

@@ -1,102 +0,0 @@
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.Services.Altcoins.Zcash.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, method),
Content = new StringContent(
JsonConvert.SerializeObject(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();
var response = JsonConvert.DeserializeObject<TResponse>(rawJson, jsonSerializer);
return response;
}
public class NoRequestModel
{
public static NoRequestModel Instance = new NoRequestModel();
}
internal class JsonRpcApiException : Exception
{
public JsonRpcResultError Error { get; set; }
public override string Message => Error?.Message;
}
public class JsonRpcResultError
{
[JsonProperty("code")] public int Code { get; set; }
[JsonProperty("message")] public string Message { get; set; }
[JsonProperty("data")] dynamic Data { get; set; }
}
internal class JsonRpcResult<T>
{
[JsonProperty("result")] public T Result { get; set; }
[JsonProperty("error")] public JsonRpcResultError Error { get; set; }
[JsonProperty("id")] public string Id { get; set; }
}
internal class JsonRpcCommand<T>
{
[JsonProperty("jsonRpc")] public string JsonRpc { get; set; } = "2.0";
[JsonProperty("id")] public string Id { get; set; } = Guid.NewGuid().ToString();
[JsonProperty("method")] public string Method { get; set; }
[JsonProperty("params")] public T Parameters { get; set; }
public JsonRpcCommand()
{
}
public JsonRpcCommand(string method, T parameters)
{
Method = method;
Parameters = parameters;
}
}
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class CreateAccountRequest
{
[JsonProperty("label")] public string Label { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class CreateAccountResponse
{
[JsonProperty("account_index")] public long AccountIndex { get; set; }
[JsonProperty("address")] public string Address { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class CreateAddressRequest
{
[JsonProperty("account_index")] public long AccountIndex { get; set; }
[JsonProperty("label")] public string Label { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class CreateAddressResponse
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("address_index")] public long AddressIndex { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetAccountsRequest
{
[JsonProperty("tag")] public string Tag { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.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; }
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public class GetFeeEstimateRequest
{
[JsonProperty("grace_blocks")] public int? GraceBlocks { get; set; }
}
}

View File

@@ -1,11 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.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; }
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetHeightResponse
{
[JsonProperty("height")] public long Height { get; set; }
}
}

View File

@@ -1,11 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public class GetTransferByTransactionIdRequest
{
[JsonProperty("txid")] public string TransactionId { get; set; }
[JsonProperty("account_index")] public long AccountIndex { get; set; }
}
}

View File

@@ -1,31 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetTransferByTransactionIdResponse
{
[JsonProperty("transfer")] public TransferItem Transfer { get; set; }
[JsonProperty("transfers")] public IEnumerable<TransferItem> Transfers { get; set; }
public partial class TransferItem
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("confirmations")] public long Confirmations { get; set; }
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("note")] public string Note { get; set; }
[JsonProperty("payment_id")] public string PaymentId { get; set; }
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
[JsonProperty("suggested_confirmations_threshold")]
public long SuggestedConfirmationsThreshold { get; set; }
[JsonProperty("timestamp")] public long Timestamp { get; set; }
[JsonProperty("txid")] public string Txid { get; set; }
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
}
}
}

View File

@@ -1,19 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.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; }
}
}

View File

@@ -1,35 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class GetTransfersResponse
{
[JsonProperty("in")] public List<GetTransfersResponseItem> In { get; set; }
[JsonProperty("out")] public List<GetTransfersResponseItem> Out { get; set; }
[JsonProperty("pending")] public List<GetTransfersResponseItem> Pending { get; set; }
[JsonProperty("failed")] public List<GetTransfersResponseItem> Failed { get; set; }
[JsonProperty("pool")] public List<GetTransfersResponseItem> Pool { get; set; }
public partial class GetTransfersResponseItem
{
[JsonProperty("address")] public string Address { get; set; }
[JsonProperty("amount")] public long Amount { get; set; }
[JsonProperty("confirmations")] public long Confirmations { get; set; }
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("note")] public string Note { get; set; }
[JsonProperty("payment_id")] public string PaymentId { get; set; }
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
[JsonProperty("suggested_confirmations_threshold")]
public long SuggestedConfirmationsThreshold { get; set; }
[JsonProperty("timestamp")] public long Timestamp { get; set; }
[JsonProperty("txid")] public string Txid { get; set; }
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
}
}
}

View File

@@ -1,33 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.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; }
}
}

View File

@@ -1,13 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.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; }
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class MakeUriResponse
{
[JsonProperty("uri")] public string Uri { get; set; }
}
}

View File

@@ -1,40 +0,0 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.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();
}
}

View File

@@ -1,9 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class Peer
{
[JsonProperty("info")] public Info Info { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
{
public partial class SubaddrIndex
{
[JsonProperty("major")] public long Major { get; set; }
[JsonProperty("minor")] public long Minor { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.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; }
}
}

View File

@@ -1,13 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.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; }
}
}

View File

@@ -1,20 +0,0 @@
using System.Globalization;
namespace BTCPayServer.Services.Altcoins.Zcash.Utils
{
public class ZcashMoney
{
public static decimal Convert(long zat)
{
var amt = zat.ToString(CultureInfo.InvariantCulture).PadLeft(8, '0');
amt = amt.Length == 8 ? $"0.{amt}" : amt.Insert(amt.Length - 8, ".");
return decimal.Parse(amt, CultureInfo.InvariantCulture);
}
public static long Convert(decimal Zcash)
{
return System.Convert.ToInt64(Zcash * 100000000);
}
}
}

View File

@@ -1,7 +0,0 @@
namespace BTCPayServer.Plugins.Altcoins;
public class ZcashLikeSpecificBtcPayNetwork : BTCPayNetworkBase
{
public int MaxTrackedConfirmation = 10;
public string UriScheme { get; set; }
}

View File

@@ -181,8 +181,7 @@
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer",
"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": "/path/to/monero_wallet"
"BTCPAY_XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082"
},
"applicationUrl": "https://localhost:14142/"
},
@@ -221,10 +220,7 @@
"BTCPAY_DOCKERDEPLOYMENT": "true",
"BTCPAY_RECOMMENDED-PLUGINS": "",
"BTCPAY_CHEATMODE": "true",
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer",
"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": "/path/to/monero_wallet"
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
},
"applicationUrl": "https://localhost:14142/"
}

View File

@@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
namespace BTCPayServer.Services.Altcoins.Monero.Configuration
{
public class MoneroLikeConfiguration
{
public Dictionary<string, MoneroLikeConfigurationItem> MoneroLikeConfigurationItems { get; set; } =
new Dictionary<string, MoneroLikeConfigurationItem>();
}
public class MoneroLikeConfigurationItem
{
public Uri DaemonRpcUri { get; set; }
public Uri InternalWalletRpcUri { get; set; }
public string WalletDirectory { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@@ -1,52 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Altcoins.Monero.Services;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Altcoins.Monero.Payments
{
public class MoneroCheckoutModelExtension : ICheckoutModelExtension
{
private readonly BTCPayNetworkBase _network;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IPaymentLinkExtension paymentLinkExtension;
public MoneroCheckoutModelExtension(
PaymentMethodId paymentMethodId,
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
BTCPayNetworkBase network,
PaymentMethodHandlerDictionary handlers)
{
PaymentMethodId = paymentMethodId;
_network = network;
_handlers = handlers;
paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
}
public PaymentMethodId PaymentMethodId { get; }
public string Image => _network.CryptoImagePath;
public string Badge => "";
public void ModifyCheckoutModel(CheckoutModelContext context)
{
if (context is not { Handler: MoneroLikePaymentMethodHandler handler })
return;
context.Model.CheckoutBodyComponentName = BitcoinCheckoutModelExtension.CheckoutBodyComponentName;
var details = context.InvoiceEntity.GetPayments(true)
.Select(p => p.GetDetails<MoneroLikePaymentData>(handler))
.Where(p => p is not null)
.FirstOrDefault();
if (details is not null)
{
context.Model.ReceivedConfirmations = details.ConfirmationCount;
context.Model.RequiredConfirmations = (int)MoneroListener.ConfirmationsRequired(details, context.InvoiceEntity.SpeedPolicy);
}
context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper);
context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl;
}
}
}

View File

@@ -1,11 +0,0 @@
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Altcoins.Monero.Payments
{
public class MoneroLikeOnChainPaymentMethodDetails
{
public long AccountIndex { get; set; }
public long AddressIndex { get; set; }
public long? InvoiceSettledConfirmationThreshold { get; set; }
}
}

View File

@@ -1,20 +0,0 @@
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Services.Altcoins.Monero.Utils;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Altcoins.Monero.Payments
{
public class MoneroLikePaymentData
{
public long SubaddressIndex { get; set; }
public long SubaccountIndex { get; set; }
public long BlockHeight { get; set; }
public long ConfirmationCount { get; set; }
public string TransactionId { get; set; }
public long? InvoiceSettledConfirmationThreshold { get; set; }
public long LockTime { get; set; } = 0;
}
}

View File

@@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Dom;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Data;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Rating;
using BTCPayServer.Services.Altcoins.Monero.RPC.Models;
using BTCPayServer.Services.Altcoins.Monero.Services;
using BTCPayServer.Services.Altcoins.Monero.Utils;
using BTCPayServer.Services.Altcoins.Zcash.Payments;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Altcoins.Monero.Payments
{
public class MoneroLikePaymentMethodHandler : IPaymentMethodHandler
{
private readonly MoneroLikeSpecificBtcPayNetwork _network;
public MoneroLikeSpecificBtcPayNetwork Network => _network;
public JsonSerializer Serializer { get; }
private readonly MoneroRPCProvider _moneroRpcProvider;
public PaymentMethodId PaymentMethodId { get; }
public MoneroLikePaymentMethodHandler(MoneroLikeSpecificBtcPayNetwork network, MoneroRPCProvider moneroRpcProvider)
{
PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
_network = network;
Serializer = BlobSerializer.CreateSerializer().Serializer;
_moneroRpcProvider = moneroRpcProvider;
}
public Task BeforeFetchingRates(PaymentMethodContext context)
{
context.Prompt.Currency = _network.CryptoCode;
context.Prompt.Divisibility = _network.Divisibility;
if (context.Prompt.Activated)
{
var supportedPaymentMethod = ParsePaymentMethodConfig(context.PaymentMethodConfig);
var walletClient = _moneroRpcProvider.WalletRpcClients[_network.CryptoCode];
var daemonClient = _moneroRpcProvider.DaemonRpcClients[_network.CryptoCode];
context.State = new Prepare()
{
GetFeeRate = daemonClient.SendCommandAsync<GetFeeEstimateRequest, GetFeeEstimateResponse>("get_fee_estimate", new GetFeeEstimateRequest()),
ReserveAddress = s => walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>("create_address", new CreateAddressRequest() { Label = $"btcpay invoice #{s}", AccountIndex = supportedPaymentMethod.AccountIndex }),
AccountIndex = supportedPaymentMethod.AccountIndex
};
}
return Task.CompletedTask;
}
public async Task ConfigurePrompt(PaymentMethodContext context)
{
if (!_moneroRpcProvider.IsAvailable(_network.CryptoCode))
throw new PaymentMethodUnavailableException($"Node or wallet not available");
var invoice = context.InvoiceEntity;
Prepare moneroPrepare = (Prepare)context.State;
var feeRatePerKb = await moneroPrepare.GetFeeRate;
var address = await moneroPrepare.ReserveAddress(invoice.Id);
var feeRatePerByte = feeRatePerKb.Fee / 1024;
var details = new MoneroLikeOnChainPaymentMethodDetails()
{
AccountIndex = moneroPrepare.AccountIndex,
AddressIndex = address.AddressIndex,
InvoiceSettledConfirmationThreshold = ParsePaymentMethodConfig(context.PaymentMethodConfig).InvoiceSettledConfirmationThreshold
};
context.Prompt.Destination = address.Address;
context.Prompt.PaymentMethodFee = MoneroMoney.Convert(feeRatePerByte * 100);
context.Prompt.Details = JObject.FromObject(details, Serializer);
context.TrackedDestinations.Add(address.Address);
}
private MoneroPaymentPromptDetails ParsePaymentMethodConfig(JToken config)
{
return config.ToObject<MoneroPaymentPromptDetails>(Serializer) ?? throw new FormatException($"Invalid {nameof(MoneroLikePaymentMethodHandler)}");
}
object IPaymentMethodHandler.ParsePaymentMethodConfig(JToken config)
{
return ParsePaymentMethodConfig(config);
}
class Prepare
{
public Task<GetFeeEstimateResponse> GetFeeRate;
public Func<string, Task<CreateAddressResponse>> ReserveAddress;
public long AccountIndex { get; internal set; }
}
public MoneroLikeOnChainPaymentMethodDetails ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details)
{
return details.ToObject<MoneroLikeOnChainPaymentMethodDetails>(Serializer);
}
object IPaymentMethodHandler.ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details)
{
return ParsePaymentPromptDetails(details);
}
public MoneroLikePaymentData ParsePaymentDetails(JToken details)
{
return details.ToObject<MoneroLikePaymentData>(Serializer) ?? throw new FormatException($"Invalid {nameof(MoneroLikePaymentMethodHandler)}");
}
object IPaymentMethodHandler.ParsePaymentDetails(JToken details)
{
return ParsePaymentDetails(details);
}
}
}

View File

@@ -1,27 +0,0 @@
#nullable enable
using System.Globalization;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Services.Altcoins.Monero.Payments
{
public class MoneroPaymentLinkExtension : IPaymentLinkExtension
{
private readonly MoneroLikeSpecificBtcPayNetwork _network;
public MoneroPaymentLinkExtension(PaymentMethodId paymentMethodId, MoneroLikeSpecificBtcPayNetwork network)
{
PaymentMethodId = paymentMethodId;
_network = network;
}
public PaymentMethodId PaymentMethodId { get; }
public string? GetPaymentLink(PaymentPrompt prompt, IUrlHelper? urlHelper)
{
var due = prompt.Calculate().Due;
return $"{_network.UriScheme}:{prompt.Destination}?tx_amount={due.ToString(CultureInfo.InvariantCulture)}";
}
}
}

View File

@@ -1,11 +0,0 @@
using BTCPayServer.Payments;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Monero.Payments
{
public class MoneroPaymentPromptDetails
{
public long AccountIndex { get; set; }
public long? InvoiceSettledConfirmationThreshold { get; set; }
}
}

View File

@@ -1,38 +0,0 @@
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Services.Altcoins.Monero.RPC
{
[Route("[controller]")]
[OnlyIfSupportAttribute("XMR-CHAIN")]
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();
}
}
}

View File

@@ -1,15 +0,0 @@
namespace BTCPayServer.Services.Altcoins.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})";
}
}
}

View File

@@ -1,70 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services.Altcoins.Monero.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.Services.Altcoins.Monero.Services
{
public class MoneroLikeSummaryUpdaterHostedService : IHostedService
{
private readonly MoneroRPCProvider _MoneroRpcProvider;
private readonly MoneroLikeConfiguration _moneroLikeConfiguration;
public Logs Logs { get; }
private CancellationTokenSource _Cts;
public MoneroLikeSummaryUpdaterHostedService(MoneroRPCProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration, Logs logs)
{
_MoneroRpcProvider = moneroRpcProvider;
_moneroLikeConfiguration = moneroLikeConfiguration;
Logs = logs;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
foreach (var moneroLikeConfigurationItem in _moneroLikeConfiguration.MoneroLikeConfigurationItems)
{
_ = StartLoop(_Cts.Token, moneroLikeConfigurationItem.Key);
}
return Task.CompletedTask;
}
private async Task StartLoop(CancellationToken cancellation, string cryptoCode)
{
Logs.PayServer.LogInformation($"Starting listening Monero-like daemons ({cryptoCode})");
try
{
while (!cancellation.IsCancellationRequested)
{
try
{
await _MoneroRpcProvider.UpdateSummary(cryptoCode);
if (_MoneroRpcProvider.IsAvailable(cryptoCode))
{
await Task.Delay(TimeSpan.FromMinutes(1), cancellation);
}
else
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
}
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
Logs.PayServer.LogError(ex, $"Unhandled exception in Summary updater ({cryptoCode})");
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
}
}
}
catch when (cancellation.IsCancellationRequested) { }
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Cts?.Cancel();
return Task.CompletedTask;
}
}
}

View File

@@ -1,401 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Services.Altcoins.Monero.Configuration;
using BTCPayServer.Services.Altcoins.Monero.Payments;
using BTCPayServer.Services.Altcoins.Monero.RPC;
using BTCPayServer.Services.Altcoins.Monero.RPC.Models;
using BTCPayServer.Services.Altcoins.Monero.Utils;
using BTCPayServer.Services.Altcoins.Zcash.Utils;
using BTCPayServer.Services.Invoices;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Altcoins.Monero.Services
{
public class MoneroListener : EventHostedServiceBase
{
private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator;
private readonly MoneroRPCProvider _moneroRpcProvider;
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly ILogger<MoneroListener> _logger;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly InvoiceActivator _invoiceActivator;
private readonly PaymentService _paymentService;
public MoneroListener(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
MoneroRPCProvider moneroRpcProvider,
MoneroLikeConfiguration moneroLikeConfiguration,
BTCPayNetworkProvider networkProvider,
ILogger<MoneroListener> logger,
PaymentMethodHandlerDictionary handlers,
InvoiceActivator invoiceActivator,
PaymentService paymentService) : base(eventAggregator, logger)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
_moneroRpcProvider = moneroRpcProvider;
_MoneroLikeConfiguration = moneroLikeConfiguration;
_networkProvider = networkProvider;
_logger = logger;
_handlers = handlers;
_invoiceActivator = invoiceActivator;
_paymentService = paymentService;
}
protected override void SubscribeToEvents()
{
base.SubscribeToEvents();
Subscribe<MoneroEvent>();
Subscribe<MoneroRPCProvider.MoneroDaemonStateChange>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (evt is MoneroRPCProvider.MoneroDaemonStateChange stateChange)
{
if (_moneroRpcProvider.IsAvailable(stateChange.CryptoCode))
{
_logger.LogInformation($"{stateChange.CryptoCode} just became available");
_ = UpdateAnyPendingMoneroLikePayment(stateChange.CryptoCode);
}
else
{
_logger.LogInformation($"{stateChange.CryptoCode} just became unavailable");
}
}
else if (evt is MoneroEvent moneroEvent)
{
if (!_moneroRpcProvider.IsAvailable(moneroEvent.CryptoCode))
return;
if (!string.IsNullOrEmpty(moneroEvent.BlockHash))
{
await OnNewBlock(moneroEvent.CryptoCode);
}
if (!string.IsNullOrEmpty(moneroEvent.TransactionHash))
{
await OnTransactionUpdated(moneroEvent.CryptoCode, moneroEvent.TransactionHash);
}
}
}
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
{
_logger.LogInformation(
$"Invoice {invoice.Id} received payment {payment.Value} {payment.Currency} {payment.Id}");
var prompt = invoice.GetPaymentPrompt(payment.PaymentMethodId);
if (prompt != null &&
prompt.Activated &&
prompt.Destination == payment.Destination &&
prompt.Calculate().Due > 0.0m)
{
await _invoiceActivator.ActivateInvoicePaymentMethod(invoice.Id, payment.PaymentMethodId, true);
invoice = await _invoiceRepository.GetInvoice(invoice.Id);
}
_eventAggregator.Publish(
new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
}
private async Task UpdatePaymentStates(string cryptoCode, InvoiceEntity[] invoices)
{
if (!invoices.Any())
{
return;
}
var moneroWalletRpcClient = _moneroRpcProvider.WalletRpcClients[cryptoCode];
var network = _networkProvider.GetNetwork(cryptoCode);
var paymentId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
var handler = (MoneroLikePaymentMethodHandler)_handlers[paymentId];
//get all the required data in one list (invoice, its existing payments and the current payment method details)
var expandedInvoices = invoices.Select(entity => (Invoice: entity,
ExistingPayments: GetAllMoneroLikePayments(entity, cryptoCode),
Prompt: entity.GetPaymentPrompt(paymentId),
PaymentMethodDetails: handler.ParsePaymentPromptDetails(entity.GetPaymentPrompt(paymentId).Details)))
.Select(tuple => (
tuple.Invoice,
tuple.PaymentMethodDetails,
tuple.Prompt,
ExistingPayments: tuple.ExistingPayments.Select(entity =>
(Payment: entity, PaymentData: handler.ParsePaymentDetails(entity.Details),
tuple.Invoice))
));
var existingPaymentData = expandedInvoices.SelectMany(tuple => tuple.ExistingPayments);
var accountToAddressQuery = new Dictionary<long, List<long>>();
//create list of subaddresses to account to query the monero wallet
foreach (var expandedInvoice in expandedInvoices)
{
var addressIndexList =
accountToAddressQuery.GetValueOrDefault(expandedInvoice.PaymentMethodDetails.AccountIndex,
new List<long>());
addressIndexList.AddRange(
expandedInvoice.ExistingPayments.Select(tuple => tuple.PaymentData.SubaddressIndex));
addressIndexList.Add(expandedInvoice.PaymentMethodDetails.AddressIndex);
accountToAddressQuery.AddOrReplace(expandedInvoice.PaymentMethodDetails.AccountIndex, addressIndexList);
}
var tasks = accountToAddressQuery.ToDictionary(datas => datas.Key,
datas => moneroWalletRpcClient.SendCommandAsync<GetTransfersRequest, GetTransfersResponse>(
"get_transfers",
new GetTransfersRequest()
{
AccountIndex = datas.Key,
In = true,
SubaddrIndices = datas.Value.Distinct().ToList()
}));
await Task.WhenAll(tasks.Values);
var transferProcessingTasks = new List<Task>();
var updatedPaymentEntities = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
foreach (var keyValuePair in tasks)
{
var transfers = keyValuePair.Value.Result.In;
if (transfers == null)
{
continue;
}
transferProcessingTasks.AddRange(transfers.Select(transfer =>
{
InvoiceEntity invoice = null;
var existingMatch = existingPaymentData.SingleOrDefault(tuple =>
tuple.Payment.Destination == transfer.Address &&
tuple.PaymentData.TransactionId == transfer.Txid);
if (existingMatch.Invoice != null)
{
invoice = existingMatch.Invoice;
}
else
{
var newMatch = expandedInvoices.SingleOrDefault(tuple =>
tuple.Prompt.Destination == transfer.Address);
if (newMatch.Invoice == null)
{
return Task.CompletedTask;
}
invoice = newMatch.Invoice;
}
return HandlePaymentData(cryptoCode, transfer.Address, transfer.Amount, transfer.SubaddrIndex.Major,
transfer.SubaddrIndex.Minor, transfer.Txid, transfer.Confirmations, transfer.Height, transfer.UnlockTime,invoice,
updatedPaymentEntities);
}));
}
transferProcessingTasks.Add(
_paymentService.UpdatePayments(updatedPaymentEntities.Select(tuple => tuple.Item1).ToList()));
await Task.WhenAll(transferProcessingTasks);
foreach (var valueTuples in updatedPaymentEntities.GroupBy(entity => entity.Item2))
{
if (valueTuples.Any())
{
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id));
}
}
}
private async Task OnNewBlock(string cryptoCode)
{
await UpdateAnyPendingMoneroLikePayment(cryptoCode);
_eventAggregator.Publish(new NewBlockEvent() { PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode) });
}
private async Task OnTransactionUpdated(string cryptoCode, string transactionHash)
{
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
var transfer = await GetTransferByTxId(cryptoCode, transactionHash, this.CancellationToken);
var paymentsToUpdate = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
//group all destinations of the tx together and loop through the sets
foreach (var destination in transfer.Transfers.GroupBy(destination => destination.Address))
{
//find the invoice corresponding to this address, else skip
var invoice = await _invoiceRepository.GetInvoiceFromAddress(paymentMethodId, destination.Key);
if (invoice == null)
continue;
var index = destination.First().SubaddrIndex;
await HandlePaymentData(cryptoCode,
destination.Key,
destination.Sum(destination1 => destination1.Amount),
index.Major,
index.Minor,
transfer.Transfer.Txid,
transfer.Transfer.Confirmations,
transfer.Transfer.Height
, transfer.Transfer.UnlockTime,invoice, paymentsToUpdate);
}
if (paymentsToUpdate.Any())
{
await _paymentService.UpdatePayments(paymentsToUpdate.Select(tuple => tuple.Payment).ToList());
foreach (var valueTuples in paymentsToUpdate.GroupBy(entity => entity.invoice))
{
if (valueTuples.Any())
{
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id));
}
}
}
}
private async Task<GetTransferByTransactionIdResponse> GetTransferByTxId(string cryptoCode,
string transactionHash, CancellationToken cancellationToken)
{
var accounts = await _moneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<GetAccountsRequest, GetAccountsResponse>("get_accounts", new GetAccountsRequest(), cancellationToken);
var accountIndexes = accounts
.SubaddressAccounts
.Select(a => new long?(a.AccountIndex))
.ToList();
if (accountIndexes.Count is 0)
accountIndexes.Add(null);
var req = accountIndexes
.Select(i => GetTransferByTxId(cryptoCode, transactionHash, i))
.ToArray();
foreach (var task in req)
{
var result = await task;
if (result != null)
return result;
}
return null;
}
private async Task<GetTransferByTransactionIdResponse> GetTransferByTxId(string cryptoCode, string transactionHash, long? accountIndex)
{
try
{
var result = await _moneroRpcProvider.WalletRpcClients[cryptoCode]
.SendCommandAsync<GetTransferByTransactionIdRequest, GetTransferByTransactionIdResponse>(
"get_transfer_by_txid",
new GetTransferByTransactionIdRequest()
{
TransactionId = transactionHash,
AccountIndex = accountIndex
});
return result;
}
catch (JsonRpcClient.JsonRpcApiException)
{
return null;
}
}
private async Task HandlePaymentData(string cryptoCode, string address, long totalAmount, long subaccountIndex,
long subaddressIndex,
string txId, long confirmations, long blockHeight, long locktime, InvoiceEntity invoice,
List<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate)
{
var network = _networkProvider.GetNetwork(cryptoCode);
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
var handler = (MoneroLikePaymentMethodHandler)_handlers[pmi];
var promptDetails = handler.ParsePaymentPromptDetails(invoice.GetPaymentPrompt(pmi).Details);
var details = new MoneroLikePaymentData()
{
SubaccountIndex = subaccountIndex,
SubaddressIndex = subaddressIndex,
TransactionId = txId,
ConfirmationCount = confirmations,
BlockHeight = blockHeight,
LockTime = locktime,
InvoiceSettledConfirmationThreshold = promptDetails.InvoiceSettledConfirmationThreshold
};
var status = GetStatus(details, invoice.SpeedPolicy) ? PaymentStatus.Settled : PaymentStatus.Processing;
var paymentData = new Data.PaymentData()
{
Status = status,
Amount = MoneroMoney.Convert(totalAmount),
Created = DateTimeOffset.UtcNow,
Id = $"{txId}#{subaccountIndex}#{subaddressIndex}",
Currency = network.CryptoCode,
InvoiceDataId = invoice.Id,
}.Set(invoice, handler, details);
//check if this tx exists as a payment to this invoice already
var alreadyExistingPaymentThatMatches = GetAllMoneroLikePayments(invoice, cryptoCode)
.SingleOrDefault(c => c.Id == paymentData.Id && c.PaymentMethodId == pmi);
//if it doesnt, add it and assign a new monerolike address to the system if a balance is still due
if (alreadyExistingPaymentThatMatches == null)
{
var payment = await _paymentService.AddPayment(paymentData, [txId]);
if (payment != null)
await ReceivedPayment(invoice, payment);
}
else
{
//else update it with the new data
alreadyExistingPaymentThatMatches.Status = status;
alreadyExistingPaymentThatMatches.Details = JToken.FromObject(details, handler.Serializer);
paymentsToUpdate.Add((alreadyExistingPaymentThatMatches, invoice));
}
}
private bool GetStatus(MoneroLikePaymentData details, SpeedPolicy speedPolicy)
=> ConfirmationsRequired(details, speedPolicy) <= details.ConfirmationCount;
public static long ConfirmationsRequired(MoneroLikePaymentData details, SpeedPolicy speedPolicy)
=> (details, speedPolicy) switch
{
(_, _) when details.ConfirmationCount < details.LockTime => details.LockTime - details.ConfirmationCount,
({ InvoiceSettledConfirmationThreshold: long v }, _) => v,
(_, SpeedPolicy.HighSpeed) => 0,
(_, SpeedPolicy.MediumSpeed) => 1,
(_, SpeedPolicy.LowMediumSpeed) => 2,
(_, SpeedPolicy.LowSpeed) => 6,
_ => 6,
};
private async Task UpdateAnyPendingMoneroLikePayment(string cryptoCode)
{
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
var invoices = await _invoiceRepository.GetMonitoredInvoices(paymentMethodId);
if (!invoices.Any())
return;
invoices = invoices.Where(entity => entity.GetPaymentPrompt(paymentMethodId)?.Activated is true).ToArray();
await UpdatePaymentStates(cryptoCode, invoices);
}
private IEnumerable<PaymentEntity> GetAllMoneroLikePayments(InvoiceEntity invoice, string cryptoCode)
{
return invoice.GetPayments(false)
.Where(p => p.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode));
}
}
}

View File

@@ -1,118 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Services.Altcoins.Monero.Configuration;
using BTCPayServer.Services.Altcoins.Monero.RPC;
using BTCPayServer.Services.Altcoins.Monero.RPC.Models;
using NBitcoin;
namespace BTCPayServer.Services.Altcoins.Monero.Services
{
public class MoneroRPCProvider
{
private readonly MoneroLikeConfiguration _moneroLikeConfiguration;
private readonly EventAggregator _eventAggregator;
public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients;
public ImmutableDictionary<string, JsonRpcClient> WalletRpcClients;
private readonly ConcurrentDictionary<string, MoneroLikeSummary> _summaries =
new ConcurrentDictionary<string, MoneroLikeSummary>();
public ConcurrentDictionary<string, MoneroLikeSummary> Summaries => _summaries;
public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration, EventAggregator eventAggregator, IHttpClientFactory httpClientFactory)
{
_moneroLikeConfiguration = moneroLikeConfiguration;
_eventAggregator = eventAggregator;
DaemonRpcClients =
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
pair => new JsonRpcClient(pair.Value.DaemonRpcUri, pair.Value.Username, pair.Value.Password, httpClientFactory.CreateClient($"{pair.Key}client")));
WalletRpcClients =
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "", httpClientFactory.CreateClient($"{pair.Key}client")));
}
public bool IsAvailable(string cryptoCode)
{
cryptoCode = cryptoCode.ToUpperInvariant();
return _summaries.ContainsKey(cryptoCode) && IsAvailable(_summaries[cryptoCode]);
}
private bool IsAvailable(MoneroLikeSummary summary)
{
return summary.Synced &&
summary.WalletAvailable;
}
public async Task<MoneroLikeSummary> UpdateSummary(string cryptoCode)
{
if (!DaemonRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var daemonRpcClient) ||
!WalletRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var walletRpcClient))
{
return null;
}
var summary = new MoneroLikeSummary();
try
{
var daemonResult =
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, GetInfoResponse>("get_info",
JsonRpcClient.NoRequestModel.Instance);
summary.TargetHeight = daemonResult.TargetHeight.GetValueOrDefault(0);
summary.CurrentHeight = daemonResult.Height;
summary.TargetHeight = summary.TargetHeight == 0 ? summary.CurrentHeight : summary.TargetHeight;
summary.Synced = !daemonResult.BusySyncing;
summary.UpdatedAt = DateTime.UtcNow;
summary.DaemonAvailable = true;
}
catch
{
summary.DaemonAvailable = false;
}
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; }
}
}
}

View File

@@ -1,45 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Altcoins.Monero.Services
{
public class MoneroSyncSummaryProvider : ISyncSummaryProvider
{
private readonly MoneroRPCProvider _moneroRpcProvider;
public MoneroSyncSummaryProvider(MoneroRPCProvider moneroRpcProvider)
{
_moneroRpcProvider = moneroRpcProvider;
}
public bool AllAvailable()
{
return _moneroRpcProvider.Summaries.All(pair => pair.Value.WalletAvailable);
}
public string Partial { get; } = "Monero/MoneroSyncSummary";
public IEnumerable<ISyncStatus> GetStatuses()
{
return _moneroRpcProvider.Summaries.Select(pair => new MoneroSyncStatus()
{
Summary = pair.Value, PaymentMethodId = PaymentMethodId.Parse(pair.Key).ToString()
});
}
}
public class MoneroSyncStatus: SyncStatus, ISyncStatus
{
public override bool Available
{
get
{
return Summary?.WalletAvailable ?? false;
}
}
public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; }
}
}

View File

@@ -1,393 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Payments;
using BTCPayServer.Services.Altcoins.Monero.Configuration;
using BTCPayServer.Services.Altcoins.Monero.Payments;
using BTCPayServer.Services.Altcoins.Monero.RPC.Models;
using BTCPayServer.Services.Altcoins.Monero.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Services.Altcoins.Monero.UI
{
[Route("stores/{storeId}/monerolike")]
[OnlyIfSupportAttribute("XMR-CHAIN")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class UIMoneroLikeStoreController : Controller
{
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
private readonly StoreRepository _StoreRepository;
private readonly MoneroRPCProvider _MoneroRpcProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private IStringLocalizer StringLocalizer { get; }
public UIMoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration,
StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider,
PaymentMethodHandlerDictionary handlers,
IStringLocalizer stringLocalizer)
{
_MoneroLikeConfiguration = moneroLikeConfiguration;
_StoreRepository = storeRepository;
_MoneroRpcProvider = moneroRpcProvider;
_handlers = handlers;
StringLocalizer = stringLocalizer;
}
public StoreData StoreData => HttpContext.GetStoreData();
[HttpGet()]
public async Task<IActionResult> GetStoreMoneroLikePaymentMethods()
{
return View(await GetVM(StoreData));
}
[NonAction]
public async Task<MoneroLikePaymentMethodListViewModel> GetVM(StoreData storeData)
{
var excludeFilters = storeData.GetStoreBlob().GetExcludedPaymentMethods();
var accountsList = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.ToDictionary(pair => pair.Key,
pair => GetAccounts(pair.Key));
await Task.WhenAll(accountsList.Values);
return new MoneroLikePaymentMethodListViewModel()
{
Items = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.Select(pair =>
GetMoneroLikePaymentMethodViewModel(storeData, pair.Key, excludeFilters,
accountsList[pair.Key].Result))
};
}
private Task<GetAccountsResponse> GetAccounts(string cryptoCode)
{
try
{
if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary) && summary.WalletAvailable)
{
return _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<GetAccountsRequest, GetAccountsResponse>("get_accounts", new GetAccountsRequest());
}
}
catch { }
return Task.FromResult<GetAccountsResponse>(null);
}
private MoneroLikePaymentMethodViewModel GetMoneroLikePaymentMethodViewModel(
StoreData storeData, string cryptoCode,
IPaymentFilter excludeFilters, GetAccountsResponse accountsResponse)
{
var monero = storeData.GetPaymentMethodConfigs(_handlers)
.Where(s => s.Value is MoneroPaymentPromptDetails)
.Select(s => (PaymentMethodId: s.Key, Details: (MoneroPaymentPromptDetails)s.Value));
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
var settings = monero.Where(method => method.PaymentMethodId == pmi).Select(m => m.Details).SingleOrDefault();
_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary);
_MoneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode,
out var configurationItem);
var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet");
var accounts = accountsResponse?.SubaddressAccounts?.Select(account =>
new SelectListItem(
$"{account.AccountIndex} - {(string.IsNullOrEmpty(account.Label) ? "No label" : account.Label)}",
account.AccountIndex.ToString(CultureInfo.InvariantCulture)));
var settlementThresholdChoice = MoneroLikeSettlementThresholdChoice.StoreSpeedPolicy;
if (settings != null && settings.InvoiceSettledConfirmationThreshold is { } confirmations)
{
settlementThresholdChoice = confirmations switch
{
0 => MoneroLikeSettlementThresholdChoice.ZeroConfirmation,
1 => MoneroLikeSettlementThresholdChoice.AtLeastOne,
10 => MoneroLikeSettlementThresholdChoice.AtLeastTen,
_ => MoneroLikeSettlementThresholdChoice.Custom
};
}
return new MoneroLikePaymentMethodViewModel()
{
WalletFileFound = System.IO.File.Exists(fileAddress),
Enabled =
settings != null &&
!excludeFilters.Match(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)),
Summary = summary,
CryptoCode = cryptoCode,
AccountIndex = settings?.AccountIndex ?? accountsResponse?.SubaddressAccounts?.FirstOrDefault()?.AccountIndex ?? 0,
Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value),
nameof(SelectListItem.Text)),
SettlementConfirmationThresholdChoice = settlementThresholdChoice,
CustomSettlementConfirmationThreshold =
settings != null &&
settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
? settings.InvoiceSettledConfirmationThreshold
: null
};
}
[HttpGet("{cryptoCode}")]
public async Task<IActionResult> GetStoreMoneroLikePaymentMethod(string cryptoCode)
{
cryptoCode = cryptoCode.ToUpperInvariant();
if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.ContainsKey(cryptoCode))
{
return NotFound();
}
var vm = GetMoneroLikePaymentMethodViewModel(StoreData, cryptoCode,
StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode));
return View(nameof(GetStoreMoneroLikePaymentMethod), vm);
}
[HttpPost("{cryptoCode}")]
[DisableRequestSizeLimit]
public async Task<IActionResult> GetStoreMoneroLikePaymentMethod(MoneroLikePaymentMethodViewModel viewModel, string command, string cryptoCode)
{
cryptoCode = cryptoCode.ToUpperInvariant();
if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode,
out var configurationItem))
{
return NotFound();
}
if (command == "add-account")
{
try
{
var newAccount = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<CreateAccountRequest, CreateAccountResponse>("create_account", new CreateAccountRequest()
{
Label = viewModel.NewAccountLabel
});
viewModel.AccountIndex = newAccount.AccountIndex;
}
catch (Exception)
{
ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not create a new account."]);
}
}
else if (command == "upload-wallet")
{
var valid = true;
if (viewModel.WalletFile == null)
{
ModelState.AddModelError(nameof(viewModel.WalletFile), StringLocalizer["Please select the view-only wallet file"]);
valid = false;
}
if (viewModel.WalletKeysFile == null)
{
ModelState.AddModelError(nameof(viewModel.WalletKeysFile), StringLocalizer["Please select the view-only wallet keys file"]);
valid = false;
}
if (valid)
{
if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary))
{
if (summary.WalletAvailable)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = StringLocalizer["There is already an active wallet configured for {0}. Replacing it would break any existing invoices!", cryptoCode].Value
});
return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod),
new { cryptoCode });
}
}
var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet");
using (var fileStream = new FileStream(fileAddress, FileMode.Create))
{
await viewModel.WalletFile.CopyToAsync(fileStream);
try
{
Exec($"chmod 666 {fileAddress}");
}
catch
{
}
}
fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet.keys");
using (var fileStream = new FileStream(fileAddress, FileMode.Create))
{
await viewModel.WalletKeysFile.CopyToAsync(fileStream);
try
{
Exec($"chmod 666 {fileAddress}");
}
catch
{
}
}
fileAddress = Path.Combine(configurationItem.WalletDirectory, "password");
using (var fileStream = new StreamWriter(fileAddress, false))
{
await fileStream.WriteAsync(viewModel.WalletPassword);
try
{
Exec($"chmod 666 {fileAddress}");
}
catch
{
}
}
try
{
var response = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<OpenWalletRequest, OpenWalletResponse>("open_wallet", new OpenWalletRequest
{
Filename = "wallet",
Password = viewModel.WalletPassword
});
if (response?.Error != null)
{
throw new Exception(response.Error.Message);
}
}
catch (Exception ex)
{
ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not open the wallet: {0}", ex.Message]);
return View(viewModel);
}
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Info,
Message = StringLocalizer["View-only wallet files uploaded. The wallet will soon become available."].Value
});
return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), new { cryptoCode });
}
}
if (!ModelState.IsValid)
{
var vm = GetMoneroLikePaymentMethodViewModel(StoreData, cryptoCode,
StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode));
vm.Enabled = viewModel.Enabled;
vm.NewAccountLabel = viewModel.NewAccountLabel;
vm.AccountIndex = viewModel.AccountIndex;
vm.SettlementConfirmationThresholdChoice = viewModel.SettlementConfirmationThresholdChoice;
vm.CustomSettlementConfirmationThreshold = viewModel.CustomSettlementConfirmationThreshold;
return View(vm);
}
var storeData = StoreData;
var blob = storeData.GetStoreBlob();
storeData.SetPaymentMethodConfig(_handlers[PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)], new MoneroPaymentPromptDetails()
{
AccountIndex = viewModel.AccountIndex,
InvoiceSettledConfirmationThreshold = viewModel.SettlementConfirmationThresholdChoice switch
{
MoneroLikeSettlementThresholdChoice.ZeroConfirmation => 0,
MoneroLikeSettlementThresholdChoice.AtLeastOne => 1,
MoneroLikeSettlementThresholdChoice.AtLeastTen => 10,
MoneroLikeSettlementThresholdChoice.Custom when viewModel.CustomSettlementConfirmationThreshold is { } custom => custom,
_ => null
}
});
blob.SetExcluded(PaymentTypes.CHAIN.GetPaymentMethodId(viewModel.CryptoCode), !viewModel.Enabled);
storeData.SetStoreBlob(blob);
await _StoreRepository.UpdateStore(storeData);
return RedirectToAction("GetStoreMoneroLikePaymentMethods",
new { StatusMessage = $"{cryptoCode} settings updated successfully", storeId = StoreData.Id });
}
private void Exec(string cmd)
{
var escapedArgs = cmd.Replace("\"", "\\\"", StringComparison.InvariantCulture);
var process = new Process
{
StartInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "/bin/sh",
Arguments = $"-c \"{escapedArgs}\""
}
};
#pragma warning disable CA1416 // Validate platform compatibility
process.Start();
#pragma warning restore CA1416 // Validate platform compatibility
process.WaitForExit();
}
public class MoneroLikePaymentMethodListViewModel
{
public IEnumerable<MoneroLikePaymentMethodViewModel> Items { get; set; }
}
public class MoneroLikePaymentMethodViewModel : IValidatableObject
{
public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; }
public string CryptoCode { get; set; }
public string NewAccountLabel { get; set; }
public long AccountIndex { get; set; }
public bool Enabled { get; set; }
public IEnumerable<SelectListItem> Accounts { get; set; }
public bool WalletFileFound { get; set; }
[Display(Name = "View-Only Wallet File")]
public IFormFile WalletFile { get; set; }
[Display(Name = "Wallet Keys File")]
public IFormFile WalletKeysFile { get; set; }
[Display(Name = "Wallet Password")]
public string WalletPassword { get; set; }
[Display(Name = "Consider the invoice settled when the payment transaction …")]
public MoneroLikeSettlementThresholdChoice SettlementConfirmationThresholdChoice { get; set; }
[Display(Name = "Required Confirmations"), Range(0, 100)]
public long? CustomSettlementConfirmationThreshold { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (SettlementConfirmationThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
&& CustomSettlementConfirmationThreshold is null)
{
yield return new ValidationResult(
"You must specify the number of required confirmations when using a custom threshold.",
new[] { nameof(CustomSettlementConfirmationThreshold) });
}
}
}
public enum MoneroLikeSettlementThresholdChoice
{
[Display(Name = "Store Speed Policy", Description = "Use the store's speed policy")]
StoreSpeedPolicy,
[Display(Name = "Zero Confirmation", Description = "Is unconfirmed")]
ZeroConfirmation,
[Display(Name = "At Least One", Description = "Has at least 1 confirmation")]
AtLeastOne,
[Display(Name = "At Least Ten", Description = "Has at least 10 confirmations")]
AtLeastTen,
[Display(Name = "Custom", Description = "Custom")]
Custom
}
}
}

View File

@@ -1,17 +0,0 @@
using System;
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Altcoins.Monero.UI
{
public class MoneroPaymentViewModel
{
public PaymentMethodId PaymentMethodId { get; set; }
public string Confirmations { get; set; }
public string DepositAddress { get; set; }
public string Amount { get; set; }
public string TransactionId { get; set; }
public DateTimeOffset ReceivedTime { get; set; }
public string TransactionLink { get; set; }
public string Currency { get; set; }
}
}

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
namespace BTCPayServer.Services.Altcoins.Zcash.Configuration
{
public class ZcashLikeConfiguration
{
public Dictionary<string, ZcashLikeConfigurationItem> ZcashLikeConfigurationItems { get; set; } =
new Dictionary<string, ZcashLikeConfigurationItem>();
}
public class ZcashLikeConfigurationItem
{
public Uri DaemonRpcUri { get; set; }
public Uri InternalWalletRpcUri { get; set; }
public string WalletDirectory { get; set; }
}
}

View File

@@ -1,51 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Altcoins.Monero.Payments;
using BTCPayServer.Services.Altcoins.Zcash.Services;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Altcoins.Zcash.Payments
{
public class ZcashCheckoutModelExtension : ICheckoutModelExtension
{
private readonly BTCPayNetworkBase _network;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IPaymentLinkExtension paymentLinkExtension;
public ZcashCheckoutModelExtension(
PaymentMethodId paymentMethodId,
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
BTCPayNetworkBase network,
PaymentMethodHandlerDictionary handlers)
{
PaymentMethodId = paymentMethodId;
_network = network;
_handlers = handlers;
paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
}
public PaymentMethodId PaymentMethodId { get; }
public string Image => _network.CryptoImagePath;
public string Badge => "";
public void ModifyCheckoutModel(CheckoutModelContext context)
{
if (context is not { Handler: ZcashLikePaymentMethodHandler handler })
return;
context.Model.CheckoutBodyComponentName = BitcoinCheckoutModelExtension.CheckoutBodyComponentName;
var details = context.InvoiceEntity.GetPayments(true)
.Select(p => p.GetDetails<ZcashLikePaymentData>(handler))
.Where(p => p is not null)
.FirstOrDefault();
if (details is not null)
{
context.Model.ReceivedConfirmations = details.ConfirmationCount;
context.Model.RequiredConfirmations = (int)ZcashListener.ConfirmationsRequired(context.InvoiceEntity.SpeedPolicy);
}
context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper);
context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl;
}
}
}

View File

@@ -1,17 +0,0 @@
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Services.Altcoins.Zcash.Utils;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Altcoins.Zcash.Payments
{
public class ZcashLikePaymentData
{
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; }
}
}

View File

@@ -1,114 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Dom;
using BTCPayServer.Common;
using BTCPayServer.Data;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Rating;
using BTCPayServer.Services.Altcoins.Zcash.RPC.Models;
using BTCPayServer.Services.Altcoins.Zcash.Services;
using BTCPayServer.Services.Altcoins.Zcash.Utils;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Altcoins.Zcash.Payments
{
public class ZcashLikePaymentMethodHandler : IPaymentMethodHandler
{
private readonly ZcashLikeSpecificBtcPayNetwork _network;
public ZcashLikeSpecificBtcPayNetwork Network => _network;
public JsonSerializer Serializer { get; }
private readonly ZcashRPCProvider _ZcashRpcProvider;
public PaymentMethodId PaymentMethodId { get; }
public ZcashLikePaymentMethodHandler(BTCPayNetworkBase network, ZcashRPCProvider ZcashRpcProvider)
{
_network = (ZcashLikeSpecificBtcPayNetwork)network;
PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(_network.CryptoCode);
Serializer = BlobSerializer.CreateSerializer().Serializer;
_ZcashRpcProvider = ZcashRpcProvider;
}
public Task BeforeFetchingRates(PaymentMethodContext context)
{
context.Prompt.Currency = _network.CryptoCode;
context.Prompt.Divisibility = _network.Divisibility;
if (context.Prompt.Activated)
{
var walletClient = _ZcashRpcProvider.WalletRpcClients[_network.CryptoCode];
var daemonClient = _ZcashRpcProvider.DaemonRpcClients[_network.CryptoCode];
var config = ParsePaymentMethodConfig(context.PaymentMethodConfig);
context.State = new Prepare()
{
GetFeeRate = daemonClient.SendCommandAsync<GetFeeEstimateRequest, GetFeeEstimateResponse>("get_fee_estimate", new GetFeeEstimateRequest()),
ReserveAddress = s => walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>("create_address", new CreateAddressRequest() { Label = $"btcpay invoice #{s}", AccountIndex = config.AccountIndex }),
AccountIndex = config.AccountIndex
};
}
return Task.CompletedTask;
}
public async Task ConfigurePrompt(PaymentMethodContext context)
{
if (!_ZcashRpcProvider.IsAvailable(_network.CryptoCode))
throw new PaymentMethodUnavailableException($"Node or wallet not available");
var invoice = context.InvoiceEntity;
var ZcashPrepare = (Prepare)context.State;
var feeRatePerKb = await ZcashPrepare.GetFeeRate;
var address = await ZcashPrepare.ReserveAddress(invoice.Id);
var feeRatePerByte = feeRatePerKb.Fee / 1024;
context.Prompt.PaymentMethodFee = ZcashMoney.Convert(feeRatePerByte * 100);
context.Prompt.Details = JObject.FromObject(new ZcashPaymentPromptDetails()
{
AccountIndex = ZcashPrepare.AccountIndex,
AddressIndex = address.AddressIndex,
DepositAddress = address.Address
}, Serializer);
context.TrackedDestinations.Add(address.Address);
}
public ZcashPaymentPromptDetails ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details)
{
return details.ToObject<ZcashPaymentPromptDetails>(Serializer);
}
object IPaymentMethodHandler.ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details)
{
return ParsePaymentPromptDetails(details);
}
object IPaymentMethodHandler.ParsePaymentMethodConfig(JToken config)
{
return ParsePaymentMethodConfig(config);
}
public ZcashPaymentMethodConfig ParsePaymentMethodConfig(JToken config)
{
return config.ToObject<ZcashPaymentMethodConfig>(Serializer) ?? throw new FormatException($"Invalid {nameof(ZcashPaymentMethodConfig)}");
}
class Prepare
{
public Task<GetFeeEstimateResponse> GetFeeRate;
public Func<string, Task<CreateAddressResponse>> ReserveAddress;
public long AccountIndex { get; internal set; }
}
public ZcashLikePaymentData ParsePaymentDetails(JToken details)
{
return details.ToObject<ZcashLikePaymentData>(Serializer) ?? throw new FormatException($"Invalid {nameof(ZcashLikePaymentData)}");
}
object IPaymentMethodHandler.ParsePaymentDetails(JToken details)
{
return ParsePaymentDetails(details);
}
}
}

View File

@@ -1,27 +0,0 @@
#nullable enable
using System.Globalization;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Services.Altcoins.Zcash.Payments
{
public class ZcashPaymentLinkExtension : IPaymentLinkExtension
{
private readonly ZcashLikeSpecificBtcPayNetwork _network;
public ZcashPaymentLinkExtension(PaymentMethodId paymentMethodId, ZcashLikeSpecificBtcPayNetwork network)
{
PaymentMethodId = paymentMethodId;
_network = network;
}
public PaymentMethodId PaymentMethodId { get; }
public string? GetPaymentLink(PaymentPrompt prompt, IUrlHelper? urlHelper)
{
var due = prompt.Calculate().Due;
return $"{_network.UriScheme}:{prompt.Destination}?amount={due.ToString(CultureInfo.InvariantCulture)}";
}
}
}

View File

@@ -1,10 +0,0 @@
using BTCPayServer.Payments;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Altcoins.Zcash.Payments
{
public class ZcashPaymentMethodConfig
{
public long AccountIndex { get; set; }
}
}

View File

@@ -1,11 +0,0 @@
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Altcoins.Zcash.Payments
{
public class ZcashPaymentPromptDetails
{
public long AccountIndex { get; set; }
public long AddressIndex { get; set; }
public string DepositAddress { get; set; }
}
}

View File

@@ -1,38 +0,0 @@
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Services.Altcoins.Zcash.RPC
{
[Route("[controller]")]
[OnlyIfSupportAttribute("ZEC-CHAIN")]
public class ZcashLikeDaemonCallbackController : Controller
{
private readonly EventAggregator _eventAggregator;
public ZcashLikeDaemonCallbackController(EventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
}
[HttpGet("block")]
public IActionResult OnBlockNotify(string hash, string cryptoCode)
{
_eventAggregator.Publish(new ZcashEvent()
{
BlockHash = hash,
CryptoCode = cryptoCode.ToUpperInvariant()
});
return Ok();
}
[HttpGet("tx")]
public IActionResult OnTransactionNotify(string hash, string cryptoCode)
{
_eventAggregator.Publish(new ZcashEvent()
{
TransactionHash = hash,
CryptoCode = cryptoCode.ToUpperInvariant()
});
return Ok();
}
}
}

View File

@@ -1,15 +0,0 @@
namespace BTCPayServer.Services.Altcoins.Zcash.RPC
{
public class ZcashEvent
{
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})";
}
}
}

View File

@@ -1,71 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services.Altcoins.Zcash.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.Services.Altcoins.Zcash.Services
{
public class ZcashLikeSummaryUpdaterHostedService : IHostedService
{
private readonly ZcashRPCProvider _ZcashRpcProvider;
private readonly ZcashLikeConfiguration _ZcashLikeConfiguration;
private CancellationTokenSource _Cts;
public Logs Logs { get; }
public ZcashLikeSummaryUpdaterHostedService(ZcashRPCProvider ZcashRpcProvider, ZcashLikeConfiguration ZcashLikeConfiguration, Logs logs)
{
_ZcashRpcProvider = ZcashRpcProvider;
_ZcashLikeConfiguration = ZcashLikeConfiguration;
Logs = logs;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
foreach (var ZcashLikeConfigurationItem in _ZcashLikeConfiguration.ZcashLikeConfigurationItems)
{
_ = StartLoop(_Cts.Token, ZcashLikeConfigurationItem.Key);
}
return Task.CompletedTask;
}
private async Task StartLoop(CancellationToken cancellation, string cryptoCode)
{
Logs.PayServer.LogInformation($"Starting listening Zcash-like daemons ({cryptoCode})");
try
{
while (!cancellation.IsCancellationRequested)
{
try
{
await _ZcashRpcProvider.UpdateSummary(cryptoCode);
if (_ZcashRpcProvider.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;
}
}
}

View File

@@ -1,356 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Plugins.Altcoins;
using BTCPayServer.Services.Altcoins.Zcash.Configuration;
using BTCPayServer.Services.Altcoins.Zcash.Payments;
using BTCPayServer.Services.Altcoins.Zcash.RPC;
using BTCPayServer.Services.Altcoins.Zcash.RPC.Models;
using BTCPayServer.Services.Altcoins.Zcash.Utils;
using BTCPayServer.Services.Invoices;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
using Newtonsoft.Json.Linq;
using static BTCPayServer.Client.Models.InvoicePaymentMethodDataModel;
namespace BTCPayServer.Services.Altcoins.Zcash.Services
{
public class ZcashListener : EventHostedServiceBase
{
private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator;
private readonly ZcashRPCProvider _ZcashRpcProvider;
private readonly ZcashLikeConfiguration _ZcashLikeConfiguration;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly ILogger<ZcashListener> _logger;
private readonly PaymentService _paymentService;
private readonly InvoiceActivator _invoiceActivator;
private readonly PaymentMethodHandlerDictionary _handlers;
public ZcashListener(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
ZcashRPCProvider ZcashRpcProvider,
ZcashLikeConfiguration ZcashLikeConfiguration,
BTCPayNetworkProvider networkProvider,
ILogger<ZcashListener> logger,
PaymentService paymentService,
InvoiceActivator invoiceActivator,
PaymentMethodHandlerDictionary handlers) : base(eventAggregator, logger)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
_ZcashRpcProvider = ZcashRpcProvider;
_ZcashLikeConfiguration = ZcashLikeConfiguration;
_networkProvider = networkProvider;
_logger = logger;
_paymentService = paymentService;
_invoiceActivator = invoiceActivator;
_handlers = handlers;
}
protected override void SubscribeToEvents()
{
base.SubscribeToEvents();
Subscribe<ZcashEvent>();
Subscribe<ZcashRPCProvider.ZcashDaemonStateChange>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (evt is ZcashRPCProvider.ZcashDaemonStateChange stateChanged)
{
if (_ZcashRpcProvider.IsAvailable(stateChanged.CryptoCode))
{
_logger.LogInformation($"{stateChanged.CryptoCode} just became available");
_ = UpdateAnyPendingZcashLikePayment(stateChanged.CryptoCode);
}
else
{
_logger.LogInformation($"{stateChanged.CryptoCode} just became unavailable");
}
}
else if (evt is ZcashEvent zcashEvent)
{
if (!_ZcashRpcProvider.IsAvailable(zcashEvent.CryptoCode))
return;
if (!string.IsNullOrEmpty(zcashEvent.BlockHash))
{
await OnNewBlock(zcashEvent.CryptoCode);
}
if (!string.IsNullOrEmpty(zcashEvent.TransactionHash))
{
await OnTransactionUpdated(zcashEvent.CryptoCode, zcashEvent.TransactionHash);
}
}
}
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
{
_logger.LogInformation(
$"Invoice {invoice.Id} received payment {payment.Value} {payment.Currency} {payment.Id}");
var prompt = invoice.GetPaymentPrompt(payment.PaymentMethodId);
if (prompt != null &&
prompt.Activated &&
prompt.Destination == payment.Destination &&
prompt.Calculate().Due > 0.0m)
{
await _invoiceActivator.ActivateInvoicePaymentMethod(invoice.Id, payment.PaymentMethodId, true);
invoice = await _invoiceRepository.GetInvoice(invoice.Id);
}
_eventAggregator.Publish(
new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
}
private async Task UpdatePaymentStates(string cryptoCode, InvoiceEntity[] invoices)
{
if (!invoices.Any())
{
return;
}
var ZcashWalletRpcClient = _ZcashRpcProvider.WalletRpcClients[cryptoCode];
var network = _networkProvider.GetNetwork(cryptoCode);
var paymentId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
var handler = (ZcashLikePaymentMethodHandler)_handlers[paymentId];
//get all the required data in one list (invoice, its existing payments and the current payment method details)
var expandedInvoices = invoices.Select(entity => (Invoice: entity,
ExistingPayments: GetAllZcashLikePayments(entity, cryptoCode),
Prompt: entity.GetPaymentPrompt(paymentId),
PaymentMethodDetails: handler.ParsePaymentPromptDetails(entity.GetPaymentPrompt(paymentId).Details)))
.Select(tuple => (
tuple.Invoice,
tuple.PaymentMethodDetails,
tuple.Prompt,
ExistingPayments: tuple.ExistingPayments.Select(entity =>
(Payment: entity, PaymentData: handler.ParsePaymentDetails(entity.Details),
tuple.Invoice))
));
var existingPaymentData = expandedInvoices.SelectMany(tuple => tuple.ExistingPayments);
var accountToAddressQuery = new Dictionary<long, List<long>>();
//create list of subaddresses to account to query the Zcash 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 => ZcashWalletRpcClient.SendCommandAsync<GetTransfersRequest, GetTransfersResponse>(
"get_transfers",
new GetTransfersRequest()
{
AccountIndex = datas.Key,
In = true,
SubaddrIndices = datas.Value.Distinct().ToList()
}));
await Task.WhenAll(tasks.Values);
var transferProcessingTasks = new List<Task>();
var updatedPaymentEntities = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
foreach (var keyValuePair in tasks)
{
var transfers = keyValuePair.Value.Result.In;
if (transfers == null)
{
continue;
}
transferProcessingTasks.AddRange(transfers.Select(transfer =>
{
InvoiceEntity invoice = null;
var existingMatch = existingPaymentData.SingleOrDefault(tuple =>
tuple.Payment.Destination == transfer.Address &&
tuple.PaymentData.TransactionId == transfer.Txid);
if (existingMatch.Invoice != null)
{
invoice = existingMatch.Invoice;
}
else
{
var newMatch = expandedInvoices.SingleOrDefault(tuple =>
tuple.Prompt.Destination == transfer.Address);
if (newMatch.Invoice == null)
{
return Task.CompletedTask;
}
invoice = newMatch.Invoice;
}
return HandlePaymentData(cryptoCode, transfer.Address, transfer.Amount, transfer.SubaddrIndex.Major,
transfer.SubaddrIndex.Minor, transfer.Txid, transfer.Confirmations, transfer.Height, invoice,
updatedPaymentEntities);
}));
}
transferProcessingTasks.Add(
_paymentService.UpdatePayments(updatedPaymentEntities.Select(tuple => tuple.Item1).ToList()));
await Task.WhenAll(transferProcessingTasks);
foreach (var valueTuples in updatedPaymentEntities.GroupBy(entity => entity.Item2))
{
if (valueTuples.Any())
{
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id));
}
}
}
private async Task OnNewBlock(string cryptoCode)
{
await UpdateAnyPendingZcashLikePayment(cryptoCode);
_eventAggregator.Publish(new NewBlockEvent() { PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode) });
}
private async Task OnTransactionUpdated(string cryptoCode, string transactionHash)
{
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
var transfer = await _ZcashRpcProvider.WalletRpcClients[cryptoCode]
.SendCommandAsync<GetTransferByTransactionIdRequest, GetTransferByTransactionIdResponse>(
"get_transfer_by_txid",
new GetTransferByTransactionIdRequest() { TransactionId = transactionHash });
var paymentsToUpdate = new List<(PaymentEntity Payment, InvoiceEntity invoice)>();
//group all destinations of the tx together and loop through the sets
foreach (var destination in transfer.Transfers.GroupBy(destination => destination.Address))
{
//find the invoice corresponding to this address, else skip
var invoice = await _invoiceRepository.GetInvoiceFromAddress(paymentMethodId, destination.Key);
if (invoice == null)
continue;
var index = destination.First().SubaddrIndex;
await HandlePaymentData(cryptoCode,
destination.Key,
destination.Sum(destination1 => destination1.Amount),
index.Major,
index.Minor,
transfer.Transfer.Txid,
transfer.Transfer.Confirmations,
transfer.Transfer.Height
, invoice, paymentsToUpdate);
}
if (paymentsToUpdate.Any())
{
await _paymentService.UpdatePayments(paymentsToUpdate.Select(tuple => tuple.Payment).ToList());
foreach (var valueTuples in paymentsToUpdate.GroupBy(entity => entity.invoice))
{
if (valueTuples.Any())
{
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id));
}
}
}
}
private async Task HandlePaymentData(string cryptoCode, string address, long totalAmount, long subaccountIndex,
long subaddressIndex,
string txId, long confirmations, long blockHeight, InvoiceEntity invoice,
List<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate)
{
var network = _networkProvider.GetNetwork(cryptoCode);
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
var handler = (ZcashLikePaymentMethodHandler)_handlers[pmi];
var details = new ZcashLikePaymentData()
{
SubaccountIndex = subaccountIndex,
SubaddressIndex = subaddressIndex,
TransactionId = txId,
ConfirmationCount = confirmations,
BlockHeight = blockHeight
};
var status = GetStatus(details, invoice.SpeedPolicy) ? PaymentStatus.Settled : PaymentStatus.Processing;
var paymentData = new Data.PaymentData()
{
Status = status,
Amount = ZcashMoney.Convert(totalAmount),
Created = DateTimeOffset.UtcNow,
Id = $"{txId}#{subaccountIndex}#{subaddressIndex}",
Currency = network.CryptoCode
}.Set(invoice, handler, details);
var alreadyExistingPaymentThatMatches = GetAllZcashLikePayments(invoice, cryptoCode)
.SingleOrDefault(c => c.Id == paymentData.Id && c.PaymentMethodId == pmi);
//if it doesnt, add it and assign a new Zcashlike address to the system if a balance is still due
if (alreadyExistingPaymentThatMatches == null)
{
var payment = await _paymentService.AddPayment(paymentData, [txId]);
if (payment != null)
await ReceivedPayment(invoice, payment);
}
else
{
//else update it with the new data
alreadyExistingPaymentThatMatches.Status = status;
alreadyExistingPaymentThatMatches.Details = JToken.FromObject(details, handler.Serializer);
paymentsToUpdate.Add((alreadyExistingPaymentThatMatches, invoice));
}
}
private bool GetStatus(ZcashLikePaymentData details, SpeedPolicy speedPolicy)
=> details.ConfirmationCount >= ConfirmationsRequired(speedPolicy);
public static int ConfirmationsRequired(SpeedPolicy speedPolicy)
=> speedPolicy switch
{
SpeedPolicy.HighSpeed => 0,
SpeedPolicy.MediumSpeed => 1,
SpeedPolicy.LowMediumSpeed => 2,
SpeedPolicy.LowSpeed => 6,
_ => 6,
};
private async Task UpdateAnyPendingZcashLikePayment(string cryptoCode)
{
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
var invoices = await _invoiceRepository.GetMonitoredInvoices(paymentMethodId);
if (!invoices.Any())
return;
invoices = invoices.Where(entity => entity.GetPaymentPrompt(paymentMethodId).Activated).ToArray();
await UpdatePaymentStates(cryptoCode, invoices);
}
private IEnumerable<PaymentEntity> GetAllZcashLikePayments(InvoiceEntity invoice, string cryptoCode)
{
return invoice.GetPayments(false)
.Where(p => p.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode));
}
}
}

View File

@@ -1,123 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Services.Altcoins.Zcash.Configuration;
using BTCPayServer.Services.Altcoins.Zcash.RPC;
using BTCPayServer.Services.Altcoins.Zcash.RPC.Models;
using NBitcoin;
namespace BTCPayServer.Services.Altcoins.Zcash.Services
{
public class ZcashRPCProvider
{
private readonly ZcashLikeConfiguration _ZcashLikeConfiguration;
private readonly EventAggregator _eventAggregator;
public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients;
public ImmutableDictionary<string, JsonRpcClient> WalletRpcClients;
private readonly ConcurrentDictionary<string, ZcashLikeSummary> _summaries =
new ConcurrentDictionary<string, ZcashLikeSummary>();
public ConcurrentDictionary<string, ZcashLikeSummary> Summaries => _summaries;
public ZcashRPCProvider(ZcashLikeConfiguration ZcashLikeConfiguration, EventAggregator eventAggregator, IHttpClientFactory httpClientFactory)
{
_ZcashLikeConfiguration = ZcashLikeConfiguration;
_eventAggregator = eventAggregator;
DaemonRpcClients =
_ZcashLikeConfiguration.ZcashLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
pair => new JsonRpcClient(pair.Value.DaemonRpcUri, "", "", httpClientFactory.CreateClient()));
WalletRpcClients =
_ZcashLikeConfiguration.ZcashLikeConfigurationItems.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(ZcashLikeSummary summary)
{
return summary.Synced &&
summary.WalletAvailable;
}
public async Task<ZcashLikeSummary> UpdateSummary(string cryptoCode)
{
if (!DaemonRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var daemonRpcClient) ||
!WalletRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var walletRpcClient))
{
return null;
}
var summary = new ZcashLikeSummary();
try
{
var daemonResult =
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, SyncInfoResponse>("sync_info",
JsonRpcClient.NoRequestModel.Instance);
summary.TargetHeight = daemonResult.TargetHeight.GetValueOrDefault(0);
summary.CurrentHeight = daemonResult.Height;
summary.TargetHeight = summary.TargetHeight == 0 ? summary.CurrentHeight : summary.TargetHeight;
summary.Synced = daemonResult.Height >= summary.TargetHeight && summary.CurrentHeight > 0;
summary.UpdatedAt = DateTime.Now;
summary.DaemonAvailable = true;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
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 ZcashDaemonStateChange() { Summary = summary, CryptoCode = cryptoCode });
}
return summary;
}
public class ZcashDaemonStateChange
{
public string CryptoCode { get; set; }
public ZcashLikeSummary Summary { get; set; }
}
public class ZcashLikeSummary
{
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; }
public override String ToString() { return String.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3} {4} {5}", Synced, CurrentHeight, TargetHeight, WalletHeight, DaemonAvailable, WalletAvailable); }
}
}
}

View File

@@ -1,45 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Altcoins.Zcash.Services
{
public class ZcashSyncSummaryProvider : ISyncSummaryProvider
{
private readonly ZcashRPCProvider _ZcashRpcProvider;
public ZcashSyncSummaryProvider(ZcashRPCProvider ZcashRpcProvider)
{
_ZcashRpcProvider = ZcashRpcProvider;
}
public bool AllAvailable()
{
return _ZcashRpcProvider.Summaries.All(pair => pair.Value.WalletAvailable);
}
public string Partial { get; } = "Zcash/ZcashSyncSummary";
public IEnumerable<ISyncStatus> GetStatuses()
{
return _ZcashRpcProvider.Summaries.Select(pair => new ZcashSyncStatus()
{
Summary = pair.Value, PaymentMethodId = PaymentMethodId.Parse(pair.Key).ToString()
});
}
}
public class ZcashSyncStatus: SyncStatus, ISyncStatus
{
public override bool Available
{
get
{
return Summary?.WalletAvailable ?? false;
}
}
public ZcashRPCProvider.ZcashLikeSummary Summary { get; set; }
}
}

View File

@@ -1,311 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Altcoins.Zcash.Configuration;
using BTCPayServer.Services.Altcoins.Zcash.Payments;
using BTCPayServer.Services.Altcoins.Zcash.RPC.Models;
using BTCPayServer.Services.Altcoins.Zcash.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Services.Altcoins.Zcash.UI
{
[Route("stores/{storeId}/Zcashlike")]
[OnlyIfSupportAttribute("ZEC-CHAIN")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class UIZcashLikeStoreController : Controller
{
private readonly ZcashLikeConfiguration _ZcashLikeConfiguration;
private readonly StoreRepository _StoreRepository;
private readonly ZcashRPCProvider _ZcashRpcProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private IStringLocalizer StringLocalizer { get; }
public UIZcashLikeStoreController(ZcashLikeConfiguration ZcashLikeConfiguration,
StoreRepository storeRepository, ZcashRPCProvider ZcashRpcProvider,
PaymentMethodHandlerDictionary handlers,
IStringLocalizer stringLocalizer)
{
_ZcashLikeConfiguration = ZcashLikeConfiguration;
_StoreRepository = storeRepository;
_ZcashRpcProvider = ZcashRpcProvider;
_handlers = handlers;
StringLocalizer = stringLocalizer;
}
public StoreData StoreData => HttpContext.GetStoreData();
[HttpGet()]
public async Task<IActionResult> GetStoreZcashLikePaymentMethods()
{
var Zcash = StoreData.GetPaymentMethodConfigs<ZcashPaymentMethodConfig>(_handlers);
var excludeFilters = StoreData.GetStoreBlob().GetExcludedPaymentMethods();
var accountsList = _ZcashLikeConfiguration.ZcashLikeConfigurationItems.ToDictionary(pair => pair.Key,
pair => GetAccounts(pair.Key));
await Task.WhenAll(accountsList.Values);
return View(new ZcashLikePaymentMethodListViewModel()
{
Items = _ZcashLikeConfiguration.ZcashLikeConfigurationItems.Select(pair =>
GetZcashLikePaymentMethodViewModel(StoreData, pair.Key, excludeFilters,
accountsList[pair.Key].Result))
});
}
private Task<GetAccountsResponse> GetAccounts(string cryptoCode)
{
try
{
if (_ZcashRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary) && summary.WalletAvailable)
{
return _ZcashRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<GetAccountsRequest, GetAccountsResponse>("get_accounts", new GetAccountsRequest());
}
}
catch { }
return Task.FromResult<GetAccountsResponse>(null);
}
private ZcashLikePaymentMethodViewModel GetZcashLikePaymentMethodViewModel(
StoreData store, string cryptoCode,
IPaymentFilter excludeFilters, GetAccountsResponse accountsResponse)
{
var Zcash = store.GetPaymentMethodConfigs<ZcashPaymentMethodConfig>(_handlers);
var settings = Zcash.SingleOrDefault(method => ((IHasNetwork)_handlers[method.Key]).Network.CryptoCode == cryptoCode).Value;
_ZcashRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary);
_ZcashLikeConfiguration.ZcashLikeConfigurationItems.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 ZcashLikePaymentMethodViewModel()
{
WalletFileFound = System.IO.File.Exists(fileAddress),
Enabled =
settings != null &&
!excludeFilters.Match(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)),
Summary = summary,
CryptoCode = cryptoCode,
AccountIndex = settings?.AccountIndex ?? accountsResponse?.SubaddressAccounts?.FirstOrDefault()?.AccountIndex ?? 0,
Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value),
nameof(SelectListItem.Text))
};
}
[HttpGet("{cryptoCode}")]
public async Task<IActionResult> GetStoreZcashLikePaymentMethod(string cryptoCode)
{
cryptoCode = cryptoCode.ToUpperInvariant();
if (!_ZcashLikeConfiguration.ZcashLikeConfigurationItems.ContainsKey(cryptoCode))
{
return NotFound();
}
var vm = GetZcashLikePaymentMethodViewModel(StoreData, cryptoCode,
StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode));
return View(nameof(GetStoreZcashLikePaymentMethod), vm);
}
[DisableRequestSizeLimit]
[HttpPost("{cryptoCode}")]
public async Task<IActionResult> GetStoreZcashLikePaymentMethod(ZcashLikePaymentMethodViewModel viewModel, string command, string cryptoCode)
{
cryptoCode = cryptoCode.ToUpperInvariant();
if (!_ZcashLikeConfiguration.ZcashLikeConfigurationItems.TryGetValue(cryptoCode,
out var configurationItem))
{
return NotFound();
}
if (command == "add-account")
{
try
{
var newAccount = await _ZcashRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<CreateAccountRequest, CreateAccountResponse>("create_account", new CreateAccountRequest()
{
Label = viewModel.NewAccountLabel
});
viewModel.AccountIndex = newAccount.AccountIndex;
}
catch (Exception)
{
ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not create new account."]);
}
}
else if (command == "upload-wallet")
{
var valid = true;
if (viewModel.WalletFile == null)
{
ModelState.AddModelError(nameof(viewModel.WalletFile), StringLocalizer["Please select the wallet file"]);
valid = false;
}
if (viewModel.WalletKeysFile == null)
{
ModelState.AddModelError(nameof(viewModel.WalletKeysFile), StringLocalizer["Please select the wallet.keys file"]);
valid = false;
}
if (valid)
{
if (_ZcashRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary))
{
if (summary.WalletAvailable)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = StringLocalizer["There is already an active wallet configured for {0}. Replacing it would break any existing invoices!", cryptoCode].Value
});
return RedirectToAction(nameof(GetStoreZcashLikePaymentMethod),
new { cryptoCode });
}
}
var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet");
using (var fileStream = new FileStream(fileAddress, FileMode.Create))
{
await viewModel.WalletFile.CopyToAsync(fileStream);
try
{
Exec($"chmod 666 {fileAddress}");
}
catch
{
}
}
fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet.keys");
using (var fileStream = new FileStream(fileAddress, FileMode.Create))
{
await viewModel.WalletKeysFile.CopyToAsync(fileStream);
try
{
Exec($"chmod 666 {fileAddress}");
}
catch
{
}
}
fileAddress = Path.Combine(configurationItem.WalletDirectory, "password");
using (var fileStream = new StreamWriter(fileAddress, false))
{
await fileStream.WriteAsync(viewModel.WalletPassword);
try
{
Exec($"chmod 666 {fileAddress}");
}
catch
{
}
}
return RedirectToAction(nameof(GetStoreZcashLikePaymentMethod), new
{
cryptoCode,
StatusMessage = "Wallet files uploaded. If it was valid, the wallet will become available soon"
});
}
}
if (!ModelState.IsValid)
{
var vm = GetZcashLikePaymentMethodViewModel(StoreData, 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();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
storeData.SetPaymentMethodConfig(_handlers[pmi], new ZcashPaymentMethodConfig()
{
AccountIndex = viewModel.AccountIndex
});
blob.SetExcluded(PaymentTypes.CHAIN.GetPaymentMethodId(viewModel.CryptoCode), !viewModel.Enabled);
storeData.SetStoreBlob(blob);
await _StoreRepository.UpdateStore(storeData);
return RedirectToAction("GetStoreZcashLikePaymentMethods",
new { StatusMessage = $"{cryptoCode} settings updated successfully", storeId = StoreData.Id });
}
private void Exec(string cmd)
{
var escapedArgs = cmd.Replace("\"", "\\\"", StringComparison.InvariantCulture);
var process = new Process
{
StartInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "/bin/sh",
Arguments = $"-c \"{escapedArgs}\""
}
};
#pragma warning disable CA1416 // Validate platform compatibility
process.Start();
#pragma warning restore CA1416 // Validate platform compatibility
process.WaitForExit();
}
public class ZcashLikePaymentMethodListViewModel
{
public IEnumerable<ZcashLikePaymentMethodViewModel> Items { get; set; }
}
public class ZcashLikePaymentMethodViewModel
{
public ZcashRPCProvider.ZcashLikeSummary 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; }
}
}
}

View File

@@ -1,17 +0,0 @@
using System;
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Altcoins.Zcash.UI
{
public class ZcashPaymentViewModel
{
public PaymentMethodId PaymentMethodId { get; set; }
public string Confirmations { get; set; }
public string DepositAddress { get; set; }
public string Amount { get; set; }
public string TransactionId { get; set; }
public DateTimeOffset ReceivedTime { get; set; }
public string TransactionLink { get; set; }
public string Currency { get; set; }
}
}

View File

@@ -1,26 +0,0 @@
@using BTCPayServer.Services.Altcoins.Monero.Services
@inject MoneroRPCProvider MoneroRpcProvider
@inject SignInManager<ApplicationUser> SignInManager;
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroRpcProvider.Summaries.Any())
{
@foreach (var summary in MoneroRpcProvider.Summaries)
{
@if (summary.Value != null)
{
var status = summary.Value.DaemonAvailable
? summary.Value.Synced ? "enabled" : "pending"
: "disabled";
<h5 class="d-flex align-items-center fw-semibold">
<span class="me-2 btcpay-status btcpay-status--@status"></span>
@summary.Key
</h5>
<ul>
<li>Node available: @summary.Value.DaemonAvailable</li>
<li>Wallet available: @summary.Value.WalletAvailable</li>
<li>Last updated: @summary.Value.UpdatedAt</li>
<li>Synced: @summary.Value.Synced (@summary.Value.CurrentHeight / @summary.Value.TargetHeight)</li>
</ul>
}
}
}

View File

@@ -1,16 +0,0 @@
@using BTCPayServer.Services.Altcoins.Monero.Configuration
@using BTCPayServer.Services.Altcoins.Monero.UI
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@inject SignInManager<ApplicationUser> SignInManager;
@inject MoneroLikeConfiguration MoneroLikeConfiguration;
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(UIMoneroLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase);
}
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
{
<a class="nav-link @(isActive ? "active" : string.Empty)" asp-route-storeId="@storeId" asp-action="GetStoreMoneroLikePaymentMethods" asp-controller="UIMoneroLikeStore">Monero</a>
}

View File

@@ -1,34 +0,0 @@
@using BTCPayServer.Services.Altcoins.Monero.Configuration
@using BTCPayServer.Services.Altcoins.Monero.UI
@using BTCPayServer.Abstractions.Contracts
@inject SignInManager<ApplicationUser> SignInManager;
@inject MoneroLikeConfiguration MoneroLikeConfiguration;
@inject IScopeProvider ScopeProvider
@inject UIMoneroLikeStoreController UIMoneroLikeStore;
@{
var storeId = ScopeProvider.GetCurrentStoreId();
}
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
{
var store = Context.GetStoreData();
var result = await UIMoneroLikeStore.GetVM(store);
foreach (var item in result.Items)
{
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(UIMoneroLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase) &&
ViewContext.RouteData.Values.TryGetValue("cryptoCode", out var cryptoCode) && cryptoCode is not null && cryptoCode.ToString() == item.CryptoCode;
<li class="nav-item">
<a class="nav-link @(isActive? "active" : "")"
asp-route-cryptoCode="@item.CryptoCode"
asp-route-storeId="@storeId"
asp-action="GetStoreMoneroLikePaymentMethod"
asp-controller="UIMoneroLikeStore">
<span class="me-2 btcpay-status btcpay-status--@(item.Enabled ? "enabled" : "pending")"></span>
<span>@item.CryptoCode Wallet</span>
</a>
</li>
}
}

View File

@@ -1,68 +0,0 @@
@using System.Globalization
@using BTCPayServer.Plugins.Altcoins;
@using BTCPayServer.Services
@using BTCPayServer.Services.Altcoins.Monero.Payments
@using BTCPayServer.Services.Altcoins.Monero.Services
@using BTCPayServer.Services.Altcoins.Monero.UI
@using BTCPayServer.Services.Invoices
@inject DisplayFormatter DisplayFormatter
@model InvoiceDetailsModel
@inject TransactionLinkProviders TransactionLinkProviders
@inject PaymentMethodHandlerDictionary handlers
@{
var payments = Model.Payments.Select(payment =>
{
if (!handlers.TryGetValue(payment.PaymentMethodId, out var h) || h is not MoneroLikePaymentMethodHandler handler)
return null;
var m = new MoneroPaymentViewModel();
var onChainPaymentData = handler.ParsePaymentDetails(payment.Details);
m.PaymentMethodId = handler.PaymentMethodId;
m.DepositAddress = payment.Destination;
m.Amount = payment.Value.ToString(CultureInfo.InvariantCulture);
var confReq = MoneroListener.ConfirmationsRequired(onChainPaymentData, payment.InvoiceEntity.SpeedPolicy);
var confCount = onChainPaymentData.ConfirmationCount;
confCount = Math.Min(confReq, confCount);
m.Confirmations = $"{confCount} / {confReq}";
m.TransactionId = onChainPaymentData.TransactionId;
m.ReceivedTime = payment.ReceivedTime;
if (onChainPaymentData.TransactionId != null)
m.TransactionLink = TransactionLinkProviders.GetTransactionLink(m.PaymentMethodId, onChainPaymentData.TransactionId);
m.Currency = payment.Currency;
return m;
}).Where(c => c != null).ToList();
}
@if (payments.Any())
{
<section>
<h5>Monero Payments</h5>
<table class="table table-hover">
<thead>
<tr>
<th class="w-75px">Payment Method</th>
<th class="w-175px">Destination</th>
<th class="text-nowrap">Payment Proof</th>
<th class="text-end">Confirmations</th>
<th class="w-150px text-end">Paid</th>
</tr>
</thead>
<tbody>
@foreach (var payment in payments)
{
<tr >
<td>@payment.PaymentMethodId</td>
<td><vc:truncate-center text="@payment.DepositAddress" classes="truncate-center-id" /></td>
<td><vc:truncate-center text="@payment.TransactionId" link="@payment.TransactionLink" classes="truncate-center-id" /></td>
<td class="text-end">@payment.Confirmations</td>
<td class="payment-value text-end text-nowrap">
<span data-sensitive class="text-success">@DisplayFormatter.Currency(payment.Amount, payment.Currency)</span>
</td>
</tr>
}
</tbody>
</table>
</section>
}

View File

@@ -1,16 +0,0 @@
@using BTCPayServer.Services.Altcoins.Zcash.Configuration
@using BTCPayServer.Services.Altcoins.Zcash.UI
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@inject SignInManager<ApplicationUser> SignInManager;
@inject ZcashLikeConfiguration ZcashLikeConfiguration;
@inject IScopeProvider ScopeProvider
@{
var storeId = ScopeProvider.GetCurrentStoreId();
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
nameof(UIZcashLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase);
}
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && ZcashLikeConfiguration.ZcashLikeConfigurationItems.Any())
{
<a class="nav-link @(isActive ? "active" : string.Empty)" asp-route-storeId="@storeId" asp-action="GetStoreZcashLikePaymentMethods" asp-controller="UIZcashLikeStore">Zcash</a>
}

View File

@@ -1,69 +0,0 @@
@using System.Globalization
@using BTCPayServer.Plugins.Altcoins;
@using BTCPayServer.Components.TruncateCenter
@using BTCPayServer.Services
@using BTCPayServer.Services.Altcoins.Zcash.Payments
@using BTCPayServer.Services.Altcoins.Zcash.Services
@using BTCPayServer.Services.Altcoins.Zcash.UI
@using BTCPayServer.Services.Invoices
@inject DisplayFormatter DisplayFormatter
@model InvoiceDetailsModel
@inject TransactionLinkProviders TransactionLinkProviders
@inject PaymentMethodHandlerDictionary handlers
@{
var payments = Model.Payments.Select(payment =>
{
if (!handlers.TryGetValue(payment.PaymentMethodId, out var h) || h is not ZcashLikePaymentMethodHandler handler)
return null;
var m = new ZcashPaymentViewModel();
var onChainPaymentData = handler.ParsePaymentDetails(payment.Details);
m.PaymentMethodId = handler.PaymentMethodId;
m.DepositAddress = payment.Destination;
m.Amount = payment.Value.ToString(CultureInfo.InvariantCulture);
var confReq = ZcashListener.ConfirmationsRequired(payment.InvoiceEntity.SpeedPolicy);
var confCount = onChainPaymentData.ConfirmationCount;
confCount = Math.Min(confReq, confCount);
m.Confirmations = $"{confCount} / {confReq}";
m.TransactionId = onChainPaymentData.TransactionId;
m.ReceivedTime = payment.ReceivedTime;
if (onChainPaymentData.TransactionId != null)
m.TransactionLink = TransactionLinkProviders.GetTransactionLink(m.PaymentMethodId, onChainPaymentData.TransactionId);
m.Currency = payment.Currency;
return m;
}).Where(c => c != null).ToList();
}
@if (payments.Any())
{
<section>
<h5>Zcash Payments</h5>
<table class="table table-hover">
<thead>
<tr>
<th class="w-75px">Payment Method</th>
<th class="w-175px">Destination</th>
<th class="text-nowrap">Payment Proof</th>
<th class="text-end">Confirmations</th>
<th class="w-150px text-end">Paid</th>
</tr>
</thead>
<tbody>
@foreach (var payment in payments)
{
<tr >
<td>@payment.PaymentMethodId</td>
<td><vc:truncate-center text="@payment.DepositAddress" classes="truncate-center-id" /></td>
<td><vc:truncate-center text="@payment.TransactionId" link="@payment.TransactionLink" classes="truncate-center-id" /></td>
<td class="text-end">@payment.Confirmations</td>
<td class="payment-value text-end text-nowrap">
<span data-sensitive class="text-success">@DisplayFormatter.Currency(payment.Amount, payment.Currency)</span>
</td>
</tr>
}
</tbody>
</table>
</section>
}

View File

@@ -1,26 +0,0 @@
@using BTCPayServer.Services.Altcoins.Zcash.Services
@inject ZcashRPCProvider ZcashRpcProvider
@inject SignInManager<ApplicationUser> SignInManager;
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && ZcashRpcProvider.Summaries.Any())
{
@foreach (var summary in ZcashRpcProvider.Summaries)
{
@if (summary.Value != null)
{
var status = summary.Value.DaemonAvailable
? summary.Value.Synced ? "enabled" : "pending"
: "disabled";
<h5 class="d-flex align-items-center fw-semibold">
<span class="me-2 btcpay-status btcpay-status--@status"></span>
@summary.Key
</h5>
<ul>
<li>Node available: @summary.Value.DaemonAvailable</li>
<li>Wallet available: @summary.Value.WalletAvailable</li>
<li>Last updated: @summary.Value.UpdatedAt</li>
<li>Synced: @summary.Value.Synced (@summary.Value.CurrentHeight / @summary.Value.TargetHeight)</li>
</ul>
}
}
}

View File

@@ -1,148 +0,0 @@
@using MoneroLikePaymentMethodViewModel = BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodViewModel
@using MoneroLikeSettlementThresholdChoice = BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikeSettlementThresholdChoice;
@model MoneroLikePaymentMethodViewModel
@{
ViewData.SetActivePage(Model.CryptoCode, StringLocalizer["{0} Settings", Model.CryptoCode], Model.CryptoCode);
}
<partial name="_StatusMessage" />
<div class="row">
<div class="col-md-8">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All"></div>
}
@if (Model.Summary != null)
{
<div class="card">
<ul class="list-group list-group-flush">
<li class="list-group-item">Node available: @Model.Summary.DaemonAvailable</li>
<li class="list-group-item">Wallet available: @Model.Summary.WalletAvailable (@(Model.WalletFileFound ? "Wallet file present" : "Wallet file not found"))</li>
<li class="list-group-item">Last updated: @Model.Summary.UpdatedAt</li>
<li class="list-group-item">Synced: @Model.Summary.Synced (@Model.Summary.CurrentHeight / @Model.Summary.TargetHeight)</li>
</ul>
</div>
}
@if (!Model.WalletFileFound || Model.Summary.WalletHeight == default)
{
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
class="mt-4" enctype="multipart/form-data">
<div class="card my-2">
<h3 class="card-title p-2">Upload Wallet</h3>
<div class="form-group p-2">
<label asp-for="WalletFile" class="form-label"></label>
<input class="form-control" asp-for="WalletFile" required>
<span asp-validation-for="WalletFile" class="text-danger"></span>
</div>
<div class="form-group p-2">
<label asp-for="WalletKeysFile" class="form-label"></label>
<input class="form-control" asp-for="WalletKeysFile" required>
<span asp-validation-for="WalletKeysFile" class="text-danger"></span>
</div>
<div class="form-group p-2">
<label asp-for="WalletPassword" class="form-label"></label>
<input class="form-control" asp-for="WalletPassword">
<span asp-validation-for="WalletPassword" class="text-danger"></span>
</div>
<div class="card-footer text-right">
<button name="command" value="upload-wallet" class="btn btn-secondary" type="submit">Upload</button>
</div>
</div>
</form>
}
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
class="mt-4" enctype="multipart/form-data">
<input type="hidden" asp-for="CryptoCode"/>
@if (!Model.WalletFileFound || Model.Summary.WalletHeight == default)
{
<input type="hidden" asp-for="AccountIndex"/>
}
else
{
<div class="form-group">
<label asp-for="AccountIndex" class="control-label"></label>
@if (@Model.Accounts != null && Model.Accounts.Any())
{
<select asp-for="AccountIndex" asp-items="Model.Accounts" class="form-control"></select>
<span asp-validation-for="AccountIndex" class="text-danger"></span>
}
else
{
<span>No accounts available on the current wallet</span>
<input type="hidden" asp-for="AccountIndex"/>
}
</div>
<div class="form-group">
<div class="input-group my-3">
<input type="text" class="form-control" placeholder="@StringLocalizer["New account label"]" asp-for="NewAccountLabel">
<button name="command" value="add-account" class="input-group-text btn btn-secondary" type="submit">Add account</button>
</div>
</div>
}
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/>
<span asp-validation-for="Enabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SettlementConfirmationThresholdChoice" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#consider-the-invoice-confirmed-when-the-payment-transaction" target="_blank" rel="noreferrer noopener" title="@StringLocalizer["More information..."]">
<vc:icon symbol="info" />
</a>
<select
asp-for="SettlementConfirmationThresholdChoice"
asp-items="Html.GetEnumSelectList<MoneroLikeSettlementThresholdChoice>()"
class="form-select w-auto"
onchange="
document.getElementById('unconfirmed-warning').hidden = this.value !== '@((int)MoneroLikeSettlementThresholdChoice.ZeroConfirmation)';
document.getElementById('custom-confirmation-value').hidden = this.value !== '@((int)MoneroLikeSettlementThresholdChoice.Custom)';">
</select>
<span asp-validation-for="SettlementConfirmationThresholdChoice" class="text-danger"></span>
<p class="info-note my-3 text-warning" id="unconfirmed-warning" role="alert" hidden="@(Model.SettlementConfirmationThresholdChoice is not MoneroLikeSettlementThresholdChoice.ZeroConfirmation)">
<vc:icon symbol="warning" />
<span text-translate="true">Choosing to accept an unconfirmed invoice can lead to double-spending and is strongly discouraged.</span>
</p>
</div>
<div class="form-group" id="custom-confirmation-value" hidden="@(Model.SettlementConfirmationThresholdChoice is not MoneroLikeSettlementThresholdChoice.Custom)">
<label asp-for="CustomSettlementConfirmationThreshold" class="form-label"></label>
<input
asp-for="CustomSettlementConfirmationThreshold"
type="number"
value="@(Model.CustomSettlementConfirmationThreshold)"
class="form-control w-auto"
min="0"
max="100"
pattern="\d+"
/>
<span asp-validation-for="CustomSettlementConfirmationThreshold" class="text-danger"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
<a class="btn btn-secondary" asp-action="GetStoreMoneroLikePaymentMethods"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
asp-controller="UIMoneroLikeStore">
Back to list
</a>
</div>
</form>
</div>
</div>
@section PageFootContent {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -1,57 +0,0 @@
@model BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodListViewModel
@{
ViewData.SetActivePage("Monero Settings", StringLocalizer["{0} Settings", "Monero"], "Monero Settings");
}
<div class="row">
<div class="col-md-8">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All"></div>
}
<div class="table-responsive-md">
<table class="table table-hover">
<thead>
<tr>
<th text-translate="true">Crypto</th>
<th text-translate="true">Account Index</th>
<th class="text-center" text-translate="true">Enabled</th>
<th class="text-right" text-translate="true">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.CryptoCode</td>
<td>@item.AccountIndex</td>
<td class="text-center">
@if (item.Enabled)
{
<vc:icon symbol="checkmark" css-class="text-success" />
}
else
{
<vc:icon symbol="cross" css-class="text-danger" />
}
</td>
<td class="text-right">
<a id="Modify" asp-action="GetStoreMoneroLikePaymentMethod"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@item.CryptoCode"
text-translate="true">
Modify
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@section PageFootContent {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -1,112 +0,0 @@
@using BTCPayServer.Views.Stores
@model BTCPayServer.Services.Altcoins.Zcash.UI.UIZcashLikeStoreController.ZcashLikePaymentMethodViewModel
@{
ViewData.SetActivePage(StoreNavPages.OnchainSettings, StringLocalizer["{0} Settings", Model.CryptoCode], $"{Context.GetStoreData().Id}-{Model.CryptoCode}");
}
<div class="row">
<div class="col-md-8">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
}
@if (Model.Summary != null)
{
<div class="card">
<ul class="list-group list-group-flush">
<li class="list-group-item">Node available: @Model.Summary.DaemonAvailable</li>
<li class="list-group-item">Wallet available: @Model.Summary.WalletAvailable (@(Model.WalletFileFound ? "Wallet file present" : "Wallet file not found"))</li>
<li class="list-group-item">Last updated: @Model.Summary.UpdatedAt</li>
<li class="list-group-item">Synced: @Model.Summary.Synced (@Model.Summary.CurrentHeight / @Model.Summary.TargetHeight)</li>
</ul>
</div>
}
@if (!Model.WalletFileFound || Model.Summary.WalletHeight == default)
{
<form method="post" asp-action="GetStoreZcashLikePaymentMethod"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
class="mt-4" enctype="multipart/form-data">
<div class="card my-2">
<h3 class="card-title p-2" text-translate="true">Upload Wallet</h3>
<div class="form-group p-2">
<label asp-for="WalletFile" class="form-label"></label>
<input class="form-control" asp-for="WalletFile" required>
<span asp-validation-for="WalletFile" class="text-danger"></span>
</div>
<div class="form-group p-2">
<label asp-for="WalletKeysFile" class="form-label"></label>
<input class="form-control" asp-for="WalletKeysFile" required>
<span asp-validation-for="WalletKeysFile" class="text-danger"></span>
</div>
<div class="form-group p-2">
<label asp-for="WalletPassword" class="form-label"></label>
<input class="form-control" asp-for="WalletPassword">
<span asp-validation-for="WalletPassword" class="text-danger"></span>
</div>
<div class="card-footer text-right">
<button name="command" value="upload-wallet" class="btn btn-secondary" type="submit" text-translate="true">Upload</button>
</div>
</div>
</form>
}
<form method="post" asp-action="GetStoreZcashLikePaymentMethod"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
class="mt-4" enctype="multipart/form-data">
<input type="hidden" asp-for="CryptoCode"/>
@if (!Model.WalletFileFound || Model.Summary.WalletHeight == default)
{
<input type="hidden" asp-for="AccountIndex"/>
}
else
{
<div class="form-group">
<label asp-for="AccountIndex" class="control-label"></label>
@if (@Model.Accounts != null && Model.Accounts.Any())
{
<select asp-for="AccountIndex" asp-items="Model.Accounts" class="form-control"></select>
<span asp-validation-for="AccountIndex" class="text-danger"></span>
}
else
{
<span text-translate="true">No accounts available on the current wallet</span>
<input type="hidden" asp-for="AccountIndex"/>
}
</div>
<div class="form-group">
<div class="input-group my-3">
<input type="text" class="form-control" placeholder="@StringLocalizer["New account label"]" asp-for="NewAccountLabel">
<button name="command" value="add-account" class="input-group-text btn btn-secondary" type="submit" text-translate="true">Add account</button>
</div>
</div>
}
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/>
<span asp-validation-for="Enabled" class="text-danger"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" id="SaveButton" text-translate="true">Save</button>
<a class="btn btn-secondary" asp-action="GetStoreZcashLikePaymentMethods"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
asp-controller="UIZcashLikeStore"
text-translate="true">
Back to list
</a>
</div>
</form>
</div>
</div>
@section PageFootContent {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -1,58 +0,0 @@
@using BTCPayServer.Views.Stores
@model BTCPayServer.Services.Altcoins.Zcash.UI.UIZcashLikeStoreController.ZcashLikePaymentMethodListViewModel
@{
ViewData.SetActivePage(StoreNavPages.OnchainSettings, StringLocalizer["{0} Settings", "Zcash"], $"{Context.GetStoreData().Id}-ZEC");
}
<div class="row">
<div class="col-md-8">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
}
<div class="table-responsive-md">
<table class="table table-hover">
<thead>
<tr>
<th text-translate="true">Crypto</th>
<th text-translate="true">Account Index</th>
<th text-translate="true" class="text-center">Enabled</th>
<th text-translate="true" 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)
{
<vc:icon symbol="checkmark" css-class="text-success" />
}
else
{
<vc:icon symbol="cross" css-class="text-danger" />
}
</td>
<td class="text-right">
<a id="Modify" asp-action="GetStoreZcashLikePaymentMethod"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@item.CryptoCode"
text-translate="true">
Modify
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@section PageFootContent {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -1,40 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB