From f4153ade9209306a11b4810007e01f98baa37cca Mon Sep 17 00:00:00 2001 From: hhanh00 Date: Mon, 14 Feb 2022 16:04:34 +0800 Subject: [PATCH] Zcash integration (#3400) * zcash * Use Channel instead of Queue --- .../Zcash/BTCPayNetworkProvider.Zcash.cs | 29 ++ .../Altcoins/Zcash/RPC/JsonRpcClient.cs | 102 +++++ .../Zcash/RPC/Models/CreateAccountRequest.cs | 9 + .../Zcash/RPC/Models/CreateAccountResponse.cs | 10 + .../Zcash/RPC/Models/CreateAddressRequest.cs | 10 + .../Zcash/RPC/Models/CreateAddressResponse.cs | 10 + .../Zcash/RPC/Models/GetAccountsRequest.cs | 9 + .../Zcash/RPC/Models/GetAccountsResponse.cs | 14 + .../Zcash/RPC/Models/GetFeeEstimateRequest.cs | 9 + .../RPC/Models/GetFeeEstimateResponse.cs | 11 + .../Zcash/RPC/Models/GetHeightResponse.cs | 9 + .../GetTransferByTransactionIdRequest.cs | 11 + .../GetTransferByTransactionIdResponse.cs | 31 ++ .../Zcash/RPC/Models/GetTransfersRequest.cs | 19 + .../Zcash/RPC/Models/GetTransfersResponse.cs | 35 ++ .../Altcoins/Zcash/RPC/Models/Info.cs | 33 ++ .../Zcash/RPC/Models/MakeUriRequest.cs | 13 + .../Zcash/RPC/Models/MakeUriResponse.cs | 9 + .../Zcash/RPC/Models/ParseStringConverter.cs | 40 ++ .../Altcoins/Zcash/RPC/Models/Peer.cs | 9 + .../Altcoins/Zcash/RPC/Models/SubaddrIndex.cs | 10 + .../Zcash/RPC/Models/SubaddressAccount.cs | 14 + .../Zcash/RPC/Models/SyncInfoResponse.cs | 13 + .../Altcoins/Zcash/Utils/ZcashMoney.cs | 20 + .../Zcash/ZcashLikeSpecificBtcPayNetwork.cs | 8 + BTCPayServer.Common/BTCPayNetworkProvider.cs | 1 + BTCPayServer.Rating/Currencies.json | 14 + .../docker-compose.altcoins.yml | 1 + BTCPayServer/BTCPayServer.csproj | 2 + BTCPayServer/Hosting/BTCPayServerServices.cs | 3 + BTCPayServer/Payments/PaymentMethodId.cs | 4 + BTCPayServer/Payments/PaymentTypes.cs | 7 +- BTCPayServer/Properties/launchSettings.json | 2 +- .../Configuration/ZcashLikeConfiguration.cs | 20 + .../ZcashLikeOnChainPaymentMethodDetails.cs | 34 ++ .../Zcash/Payments/ZcashLikePaymentData.cs | 69 ++++ .../Payments/ZcashLikePaymentMethodHandler.cs | 136 +++++++ .../Zcash/Payments/ZcashPaymentType.cs | 79 ++++ .../Payments/ZcashSupportedPaymentMethod.cs | 14 + .../RPC/ZcashDaemonCallbackController.cs | 40 ++ .../Services/Altcoins/Zcash/RPC/ZcashEvent.cs | 17 + .../ZcashLikeSummaryUpdaterHostedService.cs | 73 ++++ .../Altcoins/Zcash/Services/ZcashListener.cs | 378 ++++++++++++++++++ .../Zcash/Services/ZcashRPCProvider.cs | 124 ++++++ .../Services/ZcashSyncSummaryProvider.cs | 46 +++ .../Zcash/UI/ZcashLikeStoreController.cs | 309 ++++++++++++++ .../Zcash/UI/ZcashPaymentViewModel.cs | 17 + .../Altcoins/Zcash/ZcashLikeExtensions.cs | 73 ++++ .../Zcash/StoreNavZcashExtension.cshtml | 12 + .../Zcash/ViewZcashLikePaymentData.cshtml | 68 ++++ .../Shared/Zcash/ZcashSyncSummary.cshtml | 20 + .../GetStoreZcashLikePaymentMethod.cshtml | 111 +++++ .../GetStoreZcashLikePaymentMethods.cshtml | 57 +++ BTCPayServer/wwwroot/imlegacy/ycash.png | Bin 0 -> 17759 bytes BTCPayServer/wwwroot/imlegacy/zcash.png | Bin 0 -> 123472 bytes 55 files changed, 2216 insertions(+), 2 deletions(-) create mode 100644 BTCPayServer.Common/Altcoins/Zcash/BTCPayNetworkProvider.Zcash.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/JsonRpcClient.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAccountRequest.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAccountResponse.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAddressRequest.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAddressResponse.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetAccountsRequest.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetAccountsResponse.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetFeeEstimateRequest.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetFeeEstimateResponse.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetHeightResponse.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdRequest.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdResponse.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransfersRequest.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransfersResponse.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Info.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/MakeUriRequest.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/MakeUriResponse.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/ParseStringConverter.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Peer.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SubaddrIndex.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SubaddressAccount.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SyncInfoResponse.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/Utils/ZcashMoney.cs create mode 100644 BTCPayServer.Common/Altcoins/Zcash/ZcashLikeSpecificBtcPayNetwork.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Configuration/ZcashLikeConfiguration.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikeOnChainPaymentMethodDetails.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentData.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentType.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashSupportedPaymentMethod.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashDaemonCallbackController.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashEvent.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Services/ZcashLikeSummaryUpdaterHostedService.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/Services/ZcashSyncSummaryProvider.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/UI/ZcashLikeStoreController.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/UI/ZcashPaymentViewModel.cs create mode 100644 BTCPayServer/Services/Altcoins/Zcash/ZcashLikeExtensions.cs create mode 100644 BTCPayServer/Views/Shared/Zcash/StoreNavZcashExtension.cshtml create mode 100644 BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml create mode 100644 BTCPayServer/Views/Shared/Zcash/ZcashSyncSummary.cshtml create mode 100644 BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethod.cshtml create mode 100644 BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethods.cshtml create mode 100644 BTCPayServer/wwwroot/imlegacy/ycash.png create mode 100644 BTCPayServer/wwwroot/imlegacy/zcash.png diff --git a/BTCPayServer.Common/Altcoins/Zcash/BTCPayNetworkProvider.Zcash.cs b/BTCPayServer.Common/Altcoins/Zcash/BTCPayNetworkProvider.Zcash.cs new file mode 100644 index 000000000..12c18bf78 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/BTCPayNetworkProvider.Zcash.cs @@ -0,0 +1,29 @@ +using NBitcoin; + +namespace BTCPayServer +{ + public partial class BTCPayNetworkProvider + { + // Change this if you want another zcash coin + public void InitZcash() + { + Add(new ZcashLikeSpecificBtcPayNetwork() + { + CryptoCode = "ZEC", + DisplayName = "Zcash", + Divisibility = 8, + BlockExplorerLink = + NetworkType == ChainName.Mainnet + ? "https://www.exploreZcash.com/transaction/{0}" + : "https://testnet.xmrchain.net/tx/{0}", + DefaultRateRules = new[] + { + "ZEC_X = ZEC_BTC * BTC_X", + "ZEC_BTC = kraken(ZEC_BTC)" + }, + CryptoImagePath = "/imlegacy/zcash.png", + UriScheme = "zcash" + }); + } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/JsonRpcClient.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/JsonRpcClient.cs new file mode 100644 index 000000000..fd6cb1bd1 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/JsonRpcClient.cs @@ -0,0 +1,102 @@ +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 SendCommandAsync(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(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 + { + + + [JsonProperty("result")] public T Result { get; set; } + [JsonProperty("error")] public JsonRpcResultError Error { get; set; } + [JsonProperty("id")] public string Id { get; set; } + } + + internal class JsonRpcCommand + { + [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; + } + } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAccountRequest.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAccountRequest.cs new file mode 100644 index 000000000..6ce4a3f80 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAccountRequest.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models +{ + public partial class CreateAccountRequest + { + [JsonProperty("label")] public string Label { get; set; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAccountResponse.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAccountResponse.cs new file mode 100644 index 000000000..16f8c6587 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAccountResponse.cs @@ -0,0 +1,10 @@ +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; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAddressRequest.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAddressRequest.cs new file mode 100644 index 000000000..752579007 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAddressRequest.cs @@ -0,0 +1,10 @@ +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; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAddressResponse.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAddressResponse.cs new file mode 100644 index 000000000..9603363b8 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/CreateAddressResponse.cs @@ -0,0 +1,10 @@ +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; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetAccountsRequest.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetAccountsRequest.cs new file mode 100644 index 000000000..fa70211b9 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetAccountsRequest.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models +{ + public partial class GetAccountsRequest + { + [JsonProperty("tag")] public string Tag { get; set; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetAccountsResponse.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetAccountsResponse.cs new file mode 100644 index 000000000..77cbe5703 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetAccountsResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models +{ + public partial class GetAccountsResponse + { + [JsonProperty("subaddress_accounts")] public List SubaddressAccounts { get; set; } + [JsonProperty("total_balance")] public decimal TotalBalance { get; set; } + + [JsonProperty("total_unlocked_balance")] + public decimal TotalUnlockedBalance { get; set; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetFeeEstimateRequest.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetFeeEstimateRequest.cs new file mode 100644 index 000000000..8f3979bd8 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetFeeEstimateRequest.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models +{ + public class GetFeeEstimateRequest + { + [JsonProperty("grace_blocks")] public int? GraceBlocks { get; set; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetFeeEstimateResponse.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetFeeEstimateResponse.cs new file mode 100644 index 000000000..f778f1d4b --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetFeeEstimateResponse.cs @@ -0,0 +1,11 @@ +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; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetHeightResponse.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetHeightResponse.cs new file mode 100644 index 000000000..22a867721 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetHeightResponse.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models +{ + public partial class GetHeightResponse + { + [JsonProperty("height")] public long Height { get; set; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdRequest.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdRequest.cs new file mode 100644 index 000000000..5e1da36a9 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdRequest.cs @@ -0,0 +1,11 @@ +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; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdResponse.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdResponse.cs new file mode 100644 index 000000000..c985c4425 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdResponse.cs @@ -0,0 +1,31 @@ +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 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; } + } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransfersRequest.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransfersRequest.cs new file mode 100644 index 000000000..8776b958e --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransfersRequest.cs @@ -0,0 +1,19 @@ +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 SubaddrIndices { get; set; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransfersResponse.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransfersResponse.cs new file mode 100644 index 000000000..faba474ab --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/GetTransfersResponse.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models +{ + public partial class GetTransfersResponse + { + [JsonProperty("in")] public List In { get; set; } + [JsonProperty("out")] public List Out { get; set; } + [JsonProperty("pending")] public List Pending { get; set; } + [JsonProperty("failed")] public List Failed { get; set; } + [JsonProperty("pool")] public List 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; } + } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Info.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Info.cs new file mode 100644 index 000000000..6f2ce5e61 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Info.cs @@ -0,0 +1,33 @@ +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; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/MakeUriRequest.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/MakeUriRequest.cs new file mode 100644 index 000000000..0ea22f945 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/MakeUriRequest.cs @@ -0,0 +1,13 @@ +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; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/MakeUriResponse.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/MakeUriResponse.cs new file mode 100644 index 000000000..6a4235dd1 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/MakeUriResponse.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models +{ + public partial class MakeUriResponse + { + [JsonProperty("uri")] public string Uri { get; set; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/ParseStringConverter.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/ParseStringConverter.cs new file mode 100644 index 000000000..3b419d519 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/ParseStringConverter.cs @@ -0,0 +1,40 @@ +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(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(); + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Peer.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Peer.cs new file mode 100644 index 000000000..3425049a0 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Peer.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models +{ + public partial class Peer + { + [JsonProperty("info")] public Info Info { get; set; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SubaddrIndex.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SubaddrIndex.cs new file mode 100644 index 000000000..516ee81cc --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SubaddrIndex.cs @@ -0,0 +1,10 @@ +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; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SubaddressAccount.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SubaddressAccount.cs new file mode 100644 index 000000000..6523f0ba0 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SubaddressAccount.cs @@ -0,0 +1,14 @@ +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; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SyncInfoResponse.cs b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SyncInfoResponse.cs new file mode 100644 index 000000000..989ad3d74 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/RPC/Models/SyncInfoResponse.cs @@ -0,0 +1,13 @@ +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 Peers { get; set; } + [JsonProperty("status")] public string Status { get; set; } + [JsonProperty("target_height")] public long? TargetHeight { get; set; } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/Utils/ZcashMoney.cs b/BTCPayServer.Common/Altcoins/Zcash/Utils/ZcashMoney.cs new file mode 100644 index 000000000..fe109e0c7 --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/Utils/ZcashMoney.cs @@ -0,0 +1,20 @@ +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); + } + } +} diff --git a/BTCPayServer.Common/Altcoins/Zcash/ZcashLikeSpecificBtcPayNetwork.cs b/BTCPayServer.Common/Altcoins/Zcash/ZcashLikeSpecificBtcPayNetwork.cs new file mode 100644 index 000000000..9ebd8158d --- /dev/null +++ b/BTCPayServer.Common/Altcoins/Zcash/ZcashLikeSpecificBtcPayNetwork.cs @@ -0,0 +1,8 @@ +namespace BTCPayServer +{ + public class ZcashLikeSpecificBtcPayNetwork : BTCPayNetworkBase + { + public int MaxTrackedConfirmation = 10; + public string UriScheme { get; set; } + } +} diff --git a/BTCPayServer.Common/BTCPayNetworkProvider.cs b/BTCPayServer.Common/BTCPayNetworkProvider.cs index 67fc1f27a..024819c6a 100644 --- a/BTCPayServer.Common/BTCPayNetworkProvider.cs +++ b/BTCPayServer.Common/BTCPayNetworkProvider.cs @@ -55,6 +55,7 @@ namespace BTCPayServer InitGroestlcoin(); InitViacoin(); InitMonero(); + InitZcash(); InitPolis(); InitChaincoin(); // InitArgoneum();//their rate source is down 9/15/20. diff --git a/BTCPayServer.Rating/Currencies.json b/BTCPayServer.Rating/Currencies.json index cbb7d2c5a..a01c4ae92 100644 --- a/BTCPayServer.Rating/Currencies.json +++ b/BTCPayServer.Rating/Currencies.json @@ -1259,6 +1259,20 @@ "symbol":null, "crypto":true }, + { + "name":"YEC", + "code":"YEC", + "divisibility":8, + "symbol":null, + "crypto":true + }, + { + "name":"ZEC", + "code":"ZEC", + "divisibility":8, + "symbol":null, + "crypto":true + }, { "name":"POLIS", "code":"POLIS", diff --git a/BTCPayServer.Tests/docker-compose.altcoins.yml b/BTCPayServer.Tests/docker-compose.altcoins.yml index 2f8ae1120..dc73f8f7e 100644 --- a/BTCPayServer.Tests/docker-compose.altcoins.yml +++ b/BTCPayServer.Tests/docker-compose.altcoins.yml @@ -385,6 +385,7 @@ services: - "19444:19444" volumes: - "elementsd_liquid_datadir:/data" + volumes: sshd_datadir: bitcoin_datadir: diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 0a420a688..dbeb925e6 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -40,7 +40,9 @@ + + diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 51ec9da7f..a9a4fe090 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -59,6 +59,7 @@ using NicolasDorier.RateLimits; using Serilog; #if ALTCOINS using BTCPayServer.Services.Altcoins.Monero; +using BTCPayServer.Services.Altcoins.Zcash; #endif namespace BTCPayServer.Hosting { @@ -90,6 +91,8 @@ namespace BTCPayServer.Hosting #if ALTCOINS if (configuration.SupportChain("xmr")) services.AddMoneroLike(); + if (configuration.SupportChain("yec") || configuration.SupportChain("zec")) + services.AddZcashLike(); #endif services.TryAddSingleton(); services.TryAddSingleton(provider => provider.GetService()); diff --git a/BTCPayServer/Payments/PaymentMethodId.cs b/BTCPayServer/Payments/PaymentMethodId.cs index 811570eb3..af30c4ed0 100644 --- a/BTCPayServer/Payments/PaymentMethodId.cs +++ b/BTCPayServer/Payments/PaymentMethodId.cs @@ -80,6 +80,8 @@ namespace BTCPayServer.Payments #if ALTCOINS if (CryptoCode == "XMR" && PaymentType == PaymentTypes.MoneroLike) return CryptoCode; + if ((CryptoCode == "YEC" || CryptoCode == "ZEC") && PaymentType == PaymentTypes.ZcashLike) + return CryptoCode; #endif return $"{CryptoCode}-{PaymentType.ToStringNormalized()}"; } @@ -105,6 +107,8 @@ namespace BTCPayServer.Payments #if ALTCOINS if (parts[0].ToUpperInvariant() == "XMR") type = PaymentTypes.MoneroLike; + if (parts[0].ToUpperInvariant() == "ZEC") + type = PaymentTypes.ZcashLike; #endif if (parts.Length == 2) { diff --git a/BTCPayServer/Payments/PaymentTypes.cs b/BTCPayServer/Payments/PaymentTypes.cs index caf365958..6526b2fa4 100644 --- a/BTCPayServer/Payments/PaymentTypes.cs +++ b/BTCPayServer/Payments/PaymentTypes.cs @@ -2,6 +2,7 @@ using System; using System.Linq; #if ALTCOINS using BTCPayServer.Services.Altcoins.Monero.Payments; +using BTCPayServer.Services.Altcoins.Zcash.Payments; #endif using BTCPayServer.Services.Invoices; using NBitcoin; @@ -18,7 +19,7 @@ namespace BTCPayServer.Payments { BTCLike, LightningLike, LNURLPay, #if ALTCOINS - MoneroLike, + MoneroLike, ZcashLike, #endif }; /// @@ -39,6 +40,10 @@ namespace BTCPayServer.Payments /// Monero payment /// public static MoneroPaymentType MoneroLike => MoneroPaymentType.Instance; + /// + /// Zcash payment + /// + public static ZcashPaymentType ZcashLike => ZcashPaymentType.Instance; #endif public static bool TryParse(string paymentType, out PaymentType type) diff --git a/BTCPayServer/Properties/launchSettings.json b/BTCPayServer/Properties/launchSettings.json index 154dd23a2..b200db5a6 100644 --- a/BTCPayServer/Properties/launchSettings.json +++ b/BTCPayServer/Properties/launchSettings.json @@ -98,7 +98,7 @@ "BTCPAY_RECOMMENDED-PLUGINS": "BTCPayServer.Plugins.Test", "BTCPAY_CHEATMODE": "true" }, - "applicationUrl": "https://localhost:14142/" + "applicationUrl": "https://localhost:14142/" } } } diff --git a/BTCPayServer/Services/Altcoins/Zcash/Configuration/ZcashLikeConfiguration.cs b/BTCPayServer/Services/Altcoins/Zcash/Configuration/ZcashLikeConfiguration.cs new file mode 100644 index 000000000..8c03a0b12 --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Configuration/ZcashLikeConfiguration.cs @@ -0,0 +1,20 @@ +#if ALTCOINS +using System; +using System.Collections.Generic; + +namespace BTCPayServer.Services.Altcoins.Zcash.Configuration +{ + public class ZcashLikeConfiguration + { + public Dictionary ZcashLikeConfigurationItems { get; set; } = + new Dictionary(); + } + + public class ZcashLikeConfigurationItem + { + public Uri DaemonRpcUri { get; set; } + public Uri InternalWalletRpcUri { get; set; } + public string WalletDirectory { get; set; } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikeOnChainPaymentMethodDetails.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikeOnChainPaymentMethodDetails.cs new file mode 100644 index 000000000..70293b9cc --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikeOnChainPaymentMethodDetails.cs @@ -0,0 +1,34 @@ +#if ALTCOINS +using BTCPayServer.Payments; + +namespace BTCPayServer.Services.Altcoins.Zcash.Payments +{ + public class ZcashLikeOnChainPaymentMethodDetails : IPaymentMethodDetails + { + public PaymentType GetPaymentType() + { + return ZcashPaymentType.Instance; + } + + public string GetPaymentDestination() + { + return DepositAddress; + } + + public decimal GetNextNetworkFee() + { + return NextNetworkFee; + } + + public decimal GetFeeRate() + { + return 0.0m; + } + public bool Activated { get; set; } = true; + public long AccountIndex { get; set; } + public long AddressIndex { get; set; } + public string DepositAddress { get; set; } + public decimal NextNetworkFee { get; set; } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentData.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentData.cs new file mode 100644 index 000000000..2a0fe450f --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentData.cs @@ -0,0 +1,69 @@ +#if ALTCOINS +using BTCPayServer.Client.Models; +using BTCPayServer.Payments; +using BTCPayServer.Services.Altcoins.Zcash.Utils; +using BTCPayServer.Services.Invoices; + +namespace BTCPayServer.Services.Altcoins.Zcash.Payments +{ + public class ZcashLikePaymentData : CryptoPaymentData + { + public long Amount { get; set; } + public string Address { get; set; } + public long SubaddressIndex { get; set; } + public long SubaccountIndex { get; set; } + public long BlockHeight { get; set; } + public long ConfirmationCount { get; set; } + public string TransactionId { get; set; } + + public BTCPayNetworkBase Network { get; set; } + + public string GetPaymentId() + { + return $"{TransactionId}#{SubaccountIndex}#{SubaddressIndex}"; + } + + public string[] GetSearchTerms() + { + return new[] { TransactionId }; + } + + public decimal GetValue() + { + return ZcashMoney.Convert(Amount); + } + + public bool PaymentCompleted(PaymentEntity entity) + { + return ConfirmationCount >= (Network as ZcashLikeSpecificBtcPayNetwork).MaxTrackedConfirmation; + } + + public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy) + { + switch (speedPolicy) + { + case SpeedPolicy.HighSpeed: + return ConfirmationCount >= 0; + case SpeedPolicy.MediumSpeed: + return ConfirmationCount >= 1; + case SpeedPolicy.LowMediumSpeed: + return ConfirmationCount >= 2; + case SpeedPolicy.LowSpeed: + return ConfirmationCount >= 6; + default: + return false; + } + } + + public PaymentType GetPaymentType() + { + return ZcashPaymentType.Instance; + } + + public string GetDestination() + { + return Address; + } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs new file mode 100644 index 000000000..ef767aff9 --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs @@ -0,0 +1,136 @@ +#if ALTCOINS +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Logging; +using BTCPayServer.Models; +using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.Payments; +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; + +namespace BTCPayServer.Services.Altcoins.Zcash.Payments +{ + public class ZcashLikePaymentMethodHandler : PaymentMethodHandlerBase + { + private readonly BTCPayNetworkProvider _networkProvider; + private readonly ZcashRPCProvider _ZcashRpcProvider; + + public ZcashLikePaymentMethodHandler(BTCPayNetworkProvider networkProvider, ZcashRPCProvider ZcashRpcProvider) + { + _networkProvider = networkProvider; + _ZcashRpcProvider = ZcashRpcProvider; + } + public override PaymentType PaymentType => ZcashPaymentType.Instance; + + public override async Task CreatePaymentMethodDetails(InvoiceLogs logs, ZcashSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, + StoreData store, ZcashLikeSpecificBtcPayNetwork network, object preparePaymentObject) + { + + if (preparePaymentObject is null) + { + return new ZcashLikeOnChainPaymentMethodDetails() + { + Activated = false + }; + } + + if (!_ZcashRpcProvider.IsAvailable(network.CryptoCode)) + throw new PaymentMethodUnavailableException($"Node or wallet not available"); + var invoice = paymentMethod.ParentEntity; + if (!(preparePaymentObject is Prepare ZcashPrepare)) + throw new ArgumentException(); + var feeRatePerKb = await ZcashPrepare.GetFeeRate; + var address = await ZcashPrepare.ReserveAddress(invoice.Id); + + var feeRatePerByte = feeRatePerKb.Fee / 1024; + return new ZcashLikeOnChainPaymentMethodDetails() + { + NextNetworkFee = ZcashMoney.Convert(feeRatePerByte * 100), + AccountIndex = supportedPaymentMethod.AccountIndex, + AddressIndex = address.AddressIndex, + DepositAddress = address.Address, + Activated = true + }; + + } + + public override object PreparePayment(ZcashSupportedPaymentMethod supportedPaymentMethod, StoreData store, + BTCPayNetworkBase network) + { + + var walletClient = _ZcashRpcProvider.WalletRpcClients[supportedPaymentMethod.CryptoCode]; + var daemonClient = _ZcashRpcProvider.DaemonRpcClients[supportedPaymentMethod.CryptoCode]; + return new Prepare() + { + GetFeeRate = daemonClient.SendCommandAsync("get_fee_estimate", new GetFeeEstimateRequest()), + ReserveAddress = s => walletClient.SendCommandAsync("create_address", new CreateAddressRequest() { Label = $"btcpay invoice #{s}", AccountIndex = supportedPaymentMethod.AccountIndex }) + }; + } + + class Prepare + { + public Task GetFeeRate; + public Func> ReserveAddress; + } + + public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse, + StoreBlob storeBlob, IPaymentMethod paymentMethod) + { + var paymentMethodId = paymentMethod.GetId(); + var network = _networkProvider.GetNetwork(model.CryptoCode); + model.PaymentMethodName = GetPaymentMethodName(network); + model.CryptoImage = GetCryptoImage(network); + if (model.Activated) + { + var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); + model.InvoiceBitcoinUrl = ZcashPaymentType.Instance.GetPaymentLink(network, + new ZcashLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due, + null); + model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; + } + else + { + model.InvoiceBitcoinUrl = ""; + model.InvoiceBitcoinUrlQR = ""; + } + } + public override string GetCryptoImage(PaymentMethodId paymentMethodId) + { + var network = _networkProvider.GetNetwork(paymentMethodId.CryptoCode); + return GetCryptoImage(network); + } + + public override string GetPaymentMethodName(PaymentMethodId paymentMethodId) + { + var network = _networkProvider.GetNetwork(paymentMethodId.CryptoCode); + return GetPaymentMethodName(network); + } + public override IEnumerable GetSupportedPaymentMethods() + { + return _networkProvider.GetAll() + .Where(network => network is ZcashLikeSpecificBtcPayNetwork) + .Select(network => new PaymentMethodId(network.CryptoCode, PaymentType)); + } + + private string GetCryptoImage(ZcashLikeSpecificBtcPayNetwork network) + { + return network.CryptoImagePath; + } + + + private string GetPaymentMethodName(ZcashLikeSpecificBtcPayNetwork network) + { + return $"{network.DisplayName}"; + } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentType.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentType.cs new file mode 100644 index 000000000..965994e7d --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentType.cs @@ -0,0 +1,79 @@ +#if ALTCOINS +using System.Globalization; +using BTCPayServer.Payments; +using BTCPayServer.Services.Invoices; +using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Services.Altcoins.Zcash.Payments +{ + public class ZcashPaymentType : PaymentType + { + public static ZcashPaymentType Instance { get; } = new ZcashPaymentType(); + public override string ToPrettyString() => "On-Chain"; + + public override string GetId() => "ZcashLike"; + public override string ToStringNormalized() + { + return "Zcash"; + } + + public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) + { + return JsonConvert.DeserializeObject(str); + } + + public override string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData) + { + return JsonConvert.SerializeObject(paymentData); + } + + public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str) + { + return JsonConvert.DeserializeObject(str); + } + + public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details) + { + return JsonConvert.SerializeObject(details); + } + + public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value) + { + return JsonConvert.DeserializeObject(value.ToString()); + } + + public override string GetTransactionLink(BTCPayNetworkBase network, string txId) + { + return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId); + } + + public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) + { + return paymentMethodDetails.Activated + ? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}" + : string.Empty; + } + + public override string InvoiceViewPaymentPartialName { get; } = "Zcash/ViewZcashLikePaymentData"; + public override object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore) + { + if (supportedPaymentMethod is ZcashSupportedPaymentMethod ZcashSupportedPaymentMethod) + { + return new + { + ZcashSupportedPaymentMethod.AccountIndex, + }; + } + + return null; + } + + public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl) + { + + } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashSupportedPaymentMethod.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashSupportedPaymentMethod.cs new file mode 100644 index 000000000..fed048d8e --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashSupportedPaymentMethod.cs @@ -0,0 +1,14 @@ +#if ALTCOINS +using BTCPayServer.Payments; + +namespace BTCPayServer.Services.Altcoins.Zcash.Payments +{ + public class ZcashSupportedPaymentMethod : ISupportedPaymentMethod + { + + public string CryptoCode { get; set; } + public long AccountIndex { get; set; } + public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, ZcashPaymentType.Instance); + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashDaemonCallbackController.cs b/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashDaemonCallbackController.cs new file mode 100644 index 000000000..15fa861b7 --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashDaemonCallbackController.cs @@ -0,0 +1,40 @@ +#if ALTCOINS +using BTCPayServer.Filters; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Services.Altcoins.Zcash.RPC +{ + [Route("[controller]")] + [OnlyIfSupportAttribute("ZEC")] + 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(); + } + + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashEvent.cs b/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashEvent.cs new file mode 100644 index 000000000..ca06dde76 --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashEvent.cs @@ -0,0 +1,17 @@ +#if ALTCOINS +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})"; + } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashLikeSummaryUpdaterHostedService.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashLikeSummaryUpdaterHostedService.cs new file mode 100644 index 000000000..9a4590319 --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashLikeSummaryUpdaterHostedService.cs @@ -0,0 +1,73 @@ +#if ALTCOINS +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; + } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs new file mode 100644 index 000000000..cb18e3c21 --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs @@ -0,0 +1,378 @@ +#if ALTCOINS +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.Events; +using BTCPayServer.Payments; +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.Invoices; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NBitcoin; +using NBXplorer; + +namespace BTCPayServer.Services.Altcoins.Zcash.Services +{ + public class ZcashListener : IHostedService + { + private readonly InvoiceRepository _invoiceRepository; + private readonly EventAggregator _eventAggregator; + private readonly ZcashRPCProvider _ZcashRpcProvider; + private readonly ZcashLikeConfiguration _ZcashLikeConfiguration; + private readonly BTCPayNetworkProvider _networkProvider; + private readonly ILogger _logger; + private readonly PaymentService _paymentService; + private readonly CompositeDisposable leases = new CompositeDisposable(); + private readonly Channel> _requests = Channel.CreateUnbounded>(); + private CancellationTokenSource _Cts; + + public ZcashListener(InvoiceRepository invoiceRepository, + EventAggregator eventAggregator, + ZcashRPCProvider ZcashRpcProvider, + ZcashLikeConfiguration ZcashLikeConfiguration, + BTCPayNetworkProvider networkProvider, + ILogger logger, + PaymentService paymentService) + { + _invoiceRepository = invoiceRepository; + _eventAggregator = eventAggregator; + _ZcashRpcProvider = ZcashRpcProvider; + _ZcashLikeConfiguration = ZcashLikeConfiguration; + _networkProvider = networkProvider; + _logger = logger; + _paymentService = paymentService; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + if (!_ZcashLikeConfiguration.ZcashLikeConfigurationItems.Any()) + { + return Task.CompletedTask; + } + _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + leases.Add(_eventAggregator.Subscribe(OnZcashEvent)); + leases.Add(_eventAggregator.Subscribe(e => + { + if (_ZcashRpcProvider.IsAvailable(e.CryptoCode)) + { + _logger.LogInformation($"{e.CryptoCode} just became available"); + _ = UpdateAnyPendingZcashLikePayment(e.CryptoCode); + } + else + { + _logger.LogInformation($"{e.CryptoCode} just became unavailable"); + } + })); + _ = WorkThroughQueue(_Cts.Token); + return Task.CompletedTask; + } + + private async Task WorkThroughQueue(CancellationToken token) + { + while (await _requests.Reader.WaitToReadAsync(token) && _requests.Reader.TryRead(out var action)) { + token.ThrowIfCancellationRequested(); + try { + await action.Invoke(); + } + catch (Exception e) + { + _logger.LogError($"error with action item {e}"); + } + } + } + + private void OnZcashEvent(ZcashEvent obj) + { + if (!_ZcashRpcProvider.IsAvailable(obj.CryptoCode)) + { + return; + } + + if (!string.IsNullOrEmpty(obj.BlockHash)) + { + if (!_requests.Writer.TryWrite(() => OnNewBlock(obj.CryptoCode))) { + _logger.LogWarning($"Failed to write new block task to channel"); + } + } + + if (!string.IsNullOrEmpty(obj.TransactionHash)) + { + if (!_requests.Writer.TryWrite(() => OnTransactionUpdated(obj.CryptoCode, obj.TransactionHash))) { + _logger.LogWarning($"Failed to write new tx task to channel"); + } + } + } + + private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment) + { + _logger.LogInformation( + $"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}"); + var paymentData = (ZcashLikePaymentData)payment.GetCryptoPaymentData(); + var paymentMethod = invoice.GetPaymentMethod(payment.Network, ZcashPaymentType.Instance); + if (paymentMethod != null && + paymentMethod.GetPaymentMethodDetails() is ZcashLikeOnChainPaymentMethodDetails Zcash && + Zcash.Activated && + Zcash.GetPaymentDestination() == paymentData.GetDestination() && + paymentMethod.Calculate().Due > Money.Zero) + { + var walletClient = _ZcashRpcProvider.WalletRpcClients[payment.GetCryptoCode()]; + + var address = await walletClient.SendCommandAsync( + "create_address", + new CreateAddressRequest() + { + Label = $"btcpay invoice #{invoice.Id}", + AccountIndex = Zcash.AccountIndex + }); + Zcash.DepositAddress = address.Address; + Zcash.AddressIndex = address.AddressIndex; + await _invoiceRepository.NewPaymentDetails(invoice.Id, Zcash, payment.Network); + _eventAggregator.Publish( + new InvoiceNewPaymentDetailsEvent(invoice.Id, Zcash, payment.GetPaymentMethodId())); + paymentMethod.SetPaymentMethodDetails(Zcash); + invoice.SetPaymentMethod(paymentMethod); + } + + _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); + + + //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), + PaymentMethodDetails: entity.GetPaymentMethod(network, ZcashPaymentType.Instance) + .GetPaymentMethodDetails() as ZcashLikeOnChainPaymentMethodDetails)) + .Select(tuple => ( + tuple.Invoice, + tuple.PaymentMethodDetails, + ExistingPayments: tuple.ExistingPayments.Select(entity => + (Payment: entity, PaymentData: (ZcashLikePaymentData)entity.GetCryptoPaymentData(), + tuple.Invoice)) + )); + + var existingPaymentData = expandedInvoices.SelectMany(tuple => tuple.ExistingPayments); + + var accountToAddressQuery = new Dictionary>(); + //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()); + + 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( + "get_transfers", + new GetTransfersRequest() + { + AccountIndex = datas.Key, + In = true, + SubaddrIndices = datas.Value.Distinct().ToList() + })); + + await Task.WhenAll(tasks.Values); + + + var transferProcessingTasks = new List(); + + var updatedPaymentEntities = new BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)>(); + foreach (var keyValuePair in tasks) + { + var transfers = keyValuePair.Value.Result.In; + if (transfers == null) + { + continue; + } + + transferProcessingTasks.AddRange(transfers.Select(transfer => + { + InvoiceEntity invoice = null; + var existingMatch = existingPaymentData.SingleOrDefault(tuple => + tuple.PaymentData.Address == transfer.Address && + tuple.PaymentData.TransactionId == transfer.Txid); + + if (existingMatch.Invoice != null) + { + invoice = existingMatch.Invoice; + } + else + { + var newMatch = expandedInvoices.SingleOrDefault(tuple => + tuple.PaymentMethodDetails.GetPaymentDestination() == transfer.Address); + + if (newMatch.Invoice == null) + { + return Task.CompletedTask; + } + + invoice = newMatch.Invoice; + } + + + return 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)); + } + } + } + + + public Task StopAsync(CancellationToken cancellationToken) + { + leases.Dispose(); + _Cts?.Cancel(); + return Task.CompletedTask; + } + + private async Task OnNewBlock(string cryptoCode) + { + await UpdateAnyPendingZcashLikePayment(cryptoCode); + _eventAggregator.Publish(new NewBlockEvent() { CryptoCode = cryptoCode }); + } + + private async Task OnTransactionUpdated(string cryptoCode, string transactionHash) + { + var paymentMethodId = new PaymentMethodId(cryptoCode, ZcashPaymentType.Instance); + var transfer = await _ZcashRpcProvider.WalletRpcClients[cryptoCode] + .SendCommandAsync( + "get_transfer_by_txid", + new GetTransferByTransactionIdRequest() { TransactionId = transactionHash }); + + var paymentsToUpdate = new BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)>(); + + //group all destinations of the tx together and loop through the sets + foreach (var destination in transfer.Transfers.GroupBy(destination => destination.Address)) + { + //find the invoice corresponding to this address, else skip + var address = destination.Key + "#" + paymentMethodId; + var invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { address })).FirstOrDefault(); + if (invoice == null) + { + continue; + } + + var index = destination.First().SubaddrIndex; + + await 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, + BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate) + { + //construct the payment data + var paymentData = new ZcashLikePaymentData() + { + Address = address, + SubaccountIndex = subaccountIndex, + SubaddressIndex = subaddressIndex, + TransactionId = txId, + ConfirmationCount = confirmations, + Amount = totalAmount, + BlockHeight = blockHeight, + Network = _networkProvider.GetNetwork(cryptoCode) + }; + + //check if this tx exists as a payment to this invoice already + var alreadyExistingPaymentThatMatches = GetAllZcashLikePayments(invoice, cryptoCode) + .Select(entity => (Payment: entity, PaymentData: entity.GetCryptoPaymentData())) + .SingleOrDefault(c => c.PaymentData.GetPaymentId() == paymentData.GetPaymentId()); + + //if it doesnt, add it and assign a new Zcashlike address to the system if a balance is still due + if (alreadyExistingPaymentThatMatches.Payment == null) + { + var payment = await _paymentService.AddPayment(invoice.Id, DateTimeOffset.UtcNow, + paymentData, _networkProvider.GetNetwork(cryptoCode), true); + if (payment != null) + await ReceivedPayment(invoice, payment); + } + else + { + //else update it with the new data + alreadyExistingPaymentThatMatches.PaymentData = paymentData; + alreadyExistingPaymentThatMatches.Payment.SetCryptoPaymentData(paymentData); + paymentsToUpdate.Add((alreadyExistingPaymentThatMatches.Payment, invoice)); + } + } + + private async Task UpdateAnyPendingZcashLikePayment(string cryptoCode) + { + var invoiceIds = await _invoiceRepository.GetPendingInvoices(); + if (!invoiceIds.Any()) + { + return; + } + + var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() { InvoiceId = invoiceIds }); + invoices = invoices.Where(entity => entity.GetPaymentMethod(new PaymentMethodId(cryptoCode, ZcashPaymentType.Instance)) + ?.GetPaymentMethodDetails().Activated is true).ToArray(); + _logger.LogInformation($"Updating pending payments for {cryptoCode} in {string.Join(',', invoiceIds)}"); + await UpdatePaymentStates(cryptoCode, invoices); + } + + private IEnumerable GetAllZcashLikePayments(InvoiceEntity invoice, string cryptoCode) + { + return invoice.GetPayments(false) + .Where(p => p.GetPaymentMethodId() == new PaymentMethodId(cryptoCode, ZcashPaymentType.Instance)); + } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs new file mode 100644 index 000000000..33f335d1e --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs @@ -0,0 +1,124 @@ +#if ALTCOINS +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +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 DaemonRpcClients; + public ImmutableDictionary WalletRpcClients; + + private readonly ConcurrentDictionary _summaries = + new ConcurrentDictionary(); + + public ConcurrentDictionary 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 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("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( + "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("{0} {1} {2} {3} {4} {5}", Synced, CurrentHeight, TargetHeight, WalletHeight, DaemonAvailable, WalletAvailable); } + } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashSyncSummaryProvider.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashSyncSummaryProvider.cs new file mode 100644 index 000000000..1328db180 --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashSyncSummaryProvider.cs @@ -0,0 +1,46 @@ +#if ALTCOINS +using System.Collections.Generic; +using System.Linq; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Client.Models; + +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 GetStatuses() + { + return _ZcashRpcProvider.Summaries.Select(pair => new ZcashSyncStatus() + { + Summary = pair.Value, CryptoCode = pair.Key + }); + } + } + + public class ZcashSyncStatus: SyncStatus, ISyncStatus + { + public override bool Available + { + get + { + return Summary?.WalletAvailable ?? false; + } + } + + public ZcashRPCProvider.ZcashLikeSummary Summary { get; set; } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashLikeStoreController.cs b/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashLikeStoreController.cs new file mode 100644 index 000000000..eb139f2ac --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashLikeStoreController.cs @@ -0,0 +1,309 @@ +#if ALTCOINS +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.Models; +using BTCPayServer.Payments; +using BTCPayServer.Security; +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.Stores; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace BTCPayServer.Services.Altcoins.Zcash.UI +{ + [Route("stores/{storeId}/Zcashlike")] + [OnlyIfSupportAttribute("ZEC")] + [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 BTCPayNetworkProvider _BtcPayNetworkProvider; + + public UIZcashLikeStoreController(ZcashLikeConfiguration ZcashLikeConfiguration, + StoreRepository storeRepository, ZcashRPCProvider ZcashRpcProvider, + BTCPayNetworkProvider btcPayNetworkProvider) + { + _ZcashLikeConfiguration = ZcashLikeConfiguration; + _StoreRepository = storeRepository; + _ZcashRpcProvider = ZcashRpcProvider; + _BtcPayNetworkProvider = btcPayNetworkProvider; + } + + public StoreData StoreData => HttpContext.GetStoreData(); + + [HttpGet()] + public async Task GetStoreZcashLikePaymentMethods() + { + var Zcash = StoreData.GetSupportedPaymentMethods(_BtcPayNetworkProvider) + .OfType(); + + 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(Zcash, pair.Key, excludeFilters, + accountsList[pair.Key].Result)) + }); + } + + private Task GetAccounts(string cryptoCode) + { + try + { + if (_ZcashRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary) && summary.WalletAvailable) + { + + return _ZcashRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("get_accounts", new GetAccountsRequest()); + } + } + catch { } + return Task.FromResult(null); + } + + private ZcashLikePaymentMethodViewModel GetZcashLikePaymentMethodViewModel( + IEnumerable Zcash, string cryptoCode, + IPaymentFilter excludeFilters, GetAccountsResponse accountsResponse) + { + var settings = Zcash.SingleOrDefault(method => method.CryptoCode == cryptoCode); + _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(new PaymentMethodId(cryptoCode, ZcashPaymentType.Instance)), + 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 GetStoreZcashLikePaymentMethod(string cryptoCode) + { + cryptoCode = cryptoCode.ToUpperInvariant(); + if (!_ZcashLikeConfiguration.ZcashLikeConfigurationItems.ContainsKey(cryptoCode)) + { + return NotFound(); + } + + var vm = GetZcashLikePaymentMethodViewModel(StoreData.GetSupportedPaymentMethods(_BtcPayNetworkProvider) + .OfType(), cryptoCode, + StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode)); + return View(nameof(GetStoreZcashLikePaymentMethod), vm); + } + + [HttpPost("{cryptoCode}")] + public async Task 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("create_account", new CreateAccountRequest() + { + Label = viewModel.NewAccountLabel + }); + viewModel.AccountIndex = newAccount.AccountIndex; + } + catch (Exception) + { + ModelState.AddModelError(nameof(viewModel.AccountIndex), "Could not create new account."); + } + + } + else if (command == "upload-wallet") + { + var valid = true; + if (viewModel.WalletFile == null) + { + ModelState.AddModelError(nameof(viewModel.WalletFile), "Please select the wallet file"); + valid = false; + } + if (viewModel.WalletKeysFile == null) + { + ModelState.AddModelError(nameof(viewModel.WalletKeysFile), "Please select the wallet.keys file"); + valid = false; + } + + if (valid) + { + if (_ZcashRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary)) + { + if (summary.WalletAvailable) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = $"There is already an active wallet configured for {cryptoCode}. Replacing it would break any existing invoices" + }); + 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 + .GetSupportedPaymentMethods(_BtcPayNetworkProvider) + .OfType(), cryptoCode, + StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode)); + + vm.Enabled = viewModel.Enabled; + vm.NewAccountLabel = viewModel.NewAccountLabel; + vm.AccountIndex = viewModel.AccountIndex; + return View(vm); + } + + var storeData = StoreData; + var blob = storeData.GetStoreBlob(); + storeData.SetSupportedPaymentMethod(new ZcashSupportedPaymentMethod() + { + AccountIndex = viewModel.AccountIndex, + CryptoCode = viewModel.CryptoCode + }); + + blob.SetExcluded(new PaymentMethodId(viewModel.CryptoCode, ZcashPaymentType.Instance), !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}\"" + } + }; + + process.Start(); + process.WaitForExit(); + } + + public class ZcashLikePaymentMethodListViewModel + { + public IEnumerable 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 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; } + } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashPaymentViewModel.cs b/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashPaymentViewModel.cs new file mode 100644 index 000000000..7c32c04ce --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashPaymentViewModel.cs @@ -0,0 +1,17 @@ +#if ALTCOINS +using System; + +namespace BTCPayServer.Services.Altcoins.Zcash.UI +{ + public class ZcashPaymentViewModel + { + public string Crypto { get; set; } + public string Confirmations { get; set; } + public string DepositAddress { get; set; } + public string Amount { get; set; } + public string TransactionId { get; set; } + public DateTimeOffset ReceivedTime { get; set; } + public string TransactionLink { get; set; } + } +} +#endif diff --git a/BTCPayServer/Services/Altcoins/Zcash/ZcashLikeExtensions.cs b/BTCPayServer/Services/Altcoins/Zcash/ZcashLikeExtensions.cs new file mode 100644 index 000000000..d6ed4d359 --- /dev/null +++ b/BTCPayServer/Services/Altcoins/Zcash/ZcashLikeExtensions.cs @@ -0,0 +1,73 @@ +#if ALTCOINS +using System; +using System.Linq; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Services; +using BTCPayServer.Configuration; +using BTCPayServer.Payments; +using BTCPayServer.Services.Altcoins.Zcash.Configuration; +using BTCPayServer.Services.Altcoins.Zcash.Payments; +using BTCPayServer.Services.Altcoins.Zcash.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Services.Altcoins.Zcash +{ + public static class ZcashLikeExtensions + { + public static IServiceCollection AddZcashLike(this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(provider => + provider.ConfigureZcashLikeConfiguration()); + serviceCollection.AddSingleton(); + serviceCollection.AddHostedService(); + serviceCollection.AddHostedService(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(provider => provider.GetService()); + serviceCollection.AddSingleton(new UIExtension("Zcash/StoreNavZcashExtension", "store-nav")); + serviceCollection.AddSingleton(); + + return serviceCollection; + } + + private static ZcashLikeConfiguration ConfigureZcashLikeConfiguration(this IServiceProvider serviceProvider) + { + var configuration = serviceProvider.GetService(); + var btcPayNetworkProvider = serviceProvider.GetService(); + var result = new ZcashLikeConfiguration(); + + var supportedChains = configuration.GetOrDefault("chains", string.Empty) + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.ToUpperInvariant()); + + var supportedNetworks = btcPayNetworkProvider.Filter(supportedChains.ToArray()).GetAll() + .OfType(); + + foreach (var ZcashLikeSpecificBtcPayNetwork in supportedNetworks) + { + var daemonUri = + configuration.GetOrDefault($"{ZcashLikeSpecificBtcPayNetwork.CryptoCode}_daemon_uri", + null); + var walletDaemonUri = + configuration.GetOrDefault( + $"{ZcashLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_uri", null); + var walletDaemonWalletDirectory = + configuration.GetOrDefault( + $"{ZcashLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null); + if (daemonUri == null || walletDaemonUri == null) + { + throw new ConfigException($"{ZcashLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured"); + } + + result.ZcashLikeConfigurationItems.Add(ZcashLikeSpecificBtcPayNetwork.CryptoCode, new ZcashLikeConfigurationItem() + { + DaemonRpcUri = daemonUri, + InternalWalletRpcUri = walletDaemonUri, + WalletDirectory = walletDaemonWalletDirectory + }); + } + return result; + } + } +} +#endif diff --git a/BTCPayServer/Views/Shared/Zcash/StoreNavZcashExtension.cshtml b/BTCPayServer/Views/Shared/Zcash/StoreNavZcashExtension.cshtml new file mode 100644 index 000000000..2a025c707 --- /dev/null +++ b/BTCPayServer/Views/Shared/Zcash/StoreNavZcashExtension.cshtml @@ -0,0 +1,12 @@ +@using BTCPayServer.Services.Altcoins.Zcash.Configuration +@using BTCPayServer.Services.Altcoins.Zcash.UI +@inject SignInManager SignInManager; +@inject ZcashLikeConfiguration ZcashLikeConfiguration; +@{ + var controller = ViewContext.RouteData.Values["Controller"].ToString(); + var isZcash = controller.Equals(nameof(UIZcashLikeStoreController), StringComparison.InvariantCultureIgnoreCase); +} +@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && ZcashLikeConfiguration.ZcashLikeConfigurationItems.Any()) +{ + Zcash +} diff --git a/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml new file mode 100644 index 000000000..89eea0072 --- /dev/null +++ b/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml @@ -0,0 +1,68 @@ +@using System.Globalization +@using BTCPayServer.Services.Altcoins.Zcash.Payments +@using BTCPayServer.Services.Altcoins.Zcash.UI +@model IEnumerable + +@{ + var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == ZcashPaymentType.Instance).Select(payment => + { + var m = new ZcashPaymentViewModel(); + var onChainPaymentData = payment.GetCryptoPaymentData() as ZcashLikePaymentData; + m.Crypto = payment.GetPaymentMethodId().CryptoCode; + m.DepositAddress = onChainPaymentData.GetDestination(); + m.Amount = onChainPaymentData.GetValue().ToString(CultureInfo.InvariantCulture); + var confirmationCount = onChainPaymentData.ConfirmationCount; + var network = payment.Network as ZcashLikeSpecificBtcPayNetwork; + if (confirmationCount >= network.MaxTrackedConfirmation) + { + m.Confirmations = "At least " + (network.MaxTrackedConfirmation); + } + else + { + m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture); + } + + m.TransactionId = onChainPaymentData.TransactionId; + m.ReceivedTime = payment.ReceivedTime; + m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId); + return m; + }); +} + +@if (onchainPayments.Any()) +{ +
+
+

Zcash payments

+ + + + + + + + + + + + @foreach (var payment in onchainPayments) + { + + + + + + + + } + +
CryptoDeposit addressAmountTransaction IdConfirmations
@payment.Crypto@payment.DepositAddress@payment.Amount + + @payment.Confirmations
+
+
+} diff --git a/BTCPayServer/Views/Shared/Zcash/ZcashSyncSummary.cshtml b/BTCPayServer/Views/Shared/Zcash/ZcashSyncSummary.cshtml new file mode 100644 index 000000000..5781d9a9b --- /dev/null +++ b/BTCPayServer/Views/Shared/Zcash/ZcashSyncSummary.cshtml @@ -0,0 +1,20 @@ +@using BTCPayServer.Services.Altcoins.Zcash.Services +@inject ZcashRPCProvider ZcashRpcProvider +@inject SignInManager SignInManager; + +@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && ZcashRpcProvider.Summaries.Any()) +{ + @foreach (var summary in ZcashRpcProvider.Summaries) + { + @if (summary.Value != null) + { +

@summary.Key

+
    +
  • Node available: @summary.Value.DaemonAvailable
  • +
  • Wallet available: @summary.Value.WalletAvailable
  • +
  • Last updated: @summary.Value.UpdatedAt
  • +
  • Synced: @summary.Value.Synced (@summary.Value.CurrentHeight / @summary.Value.TargetHeight)
  • +
+ } + } +} diff --git a/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethod.cshtml b/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethod.cshtml new file mode 100644 index 000000000..1666a53a8 --- /dev/null +++ b/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethod.cshtml @@ -0,0 +1,111 @@ +@using BTCPayServer.Views.Stores +@using BTCPayServer.Abstractions.Extensions +@model BTCPayServer.Services.Altcoins.Zcash.UI.UIZcashLikeStoreController.ZcashLikePaymentMethodViewModel + +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["NavPartialName"] = "../UIStores/_Nav"; + ViewData.SetActivePage(StoreNavPages.OnchainSettings, $"{Model.CryptoCode} Settings"); +} + +
+
+
+ @if (Model.Summary != null) + { +
+
    +
  • Node available: @Model.Summary.DaemonAvailable
  • +
  • Wallet available: @Model.Summary.WalletAvailable (@(Model.WalletFileFound ? "Wallet file present" : "Wallet file not found"))
  • +
  • Last updated: @Model.Summary.UpdatedAt
  • +
  • Synced: @Model.Summary.Synced (@Model.Summary.CurrentHeight / @Model.Summary.TargetHeight)
  • +
+
+ } + + @if (!Model.WalletFileFound || Model.Summary.WalletHeight == default) + { +
+ +
+

Upload Wallet

+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+ } +
+ + + @if (!Model.WalletFileFound || Model.Summary.WalletHeight == default) + { + + } + else + { +
+ + @if (@Model.Accounts != null && Model.Accounts.Any()) + { + + + } + else + { + No accounts available on the current wallet + + } +
+
+
+ + +
+
+ } + +
+ + + +
+ +
+ + + + Back to list + +
+
+
+
+ +@section PageFootContent { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethods.cshtml b/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethods.cshtml new file mode 100644 index 000000000..a03b2b228 --- /dev/null +++ b/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethods.cshtml @@ -0,0 +1,57 @@ +@using BTCPayServer.Views.Stores +@using BTCPayServer.Abstractions.Extensions +@model BTCPayServer.Services.Altcoins.Zcash.UI.UIZcashLikeStoreController.ZcashLikePaymentMethodListViewModel + +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData.SetActivePage(StoreNavPages.OnchainSettings, "Zcash Settings"); + ViewData["NavPartialName"] = "../UIStores/_Nav"; +} + +
+
+
+
+ + + + + + + + + + + @foreach (var item in Model.Items) + { + + + + + + + } + +
CryptoAccount IndexEnabledActions
@item.CryptoCode@item.AccountIndex + @if (item.Enabled) + { + + } + else + { + + } + + + Modify + +
+
+
+
+ +@section PageFootContent { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/wwwroot/imlegacy/ycash.png b/BTCPayServer/wwwroot/imlegacy/ycash.png new file mode 100644 index 0000000000000000000000000000000000000000..e8908b426796477337e6c882ce613d0ced65794d GIT binary patch literal 17759 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 literal 0 HcmV?d00001 diff --git a/BTCPayServer/wwwroot/imlegacy/zcash.png b/BTCPayServer/wwwroot/imlegacy/zcash.png new file mode 100644 index 0000000000000000000000000000000000000000..82de5b7ac1de0e9ff468a680980311b61c84c653 GIT binary patch literal 123472 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

$9+r_knkkIfTwf1vI#>`st{b)eAVwSUleMVZ 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 literal 0 HcmV?d00001