Zcash integration (#3400)

* zcash

* Use Channel instead of Queue
This commit is contained in:
hhanh00 2022-02-14 16:04:34 +08:00 committed by GitHub
parent 44e84b46b8
commit f4153ade92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 2216 additions and 2 deletions

View File

@ -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"
});
}
}
}

View File

@ -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<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

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<SubaddressAccount> SubaddressAccounts { get; set; }
[JsonProperty("total_balance")] public decimal TotalBalance { get; set; }
[JsonProperty("total_unlocked_balance")]
public decimal TotalUnlockedBalance { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<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

@ -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<long> SubaddrIndices { get; set; }
}
}

View File

@ -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<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

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<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

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<Peer> Peers { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("target_height")] public long? TargetHeight { get; set; }
}
}

View File

@ -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);
}
}
}

View File

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

View File

@ -55,6 +55,7 @@ namespace BTCPayServer
InitGroestlcoin();
InitViacoin();
InitMonero();
InitZcash();
InitPolis();
InitChaincoin();
// InitArgoneum();//their rate source is down 9/15/20.

View File

@ -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",

View File

@ -385,6 +385,7 @@ services:
- "19444:19444"
volumes:
- "elementsd_liquid_datadir:/data"
volumes:
sshd_datadir:
bitcoin_datadir:

View File

@ -40,7 +40,9 @@
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Content Remove="Services\Altcoins\**\*" />
<Content Remove="Views\UIMoneroLikeStore\**\*" />
<Content Remove="Views\UIZcashLikeStore\**\*" />
<Content Remove="Views\Shared\Monero\**\*" />
<Content Remove="Views\Shared\Zcash\**\*" />
</ItemGroup>
<ItemGroup>

View File

@ -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<SettingsRepository>();
services.TryAddSingleton<ISettingsRepository>(provider => provider.GetService<SettingsRepository>());

View File

@ -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)
{

View File

@ -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
};
/// <summary>
@ -39,6 +40,10 @@ namespace BTCPayServer.Payments
/// Monero payment
/// </summary>
public static MoneroPaymentType MoneroLike => MoneroPaymentType.Instance;
/// <summary>
/// Zcash payment
/// </summary>
public static ZcashPaymentType ZcashLike => ZcashPaymentType.Instance;
#endif
public static bool TryParse(string paymentType, out PaymentType type)

View File

@ -98,7 +98,7 @@
"BTCPAY_RECOMMENDED-PLUGINS": "BTCPayServer.Plugins.Test",
"BTCPAY_CHEATMODE": "true"
},
"applicationUrl": "https://localhost:14142/"
"applicationUrl": "https://localhost:14142/"
}
}
}

View File

@ -0,0 +1,20 @@
#if ALTCOINS
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; }
}
}
#endif

View File

@ -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

View File

@ -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

View File

@ -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<ZcashSupportedPaymentMethod, ZcashLikeSpecificBtcPayNetwork>
{
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<IPaymentMethodDetails> 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<GetFeeEstimateRequest, GetFeeEstimateResponse>("get_fee_estimate", new GetFeeEstimateRequest()),
ReserveAddress = s => walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>("create_address", new CreateAddressRequest() { Label = $"btcpay invoice #{s}", AccountIndex = supportedPaymentMethod.AccountIndex })
};
}
class Prepare
{
public Task<GetFeeEstimateResponse> GetFeeRate;
public Func<string, Task<CreateAddressResponse>> ReserveAddress;
}
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse,
StoreBlob storeBlob, IPaymentMethod paymentMethod)
{
var paymentMethodId = paymentMethod.GetId();
var network = _networkProvider.GetNetwork<ZcashLikeSpecificBtcPayNetwork>(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<ZcashLikeSpecificBtcPayNetwork>(paymentMethodId.CryptoCode);
return GetCryptoImage(network);
}
public override string GetPaymentMethodName(PaymentMethodId paymentMethodId)
{
var network = _networkProvider.GetNetwork<ZcashLikeSpecificBtcPayNetwork>(paymentMethodId.CryptoCode);
return GetPaymentMethodName(network);
}
public override IEnumerable<PaymentMethodId> 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

View File

@ -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<ZcashLikePaymentData>(str);
}
public override string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData)
{
return JsonConvert.SerializeObject(paymentData);
}
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
{
return JsonConvert.DeserializeObject<ZcashLikeOnChainPaymentMethodDetails>(str);
}
public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details)
{
return JsonConvert.SerializeObject(details);
}
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value)
{
return JsonConvert.DeserializeObject<ZcashSupportedPaymentMethod>(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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<ZcashListener> _logger;
private readonly PaymentService _paymentService;
private readonly CompositeDisposable leases = new CompositeDisposable();
private readonly Channel<Func<Task>> _requests = Channel.CreateUnbounded<Func<Task>>();
private CancellationTokenSource _Cts;
public ZcashListener(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
ZcashRPCProvider ZcashRpcProvider,
ZcashLikeConfiguration ZcashLikeConfiguration,
BTCPayNetworkProvider networkProvider,
ILogger<ZcashListener> 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<ZcashEvent>(OnZcashEvent));
leases.Add(_eventAggregator.Subscribe<ZcashRPCProvider.ZcashDaemonStateChange>(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<CreateAddressRequest, CreateAddressResponse>(
"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<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 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<GetTransferByTransactionIdRequest, GetTransferByTransactionIdResponse>(
"get_transfer_by_txid",
new GetTransferByTransactionIdRequest() { TransactionId = transactionHash });
var paymentsToUpdate = new BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)>();
//group all destinations of the tx together and loop through the sets
foreach (var destination in transfer.Transfers.GroupBy(destination => destination.Address))
{
//find the invoice corresponding to this address, else skip
var address = destination.Key + "#" + paymentMethodId;
var invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { address })).FirstOrDefault();
if (invoice == null)
{
continue;
}
var index = destination.First().SubaddrIndex;
await 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<ZcashLikeSpecificBtcPayNetwork>(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<PaymentEntity> GetAllZcashLikePayments(InvoiceEntity invoice, string cryptoCode)
{
return invoice.GetPayments(false)
.Where(p => p.GetPaymentMethodId() == new PaymentMethodId(cryptoCode, ZcashPaymentType.Instance));
}
}
}
#endif

View File

@ -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<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("{0} {1} {2} {3} {4} {5}", Synced, CurrentHeight, TargetHeight, WalletHeight, DaemonAvailable, WalletAvailable); }
}
}
}
#endif

View File

@ -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<ISyncStatus> 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

View File

@ -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<IActionResult> GetStoreZcashLikePaymentMethods()
{
var Zcash = StoreData.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
.OfType<ZcashSupportedPaymentMethod>();
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<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(
IEnumerable<ZcashSupportedPaymentMethod> 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<IActionResult> GetStoreZcashLikePaymentMethod(string cryptoCode)
{
cryptoCode = cryptoCode.ToUpperInvariant();
if (!_ZcashLikeConfiguration.ZcashLikeConfigurationItems.ContainsKey(cryptoCode))
{
return NotFound();
}
var vm = GetZcashLikePaymentMethodViewModel(StoreData.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
.OfType<ZcashSupportedPaymentMethod>(), cryptoCode,
StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode));
return View(nameof(GetStoreZcashLikePaymentMethod), vm);
}
[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), "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<ZcashSupportedPaymentMethod>(), 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<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; }
}
}
}
#endif

View File

@ -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

View File

@ -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<ZcashRPCProvider>();
serviceCollection.AddHostedService<ZcashLikeSummaryUpdaterHostedService>();
serviceCollection.AddHostedService<ZcashListener>();
serviceCollection.AddSingleton<ZcashLikePaymentMethodHandler>();
serviceCollection.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<ZcashLikePaymentMethodHandler>());
serviceCollection.AddSingleton<IUIExtension>(new UIExtension("Zcash/StoreNavZcashExtension", "store-nav"));
serviceCollection.AddSingleton<ISyncSummaryProvider, ZcashSyncSummaryProvider>();
return serviceCollection;
}
private static ZcashLikeConfiguration ConfigureZcashLikeConfiguration(this IServiceProvider serviceProvider)
{
var configuration = serviceProvider.GetService<IConfiguration>();
var btcPayNetworkProvider = serviceProvider.GetService<BTCPayNetworkProvider>();
var result = new ZcashLikeConfiguration();
var supportedChains = configuration.GetOrDefault<string>("chains", string.Empty)
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant());
var supportedNetworks = btcPayNetworkProvider.Filter(supportedChains.ToArray()).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)
{
throw new ConfigException($"{ZcashLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured");
}
result.ZcashLikeConfigurationItems.Add(ZcashLikeSpecificBtcPayNetwork.CryptoCode, new ZcashLikeConfigurationItem()
{
DaemonRpcUri = daemonUri,
InternalWalletRpcUri = walletDaemonUri,
WalletDirectory = walletDaemonWalletDirectory
});
}
return result;
}
}
}
#endif

View File

@ -0,0 +1,12 @@
@using BTCPayServer.Services.Altcoins.Zcash.Configuration
@using BTCPayServer.Services.Altcoins.Zcash.UI
@inject SignInManager<ApplicationUser> 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())
{
<a class="nav-link @(isZcash ? "active" : string.Empty)" asp-route-storeId="@this.Context.GetRouteValue("storeId")" asp-action="GetStoreZcashLikePaymentMethods" asp-controller="UIZcashLikeStore">Zcash</a>
}

View File

@ -0,0 +1,68 @@
@using System.Globalization
@using BTCPayServer.Services.Altcoins.Zcash.Payments
@using BTCPayServer.Services.Altcoins.Zcash.UI
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
@{
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())
{
<div class="row">
<div class="col-md-12 invoice-payments">
<h3>Zcash payments</h3>
<table class="table table-hover table-responsive-lg">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Deposit address</th>
<th>Amount</th>
<th>Transaction Id</th>
<th class="text-right">Confirmations</th>
</tr>
</thead>
<tbody>
@foreach (var payment in onchainPayments)
{
<tr >
<td>@payment.Crypto</td>
<td>@payment.DepositAddress</td>
<td>@payment.Amount</td>
<td>
<div class="wraptextAuto">
<a href="@payment.TransactionLink" target="_blank" rel="noreferrer noopener">
@payment.TransactionId
</a>
</div>
</td>
<td class="text-right">@payment.Confirmations</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}

View File

@ -0,0 +1,20 @@
@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)
{
<h4>@summary.Key</h4>
<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

@ -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");
}
<div class="row">
<div class="col-md-8">
<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">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="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>No accounts available on the current wallet</span>
<input type="hidden" asp-for="AccountIndex"/>
}
</div>
<div class="form-group">
<div class="input-group my-3">
<input type="text" class="form-control" placeholder="New account label" asp-for="NewAccountLabel">
<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">
<button type="submit" class="btn btn-primary" id="SaveButton">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">
Back to list
</a>
</div>
</form>
</div>
</div>
@section PageFootContent {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@ -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";
}
<div class="row">
<div class="col-md-8">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<table class="table table-hover table-responsive-md">
<thead>
<tr>
<th>Crypto</th>
<th>Account Index</th>
<th class="text-center">Enabled</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.CryptoCode</td>
<td>@item.AccountIndex</td>
<td class="text-center">
@if (item.Enabled)
{
<span class="text-success fa fa-check"></span>
}
else
{
<span class="text-danger fa fa-times"></span>
}
</td>
<td class="text-right">
<a id="Modify" asp-action="GetStoreZcashLikePaymentMethod"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@item.CryptoCode">
Modify
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@section PageFootContent {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB