Compare commits

...

21 Commits

Author SHA1 Message Date
94939f0fcb Update changelog 2021-12-14 12:13:31 +09:00
ba49b7b848 Update Bitbank API (#3157)
Co-authored-by: Kukks <evilkukka@gmail.com>
2021-12-14 12:01:06 +09:00
49c3355721 Fix code display (#3172) 2021-12-14 11:58:47 +09:00
7ec66f9a91 Swagger fixes (#3170) 2021-12-14 11:58:32 +09:00
c08674beae Fix copy pay button code (#3175) 2021-12-14 11:58:07 +09:00
2be97c1e6a Fix LN Node availability check (#3189) 2021-12-14 11:56:46 +09:00
ee1a034c0a Fix: serverinfo 'available' wasn't set for BTC 2021-12-14 11:56:26 +09:00
e5113f8b74 Format perk value correctly in crowdfund app (#3141)
close #3109
2021-12-14 11:56:02 +09:00
617fa32f47 Fix issue in invoice - changing the payment method (#3169) 2021-12-14 11:54:39 +09:00
34add92280 changelog and bump 2021-11-20 22:44:44 +09:00
682604c116 Bump LND (#3130) 2021-11-20 22:42:57 +09:00
0b95da1760 Update changelog 2021-11-15 18:26:51 +09:00
7aa92e7012 Add crypto code for invoice and pull payment payout API response (#3099)
* Add "cryptoCode" for invoice payment method API endpoint response

* Add "cryptoCode" for pull payment payout API endpoint response

* Add "#nullable enable" to GreenFieldInvoiceController

* Add "#nullable enable" to GreenfieldPullPaymentController
2021-11-15 17:43:37 +09:00
3bb9e38773 Checkout page of 0 amount invoices shouldn't crash, but 404 2021-11-15 17:42:28 +09:00
6597fe7dff Prevent creation of on-chain invoices below the dust limit (#3082)
* Prevent creation of on-chain invoices below the dust limit

Fixes #3071.

* Apply suggestions from code review

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>

* small fix

* Fix selenium test

0.000000012 BTC (whether rounded or not) is below the dust threshold, causing this test to fail.

* fix CanCreateTopupInvoices test

Don't apply dust threshold conditional for topup invoices.

* Fix test, and minor changes

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2021-11-15 17:42:22 +09:00
f5677747a4 Fix type of property cryptoCode (#3088) 2021-11-15 17:42:13 +09:00
74967dc849 Fix bug with fraction amount display in crowdfund app (#3098)
* Update formatting

* Adjust formatting

* Adjust formatting

* Fix bug with BTC fraction amount display
2021-11-15 17:42:08 +09:00
4aef93985d Fix typo: "Prioriy" -> "Priority" (#3103) 2021-11-15 17:41:49 +09:00
e5dc4fa854 Update Swagger docs for webhook event types (#3104)
close #2999
2021-11-15 17:41:43 +09:00
1830d5db64 Fix label display issue 2021-11-15 17:40:08 +09:00
6f2d29f4e6 Fix payout/pull payment crash when no payment method bug
Fixes #3084.
2021-11-15 17:39:33 +09:00
39 changed files with 260 additions and 86 deletions

View File

@ -27,7 +27,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="6.0.15" />
<PackageReference Include="NBitcoin" Version="6.0.17" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

View File

@ -33,6 +33,8 @@ namespace BTCPayServer.Client.Models
public List<Payment> Payments { get; set; }
public string PaymentMethod { get; set; }
public string CryptoCode { get; set; }
public class Payment
{
public string Id { get; set; }

View File

@ -21,6 +21,7 @@ namespace BTCPayServer.Client.Models
public string PullPaymentId { get; set; }
public string Destination { get; set; }
public string PaymentMethod { get; set; }
public string CryptoCode { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]

View File

@ -1,3 +1,4 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
@ -17,18 +18,24 @@ namespace BTCPayServer.Services.Rates
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken);
var response = await _httpClient.GetAsync("https://public.bitbank.cc/tickers", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
return ((jobj["data"] as JObject) ?? new JObject())
.Properties()
.Select(p => new PairRate(CurrencyPair.Parse(p.Name), CreateBidAsk(p)))
var data = jobj.ContainsKey("data") ? jobj["data"] : null;
if (jobj["success"]?.Value<int>() != 1)
{
var errorCode = data is null? "Unknown": data["code"].Value<string>();
throw new Exception(
$"BitBank Rates API Error: {errorCode}. See https://github.com/bitbankinc/bitbank-api-docs/blob/master/errors.md for more details.");
}
return ((data as JArray) ?? new JArray())
.Select(item => new PairRate(CurrencyPair.Parse(item["pair"].ToString()), CreateBidAsk(item as JObject)))
.ToArray();
}
private static BidAsk CreateBidAsk(JProperty p)
private static BidAsk CreateBidAsk(JObject o)
{
var buy = p.Value["buy"].Value<decimal>();
var sell = p.Value["sell"].Value<decimal>();
var buy = o["buy"].Value<decimal>();
var sell = o["sell"].Value<decimal>();
// Bug from their API (https://github.com/btcpayserver/btcpayserver/issues/741)
return buy < sell ? new BidAsk(buy, sell) : new BidAsk(sell, buy);
}

View File

@ -161,7 +161,7 @@ namespace BTCPayServer.Rating
public ExpressionSyntax FindBestCandidate(CurrencyPair p)
{
var invP = p.Inverse();
var candidates = new List<(CurrencyPair Pair, int Prioriy, ExpressionSyntax Expression, bool Inverse)>();
var candidates = new List<(CurrencyPair Pair, int Priority, ExpressionSyntax Expression, bool Inverse)>();
foreach (var pair in new[]
{
(Pair: p, Priority: 0, Inverse: false),
@ -181,7 +181,7 @@ namespace BTCPayServer.Rating
if (candidates.Count == 0)
return CreateExpression($"ERR_NO_RULE_MATCH({p})");
var best = candidates
.OrderBy(c => c.Prioriy)
.OrderBy(c => c.Priority)
.ThenBy(c => c.Expression.Span.Start)
.First();
return best.Inverse

View File

@ -72,7 +72,7 @@ namespace BTCPayServer.Services.Rates
yield return new AvailableRateProvider("coingecko", "CoinGecko", "https://api.coingecko.com/api/v3/exchange_rates");
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD");
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices");
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/tickers");
yield return new AvailableRateProvider("bitflyer", "Bitflyer", "https://api.bitflyer.com/v1/ticker");
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates");
yield return new AvailableRateProvider("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");

View File

@ -437,6 +437,8 @@ namespace BTCPayServer.Tests
Assert.Equal(payout.Id, payout2.Id);
Assert.Equal(destination, payout2.Destination);
Assert.Equal(PayoutState.AwaitingApproval, payout.State);
Assert.Equal("BTC", payout2.PaymentMethod);
Assert.Equal("BTC", payout2.CryptoCode);
Assert.Null(payout.PaymentMethodAmount);
Logs.Tester.LogInformation("Can't overdraft");
@ -1186,6 +1188,7 @@ namespace BTCPayServer.Tests
Assert.Single(paymentMethods);
var paymentMethod = paymentMethods.First();
Assert.Equal("BTC", paymentMethod.PaymentMethod);
Assert.Equal("BTC", paymentMethod.CryptoCode);
Assert.Empty(paymentMethod.Payments);

View File

@ -493,7 +493,7 @@ namespace BTCPayServer.Tests
var client = new NBitpayClient.Bitpay(new Key(), s.ServerUri);
await client.AuthorizeClient(new NBitpayClient.PairingCode(pairingCode));
await client.CreateInvoiceAsync(
new NBitpayClient.Invoice() { Price = 0.000000012m, Currency = "USD", FullNotifications = true },
new NBitpayClient.Invoice() { Price = 1.000000012m, Currency = "USD", FullNotifications = true },
NBitpayClient.Facade.Merchant);
client = new NBitpayClient.Bitpay(new Key(), s.ServerUri);
@ -503,7 +503,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("ApprovePairing")).Click();
await client.CreateInvoiceAsync(
new NBitpayClient.Invoice() { Price = 0.000000012m, Currency = "USD", FullNotifications = true },
new NBitpayClient.Invoice() { Price = 1.000000012m, Currency = "USD", FullNotifications = true },
NBitpayClient.Facade.Merchant);
s.Driver.Navigate().GoToUrl(s.Link("/api-tokens"));

View File

@ -3004,15 +3004,31 @@ namespace BTCPayServer.Tests
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task CanCreateStrangeInvoice()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLightning();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC");
DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21);
// This should fail, the amount is too low to be above the dust limit of bitcoin
var ex = Assert.Throws<BitPayException>(() => user.BitPay.CreateInvoice(
new Invoice()
{
Price = 0.000000012m,
Currency = "USD",
FullNotifications = true,
ExpirationTime = expiration
}, Facade.Merchant));
Assert.Contains("dust threshold", ex.Message);
await user.RegisterLightningNodeAsync("BTC");
var invoice1 = user.BitPay.CreateInvoice(
new Invoice()
{
@ -3021,6 +3037,7 @@ namespace BTCPayServer.Tests
FullNotifications = true,
ExpirationTime = expiration
}, Facade.Merchant);
Assert.Equal(expiration.ToUnixTimeSeconds(), invoice1.ExpirationTime.ToUnixTimeSeconds());
var invoice2 = user.BitPay.CreateInvoice(new Invoice() { Price = 0.000000019m, Currency = "USD" },
Facade.Merchant);

View File

@ -234,7 +234,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.13.3-beta
image: btcpayserver/lnd:v0.14.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -268,7 +268,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.13.3-beta
image: btcpayserver/lnd:v0.14.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -222,7 +222,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.13.3-beta
image: btcpayserver/lnd:v0.14.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -256,7 +256,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.13.3-beta
image: btcpayserver/lnd:v0.14.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -1,4 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
@ -55,7 +55,7 @@
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.14" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.16" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />

View File

@ -1,8 +1,8 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Globalization;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
@ -49,14 +49,14 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices")]
public async Task<IActionResult> GetInvoices(string storeId, [FromQuery] string[] orderId = null, [FromQuery] string[] status = null,
public async Task<IActionResult> GetInvoices(string storeId, [FromQuery] string[]? orderId = null, [FromQuery] string[]? status = null,
[FromQuery]
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
DateTimeOffset? startDate = null,
[FromQuery]
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
DateTimeOffset? endDate = null,
[FromQuery] string textSearch = null,
[FromQuery] string? textSearch = null,
[FromQuery] bool includeArchived = false,
[FromQuery] int? skip = null,
[FromQuery] int? take = null
@ -354,6 +354,7 @@ namespace BTCPayServer.Controllers.GreenField
{
Activated = details.Activated,
PaymentMethod = method.GetId().ToStringNormalized(),
CryptoCode = method.GetId().CryptoCode,
Destination = details.GetPaymentDestination(),
Rate = method.Rate,
Due = accounting.DueUncapped.ToDecimal(MoneyUnit.BTC),

View File

@ -240,7 +240,7 @@ namespace BTCPayServer.Controllers.GreenField
var inv = await lightningClient.GetInvoice(id);
if (inv == null)
{
return NotFound();
return this.CreateAPIError(404, "invoice-not-found", "Impossible to find a lightning invoice with this id");
}
return Ok(ToModel(inv));
}

View File

@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
@ -98,7 +99,7 @@ namespace BTCPayServer.Controllers.GreenField
{
ModelState.AddModelError(nameof(request.Period), $"The period should be positive");
}
PaymentMethodId[] paymentMethods = null;
PaymentMethodId?[]? paymentMethods = null;
if (request.PaymentMethods is { } paymentMethodsStr)
{
paymentMethods = paymentMethodsStr.Select(s =>
@ -218,6 +219,7 @@ namespace BTCPayServer.Controllers.GreenField
};
model.Destination = blob.Destination;
model.PaymentMethod = p.PaymentMethodId;
model.CryptoCode = p.GetPaymentMethodId().CryptoCode;
return model;
}
@ -245,7 +247,7 @@ namespace BTCPayServer.Controllers.GreenField
if (pp is null)
return PullPaymentNotFound();
var ppBlob = pp.GetBlob();
var destination = await payoutHandler.ParseClaimDestination(paymentMethodId,request.Destination, true);
var destination = await payoutHandler.ParseClaimDestination(paymentMethodId, request!.Destination, true);
if (destination.destination is null)
{
ModelState.AddModelError(nameof(request.Destination), destination.error??"The destination is invalid for the payment specified");
@ -338,7 +340,7 @@ namespace BTCPayServer.Controllers.GreenField
var payout = await ctx.Payouts.GetPayout(payoutId, storeId, true, true);
if (payout is null)
return PayoutNotFound();
RateResult rateResult = null;
RateResult? rateResult = null;
try
{
rateResult = await _pullPaymentService.GetRate(payout, approvePayoutRequest?.RateRule, cancellationToken);
@ -357,7 +359,7 @@ namespace BTCPayServer.Controllers.GreenField
var result = await _pullPaymentService.Approve(new PullPaymentHostedService.PayoutApproval()
{
PayoutId = payoutId,
Revision = revision.Value,
Revision = revision!.Value,
Rate = rateResult.BidAsk.Ask
});
var errorMessage = PullPaymentHostedService.PayoutApproval.GetErrorMessage(result);

View File

@ -516,10 +516,12 @@ namespace BTCPayServer.Controllers
{
paymentMethodId = enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == "BTC" && e.PaymentType == PaymentTypes.BTCLike) ??
enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == "BTC" && e.PaymentType == PaymentTypes.LightningLike) ??
enabledPaymentIds.First();
enabledPaymentIds.FirstOrDefault();
}
isDefaultPaymentId = true;
}
if (paymentMethodId is null)
return null;
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network is null || !invoice.Support(paymentMethodId))
{
@ -529,7 +531,9 @@ namespace BTCPayServer.Controllers
.GetPaymentMethods()
.FirstOrDefault(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode);
if (paymentMethodTemp == null)
paymentMethodTemp = invoice.GetPaymentMethods().First();
paymentMethodTemp = invoice.GetPaymentMethods().FirstOrDefault();
if (paymentMethodTemp is null)
return null;
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
}

View File

@ -47,7 +47,7 @@ namespace BTCPayServer.Controllers
return View(new ShowLightningNodeInfoViewModel
{
Available = true,
Available = nodeInfo.Any(),
NodeInfo = nodeInfo.Select(n => new ShowLightningNodeInfoViewModel.NodeData(n)).ToArray(),
CryptoCode = cryptoCode,
CryptoImage = GetImage(paymentMethodDetails.PaymentId, network),

View File

@ -65,14 +65,23 @@ namespace BTCPayServer.Controllers
if (CurrentStore is null)
return NotFound();
var paymentMethodOptions = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
if (!paymentMethods.Any())
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a pull payment.",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction("PaymentMethods", "Stores", new { storeId });
}
return View(new NewPullPaymentModel
{
Name = "",
Currency = "BTC",
CustomCSSLink = "",
EmbeddedCSS = "",
PaymentMethodItems = paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true))
PaymentMethodItems = paymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true))
});
}
@ -406,6 +415,16 @@ namespace BTCPayServer.Controllers
int skip = 0, int count = 50)
{
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
if (!paymentMethods.Any())
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a payout.",
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction("PaymentMethods", "Stores", new { storeId });
}
var vm = this.ParseListQuery(new PayoutsModel
{
PaymentMethods = paymentMethods,

View File

@ -136,7 +136,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
claimDestination is IBitcoinLikeClaimDestination bitcoinLikeClaimDestination)
{
txout.ScriptPubKey = bitcoinLikeClaimDestination.Address.ScriptPubKey;
return Task.FromResult(txout.GetDustThreshold(new FeeRate(1.0m)).ToDecimal(MoneyUnit.BTC));
return Task.FromResult(txout.GetDustThreshold().ToDecimal(MoneyUnit.BTC));
}
return Task.FromResult(0m);

View File

@ -190,6 +190,15 @@ namespace BTCPayServer.Payments.Bitcoin
}
var reserved = await prepare.ReserveAddress;
if (paymentMethod.ParentEntity.Type != InvoiceType.TopUp)
{
var txOut = network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut();
txOut.ScriptPubKey = reserved.Address.ScriptPubKey;
var dust = txOut.GetDustThreshold();
var amount = paymentMethod.Calculate().Due;
if (amount < dust)
throw new PaymentMethodUnavailableException("Amount below the dust threshold. For amounts of this size, it is recommended to enable an off-chain (Lightning) payment method");
}
onchainMethod.DepositAddress = reserved.Address.ToString();
onchainMethod.KeyPath = reserved.KeyPath;
onchainMethod.PayjoinEnabled = blob.PayJoinEnabled &&

View File

@ -424,7 +424,7 @@ namespace BTCPayServer.Payments.PayJoin
{
var outputContribution = Money.Min(additionalFee, -due);
outputContribution = Money.Min(outputContribution,
newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee));
newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold());
newTx.Outputs[i].Value -= outputContribution;
additionalFee -= outputContribution;
due += outputContribution;
@ -437,7 +437,7 @@ namespace BTCPayServer.Payments.PayJoin
{
var outputContribution = Money.Min(additionalFee, feeOutput.Value);
outputContribution = Money.Min(outputContribution,
feeOutput.Value - feeOutput.GetDustThreshold(minRelayTxFee));
feeOutput.Value - feeOutput.GetDustThreshold());
outputContribution = Money.Min(outputContribution, allowedSenderFeeContribution);
feeOutput.Value -= outputContribution;
additionalFee -= outputContribution;

View File

@ -36,7 +36,8 @@ namespace BTCPayServer.Services
VerificationProgress = s.VerificationProgress
} : null,
ChainHeight = summary.Status.ChainHeight,
SyncHeight = summary.Status.SyncHeight
SyncHeight = summary.Status.SyncHeight,
Available = summary.Status.IsFullySynched
});
}

View File

@ -65,7 +65,7 @@
{
<span v-if="srvModel.targetAmount" class="mt-3" id="crowdfund-header-target">
<h3 class="d-inline-block">
<span class="badge bg-info px-3" v-text="`${new Intl.NumberFormat().format(srvModel.targetAmount)} ${targetCurrency}`">@Math.Round(Model.TargetAmount.GetValueOrDefault(0)) @Model.TargetCurrency</span>
<span class="badge bg-info px-3" v-text="`${targetAmount} ${targetCurrency}`">@Math.Round(Model.TargetAmount.GetValueOrDefault(0)) @Model.TargetCurrency</span>
</h3>
@if (Model.ResetEveryAmount > 0 && !Model.NeverReset)
{
@ -123,7 +123,7 @@
<div class="card-body">
<div class="row py-2 text-center crowdfund-stats">
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-raised-amount">
<h3 v-text="`${new Intl.NumberFormat().format(raisedAmount)} ${targetCurrency}`">@Math.Round(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount) @Model.TargetCurrency</h3>
<h3 v-text="`${raisedAmount} ${targetCurrency}`">@Math.Round(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount, Model.CurrencyData.Divisibility) @Model.TargetCurrency</h3>
<h5 class="text-muted fst-italic mb-0">Raised</h5>
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0" class="only-for-js">
<ul class="p-0 text-uppercase">
@ -287,8 +287,8 @@
<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 v-if="!perks || perks.length === 0"
:perk="{title: 'Donate Custom Amount', price: { type: 0, value: null}}"
:target-currency="targetCurrency"
:active="active"
:loading="loading"
@ -324,10 +324,10 @@
<img v-if="perk.image && perk.image != 'null'" class="card-img-top" :src="perk.image" />
<div class="card-body">
<div class="card-title d-flex justify-content-between">
<span class="h5">{{perk.title? perk.title : perk.id}}</span>
<span class="h5">{{perk.title ? perk.title : perk.id}}</span>
<span class="text-muted">
<template v-if="perk.price && perk.price.value">
{{new Intl.NumberFormat().format(perk.price.value.noExponents())}}
{{formatAmount(perk.price.value.noExponents(), srvModel.currencyData.divisibility)}}
{{targetCurrency}}
<template v-if="perk.price.type == 1">or more</template>
</template>
@ -369,7 +369,7 @@
<span v-if="perk.inventory != null && perk.inventory > 0" class="text-center text-muted">{{new Intl.NumberFormat().format(perk.inventory)}} left</span>
<span v-if="perk.inventory != null && perk.inventory <= 0" class="text-center text-muted">Sold out</span>
<span v-if="perk.sold">{{new Intl.NumberFormat().format(perk.sold)}} Contributor{{perk.sold === 1 ? "": "s"}}</span>
<span v-if="perk.value">{{new Intl.NumberFormat().format(perk.value)}} {{targetCurrency}} total</span>
<span v-if="perk.value">{{formatAmount(perk.value, srvModel.currencyData.divisibility)}} {{targetCurrency}} total</span>
</div>
</form>
</div>

View File

@ -314,10 +314,9 @@
<div class="row" v-show="!errors.any()">
<div class="col-lg-12">
<pre><code id="mainCode" class="html"></code></pre>
<button class="btn btn-primary" id="copyCode">
<button class="btn btn-primary" data-clipboard-target="#mainCode">
<i class="fa fa-copy"></i> Copy Code
</button>
<span class="copyLabelPopup" style="display:none;">Copied</span>
</div>
</div>
<div class="row" v-show="errors.any()">
@ -328,7 +327,7 @@
</div>
<script id="template-paybutton-styles" type="text/template">
<style type="text/css">
<style>
.btcpay-form {
display: inline-flex;
align-items: center;
@ -405,7 +404,7 @@
</script>
<script id="template-slider-styles" type="text/template">
<style type="text/css">
<style>
input[type=range].btcpay-input-range {
-webkit-appearance:none;
width:100%;

View File

@ -121,10 +121,10 @@ else
</ul>
<div class="tab-content" id="export-tabContent">
<div class="tab-pane fade show active" id="export-base64" role="tabpanel" aria-labelledby="export-base64-tab">
<pre class="mb-4"><code class="text text-wrap">@Model.PSBT</code></pre>
<pre class="mb-4 text-wrap"><code class="text">@Model.PSBT</code></pre>
</div>
<div class="tab-pane fade" id="export-hex" role="tabpanel" aria-labelledby="export-hex-tab">
<pre class="mb-4"><code class="text text-wrap">@Model.PSBTHex</code></pre>
<pre class="mb-4 text-wrap"><code class="text">@Model.PSBTHex</code></pre>
</div>
<div class="tab-pane fade" id="export-json" role="tabpanel" aria-labelledby="export-json-tab">
<pre class="mb-0"><code class="json">@Model.Decoded</code></pre>

View File

@ -1,4 +1,4 @@
@model ListTransactionsViewModel
@model ListTransactionsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(WalletsNavPages.Transactions, $"{Model.CryptoCode} Transactions", Context.GetStoreData().StoreName);
@ -35,7 +35,6 @@
.badge-container {
display: flex;
align-items: center;
flex-wrap: nowrap;
}

View File

@ -1,4 +1,4 @@
delegate('click', '.payment-method', function(e) {
closePaymentMethodDialog(e.target.dataset.paymentMethod);
return false;
})
})

View File

@ -48,8 +48,6 @@ document.addEventListener("DOMContentLoaded",function (ev) {
this.amount = this.perk.price.type === 0? null : (amount || 0).noExponents();
this.expanded = false;
}
},
mounted: function () {
this.setAmount(this.perk.price.value);
@ -63,7 +61,6 @@ document.addEventListener("DOMContentLoaded",function (ev) {
}
}
}
});
app = new Vue({
@ -82,14 +79,17 @@ document.addEventListener("DOMContentLoaded",function (ev) {
active: true,
animation: true,
sound: true,
lastUpdated:"",
lastUpdated: "",
loading: false,
timeoutState: 0
}
},
computed: {
raisedAmount: function(){
return parseFloat(this.srvModel.info.currentAmount + this.srvModel.info.currentPendingAmount ).toFixed(this.srvModel.currencyData.divisibility) ;
return this.formatAmount(this.srvModel.info.currentAmount + this.srvModel.info.currentPendingAmount);
},
targetAmount: function(){
return this.formatAmount(this.srvModel.targetAmount);
},
percentageRaisedAmount: function(){
return parseFloat(this.srvModel.info.progressPercentage + this.srvModel.info.pendingProgressPercentage ).toFixed(2);
@ -203,6 +203,9 @@ document.addEventListener("DOMContentLoaded",function (ev) {
if(this.timeoutState){
clearTimeout(this.timeoutState);
}
},
formatAmount: function(amount) {
return formatAmount(amount, this.srvModel.currencyData.divisibility)
}
},
mounted: function () {
@ -315,3 +318,18 @@ document.addEventListener("DOMContentLoaded",function (ev) {
});
});
/**
* Formats input string as a number according to browser locale
* with correctly displayed fraction amount (e.g. 0.012345 for BTC instead of just 0.0123)
*
* @param {number | string} amount Amount to format
* @param {number} divisibility Currency divisibility (e.g., 8 for BTC)
* @returns String formatted as a number according to current browser locale and correct fraction amount
*/
function formatAmount(amount, divisibility) {
var parsedAmount = parseFloat(amount).toFixed(divisibility);
var [wholeAmount, fractionAmount] = parsedAmount.split('.');
var formattedWholeAmount = new Intl.NumberFormat().format(parseInt(wholeAmount, 10));
return formattedWholeAmount + (fractionAmount ? '.' + fractionAmount : '');
}

View File

@ -1,10 +1,9 @@
window.copyToClipboard = function (e, text) {
window.copyToClipboard = function (e, data) {
if (navigator.clipboard) {
e.preventDefault();
var item = e.currentTarget;
var data = text || item.getAttribute('data-clipboard');
var confirm = item.querySelector('[data-clipboard-confirm]') || item;
var message = confirm.getAttribute('data-clipboard-confirm') || 'Copied ✔';
const item = e.target;
const confirm = item.querySelector('[data-clipboard-confirm]') || item;
const message = confirm.getAttribute('data-clipboard-confirm') || 'Copied ✔';
if (!confirm.dataset.clipboardInitialText) {
confirm.dataset.clipboardInitialText = confirm.innerText;
confirm.style.minWidth = confirm.getBoundingClientRect().width + 'px';
@ -22,7 +21,14 @@ window.copyUrlToClipboard = function (e) {
}
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("[data-clipboard]").forEach(item => {
item.addEventListener("click", window.copyToClipboard)
delegate('click', '[data-clipboard]', e => {
const data = e.target.getAttribute('data-clipboard')
window.copyToClipboard(e, data)
})
delegate('click', '[data-clipboard-target]', e => {
const selector = e.target.getAttribute('data-clipboard-target')
const target = document.querySelector(selector)
const data = target.innerText
window.copyToClipboard(e, data)
})
})

View File

@ -1,10 +1,11 @@
function delegate(eventType, selector, handler, root) {
(root || document).addEventListener(eventType, function(event) {
const target = event.target.closest(selector);
const target = event.target.closest(selector)
if (target) {
event.target = target
if (handler.call(this, event) === false) {
event.preventDefault();
event.preventDefault()
}
}
});
})
}

View File

@ -1,13 +1,5 @@
$(function () {
inputChanges();
// Clipboard Copy
new Clipboard('#copyCode', {
text: function (trigger) {
$(".copyLabelPopup").show().delay(1000).fadeOut(500);
return inputChanges();
}
});
});
function esc(input) {

View File

@ -1152,7 +1152,11 @@
"properties": {
"paymentMethod": {
"type": "string",
"description": "The payment method"
"description": "Payment method available for the invoice (e.g., \"BTC\" or \"BTC-LightningNetwork\" or \"BTC-LNURLPAY\")"
},
"cryptoCode": {
"type": "string",
"description": "Crypto code of the payment method (e.g., \"BTC\" or \"LTC\")"
},
"destination": {
"type": "string",

View File

@ -313,6 +313,46 @@
"security": []
}
},
"/api/v1/pull-payments/{pullPaymentId}/payouts/{payoutId}": {
"parameters": [
{
"name": "pullPaymentId",
"in": "path",
"required": true,
"description": "The ID of the pull payment",
"schema": { "type": "string" }
},
{
"name": "payoutId",
"in": "path",
"required": true,
"description": "The ID of the pull payment payout",
"schema": { "type": "string" }
}
],
"get": {
"summary": "Get Payout",
"operationId": "PullPayments_GetPayout",
"description": "Get payout",
"responses": {
"200": {
"description": "A specific payout of a pull payment",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayoutData"
}
}
}
},
"404": {
"description": "Pull payment payout not found"
}
},
"tags": [ "Pull payments (Public)", "Pull payments payout (Public)" ],
"security": []
}
},
"/api/v1/stores/{storeId}/payouts/{payoutId}": {
"parameters": [
{
@ -554,7 +594,12 @@
"paymentMethod": {
"type": "string",
"example": "BTC",
"description": "The payment method of the payout"
"description": "The payment method of the payout (e.g., \"BTC\" or \"BTC_LightningLike\""
},
"cryptoCode": {
"type": "string",
"example": "BTC",
"description": "Crypto code of the payment method of the payout (e.g., \"BTC\" or \"LTC\")"
},
"paymentMethodAmount": {
"type": "string",

View File

@ -103,6 +103,11 @@
"format": "integer",
"nullable": true,
"description": "The height of the latest indexed block of the internal indexer"
},
"available": {
"type": "boolean",
"description": "True if the full node and the indexer are fully synchronized",
"nullable": false
}
}
}

View File

@ -65,7 +65,7 @@
"description": "Whether the payment method is enabled"
},
"cryptoCode": {
"type": "boolean",
"type": "string",
"description": "The currency code of the payment method"
},
"data": {

View File

@ -153,7 +153,8 @@
"required": false,
"description": "Whether to generate a new address for this request even if the previous one was not used",
"schema": {
"type": "string"
"type": "boolean",
"default": false
}
}
],
@ -730,7 +731,7 @@
},
"subtractFromAmount": {
"type": "boolean",
"description": "Whether to subtract from the provided amount. Must be false if `destination` is a BIP21 link"
"description": "Whether to subtract the transaction fee from the provided amount. This makes the receiver receive less, or in other words: he or she pays the transaction fee. Also useful if you want to clear out your wallet. Must be false if `destination` is a BIP21 link"
}
}
},
@ -749,7 +750,7 @@
"feeRate": {
"type": "number",
"format": "decimal or long (sats/byte)",
"description": "A wallet address or a BIP21 payment link"
"description": "Transaction fee."
},
"proceedWithPayjoin": {
"type": "boolean",

View File

@ -625,7 +625,7 @@
"type": {
"type": "string",
"nullable": false,
"description": "The type of this event, current available are `InvoiceCreated`, `InvoiceReceivedPayment`, `InvoicePaidInFull`, `InvoiceExpired`, `InvoiceSettled`, and `InvoiceInvalid`."
"description": "The type of this event, current available are `InvoiceCreated`, `InvoiceReceivedPayment`, `InvoiceProcessing`, `InvoiceExpired`, `InvoiceSettled`, `InvoiceInvalid`, and `InvoicePaymentSettled`."
},
"timestamp": {
"description": "The timestamp when this delivery has been created",
@ -899,9 +899,9 @@
}
}
},
"InvoicePaidInFull": {
"InvoiceProcessing": {
"post": {
"summary": "InvoicePaidInFull",
"summary": "InvoiceProcessing",
"description": "Triggers when an invoice is fully paid, but doesn't have the required amount of confirmations on the blockchain yet according to your store's settings.",
"parameters": [
{

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.3.4</Version>
<Version>1.3.7</Version>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,43 @@
# Changelog
## 1.3.7
### Improvements:
* Update of Bitbank rate provider (#3157) @junderw
### Bug fixes:
* Fix visual bug when decoding PSBT (#3172) @dennisreimann
* Swagger fixes: improve API docs and property types (#3170) @woutersamaey
* Fix copy pay button code (#3175) @dennisreimann
* Fix LN Node availability check (#3189) @dennisreimann
* `available` property of nodes returned by /api/v1/server/info wasn't actually set (ee1a034c0ab7744a2988e5da874084bc7dfa8b73) @NicolasDorier
* Format perk value correctly in crowdfund app (#3141) @bolatovumar
* Invoice page: Dropdown magically disappears (#3167 #3169) @trigger67
## 1.3.6
### Improvements:
* Fix breaking changes of LND API 0.14 @NicolasDorier
## 1.3.5
### Bug fixes:
* Fix: Checkout page of for invoices of 0 amount shouldn't crash, but 404 @NicolasDorier
* Swagger doc: Fix type of property cryptoCode (#3088) @ndeet
* Fix bug with fraction amount display in crowdfund app (#3098) @bolatovumar
* Swagger doc: Update Swagger docs for webhook event types (#3104) @bolatovumar
* Payout/pull payment page would crash if no payment method are set on the store @satwo
### Improvements:
* Add crypto code for invoice and pull payment payout API response (#3099) @bolatovumar
* Prevent creation of on-chain invoices below the dust limit (#3082) @satwo
## 1.3.4
### Bug fixes: