using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using BTCPayServer.Client.JsonConverters; using BTCPayServer.JsonConverters; using BTCPayServer.Payments; using BTCPayServer.Services; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.NewtonsoftJson; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.EntityFrameworkCore; using NBitcoin; using NBitcoin.Crypto; using NBitcoin.DataEncoders; using NBitcoin.JsonConverters; using NBitcoin.Payment; using Newtonsoft.Json; namespace BTCPayServer.Data { public static class PullPaymentsExtensions { public static async Task GetPayout(this DbSet payouts, string payoutId, string storeId, bool includePullPayment = false, bool includeStore = false) { IQueryable query = payouts; if (includePullPayment) query = query.Include(p => p.PullPaymentData); if (includeStore) query = query.Include(p => p.PullPaymentData.StoreData); var payout = await query.Where(p => p.Id == payoutId && p.PullPaymentData.StoreId == storeId).FirstOrDefaultAsync(); if (payout is null) return null; return payout; } public static PullPaymentBlob GetBlob(this PullPaymentData data) { return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data.Blob)); } public static void SetBlob(this PullPaymentData data, PullPaymentBlob blob) { data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob)); } public static PaymentMethodId GetPaymentMethodId(this PayoutData data) { return PaymentMethodId.Parse(data.PaymentMethodId); } public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers) { return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)); } public static void SetBlob(this PayoutData data, PayoutBlob blob, BTCPayNetworkJsonSerializerSettings serializers) { data.Blob = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode))); } public static bool IsSupported(this PullPaymentData data, BTCPayServer.Payments.PaymentMethodId paymentId) { return data.GetBlob().SupportedPaymentMethods.Contains(paymentId); } public static PayoutTransactionOnChainBlob GetProofBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers) { if (data.Proof is null) return null; return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data.Proof), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)); } public static void SetProofBlob(this PayoutData data, PayoutTransactionOnChainBlob blob, BTCPayNetworkJsonSerializerSettings serializers) { var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode))); // We only update the property if the bytes actually changed, this prevent from hammering the DB too much if (data.Proof is null || bytes.Length != data.Proof.Length || !bytes.SequenceEqual(data.Proof)) { data.Proof = bytes; } } } public class PayoutTransactionOnChainBlob { [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] public uint256 TransactionId { get; set; } [JsonProperty(ItemConverterType = typeof(NBitcoin.JsonConverters.UInt256JsonConverter), NullValueHandling = NullValueHandling.Ignore)] public HashSet Candidates { get; set; } = new HashSet(); } public interface IClaimDestination { BitcoinAddress Address { get; } } public static class ClaimDestination { public static bool TryParse(string destination, BTCPayNetwork network, out IClaimDestination claimDestination) { if (destination == null) throw new ArgumentNullException(nameof(destination)); destination = destination.Trim(); try { if (destination.StartsWith($"{network.UriScheme}:", StringComparison.OrdinalIgnoreCase)) { claimDestination = new UriClaimDestination(new BitcoinUrlBuilder(destination, network.NBitcoinNetwork)); } else { claimDestination = new AddressClaimDestination(BitcoinAddress.Create(destination, network.NBitcoinNetwork)); } return true; } catch { claimDestination = null; return false; } } } public class AddressClaimDestination : IClaimDestination { private readonly BitcoinAddress _bitcoinAddress; public AddressClaimDestination(BitcoinAddress bitcoinAddress) { if (bitcoinAddress == null) throw new ArgumentNullException(nameof(bitcoinAddress)); _bitcoinAddress = bitcoinAddress; } public BitcoinAddress BitcoinAdress => _bitcoinAddress; public BitcoinAddress Address => _bitcoinAddress; public override string ToString() { return _bitcoinAddress.ToString(); } } public class UriClaimDestination : IClaimDestination { private readonly BitcoinUrlBuilder _bitcoinUrl; public UriClaimDestination(BitcoinUrlBuilder bitcoinUrl) { if (bitcoinUrl == null) throw new ArgumentNullException(nameof(bitcoinUrl)); if (bitcoinUrl.Address is null) throw new ArgumentException(nameof(bitcoinUrl)); _bitcoinUrl = bitcoinUrl; } public BitcoinUrlBuilder BitcoinUrl => _bitcoinUrl; public BitcoinAddress Address => _bitcoinUrl.Address; public override string ToString() { return _bitcoinUrl.ToString(); } } public class PayoutBlob { [JsonConverter(typeof(DecimalStringJsonConverter))] public decimal Amount { get; set; } [JsonConverter(typeof(DecimalStringJsonConverter))] public decimal? CryptoAmount { get; set; } public int MinimumConfirmation { get; set; } = 1; public IClaimDestination Destination { get; set; } public int Revision { get; set; } } public class ClaimDestinationJsonConverter : JsonConverter { private readonly BTCPayNetwork _network; public ClaimDestinationJsonConverter(BTCPayNetwork network) { if (network == null) throw new ArgumentNullException(nameof(network)); _network = network; } public override IClaimDestination ReadJson(JsonReader reader, Type objectType, IClaimDestination existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; if (reader.TokenType != JsonToken.String) throw new JsonObjectException("Expected string for IClaimDestination", reader); if (ClaimDestination.TryParse((string)reader.Value, _network, out var v)) return v; throw new JsonObjectException("Invalid IClaimDestination", reader); } public override void WriteJson(JsonWriter writer, IClaimDestination value, JsonSerializer serializer) { if (value is IClaimDestination v) writer.WriteValue(v.ToString()); } } public class PullPaymentBlob { public string Name { get; set; } public string Currency { get; set; } public int Divisibility { get; set; } [JsonConverter(typeof(DecimalStringJsonConverter))] public decimal Limit { get; set; } [JsonConverter(typeof(DecimalStringJsonConverter))] public decimal MinimumClaim { get; set; } public PullPaymentView View { get; set; } = new PullPaymentView(); [JsonConverter(typeof(TimeSpanJsonConverter))] public TimeSpan? Period { get; set; } [JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))] public PaymentMethodId[] SupportedPaymentMethods { get; set; } } public class PullPaymentView { public string Title { get; set; } public string Description { get; set; } public string EmbeddedCSS { get; set; } public string Email { get; set; } public string CustomCSSLink { get; set; } } }