Compare commits
36 Commits
better-lnu
...
fwoinq
Author | SHA1 | Date | |
---|---|---|---|
39052d67c6 | |||
4063a5aaee | |||
b1c81b696f | |||
0017f236a7 | |||
19d5e64063 | |||
22435a2bf5 | |||
a7def63137 | |||
3703a170e7 | |||
73fbfbd7cb | |||
acae3b8753 | |||
a618f901fc | |||
6d4918f0ab | |||
7f2c4d2e7a | |||
fd6d361e1a | |||
b5f0924651 | |||
1600dd4759 | |||
c777746b69 | |||
9f5466a41f | |||
4d1e4801bf | |||
5e469ff9c0 | |||
2f3eedea5b | |||
5c5d6dc1e2 | |||
fbe31ce64f | |||
0b082138c8 | |||
966e598f10 | |||
e998340387 | |||
f6b27cc5f9 | |||
f3dbf1e139 | |||
627d84fc91 | |||
8cde8c01df | |||
983b8c1f54 | |||
d666d8ea1a | |||
3ed81c3a78 | |||
4afec2e2b6 | |||
db83d238d5 | |||
fdcf7b3b7a |
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts
|
||||
@ -6,5 +7,8 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
{
|
||||
Task ApplyAction(string hook, object args);
|
||||
Task<object> ApplyFilter(string hook, object args);
|
||||
|
||||
event EventHandler<(string hook, object args)> ActionInvoked;
|
||||
event EventHandler<(string hook, object args)> FilterInvoked;
|
||||
}
|
||||
}
|
||||
|
@ -10,4 +10,9 @@ public class LightningAutomatedPayoutSettings
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan IntervalSeconds { get; set; }
|
||||
|
||||
public int? CancelPayoutAfterFailures { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||
|
||||
}
|
||||
|
@ -12,4 +12,8 @@ public class OnChainAutomatedPayoutSettings
|
||||
public TimeSpan IntervalSeconds { get; set; }
|
||||
|
||||
public int? FeeBlockTarget { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public decimal Threshold { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTG_X = BTG_BTC * BTC_X",
|
||||
"BTG_BTC = bitfinex(BTG_BTC)",
|
||||
"BTG_BTC = exmo(BTG_BTC)",
|
||||
},
|
||||
CryptoImagePath = "imlegacy/btg.svg",
|
||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||
|
@ -17,7 +17,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTX_X = BTX_BTC * BTC_X",
|
||||
"BTX_BTC = hitbtc(BTX_BTC)"
|
||||
"BTX_BTC = graviex(BTX_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/bitcore.svg",
|
||||
LightningImagePath = "imlegacy/bitcore-lightning.svg",
|
||||
|
@ -1,32 +0,0 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitChaincoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("CHC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Chaincoin",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet
|
||||
? "https://explorer.chaincoin.org/Explorer/Transaction/{0}"
|
||||
: "https://test.explorer.chaincoin.org/Explorer/Transaction/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"CHC_X = CHC_BTC * BTC_X",
|
||||
"CHC_BTC = txbit(CHC_X)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/chaincoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("711'")
|
||||
: new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ namespace BTCPayServer
|
||||
"LCAD_CAD = 1",
|
||||
"LCAD_X = CAD_BTC * BTC_X",
|
||||
"LCAD_BTC = bylls(CAD_BTC)",
|
||||
"CAD_BTC = LCAD_BTC"
|
||||
},
|
||||
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
|
||||
DisplayName = "Liquid CAD",
|
||||
|
@ -1,4 +1,5 @@
|
||||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Common;
|
||||
@ -34,12 +35,12 @@ namespace BTCPayServer
|
||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
|
||||
}
|
||||
|
||||
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
|
||||
{
|
||||
//precision 0: 10 = 0.00000010
|
||||
//precision 2: 10 = 0.00001000
|
||||
//precision 8: 10 = 10
|
||||
var money = cryptoInfoDue is null ? null : new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
|
||||
var money = cryptoInfoDue / (decimal)Math.Pow(10, 8 - Divisibility);
|
||||
var builder = base.GenerateBIP21(cryptoInfoAddress, money);
|
||||
builder.QueryParams.Add("assetid", AssetId.ToString());
|
||||
return builder;
|
||||
|
@ -45,10 +45,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.RPC
|
||||
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();
|
||||
HttpResponseMessage rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
||||
rawResult.EnsureSuccessStatusCode();
|
||||
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
||||
|
||||
JsonRpcResult<TResponse> response;
|
||||
try
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Common;
|
||||
@ -87,13 +88,13 @@ namespace BTCPayServer
|
||||
});
|
||||
}
|
||||
|
||||
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
|
||||
{
|
||||
var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
|
||||
builder.Host = cryptoInfoAddress;
|
||||
if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero)
|
||||
if (cryptoInfoDue is not null && cryptoInfoDue.Value != 0.0m)
|
||||
{
|
||||
builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true));
|
||||
builder.QueryParams.Add("amount", cryptoInfoDue.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ namespace BTCPayServer
|
||||
InitViacoin();
|
||||
InitMonero();
|
||||
InitZcash();
|
||||
InitChaincoin();
|
||||
// InitArgoneum();//their rate source is down 9/15/20.
|
||||
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin
|
||||
|
||||
|
@ -8,6 +8,7 @@ namespace BTCPayServer.Data;
|
||||
public class AutomatedPayoutBlob
|
||||
{
|
||||
public TimeSpan Interval { get; set; } = TimeSpan.FromHours(1);
|
||||
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||
}
|
||||
public class PayoutProcessorData : IHasBlobUntyped
|
||||
{
|
||||
|
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates;
|
||||
|
||||
|
||||
public class ExchangeRateHostRateProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("exchangeratehost", "Yadio", "https://api.exchangerate.host/latest?base=BTC");
|
||||
private readonly HttpClient _httpClient;
|
||||
public ExchangeRateHostRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
if(jobj["success"].Value<bool>() is not true || !jobj["base"].Value<string>().Equals("BTC", StringComparison.InvariantCulture))
|
||||
throw new Exception("exchangerate.host returned a non success response or the base currency was not the requested one (BTC)");
|
||||
var results = (JObject) jobj["rates"] ;
|
||||
//key value is currency code to rate value
|
||||
var list = new List<PairRate>();
|
||||
foreach (var item in results)
|
||||
{
|
||||
string name = item.Key;
|
||||
var value = item.Value.Value<decimal>();
|
||||
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates;
|
||||
|
||||
public class FreeCurrencyRatesRateProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/btc.min.json");
|
||||
private readonly HttpClient _httpClient;
|
||||
public FreeCurrencyRatesRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var results = (JObject) jobj["btc"] ;
|
||||
//key value is currency code to rate value
|
||||
var list = new List<PairRate>();
|
||||
foreach (var item in results)
|
||||
{
|
||||
string name = item.Key;
|
||||
var value = item.Value.Value<decimal>();
|
||||
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="112.0.5615.4900" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="114.0.5735.9000" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -346,165 +346,213 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
}
|
||||
[Fact]
|
||||
public void CanCalculateDust()
|
||||
{
|
||||
var entity = new InvoiceEntity() { Currency = "USD" };
|
||||
entity.Networks = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Rate = 34_000m
|
||||
});
|
||||
entity.Price = 4000;
|
||||
entity.UpdateTotals();
|
||||
var accounting = entity.GetPaymentMethods().First().Calculate();
|
||||
// Exact price should be 0.117647059..., but the payment method round up to one sat
|
||||
Assert.Equal(0.11764706m, accounting.Due);
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.11764706m), new Key()),
|
||||
Accounted = true
|
||||
});
|
||||
entity.UpdateTotals();
|
||||
Assert.Equal(0.0m, entity.NetDue);
|
||||
// The dust's value is below 1 sat
|
||||
Assert.True(entity.Dust > 0.0m);
|
||||
Assert.True(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC) * entity.Rates["BTC"] > entity.Dust);
|
||||
Assert.True(!entity.IsOverPaid);
|
||||
Assert.True(!entity.IsUnderPaid);
|
||||
|
||||
// Now, imagine there is litecoin. It might seem from its
|
||||
// perspecitve that there has been a slight over payment.
|
||||
// However, Calculate() should just cap it to 0.0m
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
Currency = "LTC",
|
||||
Rate = 3400m
|
||||
});
|
||||
entity.UpdateTotals();
|
||||
var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC");
|
||||
accounting = method.Calculate();
|
||||
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
#if ALTCOINS
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue()
|
||||
{
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var entity = new InvoiceEntity();
|
||||
var entity = new InvoiceEntity() { Currency = "USD" };
|
||||
entity.Networks = networkProvider;
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.Price = 5000;
|
||||
entity.UpdateTotals();
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||
Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
|
||||
Assert.Equal(1.1m, accounting.Due);
|
||||
Assert.Equal(1.1m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.5m), new Key()),
|
||||
Rate = 5000,
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
||||
Assert.Equal(Money.Coins(0.7m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
|
||||
Assert.Equal(0.7m, accounting.Due);
|
||||
Assert.Equal(1.2m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.2m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.6m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
Assert.Equal(0.6m, accounting.Due);
|
||||
Assert.Equal(1.3m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.6m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.3m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(
|
||||
new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
|
||||
new PaymentEntity() { Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.3m, accounting.TotalDue);
|
||||
|
||||
entity = new InvoiceEntity();
|
||||
entity.Networks = networkProvider;
|
||||
entity.Price = 5000;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod() { CryptoCode = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
|
||||
new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(5.1m), accounting.Due);
|
||||
Assert.Equal(5.1m, accounting.Due);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
|
||||
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
|
||||
Assert.Equal(10.01m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
|
||||
Assert.Equal(4.2m, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(1.0m, accounting.Paid);
|
||||
Assert.Equal(5.2m, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
|
||||
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
|
||||
Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due);
|
||||
Assert.Equal(0.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(2.0m, accounting.Paid);
|
||||
Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
Currency = "LTC",
|
||||
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.01m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
|
||||
Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(1.5m, accounting.Paid);
|
||||
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
|
||||
Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(3.0m, accounting.Paid);
|
||||
Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
|
||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC);
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Output = new TxOut(remaining, new Key()),
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(remaining), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.0m + remaining, accounting.CryptoPaid);
|
||||
Assert.Equal(1.5m + remaining, accounting.Paid);
|
||||
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(3.0m + remaining * 2, accounting.Paid);
|
||||
// Paying 2 BTC fee, LTC fee removed because fully paid
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */),
|
||||
Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */,
|
||||
accounting.TotalDue);
|
||||
Assert.Equal(1, accounting.TxRequired);
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
@ -548,27 +596,29 @@ namespace BTCPayServer.Tests
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.Price = 5000;
|
||||
entity.PaymentTolerance = 0;
|
||||
|
||||
entity.UpdateTotals();
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue);
|
||||
Assert.Equal(1.1m, accounting.Due);
|
||||
Assert.Equal(1.1m, accounting.TotalDue);
|
||||
Assert.Equal(1.1m, accounting.MinimumTotalDue);
|
||||
|
||||
entity.PaymentTolerance = 10;
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue);
|
||||
Assert.Equal(0.99m, accounting.MinimumTotalDue);
|
||||
|
||||
entity.PaymentTolerance = 100;
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue);
|
||||
Assert.Equal(0.0000_0001m, accounting.MinimumTotalDue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1064,7 +1114,7 @@ namespace BTCPayServer.Tests
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
||||
Assert.Equal("hekki", search.TextSearch);
|
||||
|
||||
|
||||
// modify search
|
||||
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
|
||||
search = new SearchString(filter);
|
||||
@ -1074,33 +1124,33 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(search.Filters["status"], "settled");
|
||||
Assert.Single(search.Filters["exceptionstatus"], "paidLate");
|
||||
Assert.Single(search.Filters["unusual"], "true");
|
||||
|
||||
|
||||
// toggle off bool with same value
|
||||
var modified = new SearchString(search.Toggle("unusual", "true"));
|
||||
Assert.Null(modified.GetFilterBool("unusual"));
|
||||
|
||||
|
||||
// add to array
|
||||
modified = new SearchString(modified.Toggle("status", "processing"));
|
||||
var statusArray = modified.GetFilterArray("status");
|
||||
Assert.Equal(2, statusArray.Length);
|
||||
Assert.Contains("processing", statusArray);
|
||||
Assert.Contains("settled", statusArray);
|
||||
|
||||
|
||||
// toggle off array with same value
|
||||
modified = new SearchString(modified.Toggle("status", "settled"));
|
||||
statusArray = modified.GetFilterArray("status");
|
||||
Assert.Single(statusArray, "processing");
|
||||
|
||||
|
||||
// toggle off array with null value
|
||||
modified = new SearchString(modified.Toggle("status", null));
|
||||
Assert.Null(modified.GetFilterArray("status"));
|
||||
|
||||
|
||||
// toggle off date with null value
|
||||
modified = new SearchString(modified.Toggle("startdate", "-7d"));
|
||||
Assert.Single(modified.GetFilterArray("startdate"), "-7d");
|
||||
modified = new SearchString(modified.Toggle("startdate", null));
|
||||
Assert.Null(modified.GetFilterArray("startdate"));
|
||||
|
||||
|
||||
// toggle off date with same value
|
||||
modified = new SearchString(modified.Toggle("enddate", "-7d"));
|
||||
Assert.Single(modified.GetFilterArray("enddate"), "-7d");
|
||||
@ -1145,6 +1195,45 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("000000161", m.OrderId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseOldPosAppData()
|
||||
{
|
||||
var data = new JObject()
|
||||
{
|
||||
["price"] = 1.64m
|
||||
}.ToString();
|
||||
Assert.Equal(1.64m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
|
||||
data = new JObject()
|
||||
{
|
||||
["price"] = new JObject()
|
||||
{
|
||||
["value"] = 1.65m
|
||||
}
|
||||
}.ToString();
|
||||
Assert.Equal(1.65m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
data = new JObject()
|
||||
{
|
||||
["price"] = new JObject()
|
||||
{
|
||||
["value"] = "1.6305"
|
||||
}
|
||||
}.ToString();
|
||||
Assert.Equal(1.6305m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
|
||||
data = new JObject()
|
||||
{
|
||||
["price"] = new JObject()
|
||||
{
|
||||
["value"] = null
|
||||
}
|
||||
}.ToString();
|
||||
Assert.Equal(0.0m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
|
||||
var o = JObject.Parse(JsonConvert.SerializeObject(new PosAppCartItem() { Price = 1.356m }));
|
||||
Assert.Equal(1.356m, o["price"].Value<decimal>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseCurrencyValue()
|
||||
{
|
||||
@ -1845,11 +1934,6 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning disable CS0618
|
||||
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
|
||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
||||
{
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
||||
});
|
||||
var networkBTC = networkProvider.GetNetwork("BTC");
|
||||
var networkLTC = networkProvider.GetNetwork("LTC");
|
||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||
@ -1857,14 +1941,14 @@ namespace BTCPayServer.Tests
|
||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
invoiceEntity.Price = 100;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, }
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, }
|
||||
.SetPaymentMethodDetails(
|
||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
NextNetworkFee = Money.Coins(0.00000100m),
|
||||
DepositAddress = dummy
|
||||
}));
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m }
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, Currency = "LTC", Rate = 216.79m }
|
||||
.SetPaymentMethodDetails(
|
||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
@ -1880,7 +1964,7 @@ namespace BTCPayServer.Tests
|
||||
new PaymentEntity()
|
||||
{
|
||||
Accounted = true,
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
NetworkFee = 0.00000100m,
|
||||
Network = networkProvider.GetNetwork("BTC"),
|
||||
}
|
||||
@ -1889,34 +1973,33 @@ namespace BTCPayServer.Tests
|
||||
Network = networkProvider.GetNetwork("BTC"),
|
||||
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
|
||||
}));
|
||||
invoiceEntity.UpdateTotals();
|
||||
accounting = btc.Calculate();
|
||||
invoiceEntity.Payments.Add(
|
||||
new PaymentEntity()
|
||||
{
|
||||
Accounted = true,
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
NetworkFee = 0.00000100m,
|
||||
Network = networkProvider.GetNetwork("BTC")
|
||||
}
|
||||
.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||
{
|
||||
Network = networkProvider.GetNetwork("BTC"),
|
||||
Output = new TxOut() { Value = accounting.Due }
|
||||
Output = new TxOut() { Value = Money.Coins(accounting.Due) }
|
||||
}));
|
||||
invoiceEntity.UpdateTotals();
|
||||
accounting = btc.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Zero, accounting.DueUncapped);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||
|
||||
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = ltc.Calculate();
|
||||
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up)
|
||||
Assert.True(accounting.DueUncapped < Money.Zero);
|
||||
|
||||
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2);
|
||||
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
|
||||
#pragma warning restore CS0618
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
// LTC might should be over paid due to BTC paying above what it should (round 1 satoshi up), but we handle this case
|
||||
// and set DueUncapped to zero.
|
||||
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.OnChain;
|
||||
using BTCPayServer.Plugins;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
@ -3669,9 +3670,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
|
||||
});
|
||||
|
||||
var txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||
uint256 txid = null;
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
|
||||
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
@ -3679,6 +3683,122 @@ namespace BTCPayServer.Tests
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
||||
});
|
||||
|
||||
// settings that were added later
|
||||
var settings =
|
||||
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.False( settings.ProcessNewPayoutsInstantly);
|
||||
Assert.Equal(0m, settings.Threshold);
|
||||
|
||||
//let's use the ProcessNewPayoutsInstantly so that it will trigger instantly
|
||||
|
||||
settings.IntervalSeconds = TimeSpan.FromDays(1);
|
||||
settings.ProcessNewPayoutsInstantly = true;
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(1m) + fee);
|
||||
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||
|
||||
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
|
||||
settings =
|
||||
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.True( settings.ProcessNewPayoutsInstantly);
|
||||
|
||||
var pluginHookService = tester.PayTester.GetService<IPluginHookService>();
|
||||
var beforeHookTcs = new TaskCompletionSource();
|
||||
var afterHookTcs = new TaskCompletionSource();
|
||||
pluginHookService.ActionInvoked += (sender, tuple) =>
|
||||
{
|
||||
switch (tuple.hook)
|
||||
{
|
||||
case "before-automated-payout-processing":
|
||||
beforeHookTcs.TrySetResult();
|
||||
break;
|
||||
case "after-automated-payout-processing":
|
||||
afterHookTcs.TrySetResult();
|
||||
break;
|
||||
}
|
||||
};
|
||||
var payoutThatShouldBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
PullPaymentId = pullPayment.Id,
|
||||
Amount = 0.5m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id));
|
||||
|
||||
beforeHookTcs = new TaskCompletionSource();
|
||||
afterHookTcs = new TaskCompletionSource();
|
||||
//let's test the threshold limiter
|
||||
settings.Threshold = 0.5m;
|
||||
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
|
||||
|
||||
//quick test: when updating processor, it processes instantly
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
settings =
|
||||
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.Equal(0.5m, settings.Threshold);
|
||||
|
||||
//create a payout that should not be processed straight away due to threshold
|
||||
|
||||
beforeHookTcs = new TaskCompletionSource();
|
||||
afterHookTcs = new TaskCompletionSource();
|
||||
var payoutThatShouldNotBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Amount = 0.1m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Single(payouts.Where(data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id));
|
||||
|
||||
beforeHookTcs = new TaskCompletionSource();
|
||||
afterHookTcs = new TaskCompletionSource();
|
||||
var payoutThatShouldNotBeProcessedStraightAway2 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Amount = 0.3m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Equal(2, payouts.Count(data => data.State == PayoutState.AwaitingPayment &&
|
||||
(data.Id == payoutThatShouldNotBeProcessedStraightAway.Id || data.Id == payoutThatShouldNotBeProcessedStraightAway2.Id)));
|
||||
|
||||
beforeHookTcs = new TaskCompletionSource();
|
||||
afterHookTcs = new TaskCompletionSource();
|
||||
var payoutThatShouldNotBeProcessedStraightAway3 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Amount = 0.3m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
||||
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
|
@ -393,6 +393,10 @@ namespace BTCPayServer.Tests
|
||||
public void GoToHome()
|
||||
{
|
||||
Driver.Navigate().GoToUrl(ServerUri);
|
||||
if (Driver.PageSource.Contains("id=\"SkipWizard\""))
|
||||
{
|
||||
Driver.FindElement(By.Id("SkipWizard")).Click();
|
||||
}
|
||||
}
|
||||
|
||||
public void Logout()
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@ -56,10 +57,11 @@ namespace BTCPayServer.Tests
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
|
||||
s.GoToHome();
|
||||
s.GoToServer();
|
||||
s.Driver.AssertNoError();
|
||||
s.ClickOnAllSectionLinks();
|
||||
s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
|
||||
s.GoToServer();
|
||||
s.Driver.FindElement(By.LinkText("Services")).Click();
|
||||
|
||||
TestLogs.LogInformation("Let's check if we can access the logs");
|
||||
@ -246,7 +248,8 @@ namespace BTCPayServer.Tests
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
|
||||
s.GoToHome();
|
||||
s.GoToServer();
|
||||
s.Driver.AssertNoError();
|
||||
s.Driver.FindElement(By.LinkText("Services")).Click();
|
||||
|
||||
@ -313,6 +316,7 @@ namespace BTCPayServer.Tests
|
||||
await s.StartAsync();
|
||||
//Register & Log Out
|
||||
var email = s.RegisterNewUser();
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
s.Driver.AssertNoError();
|
||||
Assert.Contains("/login", s.Driver.Url);
|
||||
@ -348,6 +352,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Password")).SendKeys("abc???");
|
||||
s.Driver.FindElement(By.Id("LoginButton")).Click();
|
||||
|
||||
s.GoToHome();
|
||||
s.GoToProfile();
|
||||
s.ClickOnAllSectionLinks();
|
||||
|
||||
@ -355,6 +360,7 @@ namespace BTCPayServer.Tests
|
||||
s.Logout();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser(true);
|
||||
s.GoToHome();
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("CreateUser")).Click();
|
||||
|
||||
@ -377,6 +383,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("LoginButton")).Click();
|
||||
|
||||
// We should be logged in now
|
||||
s.GoToHome();
|
||||
s.Driver.FindElement(By.Id("mainNav"));
|
||||
|
||||
//let's test delete user quickly while we're at it
|
||||
@ -641,7 +648,7 @@ namespace BTCPayServer.Tests
|
||||
// verify redirected to create store page
|
||||
Assert.EndsWith("/stores/create", s.Driver.Url);
|
||||
Assert.Contains("Create your first store", s.Driver.PageSource);
|
||||
Assert.Contains("To start accepting payments, set up a store.", s.Driver.PageSource);
|
||||
Assert.Contains("Create a store to begin accepting payments", s.Driver.PageSource);
|
||||
Assert.False(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should not be present");
|
||||
|
||||
(_, string storeId) = s.CreateNewStore();
|
||||
@ -961,11 +968,13 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
|
||||
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
|
||||
s.Driver.FindElement(By.Id("EditorCategories-ts-control")).SendKeys("Drinks");
|
||||
s.Driver.FindElement(By.Id("SaveItemChanges")).Click();
|
||||
s.Driver.FindElement(By.Id("ToggleRawEditor")).Click();
|
||||
|
||||
var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value");
|
||||
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
|
||||
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
@ -979,6 +988,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
|
||||
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
|
||||
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")).Count);
|
||||
|
||||
var drinks = s.Driver.FindElement(By.CssSelector("label[for='Category-Drinks']"));
|
||||
Assert.Equal("Drinks", drinks.Text);
|
||||
drinks.Click();
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")));
|
||||
s.Driver.FindElement(By.CssSelector("label[for='Category-*']")).Click();
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")).Count);
|
||||
|
||||
s.Driver.Url = posBaseUrl + "/static";
|
||||
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
|
||||
@ -1145,12 +1162,13 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("ArchivePaymentRequest")).Click();
|
||||
Assert.Contains("The payment request has been archived", s.FindAlertMessage().Text);
|
||||
Assert.DoesNotContain("Pay123", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("SearchDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("SearchIncludeArchived")).Click();
|
||||
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("StatusOptionsIncludeArchived")).Click();
|
||||
Assert.Contains("Pay123", s.Driver.PageSource);
|
||||
|
||||
// unarchive (from list)
|
||||
s.Driver.FindElement(By.Id($"ToggleArchival-{payReqId}")).Click();
|
||||
s.Driver.FindElement(By.Id($"ToggleActions-{payReqId}")).Click();
|
||||
s.Driver.WaitForElement(By.Id($"ToggleArchival-{payReqId}")).Click();
|
||||
Assert.Contains("The payment request has been unarchived", s.FindAlertMessage().Text);
|
||||
Assert.Contains("Pay123", s.Driver.PageSource);
|
||||
}
|
||||
@ -2150,6 +2168,70 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("1 222,21 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUsePOSCart()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
|
||||
s.Driver.FindElement(By.Id("ShowCustomAmount")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.WaitForElement(By.Id("js-cart-list"));
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr")));
|
||||
Assert.Equal("0,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
Assert.False(s.Driver.FindElement(By.Id("CartClear")).Displayed);
|
||||
|
||||
// Select and clear
|
||||
s.Driver.FindElement(By.CssSelector(".card.js-add-cart:nth-child(1)")).Click();
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr")));
|
||||
s.Driver.FindElement(By.Id("CartClear")).Click();
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr")));
|
||||
Thread.Sleep(250);
|
||||
|
||||
// Select items
|
||||
s.Driver.FindElement(By.CssSelector(".card.js-add-cart:nth-child(2)")).Click();
|
||||
Thread.Sleep(250);
|
||||
s.Driver.FindElement(By.CssSelector(".card.js-add-cart:nth-child(1)")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#js-cart-list tbody tr")).Count);
|
||||
Assert.Equal("2,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Custom amount
|
||||
s.Driver.FindElement(By.Id("CartCustomAmount")).SendKeys("1.5");
|
||||
s.Driver.FindElement(By.Id("CartTotal")).Click();
|
||||
Assert.Equal("3,50 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
s.Driver.FindElement(By.Id("js-cart-confirm")).Click();
|
||||
|
||||
// Pay
|
||||
Assert.Equal("3,50 €", s.Driver.FindElement(By.Id("CartSummaryTotal")).Text);
|
||||
s.Driver.FindElement(By.Id("js-cart-pay")).Click();
|
||||
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
|
||||
Assert.Contains("3,50 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
@ -2379,7 +2461,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var addresses = s.Driver.FindElements(By.ClassName("lightning-address-value"));
|
||||
Assert.Equal(2, addresses.Count);
|
||||
|
||||
var callbacks = new List<Uri>();
|
||||
foreach (IWebElement webElement in addresses)
|
||||
{
|
||||
var value = webElement.GetAttribute("value");
|
||||
@ -2397,6 +2479,7 @@ namespace BTCPayServer.Tests
|
||||
lnaddress2 = m["text/identifier"];
|
||||
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
callbacks.Add(request.Callback);
|
||||
break;
|
||||
|
||||
case { } v when v.StartsWith(lnaddress1):
|
||||
@ -2404,6 +2487,7 @@ namespace BTCPayServer.Tests
|
||||
lnaddress1 = m["text/identifier"];
|
||||
Assert.Equal(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
|
||||
callbacks.Add(request.Callback);
|
||||
break;
|
||||
default:
|
||||
Assert.False(true, "Should have matched");
|
||||
@ -2411,7 +2495,19 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
var repo = s.Server.PayTester.GetService<InvoiceRepository>();
|
||||
|
||||
var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
|
||||
// Resolving a ln address shouldn't create any btcpay invoice.
|
||||
// This must be done because some NOST clients resolve ln addresses preemptively without user interaction
|
||||
Assert.Empty(invoices);
|
||||
|
||||
// Calling the callbacks should create the invoices
|
||||
foreach (var callback in callbacks)
|
||||
{
|
||||
using var r = await s.Server.PayTester.HttpClient.GetAsync(callback);
|
||||
await r.Content.ReadAsStringAsync();
|
||||
}
|
||||
invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
|
||||
Assert.Equal(2, invoices.Length);
|
||||
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
|
||||
foreach (var i in invoices)
|
||||
@ -2493,6 +2589,7 @@ namespace BTCPayServer.Tests
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
var user = s.RegisterNewUser();
|
||||
s.GoToHome();
|
||||
s.GoToProfile(ManageNavPages.LoginCodes);
|
||||
var code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||
s.Driver.FindElement(By.Id("regeneratecode")).Click();
|
||||
@ -2504,14 +2601,12 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.SetAttribute("LoginCode", "value", "bad code");
|
||||
s.Driver.InvokeJSFunction("logincode-form", "submit");
|
||||
|
||||
|
||||
s.Driver.SetAttribute("LoginCode", "value", code);
|
||||
s.Driver.InvokeJSFunction("logincode-form", "submit");
|
||||
s.GoToProfile();
|
||||
s.GoToHome();
|
||||
Assert.Contains(user, s.Driver.PageSource);
|
||||
}
|
||||
|
||||
|
||||
// For god know why, selenium have problems clicking on the save button, resulting in ultimate hacks
|
||||
// to make it works.
|
||||
private void SudoForceSaveLightningSettingsRightNowAndFast(SeleniumTester s, string cryptoCode)
|
||||
@ -2530,7 +2625,6 @@ retry:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanUseLNURLAuth()
|
||||
@ -2538,6 +2632,7 @@ retry:
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
var user = s.RegisterNewUser(true);
|
||||
s.GoToHome();
|
||||
s.GoToProfile(ManageNavPages.TwoFactorAuthentication);
|
||||
s.Driver.FindElement(By.Name("Name")).SendKeys("ln wallet");
|
||||
s.Driver.FindElement(By.Name("type"))
|
||||
@ -2586,7 +2681,8 @@ retry:
|
||||
{
|
||||
using var s = CreateSeleniumTester(newDb: true);
|
||||
await s.StartAsync();
|
||||
var user = s.RegisterNewUser(true);
|
||||
s.RegisterNewUser(true);
|
||||
s.GoToHome();
|
||||
s.GoToServer(ServerNavPages.Roles);
|
||||
var existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||
Assert.Equal(3, existingServerRoles.Count);
|
||||
|
@ -290,9 +290,9 @@ retry:
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanGetRateCryptoCurrenciesByDefault()
|
||||
public async Task CanGetRateCryptoCurrenciesByDefault()
|
||||
{
|
||||
string[] brokenShitcoins = { "BTX_USD", "CHC_USD" };
|
||||
string[] brokenShitcoins = { };
|
||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
@ -305,17 +305,37 @@ retry:
|
||||
var result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = value.GetAwaiter().GetResult();
|
||||
if (key.ToString() == "BTG_USD")
|
||||
continue; // shitcoin not supported by bitfinex anymore
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
|
||||
var b = new StoreBlob();
|
||||
foreach (var k in StoreBlob.RecommendedExchanges)
|
||||
{
|
||||
b.DefaultCurrency = k.Key;
|
||||
rules = b.GetDefaultRateRules(provider);
|
||||
pairs =
|
||||
provider.GetAll()
|
||||
.Select(c => new CurrencyPair(c.CryptoCode, k.Key))
|
||||
.ToHashSet();
|
||||
result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CheckJsContent()
|
||||
{
|
||||
// This test verify that no malicious js is added in the minified files.
|
||||
@ -324,52 +344,63 @@ retry:
|
||||
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
|
||||
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js");
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim();
|
||||
version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim();
|
||||
version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||
version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://unpkg.com/sortablejs@{version}/Sortable.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap-vue", "bootstrap-vue.min.js").Trim();
|
||||
version = Regex.Match(actual, "BootstrapVue ([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/{version}/bootstrap-vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sanitize-directive", "vue-sanitize-directive.umd.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/vue-sanitize-directive@([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/vue-sanitize-directive@{version}/dist/vue-sanitize-directive.umd.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
}
|
||||
|
||||
private void EqualJsContent(string expected, string actual)
|
||||
{
|
||||
if (expected != actual)
|
||||
Assert.Equal(expected, actual.ReplaceLineEndings("\n"));
|
||||
}
|
||||
|
||||
string GetFileContent(params string[] path)
|
||||
|
@ -1761,7 +1761,7 @@ namespace BTCPayServer.Tests
|
||||
var parsedJson = await GetExport(user);
|
||||
Assert.Equal(3, parsedJson.Length);
|
||||
|
||||
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
|
||||
var invoiceDueAfterFirstPayment = 3 * networkFee.ToDecimal(MoneyUnit.BTC) * invoice.Rate;
|
||||
var pay1str = parsedJson[0].ToString();
|
||||
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
|
||||
|
@ -224,7 +224,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -259,7 +259,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -211,7 +211,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -248,7 +248,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -24,7 +24,7 @@ public class AppTopItems : ViewComponent
|
||||
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
||||
{
|
||||
var type = _appService.GetAppType(appType);
|
||||
if (type is not IHasItemStatsAppType salesAppType || type is not AppBaseType appBaseType)
|
||||
if (type is not (IHasItemStatsAppType and AppBaseType appBaseType))
|
||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||
|
||||
var vm = new AppTopItemsViewModel
|
||||
|
@ -3,6 +3,7 @@
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
|
||||
|
||||
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
|
||||
@ -51,27 +52,41 @@
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
@if (invoice.Details.Archived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString()
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if (invoice.Details.Archived)
|
||||
{
|
||||
@($"({invoice.Status.ExceptionStatus.ToString()})")
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
</span>
|
||||
@foreach (var paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))
|
||||
{
|
||||
<span class="badge">@paymentType.GetBadge()</span>
|
||||
}
|
||||
@if (invoice.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">
|
||||
Refund
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString()
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
@($"({invoice.Status.ExceptionStatus.ToString()})")
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@foreach (var paymentMethodId in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()).Distinct())
|
||||
{
|
||||
var image = PaymentMethodHandlerDictionary[paymentMethodId]?.GetCryptoImage(paymentMethodId);
|
||||
var badge = paymentMethodId.PaymentType.GetBadge();
|
||||
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
|
||||
{
|
||||
<span class="d-inline-flex align-items-center gap-1">
|
||||
@if (!string.IsNullOrEmpty(image))
|
||||
{
|
||||
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.PaymentType.ToString()" style="height:1.5em" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(badge))
|
||||
{
|
||||
@badge
|
||||
}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
@if (invoice.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">Refund</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
|
@ -1,6 +1,7 @@
|
||||
@model BTCPayServer.Components.TruncateCenter.TruncateCenterViewModel
|
||||
@{
|
||||
var classes = string.IsNullOrEmpty(Model.Classes) ? string.Empty : Model.Classes.Trim();
|
||||
var isTruncated = !string.IsNullOrEmpty(Model.Start) && !string.IsNullOrEmpty(Model.End);
|
||||
@if (Model.Copy) classes += " truncate-center--copy";
|
||||
@if (Model.Elastic) classes += " truncate-center--elastic";
|
||||
}
|
||||
@ -15,9 +16,12 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="truncate-center-truncated" @(!string.IsNullOrEmpty(Model.Start) ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>
|
||||
<span class="truncate-center-start">@(Model.Elastic ? Model.Text : $"{Model.Start}…")</span>
|
||||
<span class="truncate-center-end">@Model.End</span>
|
||||
<span class="truncate-center-truncated" @(isTruncated ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>
|
||||
<span class="truncate-center-start">@(Model.Elastic || !isTruncated ? Model.Text : $"{Model.Start}…")</span>
|
||||
@if (isTruncated)
|
||||
{
|
||||
<span class="truncate-center-end">@Model.End</span>
|
||||
}
|
||||
</span>
|
||||
<span class="truncate-center-text">@Model.Text</span>
|
||||
}
|
||||
|
@ -396,7 +396,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var accounting = invoicePaymentMethod.Calculate();
|
||||
var cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
|
||||
var cryptoPaid = accounting.Paid;
|
||||
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
|
||||
var rateResult = await _rateProvider.FetchRate(
|
||||
@ -464,7 +464,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
|
||||
var dueAmount = accounting.TotalDue;
|
||||
createPullPayment.Currency = cryptoCode;
|
||||
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
@ -580,11 +580,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
CryptoCode = method.GetId().CryptoCode,
|
||||
Destination = details.GetPaymentDestination(),
|
||||
Rate = method.Rate,
|
||||
Due = accounting.DueUncapped.ToDecimal(MoneyUnit.BTC),
|
||||
TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC),
|
||||
PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
|
||||
Amount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC),
|
||||
NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC),
|
||||
Due = accounting.DueUncapped,
|
||||
TotalPaid = accounting.Paid,
|
||||
PaymentMethodPaid = accounting.CryptoPaid,
|
||||
Amount = accounting.TotalDue,
|
||||
NetworkFee = accounting.NetworkFee,
|
||||
PaymentLink =
|
||||
method.GetId().PaymentType.GetPaymentLink(method.Network, entity, details, accounting.Due,
|
||||
Request.GetAbsoluteRoot()),
|
||||
|
@ -53,16 +53,23 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
private static LightningAutomatedPayoutSettings ToModel(PayoutProcessorData data)
|
||||
{
|
||||
var blob = data.HasTypedBlob<LightningAutomatedPayoutBlob>().GetBlob();
|
||||
return new LightningAutomatedPayoutSettings()
|
||||
{
|
||||
PaymentMethod = data.PaymentMethod,
|
||||
IntervalSeconds = data.HasTypedBlob<AutomatedPayoutBlob>().GetBlob()!.Interval
|
||||
IntervalSeconds = blob.Interval,
|
||||
CancelPayoutAfterFailures = blob.CancelPayoutAfterFailures,
|
||||
ProcessNewPayoutsInstantly = blob.ProcessNewPayoutsInstantly
|
||||
};
|
||||
}
|
||||
|
||||
private static AutomatedPayoutBlob FromModel(LightningAutomatedPayoutSettings data)
|
||||
private static LightningAutomatedPayoutBlob FromModel(LightningAutomatedPayoutSettings data)
|
||||
{
|
||||
return new AutomatedPayoutBlob() { Interval = data.IntervalSeconds };
|
||||
return new LightningAutomatedPayoutBlob() {
|
||||
Interval = data.IntervalSeconds,
|
||||
CancelPayoutAfterFailures = data.CancelPayoutAfterFailures,
|
||||
ProcessNewPayoutsInstantly = data.ProcessNewPayoutsInstantly
|
||||
};
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
@ -84,7 +91,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
activeProcessor ??= new PayoutProcessorData();
|
||||
activeProcessor.HasTypedBlob<AutomatedPayoutBlob>().SetBlob(FromModel(request));
|
||||
activeProcessor.HasTypedBlob<LightningAutomatedPayoutBlob>().SetBlob(FromModel(request));
|
||||
activeProcessor.StoreId = storeId;
|
||||
activeProcessor.PaymentMethod = paymentMethod;
|
||||
activeProcessor.Processor = LightningAutomatedPayoutSenderFactory.ProcessorName;
|
||||
|
@ -59,7 +59,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
FeeBlockTarget = blob.FeeTargetBlock,
|
||||
PaymentMethod = data.PaymentMethod,
|
||||
IntervalSeconds = blob.Interval
|
||||
IntervalSeconds = blob.Interval,
|
||||
Threshold = blob.Threshold,
|
||||
ProcessNewPayoutsInstantly = blob.ProcessNewPayoutsInstantly
|
||||
};
|
||||
}
|
||||
|
||||
@ -68,7 +70,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return new OnChainAutomatedPayoutBlob()
|
||||
{
|
||||
FeeTargetBlock = data.FeeBlockTarget ?? 1,
|
||||
Interval = data.IntervalSeconds
|
||||
Interval = data.IntervalSeconds,
|
||||
Threshold = data.Threshold,
|
||||
ProcessNewPayoutsInstantly = data.ProcessNewPayoutsInstantly
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -129,6 +129,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
|
||||
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
DefaultCurrency = storeBlob.DefaultCurrency,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
CheckoutType = storeBlob.CheckoutType,
|
||||
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
||||
|
@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
@ -8,6 +11,7 @@ using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -21,17 +25,20 @@ namespace BTCPayServer.Controllers
|
||||
public UIAppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository,
|
||||
IFileService fileService,
|
||||
AppService appService,
|
||||
IHtmlHelper html)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_storeRepository = storeRepository;
|
||||
_fileService = fileService;
|
||||
_appService = appService;
|
||||
Html = html;
|
||||
}
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly AppService _appService;
|
||||
|
||||
public string CreatedAppId { get; set; }
|
||||
@ -184,13 +191,50 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/upload-file")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> FileUpload(IFormFile file)
|
||||
{
|
||||
var app = GetCurrentApp();
|
||||
var userId = GetUserId();
|
||||
if (app is null || userId is null)
|
||||
return NotFound();
|
||||
|
||||
if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
{
|
||||
return Json(new { error = "The file needs to be an image" });
|
||||
}
|
||||
if (file.Length > 500_000)
|
||||
{
|
||||
return Json(new { error = "The image file size should be less than 0.5MB" });
|
||||
}
|
||||
var formFile = await file.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
{
|
||||
return Json(new { error = "The file needs to be an image" });
|
||||
}
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(file, userId);
|
||||
var fileId = storedFile.Id;
|
||||
var fileUrl = await _fileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
|
||||
return Json(new { fileId, fileUrl });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Json(new { error = $"Could not save file: {e.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currency))
|
||||
{
|
||||
currency = (await _storeRepository.FindStore(storeId)).GetStoreBlob().DefaultCurrency;
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
currency = store?.GetStoreBlob().DefaultCurrency;
|
||||
}
|
||||
return currency.Trim().ToUpperInvariant();
|
||||
return currency?.Trim().ToUpperInvariant();
|
||||
}
|
||||
|
||||
private string GetUserId() => _userManager.GetUserId(User);
|
||||
|
@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
return Ok(new
|
||||
{
|
||||
Txid = txid,
|
||||
AmountRemaining = (paymentMethod.Calculate().Due - amount).ToUnit(MoneyUnit.BTC),
|
||||
AmountRemaining = paymentMethod.Calculate().Due - amount.ToDecimal(MoneyUnit.BTC),
|
||||
SuccessMessage = $"Created transaction {txid}"
|
||||
});
|
||||
|
||||
@ -70,11 +70,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var bolt11 = BOLT11PaymentRequest.Parse(destination, network);
|
||||
var paymentHash = bolt11.PaymentHash?.ToString();
|
||||
var paid = new Money(response.Details.TotalAmount.ToUnit(LightMoneyUnit.Satoshi), MoneyUnit.Satoshi);
|
||||
var paid = response.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||
return Ok(new
|
||||
{
|
||||
Txid = paymentHash,
|
||||
AmountRemaining = (paymentMethod.Calculate().TotalDue - paid).ToUnit(MoneyUnit.BTC),
|
||||
AmountRemaining = paymentMethod.Calculate().TotalDue - paid,
|
||||
SuccessMessage = $"Sent payment {paymentHash}"
|
||||
});
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
@ -228,18 +229,14 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
string txId = paymentData.GetPaymentId();
|
||||
string? link = GetTransactionLink(paymentMethodId, txId);
|
||||
var paymentMethod = i.GetPaymentMethod(paymentMethodId);
|
||||
var amount = paymentData.GetValue();
|
||||
var rate = paymentMethod.Rate;
|
||||
var paid = (amount - paymentEntity.NetworkFee) * rate;
|
||||
|
||||
|
||||
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
|
||||
{
|
||||
Amount = amount,
|
||||
Paid = paid,
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.PaidAmount.Net,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
PaidFormatted = _displayFormatter.Currency(paid, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Link = link,
|
||||
Id = txId,
|
||||
@ -364,8 +361,8 @@ namespace BTCPayServer.Controllers
|
||||
if (paymentMethod != null)
|
||||
{
|
||||
accounting = paymentMethod.Calculate();
|
||||
cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
|
||||
dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
|
||||
cryptoPaid = accounting.Paid;
|
||||
dueAmount = accounting.TotalDue;
|
||||
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
|
||||
}
|
||||
|
||||
@ -560,7 +557,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var accounting = data.Calculate();
|
||||
var paymentMethodId = data.GetId();
|
||||
var overpaidAmount = accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC);
|
||||
var overpaidAmount = accounting.OverpaidHelper;
|
||||
|
||||
if (overpaidAmount > 0)
|
||||
{
|
||||
@ -571,8 +568,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
PaymentMethodId = paymentMethodId,
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Due = _displayFormatter.Currency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
|
||||
Paid = _displayFormatter.Currency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
|
||||
Due = _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode),
|
||||
Paid = _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode),
|
||||
Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode),
|
||||
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
|
||||
Rate = ExchangeRate(data.GetId().CryptoCode, data),
|
||||
@ -827,7 +824,6 @@ namespace BTCPayServer.Controllers
|
||||
var dto = invoice.EntityToDTO();
|
||||
var accounting = paymentMethod.Calculate();
|
||||
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
|
||||
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
|
||||
|
||||
switch (lang?.ToLowerInvariant())
|
||||
{
|
||||
@ -885,10 +881,10 @@ namespace BTCPayServer.Controllers
|
||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ShowMoney(divisibility),
|
||||
BtcPaid = accounting.Paid.ShowMoney(divisibility),
|
||||
BtcDue = accounting.ShowMoney(accounting.Due),
|
||||
BtcPaid = accounting.ShowMoney(accounting.Paid),
|
||||
InvoiceCurrency = invoice.Currency,
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
|
||||
OrderAmount = accounting.ShowMoney(accounting.TotalDue - accounting.NetworkFee),
|
||||
IsUnsetTopUp = invoice.IsUnsetTopUp(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail,
|
||||
@ -1089,22 +1085,22 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
model.Search = fs;
|
||||
model.SearchText = fs.TextSearch;
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
|
||||
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, apps, timezoneOffset);
|
||||
invoiceQuery.StoreId = storeIds.ToArray();
|
||||
invoiceQuery.Take = model.Count;
|
||||
invoiceQuery.Skip = model.Skip;
|
||||
invoiceQuery.IncludeRefunds = true;
|
||||
|
||||
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
|
||||
// Apps
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
model.Apps = apps.Select(a => new InvoiceAppModel
|
||||
{
|
||||
Id = a.Id,
|
||||
AppName = a.AppName,
|
||||
AppType = a.AppType,
|
||||
AppOrderId = AppService.GetAppOrderId(a.AppType, a.Id)
|
||||
AppType = a.AppType
|
||||
}).ToList();
|
||||
|
||||
foreach (var invoice in list)
|
||||
@ -1129,11 +1125,21 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private InvoiceQuery GetInvoiceQuery(SearchString fs, int timezoneOffset = 0)
|
||||
private InvoiceQuery GetInvoiceQuery(SearchString fs, ListAppsViewModel.ListAppViewModel[] apps, int timezoneOffset = 0)
|
||||
{
|
||||
var textSearch = fs.TextSearch;
|
||||
if (fs.GetFilterArray("appid") is { } appIds)
|
||||
{
|
||||
var appsById = apps.ToDictionary(a => a.Id);
|
||||
var searchTexts = appIds.Select(a => appsById.TryGet(a)).Where(a => a != null)
|
||||
.Select(a => AppService.GetAppSearchTerm(a.AppType, a.Id))
|
||||
.ToList();
|
||||
searchTexts.Add(fs.TextSearch);
|
||||
textSearch = string.Join(' ', searchTexts.Where(t => !string.IsNullOrEmpty(t)).ToList());
|
||||
}
|
||||
return new InvoiceQuery
|
||||
{
|
||||
TextSearch = fs.TextSearch,
|
||||
TextSearch = textSearch,
|
||||
UserId = GetUserId(),
|
||||
Unusual = fs.GetFilterBool("unusual"),
|
||||
IncludeArchived = fs.GetFilterBool("includearchived") ?? false,
|
||||
@ -1165,7 +1171,8 @@ namespace BTCPayServer.Controllers
|
||||
storeIds.Add(i);
|
||||
}
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, apps, timezoneOffset);
|
||||
invoiceQuery.StoreId = storeIds.ToArray();
|
||||
invoiceQuery.Skip = 0;
|
||||
invoiceQuery.Take = int.MaxValue;
|
||||
|
@ -121,7 +121,7 @@ namespace BTCPayServer.Controllers
|
||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice(store.Id);
|
||||
entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration;
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration;
|
||||
if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime)
|
||||
@ -237,7 +237,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice(store.Id);
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration);
|
||||
@ -314,6 +314,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.RefundMail = entity.Metadata.BuyerEmail;
|
||||
}
|
||||
entity.Status = InvoiceStatusLegacy.New;
|
||||
entity.UpdateTotals();
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
@ -402,7 +403,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
using (logs.Measure("Saving invoice"))
|
||||
{
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, additionalSearchTerms);
|
||||
await _InvoiceRepository.CreateInvoiceAsync(entity, additionalSearchTerms);
|
||||
foreach (var method in paymentMethods)
|
||||
{
|
||||
if (method.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod bp)
|
||||
@ -506,7 +507,7 @@ namespace BTCPayServer.Controllers
|
||||
await fetchingByCurrencyPair[new CurrencyPair(supportedPaymentMethod.PaymentId.CryptoCode, criteria.Value.Currency)];
|
||||
if (currentRateToCrypto?.BidAsk != null)
|
||||
{
|
||||
var amount = paymentMethod.Calculate().Due.GetValue(network as BTCPayNetwork);
|
||||
var amount = paymentMethod.Calculate().Due;
|
||||
var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid;
|
||||
|
||||
if (amount < limitValueCrypto && criteria.Above)
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@ -295,7 +296,7 @@ namespace BTCPayServer
|
||||
|
||||
var createInvoice = new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = item?.Price.Value,
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup? null: item?.Price,
|
||||
Currency = currencyCode,
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions()
|
||||
{
|
||||
@ -305,11 +306,11 @@ namespace BTCPayServer
|
||||
HttpContext.Request.GetAbsoluteUri($"/apps/{app.Id}/pos"),
|
||||
_ => null
|
||||
}
|
||||
}
|
||||
},
|
||||
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
|
||||
};
|
||||
|
||||
var invoiceMetadata = new InvoiceMetadata();
|
||||
invoiceMetadata.OrderId = AppService.GetAppOrderId(app);
|
||||
var invoiceMetadata = new InvoiceMetadata { OrderId = AppService.GetRandomOrderId() };
|
||||
if (item != null)
|
||||
{
|
||||
invoiceMetadata.ItemCode = item.Id;
|
||||
@ -317,7 +318,6 @@ namespace BTCPayServer
|
||||
}
|
||||
createInvoice.Metadata = invoiceMetadata.ToJObject();
|
||||
|
||||
|
||||
return await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
@ -373,13 +373,52 @@ namespace BTCPayServer
|
||||
return NotFound("Unknown username");
|
||||
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
var cryptoCode = "BTC";
|
||||
if (store is null)
|
||||
return NotFound("Unknown username");
|
||||
if (GetLNUrlPaymentMethodId(cryptoCode, store, out var lnUrlMethod) is null)
|
||||
return NotFound("LNUrl not available for store");
|
||||
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
|
||||
return await GetLNURLRequest(
|
||||
"BTC",
|
||||
var lnurlRequest = new LNURLPayRequest()
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
|
||||
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
|
||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0
|
||||
};
|
||||
NormalizeSendable(lnurlRequest);
|
||||
|
||||
var lnUrlMetadata = new Dictionary<string, string>()
|
||||
{
|
||||
["text/identifier"] = $"{username}@{Request.Host}"
|
||||
};
|
||||
SetLNUrlDescriptionMetadata(lnUrlMetadata, store, store.GetStoreBlob(), null);
|
||||
lnurlRequest.Metadata =
|
||||
JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||
|
||||
lnurlRequest.Callback = new Uri(_linkGenerator.GetUriByAction(
|
||||
action: nameof(GetLNURLForLightningAddress),
|
||||
controller: "UILNURL",
|
||||
values: new { cryptoCode, username }, Request.Scheme, Request.Host, Request.PathBase));
|
||||
|
||||
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
|
||||
return Ok(lnurlRequest);
|
||||
}
|
||||
|
||||
[HttpGet("pay/lnaddress/{username}")]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> GetLNURLForLightningAddress(string cryptoCode, string username, [FromQuery] long? amount = null, string comment = null)
|
||||
{
|
||||
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
|
||||
if (lightningAddressSettings is null || username is null)
|
||||
return NotFound("Unknown username");
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
var result = await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
store.GetStoreBlob(),
|
||||
new CreateInvoiceRequest()
|
||||
@ -396,31 +435,44 @@ namespace BTCPayServer
|
||||
{
|
||||
{ "text/identifier", $"{username}@{Request.Host}" }
|
||||
});
|
||||
if (result is not OkObjectResult ok || ok.Value is not LNURLPayRequest payRequest)
|
||||
return result;
|
||||
var invoiceId = payRequest.Callback.AbsoluteUri.Split('/').Last();
|
||||
return await GetLNURLForInvoice(invoiceId, cryptoCode, amount, comment);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("pay")]
|
||||
[HttpGet("{storeId}/pay")]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> GetLNUrlForStore(
|
||||
string cryptoCode,
|
||||
string storeId,
|
||||
string currencyCode = null)
|
||||
string currency = null,
|
||||
string orderId = null,
|
||||
decimal? amount = null)
|
||||
{
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var blob = store.GetStoreBlob();
|
||||
if (!blob.AnyoneCanInvoice)
|
||||
return NotFound("'Anyone can invoice' is turned off");
|
||||
var metadata = new InvoiceMetadata();
|
||||
if (!string.IsNullOrEmpty(orderId))
|
||||
{
|
||||
metadata.OrderId = orderId;
|
||||
}
|
||||
return await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
blob,
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Currency = currencyCode
|
||||
Amount = amount,
|
||||
Metadata = metadata.ToJObject(),
|
||||
Currency = currency
|
||||
});
|
||||
}
|
||||
|
||||
@ -482,11 +534,7 @@ namespace BTCPayServer
|
||||
|
||||
if (!lnUrlMetadata.ContainsKey("text/plain"))
|
||||
{
|
||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
lnUrlMetadata.Add("text/plain", invoiceDescription);
|
||||
SetLNUrlDescriptionMetadata(lnUrlMetadata, store, blob, i.Metadata);
|
||||
}
|
||||
|
||||
lnurlRequest.Tag = "payRequest";
|
||||
@ -498,17 +546,12 @@ namespace BTCPayServer
|
||||
lnurlRequest.Metadata = JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||
if (i.Type != InvoiceType.TopUp)
|
||||
{
|
||||
lnurlRequest.MinSendable = new LightMoney(pm.Calculate().Due.ToDecimal(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi);
|
||||
lnurlRequest.MinSendable = LightMoney.Coins(pm.Calculate().Due);
|
||||
if (!allowOverpay)
|
||||
lnurlRequest.MaxSendable = lnurlRequest.MinSendable;
|
||||
}
|
||||
|
||||
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat.
|
||||
if (lnurlRequest.MinSendable is null || lnurlRequest.MinSendable < LightMoney.Satoshis(1.0m))
|
||||
lnurlRequest.MinSendable = LightMoney.Satoshis(1.0m);
|
||||
|
||||
if (lnurlRequest.MaxSendable is null)
|
||||
lnurlRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
|
||||
NormalizeSendable(lnurlRequest);
|
||||
|
||||
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
|
||||
if (paymentMethodDetails.PayRequest is null)
|
||||
@ -524,6 +567,25 @@ namespace BTCPayServer
|
||||
return lnurlRequest;
|
||||
}
|
||||
|
||||
private void SetLNUrlDescriptionMetadata(Dictionary<string, string> lnUrlMetadata, Data.StoreData store, StoreBlob blob, InvoiceMetadata invoiceMetadata)
|
||||
{
|
||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", invoiceMetadata?.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", invoiceMetadata?.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
lnUrlMetadata.Add("text/plain", invoiceDescription);
|
||||
}
|
||||
|
||||
private static void NormalizeSendable(LNURLPayRequest lnurlRequest)
|
||||
{
|
||||
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat.
|
||||
if (lnurlRequest.MinSendable is null || lnurlRequest.MinSendable < LightMoney.Satoshis(1.0m))
|
||||
lnurlRequest.MinSendable = LightMoney.Satoshis(1.0m);
|
||||
|
||||
if (lnurlRequest.MaxSendable is null)
|
||||
lnurlRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
|
||||
}
|
||||
|
||||
PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaySupportedPaymentMethod lnUrlSettings)
|
||||
{
|
||||
lnUrlSettings = null;
|
||||
|
@ -78,16 +78,20 @@ namespace BTCPayServer.Controllers
|
||||
model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel());
|
||||
|
||||
var store = GetCurrentStore();
|
||||
var includeArchived = new SearchString(model.SearchTerm, model.TimezoneOffset ?? 0).GetFilterBool("includearchived") == true;
|
||||
var fs = new SearchString(model.SearchTerm, model.TimezoneOffset ?? 0);
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
StoreId = store.Id,
|
||||
Skip = model.Skip,
|
||||
Count = model.Count,
|
||||
IncludeArchived = includeArchived
|
||||
Status = fs.GetFilterArray("status")?.Select(s => Enum.Parse<Client.Models.PaymentRequestData.PaymentRequestStatus>(s, true)).ToArray(),
|
||||
IncludeArchived = fs.GetFilterBool("includearchived") ?? false
|
||||
});
|
||||
|
||||
|
||||
model.Search = fs;
|
||||
model.SearchText = fs.TextSearch;
|
||||
|
||||
model.Items = result.Select(data =>
|
||||
{
|
||||
var blob = data.GetBlob();
|
||||
|
@ -29,6 +29,7 @@ using BTCPayServer.Storage.Services;
|
||||
using BTCPayServer.Storage.Services.Providers;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -664,6 +665,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
[AllowAnonymous]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public IActionResult GetLNDConfig(ulong configKey)
|
||||
{
|
||||
var conf = _LnConfigProvider.GetConfig(configKey);
|
||||
|
@ -39,12 +39,12 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet("create")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
|
||||
public async Task<IActionResult> CreateStore()
|
||||
public async Task<IActionResult> CreateStore(bool skipWizard)
|
||||
{
|
||||
var stores = await _repo.GetStoresByUserId(GetUserId());
|
||||
var vm = new CreateStoreViewModel
|
||||
{
|
||||
IsFirstStore = !stores.Any(),
|
||||
IsFirstStore = !(stores.Any() || skipWizard),
|
||||
DefaultCurrency = StoreBlob.StandardDefaultCurrency,
|
||||
Exchanges = GetExchangesSelectList(null)
|
||||
};
|
||||
|
@ -303,7 +303,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
||||
bip21.Add(newUri.Uri.ToString());
|
||||
break;
|
||||
case AddressClaimDestination addressClaimDestination:
|
||||
var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), new Money(blob.CryptoAmount.Value, MoneyUnit.BTC));
|
||||
var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), blob.CryptoAmount.Value);
|
||||
bip21New.QueryParams.Add("payout", payout.Id);
|
||||
bip21.Add(bip21New.ToString());
|
||||
break;
|
||||
|
@ -277,6 +277,18 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
};
|
||||
}
|
||||
|
||||
if (bolt11PaymentRequest.ExpiryDate < DateTimeOffset.Now)
|
||||
{
|
||||
payoutData.State = PayoutState.Cancelled;
|
||||
return new ResultVM
|
||||
{
|
||||
PayoutId = payoutData.Id,
|
||||
Result = PayResult.Error,
|
||||
Message = $"The BOLT11 invoice expiry date ({bolt11PaymentRequest.ExpiryDate}) has expired",
|
||||
Destination = payoutBlob.Destination
|
||||
};
|
||||
}
|
||||
|
||||
var proofBlob = new PayoutLightningBlob() { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
|
||||
try
|
||||
{
|
||||
|
@ -199,7 +199,9 @@ namespace BTCPayServer.Data
|
||||
{ "GTQ", "bitpay" },
|
||||
{ "COP", "yadio" },
|
||||
{ "JPY", "bitbank" },
|
||||
{ "TRY", "btcturk" }
|
||||
{ "TRY", "btcturk" },
|
||||
{ "UGX", "exchangeratehost"},
|
||||
{ "RSD", "bitpay"}
|
||||
};
|
||||
|
||||
public string GetRecommendedExchange() =>
|
||||
|
@ -30,6 +30,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
|
||||
@ -38,6 +39,15 @@ namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static DateTimeOffset TruncateMilliSeconds(this DateTimeOffset dt) => new (dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0, dt.Offset);
|
||||
public static decimal? GetDue(this InvoiceCryptoInfo invoiceCryptoInfo)
|
||||
{
|
||||
if (invoiceCryptoInfo is null)
|
||||
return null;
|
||||
if (decimal.TryParse(invoiceCryptoInfo.Due, NumberStyles.Any, CultureInfo.InvariantCulture, out var v))
|
||||
return v;
|
||||
return null;
|
||||
}
|
||||
public static Task<BufferizedFormFile> Bufferize(this IFormFile formFile)
|
||||
{
|
||||
return BufferizedFormFile.Bufferize(formFile);
|
||||
@ -382,20 +392,6 @@ namespace BTCPayServer
|
||||
return controller.View("PostRedirect", redirectVm);
|
||||
}
|
||||
|
||||
public static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class
|
||||
{
|
||||
var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();
|
||||
var relationalCommandCache = enumerator.Private("_relationalCommandCache");
|
||||
var selectExpression = relationalCommandCache.Private<Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression>("_selectExpression");
|
||||
var factory = relationalCommandCache.Private<Microsoft.EntityFrameworkCore.Query.IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory");
|
||||
|
||||
var sqlGenerator = factory.Create();
|
||||
var command = sqlGenerator.GetCommand(selectExpression);
|
||||
|
||||
string sql = command.CommandText;
|
||||
return sql;
|
||||
}
|
||||
|
||||
public static BTCPayNetworkProvider ConfigureNetworkProvider(this IConfiguration configuration, Logs logs)
|
||||
{
|
||||
var _networkType = DefaultConfiguration.GetNetworkType(configuration);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
@ -10,7 +11,7 @@ public class FieldValueMirror : IFormComponentProvider
|
||||
{
|
||||
if (form.GetFieldByFullName(field.Value) is null)
|
||||
{
|
||||
field.ValidationErrors = new List<string> { $"{field.Name} requires {field.Value} to be present" };
|
||||
field.ValidationErrors = new List<string> {$"{field.Name} requires {field.Value} to be present"};
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +22,13 @@ public class FieldValueMirror : IFormComponentProvider
|
||||
|
||||
public string GetValue(Form form, Field field)
|
||||
{
|
||||
return form.GetFieldByFullName(field.Value)?.Value;
|
||||
var rawValue = form.GetFieldByFullName(field.Value)?.Value;
|
||||
if (rawValue is not null && field.AdditionalData?.TryGetValue("valuemap", out var valueMap) is true &&
|
||||
valueMap is JObject map && map.TryGetValue(rawValue, out var mappedValue))
|
||||
{
|
||||
return mappedValue.Value<string>();
|
||||
}
|
||||
|
||||
return rawValue;
|
||||
}
|
||||
}
|
||||
|
@ -151,11 +151,32 @@ public class FormDataService
|
||||
|
||||
public CreateInvoiceRequest GenerateInvoiceParametersFromForm(Form form)
|
||||
{
|
||||
var amt = GetValue(form, $"{InvoiceParameterPrefix}amount");
|
||||
var amtRaw = GetValue(form, $"{InvoiceParameterPrefix}amount");
|
||||
var amt = string.IsNullOrEmpty(amtRaw) ? (decimal?) null : decimal.Parse(amtRaw, CultureInfo.InvariantCulture);
|
||||
var adjustmentAmount = 0m;
|
||||
foreach (var adjustmentField in form.GetAllFields().Where(f => f.FullName.StartsWith($"{InvoiceParameterPrefix}amount_adjustment")))
|
||||
{
|
||||
if (!decimal.TryParse(GetValue(form, adjustmentField.Field), out var adjustment))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
adjustmentAmount += adjustment;
|
||||
}
|
||||
|
||||
if (amt is null && adjustmentAmount > 0)
|
||||
{
|
||||
amt = adjustmentAmount;
|
||||
}
|
||||
else if(amt is not null)
|
||||
{
|
||||
amt += adjustmentAmount;
|
||||
amt = Math.Max(0, amt!.Value);
|
||||
}
|
||||
return new CreateInvoiceRequest
|
||||
{
|
||||
Currency = GetValue(form, $"{InvoiceParameterPrefix}currency"),
|
||||
Amount = string.IsNullOrEmpty(amt) ? null : decimal.Parse(amt, CultureInfo.InvariantCulture),
|
||||
Amount = amt,
|
||||
Metadata = GetValues(form),
|
||||
};
|
||||
}
|
||||
|
@ -205,7 +205,10 @@ public class UIFormsController : Controller
|
||||
|
||||
var request = _formDataService.GenerateInvoiceParametersFromForm(form);
|
||||
var inv = await invoiceController.CreateInvoiceCoreRaw(request, store, Request.GetAbsoluteRoot());
|
||||
|
||||
if (inv.Price == 0 && inv.Type == InvoiceType.Standard && inv.ReceiptOptions?.Enabled is not false)
|
||||
{
|
||||
return RedirectToAction("InvoiceReceipt", "UIInvoice", new { invoiceId = inv.Id });
|
||||
}
|
||||
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = inv.Id });
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
@ -83,31 +84,13 @@ namespace BTCPayServer.HostedServices
|
||||
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial) { PaidPartial = paidPartial });
|
||||
}
|
||||
var allPaymentMethods = invoice.GetPaymentMethods();
|
||||
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting);
|
||||
if (allPaymentMethods.Any() && paymentMethod == null)
|
||||
return;
|
||||
if (accounting is null && invoice.Price is 0m)
|
||||
{
|
||||
accounting = new PaymentMethodAccounting()
|
||||
{
|
||||
Due = Money.Zero,
|
||||
Paid = Money.Zero,
|
||||
CryptoPaid = Money.Zero,
|
||||
DueUncapped = Money.Zero,
|
||||
NetworkFee = Money.Zero,
|
||||
TotalDue = Money.Zero,
|
||||
TxCount = 0,
|
||||
TxRequired = 0,
|
||||
MinimumTotalDue = Money.Zero,
|
||||
NetworkFeeAlreadyPaid = Money.Zero
|
||||
};
|
||||
}
|
||||
|
||||
var hasPayment = invoice.GetPayments(true).Any();
|
||||
if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired)
|
||||
{
|
||||
var isPaid = invoice.IsUnsetTopUp() ?
|
||||
accounting.Paid > Money.Zero :
|
||||
accounting.Paid >= accounting.MinimumTotalDue;
|
||||
hasPayment :
|
||||
!invoice.IsUnderPaid;
|
||||
if (isPaid)
|
||||
{
|
||||
if (invoice.Status == InvoiceStatusLegacy.New)
|
||||
@ -117,13 +100,15 @@ namespace BTCPayServer.HostedServices
|
||||
if (invoice.IsUnsetTopUp())
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
|
||||
invoice.Price = (accounting.Paid - accounting.NetworkFeeAlreadyPaid).ToDecimal(MoneyUnit.BTC) * paymentMethod.Rate;
|
||||
accounting = paymentMethod.Calculate();
|
||||
// We know there is at least one payment because hasPayment is true
|
||||
var payment = invoice.GetPayments(true).First();
|
||||
invoice.Price = payment.InvoicePaidAmount.Net;
|
||||
invoice.UpdateTotals();
|
||||
context.BlobUpdated();
|
||||
}
|
||||
else
|
||||
{
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
invoice.ExceptionStatus = invoice.IsOverPaid ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
}
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -135,7 +120,7 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments(true).Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
|
||||
if (hasPayment && invoice.IsUnderPaid && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
@ -145,43 +130,43 @@ namespace BTCPayServer.HostedServices
|
||||
// Just make sure RBF did not cancelled a payment
|
||||
if (invoice.Status == InvoiceStatusLegacy.Paid)
|
||||
{
|
||||
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver)
|
||||
if (!invoice.IsUnderPaid && !invoice.IsOverPaid && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
|
||||
if (invoice.IsOverPaid && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue)
|
||||
if (invoice.IsUnderPaid)
|
||||
{
|
||||
invoice.Status = InvoiceStatusLegacy.New;
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial;
|
||||
invoice.ExceptionStatus = hasPayment ? InvoiceExceptionStatus.PaidPartial : InvoiceExceptionStatus.None;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == InvoiceStatusLegacy.Paid)
|
||||
{
|
||||
var confirmedAccounting =
|
||||
paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)) ??
|
||||
accounting;
|
||||
var unconfPayments = invoice.GetPayments(true).Where(p => !p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)).ToList();
|
||||
var unconfirmedPaid = unconfPayments.Select(p => p.InvoicePaidAmount.Net).Sum();
|
||||
var minimumDue = invoice.MinimumNetDue + unconfirmedPaid;
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
(minimumDue > 0.0m))
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm));
|
||||
invoice.Status = InvoiceStatusLegacy.Invalid;
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
else if (minimumDue <= 0.0m)
|
||||
{
|
||||
invoice.Status = InvoiceStatusLegacy.Confirmed;
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed));
|
||||
@ -191,9 +176,11 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
if (invoice.Status == InvoiceStatusLegacy.Confirmed)
|
||||
{
|
||||
var completedAccounting = paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)) ??
|
||||
accounting;
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
var unconfPayments = invoice.GetPayments(true).Where(p => !p.GetCryptoPaymentData().PaymentCompleted(p)).ToList();
|
||||
var unconfirmedPaid = unconfPayments.Select(p => p.InvoicePaidAmount.Net).Sum();
|
||||
var minimumDue = invoice.MinimumNetDue + unconfirmedPaid;
|
||||
|
||||
if (minimumDue <= 0.0m)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed));
|
||||
invoice.Status = InvoiceStatusLegacy.Complete;
|
||||
@ -203,25 +190,6 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
}
|
||||
|
||||
public static PaymentMethod GetNearestClearedPayment(PaymentMethodDictionary allPaymentMethods, out PaymentMethodAccounting accounting)
|
||||
{
|
||||
PaymentMethod result = null;
|
||||
accounting = null;
|
||||
decimal nearestToZero = 0.0m;
|
||||
foreach (var paymentMethod in allPaymentMethods)
|
||||
{
|
||||
var currentAccounting = paymentMethod.Calculate();
|
||||
var distanceFromZero = Math.Abs(currentAccounting.DueUncapped.ToDecimal(MoneyUnit.BTC));
|
||||
if (result == null || distanceFromZero < nearestToZero)
|
||||
{
|
||||
result = paymentMethod;
|
||||
nearestToZero = distanceFromZero;
|
||||
accounting = currentAccounting;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Watch(string invoiceId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(invoiceId);
|
||||
@ -380,7 +348,7 @@ namespace BTCPayServer.HostedServices
|
||||
if ((onChainPaymentData.ConfirmationCount < network.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
var client = _explorerClientProvider.GetExplorerClient(payment.GetCryptoCode());
|
||||
var client = _explorerClientProvider.GetExplorerClient(payment.Currency);
|
||||
var transactionResult = client is null ? null : await client.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
|
||||
var confirmationCount = transactionResult?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
|
@ -389,7 +389,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
try
|
||||
{
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payout = await ctx.Payouts.Include(p => p.PullPaymentData).Where(p => p.Id == req.PayoutId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (payout is null)
|
||||
@ -440,6 +440,7 @@ namespace BTCPayServer.HostedServices
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Approved, payout));
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.Ok, payoutBlob.CryptoAmount));
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -604,6 +605,8 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await payoutHandler.TrackClaim(req.ClaimRequest, payout);
|
||||
await ctx.SaveChangesAsync();
|
||||
var response = new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout);
|
||||
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Created, payout));
|
||||
if (req.ClaimRequest.PreApprove.GetValueOrDefault(ppBlob?.AutoApproveClaims is true))
|
||||
{
|
||||
payout.StoreData = await ctx.Stores.FindAsync(payout.StoreDataId);
|
||||
@ -628,7 +631,7 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.Ok, payout));
|
||||
req.Completion.TrySetResult(response);
|
||||
await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId),
|
||||
new PayoutNotification()
|
||||
{
|
||||
@ -888,4 +891,14 @@ namespace BTCPayServer.HostedServices
|
||||
public string StoreId { get; set; }
|
||||
public bool? PreApprove { get; set; }
|
||||
}
|
||||
|
||||
public record PayoutEvent(PayoutEvent.PayoutEventType Type,PayoutData Payout)
|
||||
{
|
||||
public enum PayoutEventType
|
||||
{
|
||||
Created,
|
||||
Approved
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ namespace BTCPayServer.HostedServices
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType == BitcoinPaymentType.Instance &&
|
||||
invoiceEvent.Payment.GetCryptoPaymentData() is BitcoinLikePaymentData bitcoinLikePaymentData:
|
||||
{
|
||||
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
|
||||
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.Currency);
|
||||
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
|
||||
var labels = new List<Attachment>
|
||||
{
|
||||
|
@ -278,7 +278,8 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
services.TryAddSingleton<AppService>();
|
||||
services.AddTransient<PluginService>();
|
||||
services.AddSingleton<IPluginHookService, PluginHookService>();
|
||||
services.AddSingleton<PluginHookService>();
|
||||
services.AddSingleton<IPluginHookService, PluginHookService>(provider => provider.GetService<PluginHookService>());
|
||||
services.TryAddTransient<Safe>();
|
||||
services.TryAddTransient<DisplayFormatter>();
|
||||
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
||||
@ -520,6 +521,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddRateProvider<BitflyerRateProvider>();
|
||||
services.AddRateProvider<YadioRateProvider>();
|
||||
services.AddRateProvider<BtcTurkRateProvider>();
|
||||
services.AddRateProvider<FreeCurrencyRatesRateProvider>();
|
||||
services.AddRateProvider<ExchangeRateHostRateProvider>();
|
||||
|
||||
// Broken
|
||||
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
|
||||
|
@ -341,9 +341,13 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
var items = new List<ViewPointOfSaleViewModel.Item>();
|
||||
var stream = new YamlStream();
|
||||
if (string.IsNullOrEmpty(yaml))
|
||||
return items.ToArray();
|
||||
|
||||
stream.Load(new StringReader(yaml));
|
||||
|
||||
var root = stream.Documents.First().RootNode as YamlMappingNode;
|
||||
if(stream.Documents.FirstOrDefault()?.RootNode is not YamlMappingNode root)
|
||||
return items.ToArray();
|
||||
foreach (var posItem in root.Children)
|
||||
{
|
||||
var trimmedKey = ((YamlScalarNode)posItem.Key).Value?.Trim();
|
||||
|
@ -41,6 +41,5 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string Id { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public string AppOrderId { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
{
|
||||
public List<ViewPaymentRequestViewModel> Items { get; set; }
|
||||
public override int CurrentPageCount => Items.Count;
|
||||
|
||||
public SearchString Search { get; set; }
|
||||
public string SearchText { get; set; }
|
||||
}
|
||||
|
||||
public class UpdatePaymentRequestViewModel
|
||||
|
@ -167,7 +167,7 @@ namespace BTCPayServer.PaymentRequest
|
||||
new object[]
|
||||
{
|
||||
data.GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
invoiceEvent.Payment.Currency,
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString()
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
@ -123,18 +123,14 @@ namespace BTCPayServer.PaymentRequest
|
||||
|
||||
string txId = paymentData.GetPaymentId();
|
||||
string link = GetTransactionLink(paymentMethodId, txId);
|
||||
var paymentMethod = entity.GetPaymentMethod(paymentMethodId);
|
||||
var amount = paymentData.GetValue();
|
||||
var rate = paymentMethod.Rate;
|
||||
var paid = (amount - paymentEntity.NetworkFee) * rate;
|
||||
|
||||
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
|
||||
{
|
||||
Amount = amount,
|
||||
Paid = paid,
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.InvoicePaidAmount.Net,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
PaidFormatted = _displayFormatter.Currency(paid, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Link = link,
|
||||
Id = txId,
|
||||
|
@ -211,7 +211,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
new Key().GetScriptPubKey(supportedPaymentMethod.AccountDerivation.ScriptPubKeyType());
|
||||
var dust = txOut.GetDustThreshold();
|
||||
var amount = paymentMethod.Calculate().Due;
|
||||
if (amount < dust)
|
||||
if (amount < dust.ToDecimal(MoneyUnit.BTC))
|
||||
throw new PaymentMethodUnavailableException("Amount below the dust threshold. For amounts of this size, it is recommended to enable an off-chain (Lightning) payment method");
|
||||
}
|
||||
if (preparePaymentObject is null)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
@ -28,7 +29,7 @@ namespace BTCPayServer.Payments
|
||||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
|
||||
Money cryptoInfoDue, string serverUri)
|
||||
decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
if (!paymentMethodDetails.Activated)
|
||||
{
|
||||
@ -74,7 +75,7 @@ namespace BTCPayServer.Payments
|
||||
{
|
||||
AdditionalData = new Dictionary<string, JToken>()
|
||||
{
|
||||
{"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due,
|
||||
{"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.GetDue().Value,
|
||||
serverUrl))}
|
||||
}
|
||||
};
|
||||
|
@ -73,7 +73,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility);
|
||||
try
|
||||
{
|
||||
due = paymentMethod.Calculate().Due.ToDecimal(MoneyUnit.BTC);
|
||||
due = paymentMethod.Calculate().Due;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -200,7 +200,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
if (inv.Name == InvoiceEvent.ReceivedPayment && inv.Invoice.Status == InvoiceStatusLegacy.New && inv.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
var pm = inv.Invoice.GetPaymentMethods().First();
|
||||
if (pm.Calculate().Due.GetValue(pm.Network as BTCPayNetwork) > 0m)
|
||||
if (pm.Calculate().Due > 0m)
|
||||
{
|
||||
await CreateNewLNInvoiceForBTCPayInvoice(inv.Invoice);
|
||||
}
|
||||
|
@ -302,7 +302,7 @@ namespace BTCPayServer.Payments.PayJoin
|
||||
var paymentDetails = paymentMethod?.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if (paymentMethod is null || paymentDetails is null || !paymentDetails.PayjoinEnabled)
|
||||
continue;
|
||||
due = paymentMethod.Calculate().TotalDue - output.Value;
|
||||
due = Money.Coins(paymentMethod.Calculate().TotalDue) - output.Value;
|
||||
if (due > Money.Zero)
|
||||
{
|
||||
break;
|
||||
|
@ -67,7 +67,7 @@ namespace BTCPayServer.Payments
|
||||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
|
||||
Money cryptoInfoDue, string serverUri)
|
||||
decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
if (!paymentMethodDetails.Activated)
|
||||
{
|
||||
@ -105,7 +105,7 @@ namespace BTCPayServer.Payments
|
||||
{
|
||||
cryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls()
|
||||
{
|
||||
BIP21 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), cryptoInfo.Due, serverUrl),
|
||||
BIP21 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), cryptoInfo.GetDue().Value, serverUrl),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
@ -52,7 +53,7 @@ namespace BTCPayServer.Payments
|
||||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
|
||||
Money cryptoInfoDue, string serverUri)
|
||||
decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
if (!paymentMethodDetails.Activated)
|
||||
{
|
||||
@ -92,7 +93,7 @@ namespace BTCPayServer.Payments
|
||||
{
|
||||
invoiceCryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls()
|
||||
{
|
||||
BOLT11 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due,
|
||||
BOLT11 = GetPaymentLink(details.Network, invoice, details.GetPaymentMethodDetails(), invoiceCryptoInfo.GetDue().Value,
|
||||
serverUrl)
|
||||
};
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ namespace BTCPayServer.Payments
|
||||
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
|
||||
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
|
||||
public abstract string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails,
|
||||
Money cryptoInfoDue, string serverUri);
|
||||
decimal cryptoInfoDue, string serverUri);
|
||||
public abstract string InvoiceViewPaymentPartialName { get; }
|
||||
|
||||
public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore);
|
||||
|
7
BTCPayServer/PayoutProcessors/AfterPayoutActionData.cs
Normal file
7
BTCPayServer/PayoutProcessors/AfterPayoutActionData.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors;
|
||||
|
||||
public record AfterPayoutActionData(StoreData Store, PayoutProcessorData ProcessorData,
|
||||
IEnumerable<PayoutData> Payouts);
|
@ -1,19 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors;
|
||||
|
||||
public class AfterPayoutFilterData
|
||||
{
|
||||
private readonly StoreData _store;
|
||||
private readonly ISupportedPaymentMethod _paymentMethod;
|
||||
private readonly List<PayoutData> _payoutDatas;
|
||||
|
||||
public AfterPayoutFilterData(StoreData store, ISupportedPaymentMethod paymentMethod, List<PayoutData> payoutDatas)
|
||||
{
|
||||
_store = store;
|
||||
_paymentMethod = paymentMethod;
|
||||
_payoutDatas = payoutDatas;
|
||||
}
|
||||
}
|
@ -1,18 +1,17 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.Protocol;
|
||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
||||
|
||||
@ -20,89 +19,108 @@ namespace BTCPayServer.PayoutProcessors;
|
||||
|
||||
public class AutomatedPayoutConstants
|
||||
{
|
||||
public const double MinIntervalMinutes = 10.0;
|
||||
public const double MaxIntervalMinutes = 60.0;
|
||||
public const double MinIntervalMinutes = 1.0;
|
||||
public const double MaxIntervalMinutes = 24 * 60; //1 day
|
||||
public static void ValidateInterval(ModelStateDictionary modelState, TimeSpan timeSpan, string parameterName)
|
||||
{
|
||||
if (timeSpan < TimeSpan.FromMinutes(AutomatedPayoutConstants.MinIntervalMinutes))
|
||||
{
|
||||
modelState.AddModelError(parameterName, $"The minimum interval is {AutomatedPayoutConstants.MinIntervalMinutes * 60} seconds");
|
||||
modelState.AddModelError(parameterName, $"The minimum interval is {MinIntervalMinutes * 60} seconds");
|
||||
}
|
||||
if (timeSpan > TimeSpan.FromMinutes(AutomatedPayoutConstants.MaxIntervalMinutes))
|
||||
{
|
||||
modelState.AddModelError(parameterName, $"The maximum interval is {AutomatedPayoutConstants.MaxIntervalMinutes * 60} seconds");
|
||||
modelState.AddModelError(parameterName, $"The maximum interval is {MaxIntervalMinutes * 60} seconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T : AutomatedPayoutBlob
|
||||
public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T : AutomatedPayoutBlob, new()
|
||||
{
|
||||
protected readonly StoreRepository _storeRepository;
|
||||
protected readonly PayoutProcessorData _PayoutProcesserSettings;
|
||||
protected readonly PayoutProcessorData PayoutProcessorSettings;
|
||||
protected readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
protected readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
protected readonly PaymentMethodId PaymentMethodId;
|
||||
private readonly IPluginHookService _pluginHookService;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
protected BaseAutomatedPayoutProcessor(
|
||||
ILoggerFactory logger,
|
||||
StoreRepository storeRepository,
|
||||
PayoutProcessorData payoutProcesserSettings,
|
||||
PayoutProcessorData payoutProcessorSettings,
|
||||
ApplicationDbContextFactory applicationDbContextFactory,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IPluginHookService pluginHookService) : base(logger.CreateLogger($"{payoutProcesserSettings.Processor}:{payoutProcesserSettings.StoreId}:{payoutProcesserSettings.PaymentMethod}"))
|
||||
IPluginHookService pluginHookService,
|
||||
EventAggregator eventAggregator) : base(logger.CreateLogger($"{payoutProcessorSettings.Processor}:{payoutProcessorSettings.StoreId}:{payoutProcessorSettings.PaymentMethod}"))
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_PayoutProcesserSettings = payoutProcesserSettings;
|
||||
PaymentMethodId = _PayoutProcesserSettings.GetPaymentMethodId();
|
||||
PayoutProcessorSettings = payoutProcessorSettings;
|
||||
PaymentMethodId = PayoutProcessorSettings.GetPaymentMethodId();
|
||||
_applicationDbContextFactory = applicationDbContextFactory;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_pluginHookService = pluginHookService;
|
||||
_eventAggregator = eventAggregator;
|
||||
this.NoLogsOnExit = true;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
_subscription = _eventAggregator.SubscribeAsync<PayoutEvent>(OnPayoutEvent);
|
||||
return new[] { CreateLoopTask(Act) };
|
||||
}
|
||||
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_subscription?.Dispose();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private Task OnPayoutEvent(PayoutEvent arg)
|
||||
{
|
||||
if (arg.Type == PayoutEvent.PayoutEventType.Approved &&
|
||||
PayoutProcessorSettings.StoreId == arg.Payout.StoreDataId &&
|
||||
arg.Payout.GetPaymentMethodId() == PaymentMethodId &&
|
||||
GetBlob(PayoutProcessorSettings).ProcessNewPayoutsInstantly)
|
||||
{
|
||||
SkipInterval();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected abstract Task Process(ISupportedPaymentMethod paymentMethod, List<PayoutData> payouts);
|
||||
|
||||
private async Task Act()
|
||||
{
|
||||
var store = await _storeRepository.FindStore(_PayoutProcesserSettings.StoreId);
|
||||
var paymentMethod = store?.GetEnabledPaymentMethods(_btcPayNetworkProvider)?.FirstOrDefault(
|
||||
var store = await _storeRepository.FindStore(PayoutProcessorSettings.StoreId);
|
||||
var paymentMethod = store?.GetEnabledPaymentMethods(_btcPayNetworkProvider).FirstOrDefault(
|
||||
method =>
|
||||
method.PaymentId == PaymentMethodId);
|
||||
|
||||
var blob = GetBlob(_PayoutProcesserSettings);
|
||||
var blob = GetBlob(PayoutProcessorSettings);
|
||||
if (paymentMethod is not null)
|
||||
{
|
||||
|
||||
// Allow plugins to do something before the automatic payouts are executed
|
||||
await _pluginHookService.ApplyFilter("before-automated-payout-processing",
|
||||
new BeforePayoutFilterData(store, paymentMethod));
|
||||
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
var payouts = await PullPaymentHostedService.GetPayouts(
|
||||
new PullPaymentHostedService.PayoutQuery()
|
||||
{
|
||||
States = new[] { PayoutState.AwaitingPayment },
|
||||
PaymentMethods = new[] { _PayoutProcesserSettings.PaymentMethod },
|
||||
Stores = new[] { _PayoutProcesserSettings.StoreId }
|
||||
PaymentMethods = new[] { PayoutProcessorSettings.PaymentMethod },
|
||||
Stores = new[] {PayoutProcessorSettings.StoreId}
|
||||
}, context, CancellationToken);
|
||||
|
||||
await _pluginHookService.ApplyAction("before-automated-payout-processing",
|
||||
new BeforePayoutActionData(store, PayoutProcessorSettings, payouts));
|
||||
if (payouts.Any())
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{payouts.Count} found to process. Starting (and after will sleep for {blob.Interval})");
|
||||
Logs.PayServer.LogInformation(
|
||||
$"{payouts.Count} found to process. Starting (and after will sleep for {blob.Interval})");
|
||||
await Process(paymentMethod, payouts);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Allow plugins do to something after automatic payout processing
|
||||
await _pluginHookService.ApplyFilter("after-automated-payout-processing",
|
||||
new AfterPayoutFilterData(store, paymentMethod, payouts));
|
||||
}
|
||||
|
||||
// Allow plugins do to something after automatic payout processing
|
||||
await _pluginHookService.ApplyAction("after-automated-payout-processing",
|
||||
new AfterPayoutActionData(store, PayoutProcessorSettings, payouts));
|
||||
}
|
||||
|
||||
// Clip interval
|
||||
@ -110,11 +128,32 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
|
||||
blob.Interval = TimeSpan.FromMinutes(AutomatedPayoutConstants.MinIntervalMinutes);
|
||||
if (blob.Interval > TimeSpan.FromMinutes(AutomatedPayoutConstants.MaxIntervalMinutes))
|
||||
blob.Interval = TimeSpan.FromMinutes(AutomatedPayoutConstants.MaxIntervalMinutes);
|
||||
await Task.Delay(blob.Interval, CancellationToken);
|
||||
try
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken, _timerCTs.Token);
|
||||
await Task.Delay(blob.Interval, cts.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private CancellationTokenSource _timerCTs = new CancellationTokenSource();
|
||||
private IEventAggregatorSubscription? _subscription;
|
||||
|
||||
private readonly object _intervalLock = new object();
|
||||
|
||||
public void SkipInterval()
|
||||
{
|
||||
lock (_intervalLock)
|
||||
{
|
||||
_timerCTs.Cancel();
|
||||
_timerCTs = new CancellationTokenSource();
|
||||
}
|
||||
}
|
||||
|
||||
public static T GetBlob(PayoutProcessorData payoutProcesserSettings)
|
||||
{
|
||||
return payoutProcesserSettings.HasTypedBlob<T>().GetBlob();
|
||||
return payoutProcesserSettings.HasTypedBlob<T>().GetBlob() ?? new T();
|
||||
}
|
||||
}
|
||||
|
6
BTCPayServer/PayoutProcessors/BeforePayoutActionData.cs
Normal file
6
BTCPayServer/PayoutProcessors/BeforePayoutActionData.cs
Normal file
@ -0,0 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors;
|
||||
|
||||
public record BeforePayoutActionData(StoreData Store, PayoutProcessorData ProcessorData, IEnumerable<PayoutData> Payouts);
|
@ -1,16 +0,0 @@
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors;
|
||||
|
||||
public class BeforePayoutFilterData
|
||||
{
|
||||
private readonly StoreData _store;
|
||||
private readonly ISupportedPaymentMethod _paymentMethod;
|
||||
|
||||
public BeforePayoutFilterData(StoreData store, ISupportedPaymentMethod paymentMethod)
|
||||
{
|
||||
_store = store;
|
||||
_paymentMethod = paymentMethod;
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors.Lightning;
|
||||
|
||||
public class LightningAutomatedPayoutBlob : AutomatedPayoutBlob
|
||||
{
|
||||
public int? CancelPayoutAfterFailures { get; set; } = null;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -14,16 +15,15 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using LNURL;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
||||
|
||||
namespace BTCPayServer.PayoutProcessors.Lightning;
|
||||
|
||||
public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<AutomatedPayoutBlob>
|
||||
public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<LightningAutomatedPayoutBlob>
|
||||
{
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
private readonly LightningClientFactoryService _lightningClientFactoryService;
|
||||
@ -31,6 +31,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
||||
private readonly IOptions<LightningNetworkOptions> _options;
|
||||
private readonly LightningLikePayoutHandler _payoutHandler;
|
||||
private readonly BTCPayNetwork _network;
|
||||
private readonly ConcurrentDictionary<string, int> _failedPayoutCounter = new();
|
||||
|
||||
public LightningAutomatedPayoutProcessor(
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
|
||||
@ -38,11 +39,13 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
UserService userService,
|
||||
ILoggerFactory logger, IOptions<LightningNetworkOptions> options,
|
||||
StoreRepository storeRepository, PayoutProcessorData payoutProcesserSettings,
|
||||
ApplicationDbContextFactory applicationDbContextFactory, PullPaymentHostedService pullPaymentHostedService, BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IPluginHookService pluginHookService) :
|
||||
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, pullPaymentHostedService,
|
||||
btcPayNetworkProvider, pluginHookService)
|
||||
StoreRepository storeRepository, PayoutProcessorData payoutProcessorSettings,
|
||||
ApplicationDbContextFactory applicationDbContextFactory,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IPluginHookService pluginHookService,
|
||||
EventAggregator eventAggregator) :
|
||||
base(logger, storeRepository, payoutProcessorSettings, applicationDbContextFactory,
|
||||
btcPayNetworkProvider, pluginHookService, eventAggregator)
|
||||
{
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
_lightningClientFactoryService = lightningClientFactoryService;
|
||||
@ -50,15 +53,16 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
||||
_options = options;
|
||||
_payoutHandler = (LightningLikePayoutHandler)payoutHandlers.FindPayoutHandler(PaymentMethodId);
|
||||
|
||||
_network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(_PayoutProcesserSettings.GetPaymentMethodId().CryptoCode);
|
||||
_network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(PayoutProcessorSettings.GetPaymentMethodId().CryptoCode);
|
||||
}
|
||||
|
||||
protected override async Task Process(ISupportedPaymentMethod paymentMethod, List<PayoutData> payouts)
|
||||
{
|
||||
var processorBlob = GetBlob(PayoutProcessorSettings);
|
||||
var lightningSupportedPaymentMethod = (LightningSupportedPaymentMethod)paymentMethod;
|
||||
if (lightningSupportedPaymentMethod.IsInternalNode &&
|
||||
!(await Task.WhenAll((await _storeRepository.GetStoreUsers(_PayoutProcesserSettings.StoreId))
|
||||
.Where(user => user.StoreRole.ToPermissionSet( _PayoutProcesserSettings.StoreId).Contains(Policies.CanModifyStoreSettings, _PayoutProcesserSettings.StoreId)).Select(user => user.Id)
|
||||
!(await Task.WhenAll((await _storeRepository.GetStoreUsers(PayoutProcessorSettings.StoreId))
|
||||
.Where(user => user.StoreRole.ToPermissionSet( PayoutProcessorSettings.StoreId).Contains(Policies.CanModifyStoreSettings, PayoutProcessorSettings.StoreId)).Select(user => user.Id)
|
||||
.Select(s => _userService.IsAdminUser(s)))).Any(b => b))
|
||||
{
|
||||
return;
|
||||
@ -70,6 +74,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
||||
foreach (var payoutData in payouts)
|
||||
{
|
||||
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
|
||||
var failed = false;
|
||||
var claim = await _payoutHandler.ParseClaimDestination(PaymentMethodId, blob.Destination, CancellationToken);
|
||||
try
|
||||
{
|
||||
@ -83,17 +88,40 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Au
|
||||
{
|
||||
continue;
|
||||
}
|
||||
await TrypayBolt(client, blob, payoutData,
|
||||
failed = await TrypayBolt(client, blob, payoutData,
|
||||
lnurlResult.Item1);
|
||||
break;
|
||||
case BoltInvoiceClaimDestination item1:
|
||||
await TrypayBolt(client, blob, payoutData, item1.PaymentRequest);
|
||||
failed = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logs.PayServer.LogError(e, $"Could not process payout {payoutData.Id}");
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (failed && processorBlob.CancelPayoutAfterFailures is not null)
|
||||
{
|
||||
if (!_failedPayoutCounter.TryGetValue(payoutData.Id, out int counter))
|
||||
{
|
||||
counter = 0;
|
||||
}
|
||||
counter++;
|
||||
if(counter >= processorBlob.CancelPayoutAfterFailures)
|
||||
{
|
||||
payoutData.State = PayoutState.Cancelled;
|
||||
Logs.PayServer.LogError($"Payout {payoutData.Id} has failed {counter} times, cancelling it");
|
||||
}
|
||||
else
|
||||
{
|
||||
_failedPayoutCounter.AddOrReplace(payoutData.Id, counter);
|
||||
}
|
||||
}
|
||||
if (payoutData.State == PayoutState.Cancelled)
|
||||
{
|
||||
_failedPayoutCounter.TryRemove(payoutData.Id, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ public class LightningAutomatedPayoutSenderFactory : IPayoutProcessorFactory
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
|
||||
|
||||
public LightningAutomatedPayoutSenderFactory(BTCPayNetworkProvider btcPayNetworkProvider, IServiceProvider serviceProvider, LinkGenerator linkGenerator)
|
||||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
|
@ -59,7 +59,7 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
|
||||
return View(new LightningTransferViewModel(activeProcessor is null ? new AutomatedPayoutBlob() : OnChainAutomatedPayoutProcessor.GetBlob(activeProcessor)));
|
||||
return View(new LightningTransferViewModel(activeProcessor is null ? new LightningAutomatedPayoutBlob() : LightningAutomatedPayoutProcessor.GetBlob(activeProcessor)));
|
||||
}
|
||||
|
||||
[HttpPost("~/stores/{storeId}/payout-processors/lightning-automated/{cryptocode}")]
|
||||
@ -92,7 +92,7 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
activeProcessor ??= new PayoutProcessorData();
|
||||
activeProcessor.HasTypedBlob<AutomatedPayoutBlob>().SetBlob(automatedTransferBlob.ToBlob());
|
||||
activeProcessor.HasTypedBlob<LightningAutomatedPayoutBlob>().SetBlob(automatedTransferBlob.ToBlob());
|
||||
activeProcessor.StoreId = storeId;
|
||||
activeProcessor.PaymentMethod = new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString();
|
||||
activeProcessor.Processor = _lightningAutomatedPayoutSenderFactory.Processor;
|
||||
@ -119,16 +119,26 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
|
||||
|
||||
}
|
||||
|
||||
public LightningTransferViewModel(AutomatedPayoutBlob blob)
|
||||
public LightningTransferViewModel(LightningAutomatedPayoutBlob blob)
|
||||
{
|
||||
IntervalMinutes = blob.Interval.TotalMinutes;
|
||||
CancelPayoutAfterFailures = blob.CancelPayoutAfterFailures;
|
||||
ProcessNewPayoutsInstantly = blob.ProcessNewPayoutsInstantly;
|
||||
}
|
||||
|
||||
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||
|
||||
public int? CancelPayoutAfterFailures { get; set; }
|
||||
|
||||
[Range(AutomatedPayoutConstants.MinIntervalMinutes, AutomatedPayoutConstants.MaxIntervalMinutes)]
|
||||
public double IntervalMinutes { get; set; }
|
||||
|
||||
public AutomatedPayoutBlob ToBlob()
|
||||
public LightningAutomatedPayoutBlob ToBlob()
|
||||
{
|
||||
return new AutomatedPayoutBlob { Interval = TimeSpan.FromMinutes(IntervalMinutes) };
|
||||
return new LightningAutomatedPayoutBlob {
|
||||
ProcessNewPayoutsInstantly = ProcessNewPayoutsInstantly,
|
||||
Interval = TimeSpan.FromMinutes(IntervalMinutes),
|
||||
CancelPayoutAfterFailures = CancelPayoutAfterFailures};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,4 +5,5 @@ namespace BTCPayServer.PayoutProcessors.OnChain;
|
||||
public class OnChainAutomatedPayoutBlob : AutomatedPayoutBlob
|
||||
{
|
||||
public int FeeTargetBlock { get; set; } = 1;
|
||||
public decimal Threshold { get; set; } = 0;
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
@ -45,8 +44,8 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IPluginHookService pluginHookService,
|
||||
IFeeProviderFactory feeProviderFactory) :
|
||||
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, pullPaymentHostedService,
|
||||
btcPayNetworkProvider, pluginHookService)
|
||||
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory,
|
||||
btcPayNetworkProvider, pluginHookService, eventAggregator)
|
||||
{
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
_btcPayWalletProvider = btcPayWalletProvider;
|
||||
@ -97,13 +96,19 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
||||
var changeAddress = await explorerClient.GetUnusedAsync(
|
||||
storePaymentMethod.AccountDerivation, DerivationFeature.Change, 0, true);
|
||||
|
||||
var processorBlob = GetBlob(_PayoutProcesserSettings);
|
||||
var processorBlob = GetBlob(PayoutProcessorSettings);
|
||||
var payoutToBlobs = payouts.ToDictionary(data => data, data => data.GetBlob(_btcPayNetworkJsonSerializerSettings));
|
||||
if (payoutToBlobs.Sum(pair => pair.Value.CryptoAmount) < processorBlob.Threshold)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var feeRate = await FeeProvider.GetFeeRateAsync(Math.Max(processorBlob.FeeTargetBlock, 1));
|
||||
|
||||
var transfersProcessing = new List<PayoutData>();
|
||||
foreach (var transferRequest in payouts)
|
||||
var transfersProcessing = new List<KeyValuePair<PayoutData, PayoutBlob>>();
|
||||
foreach (var transferRequest in payoutToBlobs)
|
||||
{
|
||||
var blob = transferRequest.GetBlob(_btcPayNetworkJsonSerializerSettings);
|
||||
var blob = transferRequest.Value;
|
||||
if (failedAmount.HasValue && blob.CryptoAmount >= failedAmount)
|
||||
{
|
||||
continue;
|
||||
@ -153,10 +158,10 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
||||
try
|
||||
{
|
||||
var txHash = workingTx.GetHash();
|
||||
foreach (PayoutData payoutData in transfersProcessing)
|
||||
foreach (var payoutData in transfersProcessing)
|
||||
{
|
||||
payoutData.State = PayoutState.InProgress;
|
||||
_bitcoinLikePayoutHandler.SetProofBlob(payoutData,
|
||||
payoutData.Key.State = PayoutState.InProgress;
|
||||
_bitcoinLikePayoutHandler.SetProofBlob(payoutData.Key,
|
||||
new PayoutTransactionOnChainBlob()
|
||||
{
|
||||
Accounted = true,
|
||||
@ -175,12 +180,12 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
||||
{
|
||||
tcs.SetResult(false);
|
||||
}
|
||||
var walletId = new WalletId(_PayoutProcesserSettings.StoreId, PaymentMethodId.CryptoCode);
|
||||
foreach (PayoutData payoutData in transfersProcessing)
|
||||
var walletId = new WalletId(PayoutProcessorSettings.StoreId, PaymentMethodId.CryptoCode);
|
||||
foreach (var payoutData in transfersProcessing)
|
||||
{
|
||||
await WalletRepository.AddWalletTransactionAttachment(walletId,
|
||||
txHash,
|
||||
Attachment.Payout(payoutData.PullPaymentDataId, payoutData.Id));
|
||||
Attachment.Payout(payoutData.Key.PullPaymentDataId, payoutData.Key.Id));
|
||||
}
|
||||
await Task.WhenAny(tcs.Task, task);
|
||||
}
|
||||
|
@ -134,12 +134,17 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
|
||||
|
||||
public OnChainTransferViewModel(OnChainAutomatedPayoutBlob blob)
|
||||
{
|
||||
ProcessNewPayoutsInstantly = blob.ProcessNewPayoutsInstantly;
|
||||
IntervalMinutes = blob.Interval.TotalMinutes;
|
||||
FeeTargetBlock = blob.FeeTargetBlock;
|
||||
Threshold = blob.Threshold;
|
||||
}
|
||||
|
||||
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||
|
||||
[Range(1, 1000)]
|
||||
public int FeeTargetBlock { get; set; }
|
||||
public decimal Threshold { get; set; }
|
||||
|
||||
[Range(AutomatedPayoutConstants.MinIntervalMinutes, AutomatedPayoutConstants.MaxIntervalMinutes)]
|
||||
public double IntervalMinutes { get; set; }
|
||||
@ -148,8 +153,10 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
|
||||
{
|
||||
return new OnChainAutomatedPayoutBlob
|
||||
{
|
||||
ProcessNewPayoutsInstantly = ProcessNewPayoutsInstantly,
|
||||
FeeTargetBlock = FeeTargetBlock,
|
||||
Interval = TimeSpan.FromMinutes(IntervalMinutes)
|
||||
Interval = TimeSpan.FromMinutes(IntervalMinutes),
|
||||
Threshold = Threshold
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
@ -13,15 +14,16 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using NicolasDorier.RateLimits;
|
||||
using CrowdfundResetEvery = BTCPayServer.Services.Apps.CrowdfundResetEvery;
|
||||
|
||||
namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
{
|
||||
@ -176,33 +178,41 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
{
|
||||
var appPath = await _appService.ViewLink(app);
|
||||
var appUrl = HttpContext.Request.GetAbsoluteUri(appPath);
|
||||
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
var invoice = await _invoiceController.CreateInvoiceCoreRaw(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = AppService.GetAppOrderId(app),
|
||||
Amount = price,
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
SupportedTransactionCurrencies = paymentMethods,
|
||||
RedirectURL = request.RedirectUrl ?? appUrl,
|
||||
Metadata = new InvoiceMetadata()
|
||||
{
|
||||
OrderId = AppService.GetRandomOrderId(),
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email
|
||||
}.ToJObject(),
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions()
|
||||
{
|
||||
RedirectURL = request.RedirectUrl ?? appUrl,
|
||||
PaymentMethods = paymentMethods?.Where(p => p.Value.Enabled)
|
||||
.Select(p => p.Key).ToArray()
|
||||
},
|
||||
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string> { AppService.GetAppInternalTag(appId) },
|
||||
cancellationToken, entity =>
|
||||
{
|
||||
entity.NotificationURLTemplate = settings.NotificationUrl;
|
||||
entity.FullNotifications = true;
|
||||
entity.ExtendedNotifications = true;
|
||||
entity.Metadata.OrderUrl = appUrl;
|
||||
});
|
||||
|
||||
if (request.RedirectToCheckout)
|
||||
{
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice",
|
||||
new { invoiceId = invoice.Data.Id });
|
||||
new { invoiceId = invoice.Id });
|
||||
}
|
||||
|
||||
return Ok(invoice.Data.Id);
|
||||
return Ok(invoice.Id);
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
@ -249,7 +259,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
IsRecurring = resetEvery != nameof(CrowdfundResetEvery.Never),
|
||||
UseAllStoreInvoices = app.TagAllInvoices,
|
||||
AppId = appId,
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}",
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"appid:{app.Id}",
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
DisplayPerksValue = settings.DisplayPerksValue,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity,
|
||||
@ -267,6 +277,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
vm.AppId = app.Id;
|
||||
vm.TargetCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.TargetCurrency);
|
||||
if (_currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
||||
|
@ -89,15 +89,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||
.Select(entities =>
|
||||
{
|
||||
var total = entities
|
||||
.Sum(entity => entity.GetPayments(true)
|
||||
.Sum(pay =>
|
||||
{
|
||||
var paymentMethodId = pay.GetPaymentMethodId();
|
||||
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
|
||||
return rate * value;
|
||||
}));
|
||||
var total = entities.Sum(entity => entity.PaidAmount.Net);
|
||||
var itemCode = entities.Key;
|
||||
var perk = perks.FirstOrDefault(p => p.Id == itemCode);
|
||||
return new ItemStats
|
||||
@ -167,13 +159,7 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||
!string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||
.ToDictionary(entities => entities.Key, entities =>
|
||||
entities.Sum(entity => entity.GetPayments(true).Sum(pay =>
|
||||
{
|
||||
var paymentMethodId = pay.GetPaymentMethodId();
|
||||
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
|
||||
return rate * value;
|
||||
})));
|
||||
entities.Sum(entity => entity.PaidAmount.Net));
|
||||
}
|
||||
|
||||
var perks = AppService.Parse( settings.PerksTemplate, false);
|
||||
|
@ -119,7 +119,7 @@ namespace BTCPayServer.Plugins.NFC
|
||||
}
|
||||
else
|
||||
{
|
||||
due = new LightMoney(lnPaymentMethod.Calculate().Due);
|
||||
due = LightMoney.Coins(lnPaymentMethod.Calculate().Due);
|
||||
}
|
||||
|
||||
if (info.MinWithdrawable > due || due > info.MaxWithdrawable)
|
||||
@ -135,10 +135,10 @@ namespace BTCPayServer.Plugins.NFC
|
||||
|
||||
if (lnurlPaymentMethod is not null)
|
||||
{
|
||||
Money due;
|
||||
decimal due;
|
||||
if (invoice.Type == InvoiceType.TopUp && request.Amount is not null)
|
||||
{
|
||||
due = new Money(request.Amount.Value, MoneyUnit.Satoshi);
|
||||
due = new Money(request.Amount.Value, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC);
|
||||
}
|
||||
else if (invoice.Type == InvoiceType.TopUp)
|
||||
{
|
||||
@ -152,7 +152,7 @@ namespace BTCPayServer.Plugins.NFC
|
||||
try
|
||||
{
|
||||
httpClient = CreateHttpClient(info.Callback);
|
||||
var amount = LightMoney.Satoshis(due.Satoshi);
|
||||
var amount = LightMoney.Coins(due);
|
||||
var actionPath = Url.Action(nameof(UILNURLController.GetLNURLForInvoice), "UILNURL",
|
||||
new { invoiceId = request.InvoiceId, cryptoCode = "BTC", amount = amount.MilliSatoshi });
|
||||
var url = Request.GetAbsoluteUri(actionPath);
|
||||
|
@ -24,6 +24,7 @@ namespace BTCPayServer.Plugins
|
||||
// Trigger simple action hook for registered plugins
|
||||
public async Task ApplyAction(string hook, object args)
|
||||
{
|
||||
ActionInvoked?.Invoke(this, (hook, args));
|
||||
var filters = _actions
|
||||
.Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList();
|
||||
foreach (IPluginHookAction pluginHookFilter in filters)
|
||||
@ -42,6 +43,7 @@ namespace BTCPayServer.Plugins
|
||||
// Trigger hook on which registered plugins can optionally return modified args or new object back
|
||||
public async Task<object> ApplyFilter(string hook, object args)
|
||||
{
|
||||
FilterInvoked?.Invoke(this, (hook, args));
|
||||
var filters = _filters
|
||||
.Where(filter => filter.Hook.Equals(hook, StringComparison.InvariantCultureIgnoreCase)).ToList();
|
||||
foreach (IPluginHookFilter pluginHookFilter in filters)
|
||||
@ -58,5 +60,8 @@ namespace BTCPayServer.Plugins
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
public event EventHandler<(string hook, object args)> ActionInvoked;
|
||||
public event EventHandler<(string hook, object args)> FilterInvoked;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
@ -35,6 +36,7 @@ using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NicolasDorier.RateLimits;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
@ -133,6 +135,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount = null,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? tip = null,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? discount = null,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? customAmount = null,
|
||||
string email = null,
|
||||
string orderId = null,
|
||||
string notificationUrl = null,
|
||||
@ -232,9 +235,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
|
||||
price += expectedCartItemPrice * cartItem.Value;
|
||||
}
|
||||
if (discount is decimal d)
|
||||
if (customAmount is { } c)
|
||||
price += c;
|
||||
if (discount is { } d)
|
||||
price -= price * d/100.0m;
|
||||
if (tip is decimal t)
|
||||
if (tip is { } t)
|
||||
price += t;
|
||||
}
|
||||
}
|
||||
@ -275,35 +280,63 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
|
||||
}
|
||||
|
||||
var amtField = form.GetFieldByFullName($"{FormDataService.InvoiceParameterPrefix}amount");
|
||||
if (amtField is null && price.HasValue)
|
||||
{
|
||||
form.Fields.Add(new Field
|
||||
{
|
||||
Name = $"{FormDataService.InvoiceParameterPrefix}amount",
|
||||
Type = "hidden",
|
||||
Value = price.ToString(),
|
||||
Constant = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
amtField.Value = price?.ToString();
|
||||
}
|
||||
formResponseJObject = FormDataService.GetValues(form);
|
||||
|
||||
var invoiceRequest = FormDataService.GenerateInvoiceParametersFromForm(form);
|
||||
if (invoiceRequest.Amount is not null)
|
||||
{
|
||||
price = invoiceRequest.Amount.Value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
try
|
||||
{
|
||||
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest
|
||||
var invoice = await _invoiceController.CreateInvoiceCoreRaw(new CreateInvoiceRequest
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
Amount = price,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId ?? AppService.GetAppOrderId(app),
|
||||
NotificationURL =
|
||||
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
|
||||
RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl
|
||||
: !string.IsNullOrEmpty(settings.RedirectUrl) ? settings.RedirectUrl
|
||||
: Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), "UIPointOfSale", new { appId, viewType })),
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
SupportedTransactionCurrencies = paymentMethods,
|
||||
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||
? storeBlob.RequiresRefundEmail
|
||||
: requiresRefundEmail == RequiresRefundEmail.On,
|
||||
Metadata = new InvoiceMetadata()
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId ?? AppService.GetRandomOrderId()
|
||||
}.ToJObject(),
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions()
|
||||
{
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl
|
||||
: !string.IsNullOrEmpty(settings.RedirectUrl) ? settings.RedirectUrl
|
||||
: Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), "UIPointOfSale", new { appId, viewType })),
|
||||
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||
? storeBlob.RequiresRefundEmail
|
||||
: requiresRefundEmail == RequiresRefundEmail.On,
|
||||
PaymentMethods = paymentMethods?.Where(p => p.Value.Enabled).Select(p => p.Key).ToArray()
|
||||
},
|
||||
AdditionalSearchTerms = new [] { AppService.GetAppSearchTerm(app) }
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string> { AppService.GetAppInternalTag(appId) },
|
||||
cancellationToken, entity =>
|
||||
{
|
||||
entity.NotificationURLTemplate =
|
||||
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl;
|
||||
entity.FullNotifications = true;
|
||||
entity.ExtendedNotifications = true;
|
||||
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
|
||||
entity.Metadata.PosData = jposData;
|
||||
var receiptData = new JObject();
|
||||
@ -356,9 +389,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
});
|
||||
if (price is 0 && storeBlob.ReceiptOptions?.Enabled is true)
|
||||
{
|
||||
return RedirectToAction(nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", new { invoiceId = invoice.Data.Id });
|
||||
return RedirectToAction(nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", new { invoiceId = invoice.Id });
|
||||
}
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id });
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Id });
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
@ -513,7 +546,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
Description = settings.Description,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
RedirectUrl = settings.RedirectUrl,
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}",
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"appid:{app.Id}",
|
||||
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "",
|
||||
FormId = settings.FormId
|
||||
};
|
||||
@ -563,6 +596,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
vm.Id = app.Id;
|
||||
if (!ModelState.IsValid)
|
||||
return View("PointOfSale/UpdatePointOfSale", vm);
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -24,6 +27,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public string Description { get; set; }
|
||||
public string Id { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string[] Categories { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Image { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public ItemPriceType PriceType { get; set; }
|
||||
@ -63,7 +68,35 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public bool EnableTips { get; set; }
|
||||
public string Step { get; set; }
|
||||
public string Title { get; set; }
|
||||
public Item[] Items { get; set; }
|
||||
Item[] _Items;
|
||||
public Item[] Items
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Items;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Items = value;
|
||||
UpdateGroups();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGroups()
|
||||
{
|
||||
AllCategories = null;
|
||||
if (Items is null)
|
||||
return;
|
||||
var groups = Items.SelectMany(g => g.Categories ?? Array.Empty<string>())
|
||||
.ToHashSet()
|
||||
.Select(o => new KeyValuePair<string, string>(o, o))
|
||||
.ToList();
|
||||
if (groups.Count == 0)
|
||||
return;
|
||||
groups.Insert(0, new KeyValuePair<string, string>("All items", "*"));
|
||||
AllCategories = new SelectList(groups, "Value", "Key", "*");
|
||||
}
|
||||
|
||||
public string CurrencyCode { get; set; }
|
||||
public string CurrencySymbol { get; set; }
|
||||
public string AppId { get; set; }
|
||||
@ -76,6 +109,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string CustomLogoLink { get; set; }
|
||||
public string Description { get; set; }
|
||||
public SelectList AllCategories { get; set; }
|
||||
[Display(Name = "Custom CSS Code")]
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public RequiresRefundEmail RequiresRefundEmail { get; set; } = RequiresRefundEmail.InheritFromStore;
|
||||
|
@ -14,6 +14,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
[assembly: InternalsVisibleTo("BTCPayServer.Tests")]
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
class Program
|
||||
|
@ -2,8 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using ExchangeSharp;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
|
@ -15,6 +15,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Configuration
|
||||
public Uri DaemonRpcUri { get; set; }
|
||||
public Uri InternalWalletRpcUri { get; set; }
|
||||
public string WalletDirectory { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -1,6 +1,8 @@
|
||||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Configuration;
|
||||
@ -19,6 +21,19 @@ namespace BTCPayServer.Services.Altcoins.Monero
|
||||
{
|
||||
serviceCollection.AddSingleton(provider =>
|
||||
provider.ConfigureMoneroLikeConfiguration());
|
||||
serviceCollection.AddHttpClient("XMRclient")
|
||||
.ConfigurePrimaryHttpMessageHandler(provider =>
|
||||
{
|
||||
var configuration = provider.GetRequiredService<MoneroLikeConfiguration>();
|
||||
if(!configuration.MoneroLikeConfigurationItems.TryGetValue("XMR", out var xmrConfig) || xmrConfig.Username is null || xmrConfig.Password is null){
|
||||
return new HttpClientHandler();
|
||||
}
|
||||
return new HttpClientHandler
|
||||
{
|
||||
Credentials = new NetworkCredential(xmrConfig.Username, xmrConfig.Password),
|
||||
PreAuthenticate = true
|
||||
};
|
||||
});
|
||||
serviceCollection.AddSingleton<MoneroRPCProvider>();
|
||||
serviceCollection.AddHostedService<MoneroLikeSummaryUpdaterHostedService>();
|
||||
serviceCollection.AddHostedService<MoneroListener>();
|
||||
@ -55,6 +70,12 @@ namespace BTCPayServer.Services.Altcoins.Monero
|
||||
var walletDaemonWalletDirectory =
|
||||
configuration.GetOrDefault<string>(
|
||||
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null);
|
||||
var daemonUsername =
|
||||
configuration.GetOrDefault<string>(
|
||||
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_username", null);
|
||||
var daemonPassword =
|
||||
configuration.GetOrDefault<string>(
|
||||
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_password", null);
|
||||
if (daemonUri == null || walletDaemonUri == null)
|
||||
{
|
||||
throw new ConfigException($"{moneroLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured");
|
||||
@ -63,6 +84,8 @@ namespace BTCPayServer.Services.Altcoins.Monero
|
||||
result.MoneroLikeConfigurationItems.Add(moneroLikeSpecificBtcPayNetwork.CryptoCode, new MoneroLikeConfigurationItem()
|
||||
{
|
||||
DaemonRpcUri = daemonUri,
|
||||
Username = daemonUsername,
|
||||
Password = daemonPassword,
|
||||
InternalWalletRpcUri = walletDaemonUri,
|
||||
WalletDirectory = walletDaemonWalletDirectory
|
||||
});
|
||||
|
@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
{
|
||||
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
model.InvoiceBitcoinUrl = MoneroPaymentType.Instance.GetPaymentLink(network, null,
|
||||
new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due,
|
||||
new MoneroLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.GetDue().Value,
|
||||
null);
|
||||
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
|
||||
}
|
||||
|
@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
||||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri)
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
return paymentMethodDetails.Activated
|
||||
? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}"
|
||||
? $"{(network as MoneroLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?tx_amount={cryptoInfoDue}"
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
|
@ -119,16 +119,16 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
||||
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}");
|
||||
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.Currency} {payment.GetCryptoPaymentData().GetPaymentId()}");
|
||||
var paymentData = (MoneroLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var paymentMethod = invoice.GetPaymentMethod(payment.Network, MoneroPaymentType.Instance);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is MoneroLikeOnChainPaymentMethodDetails monero &&
|
||||
monero.Activated &&
|
||||
monero.GetPaymentDestination() == paymentData.GetDestination() &&
|
||||
paymentMethod.Calculate().Due > Money.Zero)
|
||||
paymentMethod.Calculate().Due > 0.0m)
|
||||
{
|
||||
var walletClient = _moneroRpcProvider.WalletRpcClients[payment.GetCryptoCode()];
|
||||
var walletClient = _moneroRpcProvider.WalletRpcClients[payment.Currency];
|
||||
|
||||
var address = await walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>(
|
||||
"create_address",
|
||||
|
@ -29,10 +29,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
||||
_eventAggregator = eventAggregator;
|
||||
DaemonRpcClients =
|
||||
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
|
||||
pair => new JsonRpcClient(pair.Value.DaemonRpcUri, "", "", httpClientFactory.CreateClient()));
|
||||
pair => new JsonRpcClient(pair.Value.DaemonRpcUri, pair.Value.Username, pair.Value.Password, httpClientFactory.CreateClient($"{pair.Key}client")));
|
||||
WalletRpcClients =
|
||||
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
|
||||
pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "", httpClientFactory.CreateClient()));
|
||||
pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "", httpClientFactory.CreateClient($"{pair.Key}client")));
|
||||
}
|
||||
|
||||
public bool IsAvailable(string cryptoCode)
|
||||
|
@ -93,7 +93,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
|
||||
{
|
||||
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
model.InvoiceBitcoinUrl = ZcashPaymentType.Instance.GetPaymentLink(network, null,
|
||||
new ZcashLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.Due,
|
||||
new ZcashLikeOnChainPaymentMethodDetails() {DepositAddress = cryptoInfo.Address}, cryptoInfo.GetDue().Value,
|
||||
null);
|
||||
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
|
||||
}
|
||||
|
@ -49,10 +49,10 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments
|
||||
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
||||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri)
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, InvoiceEntity invoice, IPaymentMethodDetails paymentMethodDetails, decimal cryptoInfoDue, string serverUri)
|
||||
{
|
||||
return paymentMethodDetails.Activated
|
||||
? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue.ToDecimal(MoneyUnit.BTC)}"
|
||||
? $"{(network as ZcashLikeSpecificBtcPayNetwork).UriScheme}:{paymentMethodDetails.GetPaymentDestination()}?amount={cryptoInfoDue}"
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
|
@ -114,16 +114,16 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
|
||||
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.GetCryptoCode()} {payment.GetCryptoPaymentData().GetPaymentId()}");
|
||||
$"Invoice {invoice.Id} received payment {payment.GetCryptoPaymentData().GetValue()} {payment.Currency} {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)
|
||||
paymentMethod.Calculate().Due > 0.0m)
|
||||
{
|
||||
var walletClient = _ZcashRpcProvider.WalletRpcClients[payment.GetCryptoCode()];
|
||||
var walletClient = _ZcashRpcProvider.WalletRpcClients[payment.Currency];
|
||||
|
||||
var address = await walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>(
|
||||
"create_address",
|
||||
|
@ -40,7 +40,7 @@ namespace BTCPayServer.Services.Apps
|
||||
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.PaymentReceived, new object[]
|
||||
{
|
||||
data.GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
invoiceEvent.Payment.Currency,
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString()
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
@ -183,18 +183,11 @@ namespace BTCPayServer.Services.Apps
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var fiatPrice = e.GetPayments(true).Sum(pay =>
|
||||
{
|
||||
var paymentMethodId = pay.GetPaymentMethodId();
|
||||
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = e.GetPaymentMethod(paymentMethodId).Rate;
|
||||
return rate * value;
|
||||
});
|
||||
{;
|
||||
res.Add(new InvoiceStatsItem
|
||||
{
|
||||
ItemCode = e.Metadata.ItemCode,
|
||||
FiatPrice = fiatPrice,
|
||||
FiatPrice = e.PaidAmount.Net,
|
||||
Date = e.InvoiceTime.Date
|
||||
});
|
||||
}
|
||||
@ -202,8 +195,8 @@ namespace BTCPayServer.Services.Apps
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetAppOrderId(AppData app) => GetAppOrderId(app.AppType, app.Id);
|
||||
public static string GetAppOrderId(string appType, string appId) =>
|
||||
public static string GetAppSearchTerm(AppData app) => GetAppSearchTerm(app.AppType, app.Id);
|
||||
public static string GetAppSearchTerm(string appType, string appId) =>
|
||||
appType switch
|
||||
{
|
||||
CrowdfundAppType.AppType => $"crowdfund-app_{appId}",
|
||||
@ -216,13 +209,18 @@ namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
return invoice.GetInternalTags("APP#");
|
||||
}
|
||||
|
||||
public static string GetRandomOrderId(int length = 16)
|
||||
{
|
||||
return Encoders.Base58.EncodeData(RandomUtils.GetBytes(length));
|
||||
}
|
||||
|
||||
public static async Task<InvoiceEntity[]> GetInvoicesForApp(InvoiceRepository invoiceRepository, AppData appData, DateTimeOffset? startDate = null, string[]? status = null)
|
||||
{
|
||||
var invoices = await invoiceRepository.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
StoreId = new[] { appData.StoreDataId },
|
||||
OrderId = appData.TagAllInvoices ? null : new[] { GetAppOrderId(appData) },
|
||||
TextSearch = appData.TagAllInvoices ? null : GetAppSearchTerm(appData),
|
||||
Status = status ?? new[]{
|
||||
InvoiceState.ToString(InvoiceStatusLegacy.New),
|
||||
InvoiceState.ToString(InvoiceStatusLegacy.Paid),
|
||||
|
19
BTCPayServer/Services/Invoices/Amounts.cs
Normal file
19
BTCPayServer/Services/Invoices/Amounts.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public class Amounts
|
||||
{
|
||||
public string Currency { get; set; }
|
||||
/// <summary>
|
||||
/// An amount with fee included
|
||||
/// </summary>
|
||||
public decimal Gross { get; set; }
|
||||
/// <summary>
|
||||
/// An amount without fee included
|
||||
/// </summary>
|
||||
public decimal Net { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Currency}: Net={Net}, Gross={Gross}";
|
||||
}
|
||||
}
|
||||
}
|
@ -64,23 +64,19 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
{
|
||||
foreach (var payment in payments)
|
||||
{
|
||||
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
|
||||
var pdata = payment.GetCryptoPaymentData();
|
||||
|
||||
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId());
|
||||
var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee;
|
||||
invoiceDue -= paidAfterNetworkFees * pmethod.Rate;
|
||||
invoiceDue -= payment.InvoicePaidAmount.Net;
|
||||
|
||||
var target = new ExportInvoiceHolder
|
||||
{
|
||||
ReceivedDate = payment.ReceivedTime.UtcDateTime,
|
||||
PaymentId = pdata.GetPaymentId(),
|
||||
CryptoCode = cryptoCode,
|
||||
ConversionRate = pmethod.Rate,
|
||||
CryptoCode = payment.Currency,
|
||||
ConversionRate = payment.Rate,
|
||||
PaymentType = payment.GetPaymentMethodId().PaymentType.ToPrettyString(),
|
||||
Destination = pdata.GetDestination(),
|
||||
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
|
||||
PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
|
||||
Paid = payment.PaidAmount.Gross.ToString(CultureInfo.InvariantCulture),
|
||||
PaidCurrency = Math.Round(payment.InvoicePaidAmount.Gross, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
|
||||
// Adding NetworkFee because Paid doesn't take into account network fees
|
||||
// so if fee is 10000 satoshis, customer can essentially send infinite number of tx
|
||||
// and merchant effectivelly would receive 0 BTC, invoice won't be paid
|
||||
|
@ -378,6 +378,82 @@ namespace BTCPayServer.Services.Invoices
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, decimal> Rates
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public void UpdateTotals()
|
||||
{
|
||||
Rates = new Dictionary<string, decimal>();
|
||||
foreach (var p in GetPaymentMethods())
|
||||
{
|
||||
Rates.TryAdd(p.Currency, p.Rate);
|
||||
}
|
||||
PaidAmount = new Amounts()
|
||||
{
|
||||
Currency = Currency
|
||||
};
|
||||
foreach (var payment in GetPayments(false))
|
||||
{
|
||||
payment.Rate = Rates[payment.Currency];
|
||||
payment.InvoiceEntity = this;
|
||||
payment.UpdateAmounts();
|
||||
if (payment.Accounted)
|
||||
{
|
||||
PaidAmount.Gross += payment.InvoicePaidAmount.Gross;
|
||||
PaidAmount.Net += payment.InvoicePaidAmount.Net;
|
||||
}
|
||||
}
|
||||
NetDue = Price - PaidAmount.Net;
|
||||
MinimumNetDue = Price * (1.0m - ((decimal)PaymentTolerance / 100.0m)) - PaidAmount.Net;
|
||||
PaidFee = PaidAmount.Gross - PaidAmount.Net;
|
||||
if (NetDue < 0.0m)
|
||||
{
|
||||
// If any payment method exactly pay the invoice, the overpayment is caused by
|
||||
// rounding limitation of the underlying payment method.
|
||||
// Document this overpayment as dust, and set the net due to 0
|
||||
if (GetPaymentMethods().Any(p => p.Calculate().DueUncapped == 0.0m))
|
||||
{
|
||||
Dust = -NetDue;
|
||||
NetDue = 0.0m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overpaid amount caused by payment method
|
||||
/// Example: If you need to pay 124.4 sats, the on-chain payment need to be technically rounded to 125 sats, the extra 0.6 sats shouldn't be considered an over payment.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal Dust { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The due to consider the invoice paid (can be negative if over payment)
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal NetDue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
/// <summary>
|
||||
/// Minumum due to consider the invoice paid (can be negative if overpaid)
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal MinimumNetDue { get; set; }
|
||||
public bool IsUnderPaid => MinimumNetDue > 0;
|
||||
[JsonIgnore]
|
||||
public bool IsOverPaid => NetDue < 0;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Total of network fee paid by accounted payments
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal PaidFee { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public InvoiceStatusLegacy Status { get; set; }
|
||||
[JsonProperty(PropertyName = "status")]
|
||||
@ -399,7 +475,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
public List<PaymentEntity> GetPayments(string cryptoCode, bool accountedOnly)
|
||||
{
|
||||
return GetPayments(accountedOnly).Where(p => p.CryptoCode == cryptoCode).ToList();
|
||||
return GetPayments(accountedOnly).Where(p => p.Currency == cryptoCode).ToList();
|
||||
}
|
||||
public List<PaymentEntity> GetPayments(BTCPayNetworkBase network, bool accountedOnly)
|
||||
{
|
||||
@ -554,13 +630,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
var minerInfo = new MinerFeeInfo();
|
||||
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
|
||||
minerInfo.TotalFee = accounting.ToSmallestUnit(accounting.NetworkFee);
|
||||
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate
|
||||
.GetFee(1).Satoshi;
|
||||
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
|
||||
|
||||
#pragma warning disable 618
|
||||
if (info.CryptoCode == "BTC")
|
||||
if (info.Currency == "BTC")
|
||||
{
|
||||
dto.BTCPrice = cryptoInfo.Price;
|
||||
dto.Rate = cryptoInfo.Rate;
|
||||
@ -576,8 +652,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
dto.CryptoInfo.Add(cryptoInfo);
|
||||
dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls);
|
||||
dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi);
|
||||
dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi);
|
||||
dto.PaymentSubtotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(subtotalPrice));
|
||||
dto.PaymentTotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(accounting.TotalDue));
|
||||
dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency()
|
||||
{
|
||||
Enabled = true
|
||||
@ -640,12 +716,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
continue;
|
||||
}
|
||||
r.CryptoCode = paymentMethodId.CryptoCode;
|
||||
r.Currency = paymentMethodId.CryptoCode;
|
||||
r.PaymentType = paymentMethodId.PaymentType.ToString();
|
||||
r.ParentEntity = this;
|
||||
if (Networks != null)
|
||||
{
|
||||
r.Network = Networks.GetNetwork<BTCPayNetworkBase>(r.CryptoCode);
|
||||
r.Network = Networks.GetNetwork<BTCPayNetworkBase>(r.Currency);
|
||||
if (r.Network is null)
|
||||
continue;
|
||||
}
|
||||
@ -671,7 +747,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
foreach (var v in paymentMethods)
|
||||
{
|
||||
var clone = serializer.ToObject<PaymentMethod>(serializer.ToString(v));
|
||||
clone.CryptoCode = null;
|
||||
clone.Currency = null;
|
||||
clone.PaymentType = null;
|
||||
obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone))));
|
||||
}
|
||||
@ -681,6 +757,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
cryptoData.ParentEntity = this;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
UpdateTotals();
|
||||
}
|
||||
|
||||
public InvoiceState GetInvoiceState()
|
||||
@ -748,6 +825,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
return Type == InvoiceType.TopUp && Price == 0.0m;
|
||||
}
|
||||
|
||||
public Amounts PaidAmount { get; set; }
|
||||
}
|
||||
|
||||
public enum InvoiceStatusLegacy
|
||||
@ -897,30 +976,31 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
public class PaymentMethodAccounting
|
||||
{
|
||||
public int Divisibility { get; set; }
|
||||
/// <summary>Total amount of this invoice</summary>
|
||||
public Money TotalDue { get; set; }
|
||||
public decimal TotalDue { get; set; }
|
||||
|
||||
/// <summary>Amount of crypto remaining to pay this invoice</summary>
|
||||
public Money Due { get; set; }
|
||||
public decimal Due { get; set; }
|
||||
|
||||
/// <summary>Same as Due, can be negative</summary>
|
||||
public Money DueUncapped { get; set; }
|
||||
public decimal DueUncapped { get; set; }
|
||||
|
||||
/// <summary>If DueUncapped is negative, that means user overpaid invoice</summary>
|
||||
public Money OverpaidHelper
|
||||
public decimal OverpaidHelper
|
||||
{
|
||||
get { return DueUncapped > Money.Zero ? Money.Zero : -DueUncapped; }
|
||||
get { return DueUncapped > 0.0m ? 0.0m : -DueUncapped; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of the invoice paid after conversion to this crypto currency
|
||||
/// </summary>
|
||||
public Money Paid { get; set; }
|
||||
public decimal Paid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of the invoice paid in this currency
|
||||
/// </summary>
|
||||
public Money CryptoPaid { get; set; }
|
||||
public decimal CryptoPaid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of transactions required to pay
|
||||
@ -934,15 +1014,25 @@ namespace BTCPayServer.Services.Invoices
|
||||
/// <summary>
|
||||
/// Total amount of network fee to pay to the invoice
|
||||
/// </summary>
|
||||
public Money NetworkFee { get; set; }
|
||||
public decimal NetworkFee { get; set; }
|
||||
/// <summary>
|
||||
/// Total amount of network fee to pay to the invoice
|
||||
/// </summary>
|
||||
public Money NetworkFeeAlreadyPaid { get; set; }
|
||||
public decimal NetworkFeeAlreadyPaid { get; set; }
|
||||
/// <summary>
|
||||
/// Minimum required to be paid in order to accept invoice as paid
|
||||
/// </summary>
|
||||
public Money MinimumTotalDue { get; set; }
|
||||
public decimal MinimumTotalDue { get; set; }
|
||||
|
||||
public decimal ToSmallestUnit(decimal v)
|
||||
{
|
||||
for (int i = 0; i < Divisibility; i++)
|
||||
{
|
||||
v *= 10.0m;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
public string ShowMoney(decimal v) => MoneyExtensions.ShowMoney(v, Divisibility);
|
||||
}
|
||||
|
||||
public interface IPaymentMethod
|
||||
@ -959,8 +1049,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
[JsonIgnore]
|
||||
public BTCPayNetworkBase Network { get; set; }
|
||||
[JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[Obsolete("Use GetId().CryptoCode instead")]
|
||||
public string CryptoCode { get; set; }
|
||||
public string Currency { get; set; }
|
||||
[JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[Obsolete("Use GetId().PaymentType instead")]
|
||||
public string PaymentType { get; set; }
|
||||
@ -975,14 +1064,14 @@ namespace BTCPayServer.Services.Invoices
|
||||
public PaymentMethodId GetId()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
return new PaymentMethodId(CryptoCode, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(PaymentType));
|
||||
return new PaymentMethodId(Currency, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(PaymentType));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public void SetId(PaymentMethodId id)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
CryptoCode = id.CryptoCode;
|
||||
Currency = id.CryptoCode;
|
||||
PaymentType = id.PaymentType.ToString();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
@ -1053,7 +1142,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
DepositAddress = bitcoinPaymentMethod.DepositAddress;
|
||||
}
|
||||
PaymentMethodDetails = JObject.Parse(paymentMethod.GetPaymentType().SerializePaymentMethodDetails(Network, paymentMethod));
|
||||
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return this;
|
||||
}
|
||||
@ -1068,86 +1156,58 @@ namespace BTCPayServer.Services.Invoices
|
||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
|
||||
public string DepositAddress { get; set; }
|
||||
|
||||
public PaymentMethodAccounting Calculate(Func<PaymentEntity, bool> paymentPredicate = null)
|
||||
public PaymentMethodAccounting Calculate()
|
||||
{
|
||||
paymentPredicate = paymentPredicate ?? new Func<PaymentEntity, bool>((p) => true);
|
||||
var paymentMethods = ParentEntity.GetPaymentMethods();
|
||||
|
||||
var totalDue = ParentEntity.Price / Rate;
|
||||
var paid = 0m;
|
||||
var cryptoPaid = 0.0m;
|
||||
|
||||
var i = ParentEntity;
|
||||
int precision = Network?.Divisibility ?? 8;
|
||||
|
||||
var totalDueNoNetworkCost = Coins(Extensions.RoundUp(totalDue, precision));
|
||||
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
||||
int txRequired = 0;
|
||||
decimal networkFeeAlreadyPaid = 0.0m;
|
||||
_ = ParentEntity.GetPayments(true)
|
||||
.Where(p => paymentPredicate(p))
|
||||
.OrderBy(p => p.ReceivedTime)
|
||||
.Select(_ =>
|
||||
{
|
||||
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee, precision);
|
||||
networkFeeAlreadyPaid += txFee;
|
||||
paid += _.GetValue(paymentMethods, GetId(), null, precision);
|
||||
if (!paidEnough)
|
||||
{
|
||||
totalDue += txFee;
|
||||
}
|
||||
|
||||
paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision);
|
||||
if (GetId() == _.GetPaymentMethodId())
|
||||
{
|
||||
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
||||
txRequired++;
|
||||
}
|
||||
|
||||
return _;
|
||||
}).ToArray();
|
||||
|
||||
var accounting = new PaymentMethodAccounting();
|
||||
accounting.TxCount = txRequired;
|
||||
if (!paidEnough)
|
||||
var thisPaymentMethodPayments = i.GetPayments(true).Where(p => GetId() == p.GetPaymentMethodId()).ToList();
|
||||
accounting.TxCount = thisPaymentMethodPayments.Count;
|
||||
accounting.TxRequired = accounting.TxCount;
|
||||
var grossDue = i.Price + i.PaidFee;
|
||||
if (i.MinimumNetDue > 0.0m)
|
||||
{
|
||||
txRequired++;
|
||||
totalDue += GetTxFee();
|
||||
accounting.TxRequired++;
|
||||
grossDue += Rate * (GetPaymentMethodDetails()?.GetNextNetworkFee() ?? 0m);
|
||||
}
|
||||
accounting.Divisibility = precision;
|
||||
accounting.TotalDue = Coins(grossDue / Rate, precision);
|
||||
accounting.Paid = Coins(i.PaidAmount.Gross / Rate, precision);
|
||||
accounting.CryptoPaid = Coins(thisPaymentMethodPayments.Sum(p => p.PaidAmount.Gross), precision);
|
||||
|
||||
accounting.TotalDue = Coins(Extensions.RoundUp(totalDue, precision));
|
||||
accounting.Paid = Coins(Extensions.RoundUp(paid, precision));
|
||||
accounting.TxRequired = txRequired;
|
||||
accounting.CryptoPaid = Coins(Extensions.RoundUp(cryptoPaid, precision));
|
||||
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
||||
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
|
||||
accounting.NetworkFeeAlreadyPaid = Coins(Extensions.RoundUp(networkFeeAlreadyPaid, precision));
|
||||
// If the total due is 0, there is no payment tolerance to calculate
|
||||
var minimumTotalDueSatoshi = accounting.TotalDue.Satoshi == 0
|
||||
? 0
|
||||
: Math.Max(1.0m,
|
||||
accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m)));
|
||||
accounting.MinimumTotalDue = Money.Satoshis(minimumTotalDueSatoshi);
|
||||
// This one deal with the fact where it might looks like a slight over payment due to the dust of another payment method.
|
||||
// So if we detect the NetDue is zero, just cap dueUncapped to 0
|
||||
var dueUncapped = i.NetDue == 0.0m ? 0.0m : grossDue - i.PaidAmount.Gross;
|
||||
accounting.DueUncapped = Coins(dueUncapped / Rate, precision);
|
||||
accounting.Due = Max(accounting.DueUncapped, 0.0m);
|
||||
|
||||
accounting.NetworkFee = Coins((grossDue - i.Price) / Rate, precision);
|
||||
accounting.NetworkFeeAlreadyPaid = Coins(i.PaidFee / Rate, precision);
|
||||
|
||||
accounting.MinimumTotalDue = Max(Smallest(precision), Coins((grossDue * (1.0m - ((decimal)i.PaymentTolerance / 100.0m))) / Rate, precision));
|
||||
return accounting;
|
||||
}
|
||||
|
||||
const decimal MaxCoinValue = decimal.MaxValue / 1_0000_0000m;
|
||||
private Money Coins(decimal v)
|
||||
private decimal Smallest(int precision)
|
||||
{
|
||||
if (v > MaxCoinValue)
|
||||
v = MaxCoinValue;
|
||||
// Clamp the value to not crash on degenerate invoices
|
||||
v *= 1_0000_0000m;
|
||||
if (v > long.MaxValue)
|
||||
return Money.Satoshis(long.MaxValue);
|
||||
if (v < long.MinValue)
|
||||
return Money.Satoshis(long.MinValue);
|
||||
return Money.Satoshis(v);
|
||||
decimal a = 1.0m;
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
a /= 10.0m;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private decimal GetTxFee()
|
||||
decimal Max(decimal a, decimal b) => a > b ? a : b;
|
||||
|
||||
const decimal MaxCoinValue = decimal.MaxValue / 1_0000_0000m;
|
||||
internal static decimal Coins(decimal v, int precision)
|
||||
{
|
||||
return GetPaymentMethodDetails()?.GetNextNetworkFee() ?? 0m;
|
||||
v = Extensions.RoundUp(v, precision);
|
||||
// Clamp the value to not crash on degenerate invoices
|
||||
if (v > MaxCoinValue)
|
||||
v = MaxCoinValue;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1209,19 +1269,60 @@ namespace BTCPayServer.Services.Invoices
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("Use GetpaymentMethodId().CryptoCode instead")]
|
||||
public string CryptoCode
|
||||
string _Currency;
|
||||
[JsonProperty("cryptoCode")]
|
||||
public string Currency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get
|
||||
{
|
||||
return _Currency ?? "BTC";
|
||||
}
|
||||
set
|
||||
{
|
||||
_Currency = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Use GetCryptoPaymentData() instead")]
|
||||
public string CryptoPaymentData { get; set; }
|
||||
[Obsolete("Use GetpaymentMethodId().PaymentType instead")]
|
||||
public string CryptoPaymentDataType { get; set; }
|
||||
[JsonIgnore]
|
||||
public decimal Rate { get; set; }
|
||||
[JsonIgnore]
|
||||
/// <summary>
|
||||
public string InvoiceCurrency => InvoiceEntity.Currency;
|
||||
/// The amount paid by this payment in the <see cref="Currency"/>
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Amounts PaidAmount { get; set; }
|
||||
/// <summary>
|
||||
/// The amount paid by this payment in the <see cref="InvoiceCurrency"/>
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Amounts InvoicePaidAmount { get; set; }
|
||||
[JsonIgnore]
|
||||
public InvoiceEntity InvoiceEntity { get; set; }
|
||||
|
||||
public void UpdateAmounts()
|
||||
{
|
||||
var pd = GetCryptoPaymentData();
|
||||
if (pd is null)
|
||||
return;
|
||||
var value = pd.GetValue();
|
||||
PaidAmount = new Amounts()
|
||||
{
|
||||
Currency = Currency,
|
||||
Gross = value,
|
||||
Net = value - NetworkFee
|
||||
};
|
||||
InvoicePaidAmount = new Amounts()
|
||||
{
|
||||
Currency = InvoiceCurrency,
|
||||
Gross = PaidAmount.Gross * Rate,
|
||||
Net = PaidAmount.Net * Rate
|
||||
};
|
||||
}
|
||||
|
||||
public CryptoPaymentData GetCryptoPaymentData()
|
||||
{
|
||||
@ -1280,21 +1381,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
#pragma warning restore CS0618
|
||||
return this;
|
||||
}
|
||||
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value, int precision)
|
||||
{
|
||||
|
||||
value = value ?? this.GetCryptoPaymentData().GetValue();
|
||||
var to = paymentMethodId;
|
||||
var from = this.GetPaymentMethodId();
|
||||
if (to == from)
|
||||
return decimal.Round(value.Value, precision);
|
||||
var fromRate = paymentMethods[from].Rate;
|
||||
var toRate = paymentMethods[to].Rate;
|
||||
|
||||
var fiatValue = fromRate * decimal.Round(value.Value, precision);
|
||||
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
|
||||
return otherCurrencyValue;
|
||||
}
|
||||
|
||||
public PaymentMethodId GetPaymentMethodId()
|
||||
{
|
||||
@ -1308,16 +1394,9 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new PaymentMethodId(CryptoCode ?? "BTC", paymentType);
|
||||
return new PaymentMethodId(Currency ?? "BTC", paymentType);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public string GetCryptoCode()
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
return CryptoCode ?? "BTC";
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// A record of a payment
|
||||
|
@ -12,7 +12,6 @@ using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -30,16 +29,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings);
|
||||
}
|
||||
|
||||
public Logs Logs { get; }
|
||||
|
||||
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory,
|
||||
BTCPayNetworkProvider networks, EventAggregator eventAggregator, Logs logs)
|
||||
BTCPayNetworkProvider networks, EventAggregator eventAggregator)
|
||||
{
|
||||
Logs = logs;
|
||||
_applicationDbContextFactory = contextFactory;
|
||||
_btcPayNetworkProvider = networks;
|
||||
_eventAggregator = eventAggregator;
|
||||
@ -54,14 +50,20 @@ namespace BTCPayServer.Services.Invoices
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public InvoiceEntity CreateNewInvoice()
|
||||
public InvoiceEntity CreateNewInvoice(string storeId)
|
||||
{
|
||||
return new InvoiceEntity()
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)),
|
||||
StoreId = storeId,
|
||||
Networks = _btcPayNetworkProvider,
|
||||
Version = InvoiceEntity.Lastest_Version,
|
||||
InvoiceTime = DateTimeOffset.UtcNow,
|
||||
Metadata = new InvoiceMetadata()
|
||||
// Truncating was an unintended side effect of previous code. Might want to remove that one day
|
||||
InvoiceTime = DateTimeOffset.UtcNow.TruncateMilliSeconds(),
|
||||
Metadata = new InvoiceMetadata(),
|
||||
#pragma warning disable CS0618
|
||||
Payments = new List<PaymentEntity>()
|
||||
#pragma warning restore CS0618
|
||||
};
|
||||
}
|
||||
|
||||
@ -173,21 +175,14 @@ namespace BTCPayServer.Services.Invoices
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, string[] additionalSearchTerms = null)
|
||||
public async Task CreateInvoiceAsync(InvoiceEntity invoice, string[] additionalSearchTerms = null)
|
||||
{
|
||||
var textSearch = new HashSet<string>();
|
||||
invoice = Clone(invoice);
|
||||
invoice.Networks = _btcPayNetworkProvider;
|
||||
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
#pragma warning disable CS0618
|
||||
invoice.Payments = new List<PaymentEntity>();
|
||||
#pragma warning restore CS0618
|
||||
invoice.StoreId = storeId;
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = new Data.InvoiceData()
|
||||
var invoiceData = new InvoiceData
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
StoreDataId = invoice.StoreId,
|
||||
Id = invoice.Id,
|
||||
Created = invoice.InvoiceTime,
|
||||
OrderId = invoice.Metadata.OrderId,
|
||||
@ -245,16 +240,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
private InvoiceEntity Clone(InvoiceEntity invoice)
|
||||
{
|
||||
var temp = new InvoiceData();
|
||||
temp.SetBlob(invoice);
|
||||
return temp.GetBlob(_btcPayNetworkProvider);
|
||||
}
|
||||
|
||||
public async Task AddInvoiceLogs(string invoiceId, InvoiceLogs logs)
|
||||
@ -544,7 +529,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
var invoiceIdSet = invoiceIds.ToHashSet();
|
||||
using var context = _applicationDbContextFactory.CreateContext();
|
||||
IQueryable<Data.InvoiceData> query =
|
||||
IQueryable<InvoiceData> query =
|
||||
context
|
||||
.Invoices
|
||||
.Include(o => o.Payments)
|
||||
@ -555,7 +540,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
private async Task<InvoiceData> GetInvoiceRaw(string id, ApplicationDbContext dbContext, bool includeAddressData = false)
|
||||
{
|
||||
IQueryable<Data.InvoiceData> query =
|
||||
IQueryable<InvoiceData> query =
|
||||
dbContext
|
||||
.Invoices
|
||||
.Include(o => o.Payments);
|
||||
@ -564,13 +549,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
query = query.Where(i => i.Id == id);
|
||||
|
||||
var invoice = (await query.ToListAsync()).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return null;
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public InvoiceEntity ToEntity(Data.InvoiceData invoice)
|
||||
public InvoiceEntity ToEntity(InvoiceData invoice)
|
||||
{
|
||||
var entity = invoice.GetBlob(_btcPayNetworkProvider);
|
||||
PaymentMethodDictionary paymentMethods = null;
|
||||
@ -617,12 +599,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
entity.Metadata.BuyerEmail = entity.RefundMail;
|
||||
}
|
||||
entity.Archived = invoice.Archived;
|
||||
entity.UpdateTotals();
|
||||
return entity;
|
||||
}
|
||||
|
||||
private IQueryable<Data.InvoiceData> GetInvoiceQuery(ApplicationDbContext context, InvoiceQuery queryObject)
|
||||
private IQueryable<InvoiceData> GetInvoiceQuery(ApplicationDbContext context, InvoiceQuery queryObject)
|
||||
{
|
||||
IQueryable<Data.InvoiceData> query = queryObject.UserId is null
|
||||
IQueryable<InvoiceData> query = queryObject.UserId is null
|
||||
? context.Invoices
|
||||
: context.UserStore
|
||||
.Where(u => u.ApplicationUserId == queryObject.UserId)
|
||||
@ -633,7 +616,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
query = query.Where(i => !i.Archived);
|
||||
}
|
||||
|
||||
if (queryObject.InvoiceId != null && queryObject.InvoiceId.Length > 0)
|
||||
if (queryObject.InvoiceId is { Length: > 0 })
|
||||
{
|
||||
if (queryObject.InvoiceId.Length > 1)
|
||||
{
|
||||
@ -647,7 +630,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
|
||||
if (queryObject.StoreId is { Length: > 0 })
|
||||
{
|
||||
if (queryObject.StoreId.Length > 1)
|
||||
{
|
||||
@ -677,18 +660,18 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (queryObject.EndDate != null)
|
||||
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
|
||||
|
||||
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
|
||||
if (queryObject.OrderId is { Length: > 0 })
|
||||
{
|
||||
var statusSet = queryObject.OrderId.ToHashSet().ToArray();
|
||||
query = query.Where(i => statusSet.Contains(i.OrderId));
|
||||
}
|
||||
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
|
||||
if (queryObject.ItemCode is { Length: > 0 })
|
||||
{
|
||||
var statusSet = queryObject.ItemCode.ToHashSet().ToArray();
|
||||
query = query.Where(i => statusSet.Contains(i.ItemCode));
|
||||
}
|
||||
|
||||
if (queryObject.Status != null && queryObject.Status.Length > 0)
|
||||
if (queryObject.Status is { Length: > 0 })
|
||||
{
|
||||
var statusSet = queryObject.Status.ToHashSet();
|
||||
// We make sure here that the old filters still work
|
||||
@ -723,7 +706,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
query = query.Where(i => unused == (i.Status == "invalid" || !string.IsNullOrEmpty(i.ExceptionStatus)));
|
||||
}
|
||||
|
||||
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
|
||||
if (queryObject.ExceptionStatus is { Length: > 0 })
|
||||
{
|
||||
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet().ToArray();
|
||||
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
|
||||
@ -741,7 +724,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
|
||||
{
|
||||
using var context = _applicationDbContextFactory.CreateContext();
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
var query = GetInvoiceQuery(context, queryObject);
|
||||
query = query.Include(o => o.Payments);
|
||||
if (queryObject.IncludeAddresses)
|
||||
@ -789,7 +772,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
return network == null ? JsonConvert.SerializeObject(data, DefaultSerializerSettings) : network.ToString(data);
|
||||
}
|
||||
|
||||
|
||||
public InvoiceStatistics GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap)
|
||||
{
|
||||
var contributions = invoices
|
||||
@ -821,16 +803,14 @@ namespace BTCPayServer.Services.Invoices
|
||||
p.Status == InvoiceStatusLegacy.Invalid)
|
||||
return new[] { contribution };
|
||||
|
||||
|
||||
// Else, we just sum the payments
|
||||
return payments
|
||||
.Select(pay =>
|
||||
{
|
||||
var paymentMethodContribution = new InvoiceStatistics.Contribution();
|
||||
paymentMethodContribution.PaymentMethodId = pay.GetPaymentMethodId();
|
||||
paymentMethodContribution.Value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = p.GetPaymentMethod(paymentMethodContribution.PaymentMethodId).Rate;
|
||||
paymentMethodContribution.CurrencyValue = rate * paymentMethodContribution.Value;
|
||||
paymentMethodContribution.CurrencyValue = pay.InvoicePaidAmount.Net;
|
||||
paymentMethodContribution.Value = pay.PaidAmount.Net;
|
||||
return paymentMethodContribution;
|
||||
})
|
||||
.ToArray();
|
||||
|
@ -51,7 +51,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
Version = 1,
|
||||
#pragma warning disable CS0618
|
||||
CryptoCode = network.CryptoCode,
|
||||
Currency = network.CryptoCode,
|
||||
#pragma warning restore CS0618
|
||||
ReceivedTime = date.UtcDateTime,
|
||||
Accounted = accounted,
|
||||
|
@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices;
|
||||
|
||||
@ -33,6 +36,7 @@ public class PosAppCartItem
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "price")]
|
||||
[JsonConverter(typeof(PosAppCartItemPriceJsonConverter))]
|
||||
public decimal Price { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "title")]
|
||||
@ -48,11 +52,40 @@ public class PosAppCartItem
|
||||
public string Image { get; set; }
|
||||
}
|
||||
|
||||
public class PosAppCartItemPrice
|
||||
public class PosAppCartItemPriceJsonConverter : JsonConverter
|
||||
{
|
||||
[JsonProperty(PropertyName = "formatted")]
|
||||
public string Formatted { get; set; }
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(decimal) || objectType == typeof(object);
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "type")]
|
||||
public ViewPointOfSaleViewModel.ItemPriceType Type { get; set; }
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
JToken token = JToken.Load(reader);
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Float:
|
||||
if (objectType == typeof(decimal))
|
||||
return token.Value<decimal>();
|
||||
throw new JsonSerializationException($"Unexpected object type: {objectType}");
|
||||
case JTokenType.Integer:
|
||||
case JTokenType.String:
|
||||
if (objectType == typeof(decimal))
|
||||
return decimal.Parse(token.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture);
|
||||
throw new JsonSerializationException($"Unexpected object type: {objectType}");
|
||||
case JTokenType.Null:
|
||||
return null;
|
||||
case JTokenType.Object:
|
||||
return token.ToObject<JObject>()?["value"]?.Value<decimal?>();
|
||||
default:
|
||||
throw new JsonSerializationException($"Unexpected token type: {token.Type}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is decimal x)
|
||||
writer.WriteValue(x);
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +277,7 @@
|
||||
<template id="perks-template">
|
||||
<div class="perks-container">
|
||||
<perk v-if="!perks || perks.length === 0"
|
||||
:perk="{title: 'Donate Custom Amount', price: { type: 0, value: null }}"
|
||||
:perk="{title: 'Donate Custom Amount', priceType: 'Topup', price: { type: 'Topup' } }"
|
||||
:target-currency="targetCurrency"
|
||||
:active="active"
|
||||
:loading="loading"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user